# Sub-Agents

## Overview

Sub-agents are specialist agents embedded inside a parent agent. Tool handlers on the parent invoke them explicitly via `requestSubAgent` — a sibling to the existing `requestInteraction` API. Each sub-agent runs its own full tool loop to completion and returns its result to the calling handler as a resolved promise.

From the LLM's perspective, it called a normal tool and received a result — it has no visibility into the sub-agent that produced it. Sub-agents are an implementation detail of a tool call, not a visible actor in the conversation.

The goal is to improve quality and reduce hallucinations by routing bounded, well-defined tasks — data extraction, validation, enrichment, judge passes — to purpose-built agents with focused prompts and tight tool sets, without burdening the parent agent's context window.

---

## Declaring sub-agents

Sub-agents are declared on the parent `AgentConfig` using the `subAgents` field, which takes an array of `AgentConfig` objects. The `name` field is used to look up the sub-agent at call time.

```ts
import { defineAgent } from '@genesislcap/ai-assistant';

const tradesAgent = defineAgent({
  name: 'Trades Assistant',
  description: 'Books, searches, and manages trades.',
  systemPrompt: '...',
  toolDefinitions: [...tradeTools],
  toolHandlers: { ...tradeHandlers },
  subAgents: [
    {
      name: 'trade_file_extractor',
      description: 'Extracts and normalises trade data from uploaded files.',
      systemPrompt: `You are a data extraction specialist. Your only job is to parse the
        provided file and return structured trade rows.
        Do not book trades. Do not ask questions. Just extract.`,
      toolDefinitions: [returnExtractedTradesDefinition],
      toolHandlers: { return_extracted_trades: returnExtractedTradesHandler },
    },
  ],
});
```

### `defineAgent` helper

Use `defineAgent` when you need the config's `name` type preserved as a string literal — required for typed `requestSubAgent` calls and `ChatToolHandlers` generic wiring (see [TypeScript types](#typescript-types)):

```ts
import { defineAgent } from '@genesislcap/ai-assistant';

export const tradeFileExtractorAgent = defineAgent({
  name: 'trade_file_extractor',
  // ...
});
```

Without `defineAgent`, TypeScript widens `name` to `string`, which loses the type safety on `requestSubAgent`'s name parameter.

---

## Invoking a sub-agent

Tool handlers receive `requestSubAgent` on their context object alongside `requestInteraction`:

```ts
const processTradeFile = async (args, context) => {
  const { file_name } = args as { file_name: string };

  const outcome = await context.requestSubAgent('trade_file_extractor', {
    task: `Extract all trade rows from the attached file named "${file_name}".`,
  });

  // `outcome` is a discriminated union — either the structured value from
  // completeSubAgent, or a typed failure reason. Decide how to recover; here we
  // hand the issue back to this agent.
  if (!outcome.ok) {
    return { error: `Extraction didn't complete (${outcome.reason}).` };
  }
  return outcome.result;
};
```

`requestSubAgent` is only present on the context when `subAgents` is declared on the parent agent config. Guard for its existence in reusable handlers:

```ts
if (!context.requestSubAgent) {
  return { error: 'Sub-agent support is not available in this context.' };
}
```

### `SubAgentRequestOptions`

```ts
interface SubAgentRequestOptions {
  /** Plain-language task description for this specific invocation. */
  task?: string;

  /**
   * Caps how many parent history messages the sub-agent receives.
   * Messages within the cap are passed verbatim; older messages have
   * their tool call args and results masked. Overrides any config-level
   * historyCap for this call.
   */
  historyCap?: number;

  /**
   * Extra dynamic data for this invocation. Injected as a primer message
   * immediately before the task, so the sub-agent sees it naturally as
   * part of its context.
   */
  context?: Record<string, unknown>;
}
```

---

## Returning structured results: `completeSubAgent`

Sub-agents run with **tool use forced**, so a sub-agent can only end a turn by
calling a tool — it cannot return a free-text answer. To finish, define a
completion tool on the sub-agent and call `completeSubAgent` from its handler:

```ts
const toolHandlers: ChatToolHandlers = {
  return_extracted_trades: async (args, context) => {
    const { rows } = args as ExtractedTrades;
    context.completeSubAgent?.({ rows });
    return `Extracted ${rows.length} trade row(s).`; // still appended to trace
  },
};
```

When `completeSubAgent` is called, the sub-agent's tool loop exits and `requestSubAgent` resolves with `{ ok: true, result }`. If the loop ends without `completeSubAgent` ever being called (it hit the iteration cap, or the provider repeatedly returned malformed/empty/unknown tool calls), `requestSubAgent` resolves with `{ ok: false, reason }` instead — there is no plain-text fallback.

> Every sub-agent must declare a completion tool and be prompted to call it when done. A sub-agent with no completion tool can never finish and will always resolve with `reason: 'max_iterations'`.

The return type of `requestSubAgent<T>` is:

```ts
Promise<{ ok: true; result: T } | { ok: false; reason: SubAgentFailureReason }>;

type SubAgentFailureReason =
  | 'max_iterations'
  | 'malformed_tool_call'
  | 'empty_response'
  | 'unknown_tool_limit'
  | 'timeout'
  | 'response_truncated'
  | 'refusal'
  | 'budget_exhausted';
