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

TypeScript
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.

TypeScript
.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.

TypeScript
.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.

TypeScript
.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()). 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).

TypeScript
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; 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 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.
  • 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.
  • 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())
  • 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
  • folds — One accessor per .folds() 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
TypeScript
.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.

TypeScript
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.

TypeScript
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