Skip to main content
Agents can delegate work to other agents. When you pass a subagents source, OpenHarness auto-registers a task tool that lets the parent spawn child agents by name.

Basic Setup

const explore = new Agent({
  name: "explore",
  description: "Read-only codebase exploration. Use for searching and reading files.",
  model: openai("gpt-5.4"),
  tools: { readFile, listFiles, grep },
  maxSteps: 30,
});

const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: [explore],
});
The parent model sees a task tool listing the available subagents. It can call task with an agent name and a prompt, and the child runs to completion autonomously.

Key Behaviors

  • Stateless by default — each task call creates a fresh subagent instance with no shared conversation state
  • No approval — subagents run autonomously without prompting for permission
  • Configurable nesting — by default subagents cannot themselves have subagents (maxSubagentDepth: 1). Set a higher depth to enable nested delegation.
  • Abort propagation — the parent’s abort signal is forwarded to the child
  • Concurrent execution — the model can call task multiple times in one response to run subagents in parallel
Existing behavior stays the same unless you explicitly opt into subagentSessions.

Dynamic Catalogs

subagents can be either a static Agent[] or a dynamic catalog:
import type { SubagentCatalog } from "@openharness/core";

const catalog: SubagentCatalog = {
  async list() {
    return [
      { name: "explore", description: "Read-only repo exploration" },
      { name: "researcher", description: "Long-form investigation" },
    ];
  },
  async resolve(name) {
    if (name === "explore") return explore;
    if (name === "researcher") return researcher;
    return undefined;
  },
};

const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: catalog,
});
The catalog is listed at run time, so the task tool schema and description stay in sync with whatever agents are currently available.

Nested Subagents

By default, subagents cannot delegate further. Set maxSubagentDepth to allow nesting:
const search = new Agent({
  name: "search",
  description: "Focused file search",
  model: openai("gpt-5.4"),
  tools: { grep, listFiles },
});

const explore = new Agent({
  name: "explore",
  description: "Read-only codebase exploration",
  model: openai("gpt-5.4"),
  tools: { readFile, listFiles, grep },
  subagents: [search], // explore can delegate to search
});

const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: [explore],
  maxSubagentDepth: 2, // allow explore -> search nesting
});
The depth decrements at each level: the root agent has depth 2, its child explore gets depth 1, and search gets depth 0.

Resumable Subagent Sessions

To let subagents keep their own message history across multiple task calls, enable subagentSessions. This layers Session persistence on top of the built-in task tool while keeping Agent itself stateless.
const messageStore = new Map<string, ModelMessage[]>();
const metadataStore = new Map<string, SubagentSessionMetadata>();

const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: [researcher],
  subagentSessions: {
    messages: {
      async load(id) { return messageStore.get(id); },
      async save(id, messages) { messageStore.set(id, messages); },
    },
    metadata: {
      async load(id) { return metadataStore.get(id); },
      async save(meta) { metadataStore.set(meta.sessionId, meta); },
    },
    defaultMode: "stateless",
  },
});
With subagentSessions enabled, task accepts an optional session object:
task({
  agent: "researcher",
  prompt: "Find auth entrypoints",
  session: { mode: "new" },
});

task({
  agent: "researcher",
  prompt: "What did you find earlier?",
  session: { mode: "resume", id: "sub-123" },
});

task({
  agent: "researcher",
  prompt: "Explore the alternative fix",
  session: { mode: "fork", id: "sub-123" },
});

Session Modes

ModeBehavior
statelessCurrent behavior. Fresh child agent, no saved history.
newCreate a new persistent subagent session and return its session_id.
resumeLoad an existing session by ID and continue from its saved history.
forkClone an existing session into a new session_id, then continue from there.

Configuration

OptionDefaultDescription
messages(required)SessionStore used to save and load subagent messages
metadatain-memoryTracks sessionId -> agentName and timestamps
defaultModestatelessDefault mode when task(..., session) is omitted. Only stateless and new are valid defaults.
sessionOptionsExtra Session options to apply to child sessions (compaction, retry, hooks, etc.)
When a task runs in new, resume, or fork, the tool result includes session_id="...":
<task_result session_id="sub-123">
  ...
</task_result>
Subagent sessions are single-writer. If the same sessionId is already running, the next resume or fork attempt fails instead of interleaving histories.

Live Subagent Events

To observe what subagents are doing in real time, pass an onSubagentEvent callback:
const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: [explore],
  onSubagentEvent: (path, event) => {
    if (event.type === "tool.done") {
      console.log(`[${path.join(" > ")}] ${event.toolName} completed`);
    }
  },
});
The path parameter is a string[] representing the full ancestry from outermost to innermost agent. Events from nested subagents automatically bubble up through the chain.

Background Subagents

By default, all subagent calls are synchronous. Enable subagentBackground to let the parent spawn subagents in the background, do other work, and collect results later.
const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: [explore, researcher, coder],
  subagentBackground: true,
});
When enabled:
  1. task gains an optional background parameter. When true, the subagent is spawned in the background and the tool returns immediately with a run ID.
  2. agent_await is registered for waiting on background runs using different strategies.
  3. agent_status and agent_cancel are registered for checking and cancelling runs.
Example:
task({ agent: "researcher", prompt: "Find deprecated APIs", background: true })
task({ agent: "coder", prompt: "Refactor config parser" })
agent_await({ ids: ["bg-1"], mode: "all" })
If resumable sessions are enabled, background spawns keep the same split:
  • run ID — returned as agent_id="bg-1" and used with agent_status, agent_await, and agent_cancel
  • session ID — returned as session_id="sub-123" and used with later task(..., session: { mode: "resume", id: "sub-123" })

Await Modes

ModeBehavior
allWait for all runs to succeed. Fails fast if any run fails.
allSettledWait for all runs to finish. Returns results and errors.
anyWait for the first run to succeed. Only fails if all runs fail.
raceWait for the first run to settle (succeed or fail).

Configuration

Pass true for sensible defaults, or an object for fine-grained control:
const agent = new Agent({
  name: "dev",
  model: openai("gpt-5.4"),
  tools: { ...fsTools, bash },
  subagents: [explore, researcher],
  subagentBackground: {
    maxConcurrent: 3,
    timeout: 120_000,
    autoCancel: true,
    tools: {
      status: true,
      cancel: true,
      await: ["all", "race"],
    },
  },
});
OptionDefaultDescription
maxConcurrentInfinityMaximum number of background runs running simultaneously
timeoutAuto-cancel background runs after this many milliseconds
autoCanceltrueCancel all running background runs when agent.close() is called
tools.statustrueRegister the agent_status tool
tools.canceltrueRegister the agent_cancel tool
tools.awaitall four modesWhich await modes to expose (true = all, false = disable, or an array)

Cleanup

Background subagents respect the parent’s abort signal. When autoCancel is true, calling agent.close() cancels any still-running background runs.