crouter
SDKRecipes

Fan-out pipeline

When independent parts of one job need separate agent work before one synthesis, read this because an orchestrator can spawn children, wait for reports, and publish a final result while the SDK streams progress.

Fan-out pipeline

Use this recipe when one result needs independent investigation first. Run npx tsx examples/guides/fan-out-pipeline.ts /path/to/repo; it asks an orchestrator to inspect a package through two children, prints live text and pushed reports, then prints the final reports retained on the node.

import Crouter from '@north-light/crouter-sdk';
import { resolve } from 'node:path';

const client = new Crouter();
const cwd = resolve(process.argv[2] ?? process.cwd());

const stream = client.nodes.stream({
  name: 'research pipeline',
  cwd,
  root: true,
  root_lifecycle: 'terminal',
  mode: 'orchestrator',
  deadline: '10m',
  prompt: `Research the package in this directory. Spawn two focused children: one should inspect package.json and one should inspect the README. Wait for their reports, reconcile them, then push a final report with the package name, purpose, and one risk or unknown.`,
});

const node = await stream.node;
console.log(`orchestrator node: ${node.node_id}`);

for await (const event of stream) {
  if (event.type === 'node.output_text.delta') process.stdout.write(event.delta);
  if (event.type === 'node.report.pushed') console.log(`\nreport: ${event.report.body}`);
}

const outcome = await stream.finalOutcome();
const reports = await client.nodes.reports.list(node.node_id, { limit: 10 });
console.log(`\nfinal outcome: ${outcome.kind}`);
for (const report of reports) console.log(`[${report.tier}] ${report.body}`);

if (outcome.kind !== 'result') process.exitCode = 1;

nodes.stream() creates the orchestrator and observes it. It does not run the orchestration in your process: the node owns its children, waits for their pushed reports, and produces the synthesis. The stream gives your application live output and reports; nodes.reports.list() gives it the durable report view after settlement.

Keep orchestration inside the canvas when children need the canvas's report delivery, durable waits, and a parent that can be inspected or resumed. Keep it in your application when tasks are simple independent calls and the application already owns their scheduling and aggregation. Do not use an orchestrator merely because a task is long; it earns the extra structure only when child work can run independently.

A local run created j6amiqxs-mud4drnk-330da196; after its two children reported, waitForOutcome() returned:

{ "kind": "result", "reason": "finalized" }

The same run's nodes.stream() observer ended during a dormant gap before it could print this outcome. A separate daemon fix is correcting that observer behavior; the example remains the intended streamed shape, but its streamed path has not yet been observed end to end.

See nodes and the canvas for reports and durable identities, and profiles, kinds, and modes for the base-versus-orchestrator decision.