```

Branch on `ok` in the calling handler — but **the right recovery depends on which
reason you got**, and the two groups are opposites:

| Reason | Recovery |
| --- | --- |
| `max_iterations`, `malformed_tool_call`, `empty_response`, `unknown_tool_limit`, `timeout` | **Retry or ask the user.** These are transient or prompt-design problems; a second attempt, a smaller task, or a clarifying question often succeeds. |
| `response_truncated` | **Do not retry as-is** — re-issuing the same request hits the same output-token cap. Retry only with a smaller step, or raise the provider's `maxTokens`. |
| `refusal` | **Terminal.** A safety-classifier decline is deterministic; the same request will be declined again. Return a terse error. |
| `budget_exhausted` | **Terminal, and already handled for you.** The AI-spend budget is gone and the proxy is refusing every request. Retrying is another `402`, and "ask the user" is asking someone who cannot fix it. Return a terse error and let the turn end — the parent driver ends the turn as soon as this outcome comes back, so your handler will not be called again this turn. |

```ts
const outcome = await context.requestSubAgent<ExtractedTrades>('trade_file_extractor', {
  task: `Extract all trades from "${file_name}".`,
});

if (!outcome.ok) {
  // Terminal reasons: report and stop. Retrying reproduces them, and for
  // budget_exhausted the parent turn is ending anyway.
  if (outcome.reason === 'budget_exhausted' || outcome.reason === 'refusal') {
    return { error: `Extraction could not run (${outcome.reason}).` };
  }
  // Everything else is worth another attempt or a question to the user.
  return { error: `Extraction didn't complete (${outcome.reason}).` };
}
const { rows } = outcome.result; // fully typed
```

Pass a type parameter so `outcome.result` is typed; without one it defaults to `never`.

> Migrating from the old `T | string` return? See [`migration-GENC-1312.md`](./migration-GENC-1312.md).

---

## TypeScript types

`ChatToolHandlers` accepts an optional type parameter that narrows the `name` argument of `requestSubAgent` to the declared sub-agent names. Pass the agent config type via `typeof`:

```ts
import { defineAgent } from '@genesislcap/ai-assistant';
import type { ChatToolHandlers } from '@genesislcap/foundation-ai';

const tradeFileExtractorAgent = defineAgent({
  name: 'trade_file_extractor',
  // ...
});

// requestSubAgent name param is now typed as 'trade_file_extractor'
const handlers: ChatToolHandlers<typeof tradeFileExtractorAgent> = {
  process_trade_file: async (args, context) => {
    // context.requestSubAgent('trade_file_extractor', ...) — type-checked
    // context.requestSubAgent('typo', ...) — TypeScript error
  },
};
```

For multiple sub-agents use a union:

```ts
type Handlers = ChatToolHandlers<typeof agentA | typeof agentB>;
```

The default (`{ name: string }`) leaves the name as `string` — no breaking change for existing handlers.

---

## History passed to the sub-agent

By default the sub-agent receives a snapshot of the full parent conversation history as context. The in-flight assistant message (the one containing the tool call that triggered the sub-agent) is excluded — it has unresolved tool calls that would confuse the sub-agent.

Use `historyCap` to limit how much history the sub-agent sees:

```ts
subAgents: [
  {
    name: 'trade_file_extractor',
    historyCap: 4,  // pass only the last 4 messages verbatim; mask older ones
    // ...
  },
],
```

Or override per call:

```ts
await context.requestSubAgent('trade_file_extractor', {
  historyCap: 2,
  task: '...',
});
```

Messages older than the cap have their tool call args and results replaced with a placeholder — the same masking used by the orchestrating driver for cross-agent history.

---

## Parallel execution

No special work is required. The parent's tool loop runs all tool calls from a single LLM turn in parallel. If two tool handlers both call `requestSubAgent`, their executions start concurrently and the parent resumes after the last one resolves.

---

## Live activity indicator

While a sub-agent is running, `FoundationAiAssistant` exposes two reactive properties you can bind in the host application:

| Property | Type | Description |
|---|---|---|
| `liveSubAgentName` | `string \| null` | Name of the currently-running sub-agent, or `null` when idle. |
| `liveSubAgentTrace` | `ChatMessage[]` | Growing history from the sub-agent's active tool loop. Cleared on stop. |

These update in real time as the sub-agent produces messages, letting the host app show a live indicator (e.g. "Consulting trade extractor...") or stream the sub-agent's intermediate steps. The built-in template renders a `live-sub-agent-trace` panel while a sub-agent is active and hides it when it stops.

---

## Debug trace

When tool calls are visible (`showToolCalls: true`), each tool call that involved a sub-agent displays a collapsed trace panel. The trace shows the sub-agent's full message history — assistant turns, tool calls, and tool results — nested inside the parent tool call card, labelled with the sub-agent name.

The trace is stored on `ChatToolCall.subAgentTrace` (UI-only — never sent to the AI provider, same as `foldPath`).

---

## Complete example

The showcase app contains a working end-to-end example:

- **Sub-agent config:** `packages/showcase/client-app/client/src/routes/ai/trades-assistant/sub-agents/trade-file-extractor.ts`
- **Parent tool handler:** `process_trade_file` in `packages/showcase/client-app/client/src/routes/ai/trades-assistant/tools/trades-tools.ts`
- **Parent agent wiring:** `packages/showcase/client-app/client/src/routes/ai/genesis-assistant/genesis-assistant.ts`

The sub-agent (`trade_file_extractor`) defines a single `return_extracted_trades` tool that calls `completeSubAgent` with the parsed rows. The parent's `process_trade_file` handler calls `requestSubAgent<ExtractedTrades>('trade_file_extractor', { task: ... })` and receives the typed `ExtractedTrades` result.

---

## Constraints (v1)

- Sub-agents are leaf nodes — their own `subAgents` field is ignored. Nesting is not supported.
- Sub-agents share the same `AIProvider` instance as the parent. They each make independent LLM calls.
