API Reference
AgentNetwork
The AgentNetwork orchestrates agents, channels, and the event plane. Use AgentNetwork.setup() to wire everything together.
Setup
const network = AgentNetwork.setup(
({ mainChannel, createChannel, proxy, registerAgent, registerAggregator, endsOn }) => {
// ...
},
);Setup Context
mainChannel
The network's main channel, named main. The network creates it, so it is a channel rather than a function: pass it straight to subscribe or publishTo. Start events are published here when the network is exposed as an API.
registerAgent(router).subscribe(mainChannel);createChannel(name)
Creates an additional named channel. Names must be kebab-case.
const client = createChannel('client');
const analytics = createChannel('analytics');proxy
Provides proxy factories: proxy.sse() (exposed with registerSSEStream) and proxy.custom(kind, config?, direction?) for your own kinds (see defineProxyKind):
const client = createChannel('client').proxy(proxy.sse());
const events = createChannel('events').proxy(proxy.custom('queue', { topic: 'events' }));Kafka and Socket.IO bridges are not built in; they are planned on top of EventTransport.
registerAgent(agent)
Registers an agent and returns a binding builder:
registerAgent(myAgent)
.subscribe(main)
.publishTo(client);An agent can subscribe to and publish to multiple channels:
registerAgent(routerAgent)
.subscribe(main)
.subscribe(feedback)
.publishTo(client)
.publishTo(analytics);endsOn(event, when?)
Declares a terminal event: when it is published in a run (and when accepts it, if given), the run completes with m4trix:run.completed, its in-flight agents are aborted and its stream closes. when receives the typed envelope. Call endsOn once per terminal event.
endsOn(AnswerEvent);
// The user's message and the reply share an event: end on the reply only.
endsOn(MessageEvent, (event) => event.payload.role === 'assistant');Terminal events live on the network because agents emit them, and because the event plane enforces them, they apply to expose(), custom proxies and network.run() alike. An agent can also end its run from logic with complete(). See Run Lifecycle.
Setup Options
The second argument of AgentNetwork.setup():
| Option | Description |
|---|---|
onAgentError | 'fail' (default): an agent that throws (including an invalid emit, kind: 'invalid-emit') fails the run with m4trix:run.failed and aborts the run's other agents. 'continue': publish m4trix:agent.error and keep the run going |
logger | Receives what the network reports without failing on it: wiring smells at setup, refused publishes, store and tracer failures, invalid usage reports, stream errors. { debug?, info?, warn, error? } (each (message, data?)); console, pino and most leveled loggers fit. error falls back to warn; a throwing logger is ignored. Default: console. See Logger |
onMetric | (metric: NetworkMetric) => void: run durations and outcomes, agent invocation latencies, mailbox depths, events per run, store writes and usage, vendor-neutral. A throwing handler never breaks the network. See Metrics |
transport | The EventTransport carrying channel events and emitAndAwait replies between the network's planes. Default: one in-memory transport per plane |
wiring | Options of the wiring check: { ignore?: WiringWarningCode[] } |
networkTracer | Custom NetworkTracer for run, event and agent hooks |
consoleTracing | Log runs, events and agent invocations to stdout. Off by default |
tracingLayer | Effect layer for event plane spans |
store | Where the network keeps event history (an AgentNetworkStore). Shared by every plane of the network. Default: createInMemoryNetworkStore() |
foldCache | Bounds of each plane's cache of context fold state: { maxEntries?, ttl?, clock? }. maxEntries (default 1000) evicts the least recently read state; ttl (default '30 minutes' after the last read) expires states lazily. Infinity lifts either; invalid values throw at setup. An evicted state is rebuilt on its next read, so the bounds only cost time, never correctness |
maxDepth | Fail a run when an event would be more than this many hops from its start event (meta.depth), e.g. two agents triggering each other in a loop. Default 25 |
maxEvents | Fail a run when it publishes more durable events than this, its start event included. Transient and m4trix:* events don't count. Default 1000 |
maxTokens | Fail a run once its agents and tools reported more totalTokens than this with reportUsage(). Off by default |
maxCostUsd | Fail a run once its reported costUsd exceeds this. Off by default |
const network = AgentNetwork.setup(
({ mainChannel, registerAgent }) => {
// ...
},
{ onAgentError: 'continue' },
);Agents can always fail their run explicitly with fail(error), whatever the policy.
The four run limits apply to every run of the network; Infinity disables one. Override them per plane with network.run({ maxDepth }) or per exposed run with registerSSEStream({ maxEvents }). A run that exceeds one fails with m4trix:run.failed, error.kind: 'limit-exceeded' and the limit ('depth' | 'events' | 'tokens' | 'cost') and its max; the event that would have exceeded maxDepth / maxEvents is not delivered. See Run limits, usage and budgets.
Wiring check
When the setup callback returns, AgentNetwork.setup() checks the network's wiring: which agents (and their tools) emit and listen to which events on which channels, against what the channels accept (.events()). Contradictions throw a NetworkWiringError that lists all of them at once (error.errors); smells are logged through the logger option and do not stop the network.
| Code | Kind | Meaning |
|---|---|---|
emit-not-accepted | error | An agent declares an event in .emits() (its own or a tool's), but none of its publishTo channels accepts it |
listen-not-accepted | error | An agent listens to an event, but none of the channels it subscribes to accepts it |
fold-checkpoint-shared | error | Agents or aggregators read different fold objects that would share checkpoints: both can compact (context scope, a state schema), with the same name and version, e.g. a copy built again per agent by a factory function. Checkpoints are stored by name and version, so each would restore states the other wrote. Give each fold its own name (or version), or define it once and share that object |
durable-event-redacted | error | A durable event's schema holds S.Redacted (or S.RedactedFromSelf) anywhere: nested, optional, in a union member, a class, an Option, behind S.suspend. Durable events are stored and streamed encoded with their schema, and S.Redacted encodes to the secret itself, so it would be in clear in the store and on every stream. Mark the event transient where its agents listen to or emit it (never stored; streamed as "<redacted>"), or remove the secret from its payload. Durable means what the plane stores: an event is transient only when an agent or aggregator declares it so; a transient: true definition on a channel or a fold alone does not make it one |
unconsumed-event | warning | An emitted event reaches no agent listening to it and no proxy that streams it out on the channels it lands on, no agent or aggregator folds it, and it does not end the run. A fold counts on every channel, since folds read the store, which keeps every durable event whatever its channel; a transient event is never stored, so folding it does not count. Expected for emitAndAwait replies, which the waiting agent receives without listening, and for channels you read yourself with plane.subscribe |
agent-publishes-nowhere | warning | An agent emits events but has no publishTo channel |
agent-not-subscribed | warning | An agent is not subscribed to any channel, so it is never invoked |
agent-unreachable | warning | An agent subscribes to channels where none of the events it listens to can arrive: no agent publishes them there, and the channel is not fed from outside (the main channel, a receiving proxy, or a channel that declares the event) |
terminal-event-not-emitted | warning | endsOn(event) names an event no agent emits; runs only end on it when it is published from outside |
fold-event-unpublished | warning | An agent's fold handles an event that nothing publishes: no agent emits it to a channel that accepts it, and no channel that carries it is fed from outside. The fold never sees it |
fold-name-shared | warning | Different fold objects share a name without sharing checkpoints (e.g. folds without a state schema, or of different versions). Each one is folded and cached on its own, and the m4trix.fold.read.events metric reports them as one fold. Give each fold its own name, or define it once and share that object. One fold object read by several agents is fine |
event-schema-mismatch | warning | One event name has different payload schemas across channels, agents (listened to, emitted) and folds (equal schemas defined twice are fine). Payloads are stored encoded: an event published from outside is encoded with the first definition, an emit with the emitter's own, and a fold decodes with its handler's, so a payload stored with one may not decode with another (folds skip and report it). A warning, as a fold may read a deliberately looser view of an event. Give the event one definition and share it |
event-not-storable | warning | A durable event's schema encodes some value to something JSON cannot hold (a declaration such as S.DateFromSelf or S.instanceOf(...), a bigint, a symbol), so a payload with a value there can never be stored and publishing it fails. A warning, as a custom S.declare may describe plain JSON values and an optional field may never be set. Use a schema that encodes to JSON (S.Date instead of S.DateFromSelf), or make the event transient. S.Redacted has its own check, an error: durable-event-redacted |
Two ways to silence a warning you expect:
-
Reply events. Declare events that only answer an
emitAndAwaitwith{ reply: true }, on agents and tools alike. The waiting agent receives them without listening, so they are not reported as unconsumed (they must still be accepted by a channel the agent publishes to):TypeScript AgentFactory.run() .listensTo([TaskRequested]) .emits([Progress]) // listened to or streamed: checked as usual .emits([TaskCompleted], { reply: true }); // only answers the requester's emitAndAwait Tool.of({ name: 'approve', description: '…' }).emits([Approved], { reply: true }); -
wiring.ignoreleaves whole warning codes out of the check, at setup and innetwork.validate(), e.g. when you read channels yourself withplane.subscribe(errors cannot be ignored):TypeScript AgentNetwork.setup(setup, { wiring: { ignore: ['unconsumed-event'] } });
Run the same check yourself, e.g. to assert a clean network in a test:
const { errors, warnings } = network.validate(); // ReadonlyArray<{ code, message, agentId?, event?, channels? }>
expect(warnings).toEqual([]);Each issue has a code, a readable message and, where they apply, the agentId, event and channels concerned. network.validate() never throws; it also reports problems introduced after setup (e.g. .events() called on a channel later).
Running the Network
HTTP API (recommended)
const api = network.expose(
registerSSEStream({
channel: 'client',
triggerEvents: [requestEvent],
}),
);See IO + Adapters.
Programmatic Run
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
// Returns once every agent is subscribed: events published next are not missed.
const plane = yield* network.run();
// publish events, wait for results...
}),
),
);Concurrency model
- Serial per run, parallel across runs. Each agent has one mailbox per run, merged across all channels it subscribes to. It handles a run's events one at a time, in publish order; a slow invocation for one run never delays another run. Cap an agent's invocations across runs with
.concurrency(n). - Back-pressure per run. Mailboxes hold
capacityevents (network.run({ capacity }), default 16). When one is full, outside publishers of that run (plane.publish, inbound proxy events) wait; other runs are unaffected. Events the run's own agents emit are queued past the bound instead, so one busy agent cannot hold back the rest of the run, e.g. the reply it is waiting for withemitAndAwait; the run'smaxEventslimit bounds that growth. Transient events replace the oldest buffered transient event instead of waiting. - No self-replies. Since an agent handles a run's events one at a time, it cannot
emitAndAwaita reply that it would produce itself (in a later invocation of the same run); that wait only ends at its timeout. Replies from other agents are fine. - Ordered emits. Everything an invocation emits (including
emitAndAwaitrequests andcomplete()) is published in call order, inside the network's scope. - Released on end. When a run ends, its queued events are dropped and its mailboxes and fibers are released; the plane only remembers the outcome of recently ended runs.
Event history and stores
Every durable event a run publishes is written to the network's store before it is delivered, so an agent always finds its trigger (and everything earlier) in history. Agents and aggregators read it on demand:
.logic(async ({ triggerEvent, history, emit }) => {
const runEvents = await history.run(); // this run so far, in publish order, trigger included
const page = await history.context({ limit: 50 }); // newest 50 of the conversation, oldest first
const older = page.cursor ? await history.context({ limit: 50, before: page.cursor }) : undefined;
const prompts = await history.context({ names: ['user-prompt'] }); // only events with these names
const newer = await history.context({ after: page.head }); // what was appended after `page`
const latest = await history.latest(['user-prompt']); // { event, cursor } | undefined
})history.run()returns the run's durable events.history.context({ limit?, before? })returns{ events, cursor?, head? }: the newestlimitevents of the context across its runs (all of them withoutlimit), oldest first. Passcursorasbeforefor the page of older events; it is absent once the first event is reached.headis a cursor at the page's newest event (present whenever the page has events). Pass it asafterto read forward:history.context({ after, limit? })returns the events appended after that cursor, oldest first, the oldestlimitof them, without acursor. Keep each page'sheadand readafterit next time to see only new events; cursors stay valid while events are appended.afterandbeforetogether are an error.nameskeeps only the events with one of these names, in every kind of read. Cursors are still positions in the whole context, so filtered reads page and chain without gaps or repeats, also when events with other names are appended in between.history.latest(names)returns the context's newest event with one ofnamesand acursorat it (pass it asafterto read what followed), orundefinedwhen there is none, e.g. to start from the latest checkpoint instead of the first event (folds read their own checkpoints this way).- History includes the runtime's fold checkpoints (
m4trix:fold.checkpoint:<name>@<version>, see Compaction):isFoldCheckpointEvent(event)tells them apart. They are stored with their run but published to no channel, so streams never carry them. - A page read
aftera cursor whose history is gone (the context was deleted or expired since, and may have started over) hasreset: true: it holds the current history from its start, so anything derived from earlier pages is stale. Context folds are built on these reads and rebuild their state on a reset. - History is scoped to the run's principal (see Auth + Multi-Tenant):
(namespace, contextId), nevercontextIdalone. - Transient events are streamed and traced but never stored, so they are never in history.
- History holds the stored form. Each durable event is stored as a deep-frozen JSON copy, its payload encoded with its event's schema (
S.encode): aDate(S.Date) is an ISO string,S.NumberFromStringa string, anOption{ _tag, value }. History events are therefore untyped (StoredEvent); treat them as read-only (the in-memory store hands out the frozen objects it got, so writing to them throws; a database store returns fresh, unfrozen ones). For plain schemas (strings, numbers, structs, arrays) the stored and the decoded form are the same; for transforming ones (a field renamed withS.fromKeyis also stored under its encoded key), decode what you read:S.decodeUnknownSync(UserPrompt.payload)(event.payload), orUserPrompt.decode(event)(anEffect).UserPrompt.ischecks the decoded form, soevents.filter(Event.is)silently matches nothing for an event whose schema transforms: no error, just an empty list. Context folds decode for you. EventAggregatorcallbacks (emitWhen,mapToEmit) get the samehistory. Outside a network (agent.invoke()), history is empty. Code holding a plane can read it withplane.history({ runId, contextId, principalId? }).
Write failures and ordering
A store write that fails is retried (3 attempts in total, 10 ms then 20 ms apart). If it still fails, the event is not delivered (history never misses an event an agent saw) and the run fails with m4trix:run.failed, error.kind: 'store-error' (a client-safe NetworkStoreError; the store's own error is its cause, for tracers only). The run-end event itself is still streamed when the store is down. Writes of one run are applied one after another in publish order, whatever the store's latency; runs and contexts write concurrently.
What is stored. The runtime encodes each durable event's payload with its event's schema and stores a deep-frozen JSON copy, so the store never shares an object with what agents received:
- An agent's emit is encoded with the agent's definition of the event when it calls
emit(changing the payload object afterwards changes nothing stored). The JSON copy is whatJSON.stringifywrites: a value with atoJSONmethod is stored as what it returns (aDatein anS.Unknownfield as its ISO string, aDecimalas its own form), and undefined properties are left out. When atoJSONchanged a value, the copy is decoded with the same schema again, so nothing is stored that reads back as something else. A payload its schema cannot encode, or that JSON cannot hold (aMap,Set,Erroror class instance without atoJSON, abigint,NaN,undefinedin an array, a getter or atoJSONthat throws, or aDateunderS.DateFromSelf, whose ISO string is noDate), throwsEmitValidationError(reason: 'invalid-payload'; for JSON, the message reads… is not JSON-compatible: a Map at output[0].rowsand the issue'spathnames the value), like a payload its schema rejects: the run fails withinvalid-emit. - An event published from outside (
plane.publish, a start event, a proxy'spublish) is encoded with the schema the network knows for its name (from a channel's.events(), an agent'slistensTooremits, or a fold's handlers; the first definition of a name wins, and the wiring check warns when a name has different schemas).plane.publishtakes the payload as agents receive it (decoded, e.g. fromEvent.makeBound); request bodies and proxy publishes are wire data and are decoded first. A payload that is not a value of the schema's type is stored as given, and folds skip it if it does not decode. The JSON copy follows the same rules as for an emit. A payload JSON cannot hold is refused by the publish itself:plane.publish,publishToChannelsandpublishAndAwaitfail withNonJsonPayloadError(_tag,eventName,path,found, e.g.a Map) in their error channel, a proxy's publish rejects with it, andexpose()answers such a start event with a 400. Nothing is published (no agent sees it, nothing is stored, nothing is retried) and the run goes on. - Effect values in
S.Unknownfields are stored as theirtoJSON, Effect's debug format, not a contract: anOptionas{ "_id": "Option", "_tag": "Some", "value": … }(which a laterS.Optiondecodes,S.OptionFromSelfdoes not), anEither,ChunkorDurationlikewise, aDateTimeas its ISO string, aData.TaggedErroras its fields.Data.structandData.Classvalues have notoJSONand are refused, like anError. Give such values a schema (S.Option(…)) where readers need them typed. S.Redactedencodes to the underlying value, the secret in clear, so a durable event's schema must not hold one: setup fails withdurable-event-redacted. A transient event may carry one: it is never stored, and streams send it as"<redacted>". ARedactedvalue inside anS.Unknownfield is stored as"<redacted>"(itstoJSON).- Runtime events (run lifecycle, fold checkpoints) are copied as they are: a checkpoint's state is encoded by its fold.
- Transient events are never stored, so they are not encoded at publish time; streams encode them as they send them (see wire format).
The in-memory store
The default store keeps history in the process: lost on restart and not shared between instances. It is bounded:
import { AgentNetwork, createInMemoryNetworkStore } from '@m4trix/core/matrix';
const network = AgentNetwork.setup(setup, {
store: createInMemoryNetworkStore({
ttl: '30 minutes', // drop a context this long after its last event (reads don't extend it)
maxContexts: 1000, // beyond this, evict the least recently used context
}),
});Both are the defaults; Infinity disables either. Expiry is lazy (on read, or when another context is written), so the store runs no timers. Pass clock (anything with unsafeCurrentTimeMillis(), such as an Effect Clock) to control time in tests. Use a persistent store when history must survive restarts or be shared across instances.
Writing a store adapter
A store implements AgentNetworkStore:
interface AgentNetworkStore {
append(key: AgentNetworkStoreKey, event: StoredEvent): Promise<void>;
readRun(key: AgentNetworkStoreKey, runId: string): Promise<readonly StoredEvent[]>;
readContext(key: AgentNetworkStoreKey, options?: ReadContextOptions): Promise<EventPage>;
readLatest?(
key: AgentNetworkStoreKey,
names: readonly string[],
): Promise<{ readonly event: StoredEvent; readonly cursor: string } | undefined>;
deleteContext?(key: AgentNetworkStoreKey): Promise<void>;
}
type AgentNetworkStoreKey = { readonly namespace: string; readonly contextId: string };
type ReadContextOptions = {
readonly limit?: number;
readonly before?: string;
readonly after?: string;
readonly names?: readonly string[];
};
type EventPage = {
readonly events: readonly StoredEvent[];
readonly cursor?: string;
readonly head?: string;
readonly reset?: true;
};appendresolves once the event is durable and rejects on failure (the runtime retries). The runtime never has two appends of one run in flight, so appending in call order keeps each run in order.readRunreturns one run's events in append order.readContextreturns the newestlimitevents (before thebeforecursor) in append order, with acursorfor the older page while older events remain. Withafterit reads forward instead: the oldestlimitevents after that cursor, without acursor. Rejectaftertogether withbefore. Every non-empty page has ahead, a cursor at its newest event that works asafter.- Cursors are opaque strings of your choosing that stand for a position in the context's log (e.g. a sequence number) and must stay valid while newer events are appended. They are persisted: a fold checkpoint keeps the cursor of the read it was computed from, so keep their format stable across versions and restarts of your store.
- Positions only grow. An event appended while a cursor is out must become visible after it, never before it: a fold reads only the events after its cursor and would never fold one that lands before. A sequence handed out before a transaction commits can break this when transactions commit out of order (concurrent runs of one context append concurrently); assign positions at commit, or serialize appends per context.
reset. Anaftercursor that does not belong to the context's current history (the context was deleted, expired or evicted since, also when it has started over) must not be read as a position in the new one. Return the current history from its start, as if read withoutafter, withreset: true. Make cursors identify the history they belong to, e.g. a generation id plus a sequence number. The same for anaftercursor your store does not recognise at all (another store's, e.g. in a migrated history): aresetpage, not an error. Folds rely on this to never add new events onto state derived from a deleted history, and to stay readable when a checkpoint carries a foreign cursor (it is then ignored). A store whose pages do not follow its cursors makes a fold read fail after 100 rebases instead of looping.nameskeeps only events with one of these names, in backward and forward reads alike.cursorandheadstay positions in the whole log:cursorpoints before the page's oldest matching event and is absent once no older matching event remains;headpoints at the page's newest event. The runtime never passes an empty list.readLatest(optional) returns the newest event with one ofnamesand a cursor at it that works asafter;undefinedwhen there is none. Implement it when your backend can look it up cheaply (e.g. an index on the event name); without it,history.latestreadsreadContext({ names, limit: 1 })and uses itshead.deleteContext(optional) removes one context of one namespace, e.g. when a user clears a chat. Call it yourself throughnetwork.getStore().- Keys. Scope every read and write by both
namespaceandcontextId. Encode them unambiguously when you build one backend key (JSON.stringify([namespace, contextId]), or two columns):principal:a+b:cmust not collide withprincipal:a:b+c. - JSON in, the same JSON out. Events are JSON-compatible (
name,meta,payload, andchannels) and arrive deep-frozen: store them as JSON and return them unchanged. Do not revive values (e.g. ISO strings intoDates): folds decode payloads with the event's schema, which expects the encoded form. Key order may change (e.g. Postgresjsonb). The in-memory store keeps the frozen objects as they are. - Do not add fields in place.
appendgets a frozen object: adding a field to it (a sequence number, a timestamp) throws in strict mode. Copy it first ({ ...event, seq }), or keep such data next to the event. - Keep
channels. The runtime stores each event with the channels it was published to (StoredEvent.channels). Resumed SSE streams replay only their own channel's events with it; without it they fall back to a guess (lifecycle events, and agents' events the channel accepts). Fold checkpoints are stored withchannels: []and never replayed. - The store's owner manages its connection: core never opens or closes it.
Check an adapter against the contract suite from @m4trix/core/testing. It does not depend on a test framework: pass your runner's describe, it and expect (only toEqual is used):
// redis-network-store.spec.ts
import { describe, expect, it } from 'vitest';
import { runStoreContract } from '@m4trix/core/testing';
import { createRedisNetworkStore } from './redis-network-store';
runStoreContract({ describe, it, expect }, () => createRedisNetworkStore(redis));It covers append and read order, JSON payloads returned exactly as appended, keeping channels, run vs context reads, paging cursors, head and forward reads with after (including chained heads while events are appended), names filters, reset for stale and unknown after cursors, namespace and context isolation, readLatest and deleteContext (when implemented), concurrent appends to different contexts, and positions that only grow while runs of one context append concurrently. Every case uses fresh keys, so one database may serve all cases. names, after, head and reset are required behavior of readContext, not optional extras: context folds read through them.
For a small, complete adapter, see examples/m4trix-code/src/file-store.ts: one JSON lines file, cursors that stay valid across restarts (jsonl:<n>), fresh objects on every read, and a torn last line dropped on open. It passes the contract and keeps a terminal session across restarts, compactions included.
Event transport
The event plane hands channel events to their subscribers (SSE streams, plane.subscribe) and emitAndAwait replies to their waiters through an EventTransport. Everything per run stays in the plane: mailboxes, ordering, back-pressure, limits, ownership and run ends.
type EventTransport = {
// To every subscriber of each channel, and to every reply waiter the event matches.
publish(channels: ReadonlyArray<string>, envelope: Envelope): Effect.Effect<void>;
// Calls `handler` for each event published to `channel` from now on, until the scope closes.
// The handler's completion acknowledges the event.
subscribe(channel: string, handler: (envelope: Envelope) => Effect.Effect<void>): Effect.Effect<void, never, Scope.Scope>;
// Waits for the first later event (not the request itself, by `eventId`) that `match` accepts.
expectReply(request: Envelope, match: (reply: Envelope) => boolean): Effect.Effect<Effect.Effect<Envelope>, never, Scope.Scope>;
};The default is a fresh createInMemoryTransport() per plane, which keeps everything in the process. Pass one instance as the transport setup option to connect all planes of the network in the process (e.g. a stream on one expose() request sees events published on another plane). A transport backed by Redis Streams, NATS or Kafka would connect several instances; see Deployment. Check an implementation with runTransportContract({ describe, it, expect }, createTransport) from @m4trix/core/testing, like a store.
plane.publishAndAwait and emitAndAwait fail with PublishAndAwaitTimeoutError (_tag, timeout) when no reply arrives in time (default 30 seconds). plane.publish, publishToChannels and publishAndAwait are the way in from outside the network: they fail with ReservedEventNameError (_tag, eventName) for a reserved m4trix: name (the runtime publishes its own lifecycle events and fold checkpoints itself) and with NonJsonPayloadError (_tag, eventName, path, found) for a durable event whose payload the store cannot keep as JSON (see What is stored). Either way nothing is published and the run goes on.
Accessors
network.getChannels(); // Map<ChannelName, ConfiguredChannel>
network.getMainChannel(); // ConfiguredChannel | undefined
network.getAgentRegistrations(); // Map<string, AgentRegistration>
network.getTerminalEvents(); // ReadonlyArray<RunTerminalEvent> (from endsOn)
network.getAgentErrorPolicy(); // 'fail' | 'continue'
network.getRunLimits(); // { maxDepth, maxEvents, maxTokens, maxCostUsd }, defaults filled in
network.getStore(); // AgentNetworkStore (the `store` option or the in-memory default)
network.getFoldCacheOptions(); // the `foldCache` option
network.getTransport(); // EventTransport | undefined (the `transport` option)
network.getLogger(); // the `logger` option (or console), with every level
network.getWiringOptions(); // the `wiring` option
network.validate(); // { errors, warnings } of the wiring check