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.
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
| 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="...":
<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:
task gains an optional background parameter. When true, the subagent is spawned in the background and the tool returns immediately with a run ID.
agent_await is registered for waiting on background runs using different strategies.
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
| 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:
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.