Concepts
Channels
Named streams of events that agents subscribe to and publish into: what they carry, who receives what, and how events leave the network.
A channel is a named stream of events inside a network: agents subscribe to channels to receive events and publish to channels to send them.
Mental model
Each lane is a channel, main first. An agent sits where the channel it subscribes to meets the channel it publishes to, and a proxy is a port on the network's border where a channel's events leave it (here sse on client).
Wiring
This setup adds a work and a client channel next to main and wires three agents to them:
export const const network: AgentNetwork<never>network = class AgentNetwork<TDeps extends DependencyLayerDef<string, unknown, S.Schema.Any> = never>AgentNetwork.AgentNetwork<TDeps extends DependencyLayerDef<string, unknown, S.Schema.Any> = never>.setup(callback: (ctx: AgentNetworkSetupContext) => void, options?: AgentNetworkSetupOptions): AgentNetwork<never>setup(
({ mainChannel: ConfiguredChannelmainChannel, createChannel: (name: string) => ConfiguredChannelcreateChannel, proxy: {
sse(): ProxyDef;
custom(kind: string, config?: unknown, direction?: ProxyDirection): ProxyDef;
}
proxy, registerAgent: (agent: AnyAgent) => AgentBindingregisterAgent, endsOn: <E extends EventDef>(event: E, when?: (envelope: EventEnvelope<E>) => boolean) => voidDeclares 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. Call it once per terminal event.endsOn }) => {
const const work: ConfiguredChannelwork = createChannel: (name: string) => ConfiguredChannelcreateChannel('work').ConfiguredChannel.events(events: ReadonlyArray<EventDef>): ConfiguredChannelDeclares the events this channel carries. Publishing any other event to it is refused: an
agent's emit throws `EmitValidationError` (reason `channel`), an external publish resolves
`false`. Runtime `m4trix:*` events always pass. Without a declaration a channel carries
every event.events([const Task: AgentNetworkEventDef<"task", S.Struct<{
topic: typeof S.String;
}>>
Task]);
const const client: ConfiguredChannelclient = createChannel: (name: string) => ConfiguredChannelcreateChannel('client').ConfiguredChannel.events(events: ReadonlyArray<EventDef>): ConfiguredChannelDeclares the events this channel carries. Publishing any other event to it is refused: an
agent's emit throws `EmitValidationError` (reason `channel`), an external publish resolves
`false`. Runtime `m4trix:*` events always pass. Without a declaration a channel carries
every event.events([const Answer: AgentNetworkEventDef<"answer", S.Struct<{
text: typeof S.String;
}>>
Answer]).ConfiguredChannel.proxy(...proxies: ProxyDef[]): ConfiguredChannelproxy(proxy: {
sse(): ProxyDef;
custom(kind: string, config?: unknown, direction?: ProxyDirection): ProxyDef;
}
proxy.function sse(): ProxyDefsse());
registerAgent: (agent: AnyAgent) => AgentBindingregisterAgent(const router: Agent<unknown, {
name: "user-message";
meta: EventMeta;
payload: {
readonly text: string;
};
}, {
name: "task";
payload: {
readonly topic: string;
};
}, never, never, never>
router).function subscribe(channel: ConfiguredChannel): AgentBindingsubscribe(mainChannel: ConfiguredChannelmainChannel).function publishTo(channel: ConfiguredChannel): AgentBindingpublishTo(const work: ConfiguredChannelwork);
registerAgent: (agent: AnyAgent) => AgentBindingregisterAgent(const worker: Agent<unknown, {
name: "task";
meta: EventMeta;
payload: {
readonly topic: string;
};
}, {
name: "answer";
payload: {
readonly text: string;
};
}, never, never, never>
worker).function subscribe(channel: ConfiguredChannel): AgentBindingsubscribe(const work: ConfiguredChannelwork).function publishTo(channel: ConfiguredChannel): AgentBindingpublishTo(const client: ConfiguredChannelclient);
registerAgent: (agent: AnyAgent) => AgentBindingregisterAgent(const auditLog: Agent<unknown, never, never, never, never, never>auditLog).function subscribe(channel: ConfiguredChannel): AgentBindingsubscribe(mainChannel: ConfiguredChannelmainChannel).function subscribe(channel: ConfiguredChannel): AgentBindingsubscribe(const work: ConfiguredChannelwork);
endsOn: <AgentNetworkEventDef<"answer", S.Struct<{
text: typeof S.String;
}>>>(event: AgentNetworkEventDef<"answer", S.Struct<{
text: typeof S.String;
}>>, when?: ((envelope: {
...;
}) => boolean) | undefined) => void
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. Call it once per terminal event.endsOn(const Answer: AgentNetworkEventDef<"answer", S.Struct<{
text: typeof S.String;
}>>
Answer);
},
);The events and agents it wires live in the same file; the player below shows them next to this code.
How a run flows
One run of this network, recorded from the real runtime, starting with a user-message published on main:
1. user-message is published on main from outside the network; router picks it up, and auditLog, which has no listensTo and so takes every event on main, gets a copy too.
Step 1 of 4: user-message is published on main from outside the network; router picks it up, and auditLog, which has no listensTo and so takes every event on main, gets a copy too.
Payload user-message
{
"text": "Why did checkout get slower?"
}export const router = AgentFactory.run()
.listensTo([UserMessage])
.emits([Task])
.logic(async ({ triggerEvent, emit }) => {
emit({ name: 'task', payload: { topic: triggerEvent.payload.text } });
})
.produce({});
export const worker = AgentFactory.run()
.listensTo([Task])
.emits([Answer])
.logic(async ({ triggerEvent, emit }) => {
emit({ name: 'answer', payload: { text: `Looked into: ${triggerEvent.payload.topic}` } });
})
.produce({});
// No listensTo: it receives every event on the channels it subscribes to.
export const auditLog = AgentFactory.run()
.logic(async ({ triggerEvent }) => {
console.log('[audit]', triggerEvent);
})
.produce({});
export const network = AgentNetwork.setup(
({ mainChannel, createChannel, proxy, registerAgent, endsOn }) => {
const work = createChannel('work').events([Task]);
const client = createChannel('client').events([Answer]).proxy(proxy.sse());
registerAgent(router).subscribe(mainChannel).publishTo(work);
registerAgent(worker).subscribe(work).publishTo(client);
registerAgent(auditLog).subscribe(mainChannel).subscribe(work);
endsOn(Answer);
},
);
/** What the docs recorder publishes to start a run of this snippet. */
export const scenario = {
start: UserMessage.make({ text: 'Why did checkout get slower?' }),
};user-messageis published onmainfrom outside the network;routerpicks it up, andauditLog, which has nolistensToand so takes every event onmain, gets a copy too.routerreceives it and emitstaskonwork, a channel that only carriestask;workertakes it from there, andauditLoggets a copy too because it also subscribes towork.workerreceives it and emitsansweronclient, a channel that only carriesanswer; no agent subscribes toclient, soanswerleaves the network through itssseproxy and, as a terminal event, ends the run.- The runtime then publishes
m4trix:run.completedon every channel (main,workandclient) to close the run; runtime events never invoke agents, so not evenauditLogreceives it.
Rules and guarantees
Defining channels
- Names are kebab-case: lowercase letters and digits in hyphen-separated segments, such as
workorclient-output. Any other name makesAgentNetwork.setup()throw. Names are typed as the brandedChannelName. mainalways exists. The network creates it, andmainChannelin the setup context is that channel, not a function: pass it straight tosubscribeorpublishTo.expose()publishes start events to it.- One channel per name.
createChannel(name)returns the existing channel when the name is taken, socreateChannel('main')ismainChannel. - Agents choose their channels. Each
subscribe(channel)orpublishTo(channel)call on the binding fromregisterAgentadds one channel; an agent can have several of each.
What a channel carries
- Everything, unless declared.
.events([...])limits a channel to those events. Calling it again replaces the list, and an empty list carries everything again. See.events(). - An agent's emit goes where it is accepted. The event is published to each of the agent's
publishTochannels that accepts it and skips the others. If the agent haspublishTochannels and none of them accepts it,emitthrowsEmitValidationError(reason: 'channel'): the invocation fails even if the agent catches the error, and the network'sonAgentErrorpolicy applies (run failure kindinvalid-emit). An agent without anypublishTochannel publishes nowhere instead (see Common mistakes). - Outside publishes are dropped, not thrown.
plane.publish, or a proxy'spublish/publishInbound, of an event the channel does not accept resolvesfalse, delivers nothing and logs a warning through the network'slogger. The run keeps going. - Start events must fit
main.expose()throws at setup whenmaindeclares events that exclude one of itstriggerEvents.
Delivery
- Broadcast, not a work queue. Every agent subscribed to the channel whose
listensTonames the event gets it, and so does every reader of the channel (plane.subscribe, SSE streams). - Catch-all agents. An agent without
.listensTo(), likeauditLog, receives every event on the channels it subscribes to, except the runtime'sm4trix:*events. - Once per agent. An event published to several channels at once reaches each agent once, even when the agent subscribes to more than one of them.
- One at a time, in order, per run. Each agent has one inbox per run, merged across its channels, and handles that run's events one at a time in publish order. Different agents handle the same event concurrently, runs proceed in parallel, and nothing orders one agent's work against another's. See Concurrency model.
- Emits keep their order. Everything one invocation emits is published in the order it called
emit.
Runtime events
- Every channel gets them. When a run ends, the runtime publishes
m4trix:run.completed,m4trix:run.failedorm4trix:run.cancelledto every channel of the network (andm4trix:agent.errorunderonAgentError: 'continue'). They pass any.events()declaration and never trigger agents. Them4trix:prefix is reserved:AgentNetworkEvent.of()rejects it. See Run Lifecycle.
Proxies
- A proxy is a declaration.
.proxy(...)marks a channel whose events cross the network's border;network.expose(...)creates the stream that reads it. A channel can declare several proxies. proxy.sse()is built in.registerSSEStream()streams one channel: the one passed aschannel(it must declaresse, orexpose()throws), else the only channel with ansseproxy, elseclient, elsemain. With severalssechannels and nochannel,expose()throws. See registerSSEStream() options.- Other kinds are your own. Declare one with
proxy.custom(kind, config?, direction?)ordefineProxyKind(kind)and expose it withregisterCustomProxyor the kind's.register(). The direction defaults to'outbound'. Kafka and Socket.IO bridges are not built in. See Custom Proxies.
Setup-time check
- Contradictions throw. When the setup callback returns,
AgentNetwork.setup()compares what each agent (and its tools) emits and listens to with what its channels accept. It throws aNetworkWiringErrorlisting everyemit-not-accepted(nopublishTochannel accepts an event from.emits()) andlisten-not-accepted(no subscribed channel accepts an event from.listensTo()). - Smells are logged.
unconsumed-event,agent-publishes-nowhere,agent-not-subscribed,agent-unreachableandterminal-event-not-emittedgo to the network'slogger, and the network runs. - What counts as the border. The check treats
mainand channels with an'inbound'or'bidirectional'proxy as fed from outside, and channels with a proxy that is not'inbound', or namedclient, as streamed out. network.validate()runs the same check without throwing. See Wiring check.
Common mistakes
- Calling
mainChannel('main').mainChannelis already themainchannel: writesubscribe(mainChannel). - Wiring an event to a channel that does not declare it. With
workdeclared as.events([Task]), an agent that emitsanswerand publishes only toworkmakes setup throwemit-not-accepted; one that listens toanswerand subscribes only toworkgetslisten-not-accepted. Add the event to.events([...])or wire the agent to a channel that carries it. - Forgetting
publishTo.registerAgent(worker).subscribe(work)only logsagent-publishes-nowhere. The run still ends onanswer(endsOn), butanswerlands on no channel, so the stream onclientonly receivesm4trix:run.completed. - Reading a quiet check as a correct one. Subscribe
workertomainChannelinstead ofworkand it never runs, yet setup reports nothing:auditLoglistens to everything onwork, sotaskcounts as consumed, andmaincounts as fed from outside. WithoutauditLog, setup warnsunconsumed-event.
What next
| Goal | Page |
|---|---|
| Define the events channels carry | Events |
| Declare what agents listen to and emit | Agents |
| Wire agents and channels into a network | Networks |
| Fan out, chain agents, wait for replies | Patterns |
| Stream a channel to the browser | Streaming |
End runs and handle m4trix:* events | Run Lifecycle |
Look up .events() and .proxy() | Channel API |
| Read every wiring-check code | Wiring check |