API Reference
AgentFactory
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
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.
.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.
.listensTo([eventA, eventB])
.listensTo([eventC]) // now listens to A, B, and COmit 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.
.emits([responseEvent, errorEvent])The declaration is enforced at runtime, not just in types, because emitted payloads often come from a model:
emitthrows, andemitAndAwaitrejects with,EmitValidationError(eventName,agentId,toolName?,reason,issues) when the event is not declared (reason: 'undeclared', also for reservedm4trix:*names), its payload fails the event's schema ('invalid-payload', withissues: { path, message }[]), or none of the agent's publish channels accepts it ('channel', see.events()). The event is not published.- The invocation fails with that error even if the logic catches it, so the network's
onAgentErrorpolicy applies: under'fail'the run ends withm4trix:run.failed,error.kind: 'invalid-emit'anderror.event; under'continue',m4trix:agent.erroris 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,executeForModeldoes 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).
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>: thestateschema'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;Dates,Options 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
emitAndAwaitsees 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
AgentAbortedErrorwhen one never will be because the run is over. See 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 astateschema (everySegmentedFold) can compact; for otherscompactis 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. - 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-sharedfor different compactable folds of one name and version (they would share checkpoints), and warns withfold-name-sharedfor 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 withEventPage.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.compactrejects withFoldCompactionError(reason: 'unsupported'): there is no store to publish the checkpoint to. - Aggregators get the read accessors only: no
compact(FoldReadAccessors). - Validation.
.folds()throwsContextFoldDefinitionErrorfor a fold that reads no events (no handler) or for something that is not a fold, a view included (also a type error). It throwsErrorfor 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 agentemit(event)— Function to emit events, typed to declared.emits()events and checked against them at runtime (see.emits())history— The durable event history of the run and its conversation, read on demand:history.run()andhistory.context({ limit?, before? })(paged, oldest first). Scoped to the run's principal; transient events are never in it. See Event history and storesfolds— One accessor per.folds()fold, by name:await folds.conversation()returns the folded state, read-onlyprincipal— Who the run acts for, asauthreturned it ({ id, ...claims });undefinedfor anonymous runs. Tools get it asprincipaltoosignal—AbortSignalaborted when the run ends or the client disconnects. Pass it tofetchand LLM calls; tools and skills receive it assignaltoocomplete()— Ends the run as completed, after the events this invocation already emittedfail(error)— Ends the run as failed witherror, whatever the network'sonAgentErrorpolicyreportUsage(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 againstmaxTokens/maxCostUsd. Tools get it asreportUsagetoo. A no-op outside a network
.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.
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.
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
triggerEventis a union of alllistensToevent envelopesemit()only accepts payloads matching declaredemitsevents, and checks them again at runtimeparamsmatches the schema from.params(), and is validated against it by.produce()foldshas one accessor per.folds()fold name, resolving to that fold's state type, read-only (FoldReadStateOf), with acompacttyped by the fold (FoldCompact:FoldCannotCompactfor a fold that cannot); unknown names, duplicate names and views are type errors; a fold without events (no handler) throwsContextFoldDefinitionErrorwhen the agent is built