> Documentation index for m4trix: https://m4trix.dev/llms.txt. Every page is available as Markdown by appending `.md` to its URL.

# AgentFactory

Source: https://m4trix.dev/docs/api-reference/agent-factory

The `AgentFactory` is a fluent builder for creating type-safe agents. It provides full TypeScript inference from trigger events through to emitted events.

## Entry Point

```ts
AgentFactory.run()
```

Creates a fresh builder with no configuration.

## Builder Methods

### `.params(schema)`

Defines the parameter schema for the agent. Parameters are static configuration passed when producing the agent.

```ts
.params(S.Struct({ model: S.String, temperature: S.Number }))
```

`.produce(params)` validates the params against this schema at runtime and hands the agent the decoded value (schema transformations applied); invalid params throw `AgentParamsError` with `issues: { path, message }[]`.

### `.listensTo(events)`

Declares which event types trigger this agent. Accepts an array of `AgentNetworkEventDef`. Can be called multiple times — events accumulate.

```ts
.listensTo([eventA, eventB])
.listensTo([eventC])  // now listens to A, B, and C
```

Omit to create a catch-all agent that receives every event on subscribed channels.

### `.emits(events)`

Declares which event types this agent can emit. Also accumulates across multiple calls.

```ts
.emits([responseEvent, errorEvent])
```

The declaration is enforced at runtime, not just in types, because emitted payloads often come from a model:

