COMPOSE / ORCHESTRATOR

Coordinate child agents explicitly.

Orchestrator runs bounded subtasks with fresh transcripts and controlled workspaces. Your code decides what to delegate, how much can run at once, and how results are reviewed and integrated.

When to use an orchestrator

Use runAgent for one agent working through one task. Add an Orchestrator when that task can be split into independent assignments that benefit from separate context or workspaces—for example, reviewing separate modules or implementing changes that do not depend on each other.

Orchestrator does not decompose a prompt automatically and does not merge child work. The caller or parent agent writes each assignment, starts the children, and reviews their reports and artifacts.

textnah
parent plans independent subtasks
  → orchestrator starts child runs
  → each child gets its own transcript + workspace strategy
  → results return with status, usage, report, and optional diff
  → parent reviews and decides what to integrate

Create an orchestrator

Provide a model, system instructions, and a tool factory. The factory receives each child’s workspace root, so tools can be confined to that child. Git worktree isolation is useful when children may edit files.

tsnah
import {
  createCodingTools,
  createGitWorktreeIsolation,
  Orchestrator,
  orchestratorPrompt,
} from "not-another-harness";
import { createNodeEnvironment } from "not-another-harness/node";

const orchestrator = new Orchestrator({
  model,
  system: [
    "You are a coding agent.",
    orchestratorPrompt({ concurrency: 3 }),
  ].join("\n\n"),
  isolation: createGitWorktreeIsolation({ cwd }),
  createTools: (root) =>
    createCodingTools(createNodeEnvironment(root), {
      withBash: true,
      approveToolCall,
    }),
  maxConcurrency: 3,
  maxSteps: 20,
  taskTimeoutMs: 10 * 60_000,
  onUsage: (usage) => session.rollSpend(usage),
});

Run one task or a plan

Call run for a single subtask. Use runAll for a plan: it schedules tasks in waves up to the concurrency limit and returns results in submission order. Give every task a self-contained assignment with scope and acceptance criteria because children do not receive the parent’s transcript.

tsnah
const results = await orchestrator.runAll([
  {
    title: "Review the parser",
    task: "Inspect src/parser.ts for correctness bugs. Do not edit. Report each issue with a location and reproduction.",
    createTools: (root) => createCodingTools(createNodeEnvironment(root), {
      withBash: false,
    }),
  },
  {
    title: "Review the API handlers",
    task: "Inspect src/api for authorization gaps. Do not edit. Report concrete findings.",
  },
]);

for (const result of results) {
  console.log(result.title, result.status, result.usage);
  if (result.artifact) {
    console.log(result.artifact.changedPaths);
    reviewDiff(result.artifact.diff);
  }
}

Each SubtaskResult includes the status, step and tool-call counts, usage, elapsed time, final text, and—when isolation collects one—changed paths and a diff. A child failure is returned as a result with status: "error", so other tasks can still finish.

Concurrency, budgets, and cancellation

maxConcurrency defaults to three. runAll accepts plans larger than the limit and runs them in waves. A direct run submitted when all slots are occupied rejects with OrchestratorBusyError rather than queueing unexpectedly.

tsnah
const controller = new AbortController();
const orchestrator = new Orchestrator({
  model, system, createTools,
  maxConcurrency: 2,
  maxSteps: 18,
  maxSpendUsd: 0.40,
  rates: { input: 3, output: 15 },
  signal: controller.signal,
  onEvent: (event) => {
    if (event.type === "subtask-start") showStarted(event.title);
    if (event.type === "subtask-event") forwardChildEvent(event);
    if (event.type === "subtask-finish") showFinished(event.result);
  },
});

controller.abort(); // abort children still in flight

A task can override its own maxSteps, maxSpendUsd, signal, isolation strategy, or tool factory. Use onUsage to fold completed child usage into the parent application’s accounting.

Choose a workspace strategy

createGitWorktreeIsolation({ cwd }) gives each child a separate checkout. By default it snapshots the parent working tree at child start, including uncommitted changes. That snapshot is frozen: later parent edits are not visible to the child. Siblings cannot write into one another’s worktrees.

Once a child finishes, its worktree is cleaned up by default and its diff is returned for review. If the diff is too large to include, the workspace is retained and its path is included in the result. Set retain: true to keep worktrees for inspection. sharedWorkspaceIsolation() is available when sharing files is intentional, but concurrent writers can conflict.

Observe child runs

Use onEvent to observe starts, each nested harness event, and completion. The child event stream is drained by the orchestrator even if your callback does not render it, so progress handling is optional. totalUsage reports cumulative usage for completed children.

tsnah
const result = await orchestrator.run({
  title: "Find the migration path",
  task: "Trace how schema migrations are loaded. Return the relevant files and a concise explanation.",
});

console.log(result.status, result.text);
console.log(orchestrator.active);      // children currently running
console.log(orchestrator.totalUsage); // completed-child usage

Forward subtask-event payloads to the same UI renderer used for a normal runAgent, while using the outer subtask ID and title to group child activity.

Use orchestration from workflows

For repeatable sequences, combine the orchestrator with a workflow. runWorkflow supplies delegate and delegateAll in each step’s context, so deterministic steps stay code and only judgement-heavy steps use child agents. A suspended workflow can be found with pendingWorkflows() and continued with resumeWorkflow(runId, answer).

tsnah
const outcome = await orchestrator.runWorkflow(reviewWorkflow, {
  inputData: { base: "main" },
  onEvent: (event) => renderWorkflowProgress(event),
});
if (outcome.status === "success") {
  useWorkflowResult(outcome.result);
} else if (outcome.status === "suspended") {
  showQuestion(outcome.suspendPayload);
}

See Workflows for defining and composing those sequences, or Delegation for the parent/child integration patterns used by the CLI.