> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open-harness.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Subagents

> Delegate work to specialized child agents

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

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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.

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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

| Mode        | Behavior                                                                     |
| ----------- | ---------------------------------------------------------------------------- |
| `stateless` | Current behavior. Fresh child agent, no saved history.                       |
| `new`       | Create a new persistent subagent session and return its `session_id`.        |
| `resume`    | Load an existing session by ID and continue from its saved history.          |
| `fork`      | Clone an existing session into a new `session_id`, then continue from there. |

### Configuration

| Option           | Default     | Description                                                                                       |
| ---------------- | ----------- | ------------------------------------------------------------------------------------------------- |
| `messages`       | (required)  | `SessionStore` used to save and load subagent messages                                            |
| `metadata`       | in-memory   | Tracks `sessionId -> agentName` and timestamps                                                    |
| `defaultMode`    | `stateless` | Default mode when `task(..., session)` is omitted. Only `stateless` and `new` are valid defaults. |
| `sessionOptions` | —           | Extra `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="..."`:

```xml theme={"dark"}
<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:

```typescript theme={"dark"}
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.

```typescript theme={"dark"}
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:

```text theme={"dark"}
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

| Mode         | Behavior                                                        |
| ------------ | --------------------------------------------------------------- |
| `all`        | Wait for all runs to succeed. Fails fast if any run fails.      |
| `allSettled` | Wait for all runs to finish. Returns results and errors.        |
| `any`        | Wait for the first run to succeed. Only fails if all runs fail. |
| `race`       | Wait for the first run to settle (succeed or fail).             |

### Configuration

Pass `true` for sensible defaults, or an object for fine-grained control:

```typescript theme={"dark"}
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"],
    },
  },
});
```

| Option          | Default        | Description                                                                |
| --------------- | -------------- | -------------------------------------------------------------------------- |
| `maxConcurrent` | `Infinity`     | Maximum number of background runs running simultaneously                   |
| `timeout`       | —              | Auto-cancel background runs after this many milliseconds                   |
| `autoCancel`    | `true`         | Cancel all running background runs when `agent.close()` is called          |
| `tools.status`  | `true`         | Register the `agent_status` tool                                           |
| `tools.cancel`  | `true`         | Register the `agent_cancel` tool                                           |
| `tools.await`   | all four modes | Which 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.