- `emit` throws, and `emitAndAwait` rejects with, `EmitValidationError` (`eventName`, `agentId`, `toolName?`, `reason`, `issues`) when the event is not declared (`reason: 'undeclared'`, also for reserved `m4trix:*` names), its payload fails the event's schema (`'invalid-payload'`, with `issues: { path, message }[]`), or none of the agent's publish channels accepts it (`'channel'`, see [`.events()`](https://m4trix.dev/docs/api-reference/channel-api.md#events)). The event is not published.
- The invocation fails with that error **even if the logic catches it**, so the network's `onAgentError` policy applies: under `'fail'` the run ends with `m4trix:run.failed`, `error.kind: 'invalid-emit'` and `error.event`; under `'continue'`, `m4trix:agent.error` is published. The client-facing message names the event and the offending paths, never the payload values.
- The published payload is the validated one: a value that already is exactly of the event's type (e.g. from `event.make()`) is published as is; anything else, such as raw model output, is decoded with the schema, so transformations apply and unknown properties are dropped.
- **An agent without `.emits()` may emit nothing.**
- Tools are checked against their own `.emits([...])`; an agent does not need to repeat its tools' events. A tool's invalid emit is not a tool error: it is not retried, `executeForModel` does not turn it into a model result, and it fails the calling agent's invocation like the agent's own.

### `.folds(folds)`

Declares the context folds (`ContextFold`, `SegmentedFold`) that the logic reads through `ctx.folds`. Each one is keyed by its own name: `.folds([Conversation])` gives `folds.conversation()`. Calls accumulate. Views are not declared here: a view (`ContextView.of`) is a function you call with the state a read returns and a budget. For defining folds, views and `ContextStrategy`, and for compaction, see [Context (Folds and Views)](https://m4trix.dev/docs/concepts/context.md).

```ts
const model = AgentFactory.run()
  .listensTo([userPrompt])
  .emits([assistantMessage])
  .folds([Conversation])
  .logic(async ({ folds, emit }) => {
    const { turns } = await folds.conversation(); // typed as the fold's state
    emit(assistantMessage.make({ text: await answer(turns) }));
  });
```

- **Read-only.** A read resolves to the fold's state, typed read-only (`FoldReadStateOf<typeof Fold>`: the `state` schema's type for a fold with one, `DeepReadonly<State>` otherwise): in a network it is the cached state itself, shared by every reader, so never change it. Its plain objects and arrays are frozen, so writing to them throws; `Date`s, `Option`s and class instances are left as they are.
- **Lazy and incremental.** A read folds the fold's events into its state when you call it. In a network, the state is cached on the event plane, so each read only folds the events published since the previous one. The result always includes the trigger event.
- **Not memoized.** Each call reads again, so a read after `emitAndAwait` sees what was published meanwhile, such as a compaction another agent made.
- **Read-your-writes.** A read includes every event the invocation emitted before it: it waits until those are stored, and rejects with `AgentAbortedError` when one never will be because the run is over. See [Caching and consistency](https://m4trix.dev/docs/concepts/context.md#caching-and-consistency).
- **Compaction.** `await folds.<name>.compact(...)` replaces a fold's state for good: the runtime publishes it as the fold's checkpoint, and every later read, warm or cold (after a restart, or after eviction), starts from the newest one. Only a context-scoped fold with a `state` schema (every `SegmentedFold`) can compact; for others `compact` is a type error. A segmented fold also takes a policy: `compact({ view, budget, keepTokens, keepFrom?, summarize })` compacts only when the view does not fit. See [Compaction](https://m4trix.dev/docs/concepts/context.md#compaction).
- **Scopes.** A `'context'` fold (the default) is cached per conversation: every run of the context, in the run's principal namespace. A `'run'` fold is cached per run and dropped when the run ends.
- **Shared state.** The state is cached per fold object: agents given the same fold share it. A fold's checkpoints are stored by its name and version, so give each fold in a network its own name and share that one object. The wiring check fails setup with `fold-checkpoint-shared` for different compactable folds of one name and version (they would share checkpoints), and warns with `fold-name-shared` for other folds that share a name.
- **Concurrency.** Reads of the same fold and context, from one agent or several, run one at a time, so an event is never folded twice.
- **Deleted history.** When a context is deleted (`network.getStore().deleteContext(key)`) or expires in the store, its cached state is rebuilt from the new history, never continued. Stores report this with `EventPage.reset`.
- **Outside a network,** `agent.invoke({ history })` folds the given history on every read, without a cache. Without a history each fold reads its initial state. `compact` rejects with `FoldCompactionError` (`reason: 'unsupported'`): there is no store to publish the checkpoint to.
- **Aggregators** get the read accessors only: no `compact` (`FoldReadAccessors`).
- **Validation.** `.folds()` throws `ContextFoldDefinitionError` for a fold that reads no events (no handler) or for something that is not a fold, a view included (also a type error). It throws `Error` for a name used twice on one agent (also a type error).

The cache is bounded per plane by the `foldCache` setup option: `{ maxEntries?: number /* 1000 */, ttl?: DurationInput /* '30 minutes' after the last read */, clock? }`. `Infinity` lifts either bound, and invalid values throw at setup. The bounds only affect memory, because an evicted state is rebuilt on its next read. The `m4trix.fold.read.events` metric (`onMetric`) reports the events each read folded, by `fold` and `start` (`cold`, `warm` or `reset`). The wiring check warns with `fold-event-unpublished` when no agent emits a folded event and no channel carrying it is fed from outside.

`EventAggregator.listensTo([...]).folds([...])` works the same way, with `folds` in both `emitWhen` and `mapToEmit`.

### `.logic(fn)`

The core handler. Receives:

- **`params`** — Resolved parameters from `.produce()`
- **`triggerEvent`** — Full envelope `{ name, meta, payload }` that triggered the agent
- **`emit(event)`** — Function to emit events, typed to declared `.emits()` events and checked against them at runtime (see [`.emits()`](#emitsevents))
- **`history`** — The durable event history of the run and its conversation, read on demand: `history.run()` and `history.context({ limit?, before? })` (paged, oldest first). Scoped to the run's principal; transient events are never in it. See [Event history and stores](https://m4trix.dev/docs/api-reference/agent-network.md#event-history-and-stores)
- **`folds`** — One accessor per [`.folds()`](#foldsfolds) fold, by name: `await folds.conversation()` returns the folded state, read-only
- **`principal`** — Who the run acts for, as `auth` returned it (`{ id, ...claims }`); `undefined` for anonymous runs. Tools get it as `principal` too
- **`signal`** — `AbortSignal` aborted when the run ends or the client disconnects. Pass it to `fetch` and LLM calls; tools and skills receive it as `signal` too
- **`complete()`** — Ends the run as completed, after the events this invocation already emitted
- **`fail(error)`** — Ends the run as failed with `error`, whatever the network's `onAgentError` policy
- **`reportUsage(usage)`** — Records what a model call used (`{ inputTokens?, outputTokens?, totalTokens?, cacheReadTokens?, cacheWriteTokens?, costUsd?, model? }`) on the run: summed per run and model, passed to the tracer, included in the run-end event and checked against `maxTokens` / `maxCostUsd`. Tools get it as `reportUsage` too. A no-op outside a network

```ts
.logic(async ({ params, triggerEvent, emit }) => {
  emit({ name: 'agent-output', payload: { reply: '...' } });
})
```

### `.concurrency(limit)`

Caps how many invocations of the agent run at once across all runs of a network, e.g. to stay within an LLM provider's rate limit. `limit` is a positive integer or `Infinity`; the default is unbounded.

```ts
const writer = AgentFactory.run()
  .listensTo([draftRequested])
  .emits([draftReady])
  .concurrency(20)
  .logic(async ({ triggerEvent, emit }) => { /* ... */ });
```

Within one run an agent always handles its events one at a time, in the order they were published, even when it subscribes to several channels (an event published to two of them arrives once). Different runs never wait on each other, apart from this cap. Invocations waiting for the cap count as pending work for the run, so they don't trip its `idleTimeout`.

### `.produce(params)`

Finalizes the builder and returns an `Agent` instance. The `params` argument must match the schema from `.params()`; it is validated and decoded, and invalid params throw `AgentParamsError`.

```ts
const agent = builder.produce({ model: 'gpt-4o' });
```

Outside a network, `agent.invoke({ signal })` passes your signal through, `complete()` does nothing and `fail(error)` makes `invoke()` reject with `error`. Emits are validated there too: your `emit` callback receives decoded events, and an invalid emit makes `invoke()` reject with `EmitValidationError`.

## Type Safety

- `triggerEvent` is a union of all `listensTo` event envelopes
- `emit()` only accepts payloads matching declared `emits` events, and checks them again at runtime
- `params` matches the schema from `.params()`, and is validated against it by `.produce()`
- `folds` has one accessor per `.folds()` fold name, resolving to that fold's state type, read-only (`FoldReadStateOf`), with a `compact` typed by the fold (`FoldCompact`: `FoldCannotCompact` for a fold that cannot); unknown names, duplicate names and views are type errors; a fold without events (no handler) throws `ContextFoldDefinitionError` when the agent is built

## See Also

- [Agents (Concepts)](https://m4trix.dev/docs/concepts/agents.md)
- [Context (Folds and Views)](https://m4trix.dev/docs/concepts/context.md)
- [AgentNetworkEvent](https://m4trix.dev/docs/api-reference/agent-network-event.md)
- [AgentNetwork](https://m4trix.dev/docs/api-reference/agent-network.md)
