Multi-agent
Five building blocks cover most multi-agent designs. Pick the one that matches the shape of your problem.
| You have... | Use |
|---|---|
| A fixed set of steps that can run at the same time | fanOut |
| A list, and one agent per item | mapAgents |
| A task to hand to an isolated sub-agent that reports back | taskNode |
| A graph you want to reuse as one step | subgraph |
| A question worth several opinions | council |
Run steps in parallel
fanOut(from, [branches], joinAt) runs the branches at the same time, each from the same state,
then continues at joinAt once all of them are done. Give each agent its own outputChannel so
their results don't overwrite each other.
import { createGraph, finalAnswer, model } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "pros-and-cons" })
.channel("decision", { type: "string", default: "" })
.channel("summary", { type: "string", default: "" })
.node("start", async () => ({}))
// Two agents run in parallel on the same state. Each writes its own channel.
.agentNode("pros", {
model: model.fast,
prompt: { system: "List the strongest argument FOR the decision." },
outputChannel: "pros"
})
.agentNode("cons", {
model: model.fast,
prompt: { system: "List the strongest argument AGAINST the decision." },
outputChannel: "cons"
})
.node("merge", async (_input, state) => ({
summary: `For: ${finalAnswer(state.channels.pros)} / Against: ${finalAnswer(state.channels.cons)}`
}))
// start fans out to both agents; the run continues at merge once both are done.
.fanOut("start", ["pros", "cons"], "merge")
.compile();
const out = await app.run({ decision: "Move the team to a four-day week" });
console.log(out.channels.summary);
Branch updates are applied in the order you list the branches, whichever finishes first, so the result is the same on every run.
One agent per item
mapAgents runs one sub-agent per item of a list channel, in parallel, and collects the results
in input order.
import { createGraph, finalAnswer, model, type AgentResult } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "review-files" })
.channel("files", { type: "json", default: [] as string[] })
// One sub-agent per item of `files`, run in parallel; results come back in input order.
.mapAgents("review", {
overChannel: "files",
subAgent: { model: model.fast, prompt: { system: "Review this file. One sentence." } },
joinAt: "reviews"
})
.compile();
const out = await app.run({ files: ["auth.ts", "billing.ts", "search.ts"] });
for (const review of out.channels.reviews as AgentResult[]) console.log(finalAnswer(review));
With suspendForApproval: true, a sub-agent that wants a gated tool suspends the whole run, as a
single agent would.
Delegate to a sub-agent
taskNode runs a sub-agent in its own child graph. It sees only the objective channel, and
only its report comes back: the parent never sees its intermediate work, and it never sees the
parent's other channels.
import { createGraph, finalAnswer, model } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "research" })
.channel("objective", { type: "string", default: "" })
// A sub-agent in its own child graph: it sees only `objective`,
// and only its report comes back (in `report`).
.taskNode("dig", {
subAgent: { model: model.balanced, prompt: { system: "Research the objective. Report in 3 bullets." } }
})
.compile();
const out = await app.run({ objective: "Compare durable execution engines" });
console.log(finalAnswer(out.channels.report));
Rename the channels with objectiveChannel and reportChannel. By default the report is kept
short (compress: true); set compress: false for the full answer.
Reuse a graph as a node
subgraph embeds another builder as one node. The mappings list which channels cross the
boundary, written destination: source.
import { createGraph } from "@ailu-ai/graph-sdk";
// A child graph with its own channels...
const normalize = createGraph({ name: "normalize" })
.channel("text", { type: "string", default: "" })
.channel("clean", { type: "string", default: "" })
.node("trim", async (_input, state) => ({ clean: state.channels.text.trim().toLowerCase() }));
// ...used as one node of a parent graph. Mappings say which channels cross the boundary,
// written destination: source.
const app = createGraph({ name: "intake" })
.channel("raw", { type: "string", default: "" })
.channel("normalized", { type: "string", default: "" })
.subgraph("normalize", normalize, {
inputMapping: { text: "raw" }, // child `text` <- parent `raw`
outputMapping: { normalized: "clean" } // parent `normalized` <- child `clean`
})
.compile();
console.log((await app.run({ raw: " HELLO World " })).channels.normalized); // "hello world"
The child shares the parent's run: a human gate inside it suspends the parent, and resuming the parent continues the child.
Ask a council
council() builds a complete graph: members answer in parallel, reviewers rank the answers
without knowing who wrote them, and a chair writes the final answer from the ranking. It runs
with runCatalogGraph.
import { council, finalAnswer, model, runCatalogGraph, type AgentResult } from "@ailu-ai/graph-sdk";
const seat = (system: string) => ({ model: model.balanced, prompt: { system } });
const definition = council({
// Each member answers on its own...
members: [seat("Answer as a lawyer."), seat("Answer as an engineer."), seat("Answer as a CFO.")],
// ...reviewers rank the answers without knowing who wrote them (labels A, B, C)...
reviewers: [seat("Rank the answers A, B, C from best to worst.")],
// ...and a chair writes the final answer from the ranking.
chair: seat("Write the final answer from the best-ranked answers."),
humanGate: false // true: stop for a human before the chair decides
});
const outcome = await runCatalogGraph(definition, {
initialData: { query: "Should we open-source our SDK?" }
});
console.log(finalAnswer(outcome.state.channels.answer as AgentResult));
Each seat sees only what it needs: members see the question; reviewers see the question and the
anonymized answers; the chair sees the answers and their ranking. The fieldKey channel maps
each label back to its member for your audit trail.
Next
- An agent that plans and delegates on its own: Deep agents.
- A complete example: Parallel agents.