Skip to main content

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 timefanOut
A list, and one agent per itemmapAgents
A task to hand to an isolated sub-agent that reports backtaskNode
A graph you want to reuse as one stepsubgraph
A question worth several opinionscouncil

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​