Composing a Fleet
Fleet() takes a map of names to agent references and boots them all in parallel.
import { Fleet } from "@axon/core"
const { barry, checker, zeno } = await Fleet({
barry: "../barry",
checker: "../checker",
zeno: "@axon/zeno",
})
Each value is an agent handle — the same handle Axon() hands back as axon, already
destructured. The name is yours: it is what you call the agent in this script, not
anything the agent knows about itself.
It is a convenience, not a layer
Fleet() does exactly what a Promise.all of Axon() calls does, with the names
attached and the references resolved up front. It holds no state, manages nothing, and
sits between you and nothing.
That is deliberate. The agent handle is the primitive, and it is complete — boot, request, observe, reload, shut down. Anything you want above it is yours to write: a pool, a queue, a state machine, a graph engine, a supervisor. A fleet manager in the platform would be us guessing at your architecture and getting in the way when we guessed wrong.
So use Fleet() when your cast is known at the top of the script, which is most of the
time. Reach for Axon() directly when it isn't — agents chosen at runtime, booted in a
loop, or held by something you built.
Parallel by construction
The reason to prefer Fleet() over repeated Axon() calls is in the waiting. Booting
an agent means starting a capsule and running its modules' setup; three agents booted in
series means paying that three times end to end. Fleet() boots them together, so the
script waits as long as its slowest agent and no longer.
Mixing local folders and registry references doesn't change this. A registry agent that has to be fetched the first time is simply the slowest one on that run.
The names are the interface
Read the top of a fleet script and you know what it is made of:
const { scout, architect, reviewer } = await Fleet({
scout: "../scout",
architect: "@arclabs/architect",
reviewer: "../reviewer",
})
Three agents, where each comes from, and what this script calls them — in five lines, at the top, before any work happens. That is the shape a fleet script should have: declare the cast, then write the scene.
Working the fleet
Handles are independent, so ordinary control flow does what it looks like it does.
In sequence, passing conclusions forward:
const review = await barry.request("review the changes on this branch")
const verdict = await checker.request(`is this review fair?\n\n${review.text}`)
In parallel, when the work doesn't depend on itself:
const [security, perf] = await Promise.all([
barry.request("audit for security vulnerabilities"),
checker.request("profile the hot paths"),
])
const summary = await zeno.request(
`two audits completed.\n\n` +
`Security: ${security.text}\n\nPerformance: ${perf.text}\n\n` +
`Write an executive summary with prioritised recommendations.`,
)
Each agent runs its own full loop, in its own capsule, under its own policy. Results reach the third agent because you put them in its prompt — the same way they would reach a colleague.
Streaming, when output should show up as it happens:
const { stream } = barry.stream("scout the repository")
for await (const entry of stream) {
if (entry.type === "text") process.stdout.write(entry.content)
}
The same agent twice
Nothing stops two names pointing at one folder, and sometimes that is what you want — two independent conversations with the same agent, neither contaminating the other:
const { alpha, beta } = await Fleet({
alpha: "../triage",
beta: "../triage",
})
That boots the agent twice: two capsules, two session logs, two conversations. For a request-response agent this is cheap and legitimate — it is how you fan work out across several independent runs of the same agent.
Two things make it the wrong choice sometimes. Module setup runs per instance, so an agent declaring a module that opens an external connection opens two of them. And continuous-mode agents are not cheap — one running a live cognitive loop is working whether or not you are talking to it.
When you only need sequential work with shared context, one instance called twice is simpler and remembers what it did:
const { triage } = await Fleet({ triage: "../triage" })
await triage.request("first issue")
await triage.request("second issue") // remembers the first
Fleets are local
Every member of a fleet is an agent that boots on this machine, from a folder on disk — whether you wrote it or the registry fetched it. That is what makes the handles uniform: each one has real tools, real prompts, a real session you can read.
A deployed agent is a different thing. It runs somewhere else, behind an API, and what you can do with it is bounded by what crosses the network — you cannot render its prompts, reach its tools, or read its session log. Rather than put it in the same bag and let half the handle throw, connecting to a deployed agent has its own surface. See Connecting.
Next: Resolving Agents — how a string becomes a running agent.