# Migration Guide — GENC-1312 (Sub-agents must return via a tool call)

Sub-agents now **always finish by calling a tool** and hand a **typed result** back
to the caller. Previously a sub-agent could also end its turn with a plain-text
answer, and `requestSubAgent` would return that text as a `string` fallback. That
fallback is gone: sub-agents run with tool use forced, and `requestSubAgent`
resolves to a discriminated union — either the structured result, or a typed
failure reason.

This affects you **only if your tool handlers call `requestSubAgent`**. Agents that
declare no sub-agents, top-level agent behaviour, `completeSubAgent`, and
`SubAgentRequestOptions` are all unchanged.

> The sub-agent API is `@beta`. This is a deliberate, documented breaking change
> on that surface.

---

## 1. `requestSubAgent` returns a discriminated union

The return type changed from `Promise<T | string>` to:

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

```ts
type SubAgentFailureReason =
  | 'max_iterations'        // loop ended without the completion tool being called
  | 'malformed_tool_call'   // provider returned unparseable tool calls after retries
  | 'empty_response'        // model returned an empty response after retries
  | 'unknown_tool_limit';   // model repeatedly called tools it doesn't have
```

`SubAgentFailureReason` is exported from `@genesislcap/foundation-ai`.

### Before

```ts
const handlers: ChatToolHandlers<typeof extractorAgent> = {
  process_file: async (args, context) => {
    if (!context.requestSubAgent) {
      return { error: 'Sub-agent support is not available in this context.' };
    }
    const { file_name } = args as { file_name: string };

    const result = await context.requestSubAgent<ExtractedData>('extractor', {
      task: `Extract all rows from "${file_name}".`,
    });

    // result was `ExtractedData | string`
    if (typeof result === 'string') {
      // sub-agent finished with plain text — handle "gracefully"
      return { error: result };
    }
    return result;
  },
};
```

### After

```ts
const handlers: ChatToolHandlers<typeof extractorAgent> = {
  process_file: async (args, context) => {
    if (!context.requestSubAgent) {
      return { error: 'Sub-agent support is not available in this context.' };
    }
    const { file_name } = args as { file_name: string };

    const outcome = await context.requestSubAgent<ExtractedData>('extractor', {
      task: `Extract all rows from "${file_name}".`,
    });

    if (!outcome.ok) {
      // The sub-agent didn't complete. Decide how to recover — typically
      // early-return the issue back to *this* agent so it can retry, ask the
      // user for help, or call a planner tool again.
      return {
        error: `Couldn't extract from "${file_name}" (${outcome.reason}). Ask the user to retry or try a different file.`,
      };
    }

    return outcome.result; // fully typed as ExtractedData
  },
};
```

### Mechanical migration

- `if (typeof result === 'string')` → `if (!outcome.ok)`.
- The success value moves from `result` to `outcome.result`.
- There is no longer an untyped string success path. If you called
  `requestSubAgent` **without** a type parameter, the success payload is now
  `never` — pass `<T>` to describe the result your completion tool returns.

---

## 2. Sub-agents must finish by calling a tool

Sub-agents now run with tool use forced every turn (Anthropic
`tool_choice: { type: 'any' }`, Gemini `functionCallingConfig.mode: 'ANY'`). A
sub-agent can no longer end a turn with a free-text answer — the only clean way
for it to finish is to call a tool whose handler invokes `completeSubAgent`.

**What you must check:** every sub-agent declares a completion tool (a normal tool
whose handler calls `context.completeSubAgent(result)`), and its prompt directs
the model to call it when done. A sub-agent with no completion tool can never
finish and will fail with `reason: 'max_iterations'` on every run.

This is the same `completeSubAgent` mechanism as before — its signature and the
per-agent schema you attach to your completion tool are unchanged. You keep full
control of the returned shape; only the *fallback* behaviour was removed.

> If a sub-agent's natural output is prose (e.g. a drafted paragraph), return it
> through the completion tool's payload — `completeSubAgent({ text })` — rather
> than relying on a free-text turn. Conversational, user-facing flows belong to
> top-level / stateful agents, not sub-agents.

---

## 3. Handling failures

A sub-agent failure is **not** surfaced to the user from inside the sub-agent
anymore — no apology message is appended. Instead the failure is returned to your
tool handler as `{ ok: false, reason }`, and **you decide** what happens next. The
recommended pattern is to early-return the issue information to the parent agent
so it can choose a recovery path:

```ts
const outcome = await context.requestSubAgent<PlannedData>('planner', { task });
if (!outcome.ok) {
  return {
    error: `Planning didn't complete (${outcome.reason}). Ask the user for the missing details, or try again.`,
  };
}
```

The parent agent then sees that tool result and reacts (retry, re-plan, ask the
user) like any other tool outcome. Each failure is also recorded in the debug log
as a `subagent.failed` meta event (with the agent name and reason) plus the
existing `turn.error` entry, now tagged `isSubAgent: true`.

---

## 4. New `ChatRequestOptions.toolChoice` (additive — no action needed)

`ChatRequestOptions` gained an optional `toolChoice?: 'auto' | 'required'`. The
built-in Anthropic and Gemini transports translate `'required'` into the
provider's force-a-tool-call setting. This is additive and defaults to `'auto'`
(may-call), so existing code is unaffected.

If you maintain a **custom `ChatTransport`**, you can ignore `toolChoice` (the
field is optional). To support forced tool use in sub-agent loops, map
`'required'` to your provider's equivalent and only apply it when tools are
present.

> `'required'` is incompatible with Anthropic extended/adaptive thinking — a
> request must not enable both. The built-in chat transport never sets `thinking`,
> so they don't collide.

---

## Quick reference checklist

- [ ] For every `requestSubAgent` call: replace `typeof result === 'string'` with
      `!outcome.ok`, and read the success value from `outcome.result`.
- [ ] Pass a type parameter (`requestSubAgent<T>(...)`) so the success payload is
      typed.
- [ ] On `!outcome.ok`, return the issue back to the parent agent (or otherwise
      recover) — don't assume a string result.
- [ ] Confirm every sub-agent has a completion tool that calls `completeSubAgent`,
      and that its prompt tells the model to call it when finished.
- [ ] If a sub-agent produced user-facing prose via a final text turn, move that
      text into the completion tool's payload.
- [ ] Custom `ChatTransport` only: optionally honour `toolChoice: 'required'`.
