# a2a-claude (/docs/reference/packages/a2a-claude)



> This page is generated from [`a2a-claude/README.md`](https://github.com/shashikanth-gs/a2a-wrapper/blob/main/a2a-claude/README.md) and stays in sync with the repository.

Claude is Anthropic's model family, and the Claude Agent SDK gives it a production-grade agent loop. It handles repository navigation, multi-step planning, shell commands, file editing, and permission management — all the plumbing you'd spend months building from scratch.

**a2a-claude** exposes that agent as a standalone, interoperable agent via the [A2A protocol](https://github.com/google-deepmind/a2a), using the [`@anthropic-ai/claude-agent-sdk`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). Drop a JSON config file in, get a fully spec-compliant A2A server out. Any orchestrator that speaks A2A can discover and call it — no Claude-specific integration code required.

> **The pattern:** MCP is the vertical rail — how agents access tools. A2A is the horizontal rail — how agents talk to each other. This library adds the horizontal rail to Claude.

**Features:**

* Native [A2A v1.0](https://a2a-protocol.org) protocol, backward compatible with v0.3.x clients — Agent Card, JSON-RPC, REST, streaming
* Powered by `@anthropic-ai/claude-agent-sdk` (pinned `0.3.245`) — `claude-sonnet-5`, `claude-opus-4-8`, and any SDK-compatible model
* Permission-mode guardrails — headless-safe modes only, with an explicit opt-in for unrestricted access
* MCP tool support — stdio and Streamable HTTP transports
* Multi-turn context continuity — each A2A `contextId` maps to a persistent Claude session (resumed via the SDK's `resume` option)
* AbortController-based cancellation
* Multi-agent delegation via A2A sub-agents
* Sideband observability events — thinking summaries, tool calls, file changes, todo lists, usage/cost
* JSON config file with layered overrides (JSON → env vars → CLI flags)
* Docker-ready
* TypeScript source with full type declarations

## Quick Start [#quick-start]

```bash
export ANTHROPIC_API_KEY=sk-ant-...
export WORKSPACE_DIR=/path/to/your/repo

npm install
npm run dev -w a2a-claude -- --config agents/example/config.json
```

Fetch the agent card:

```bash
curl -s http://localhost:3030/.well-known/agent-card.json | jq .
```

The CLI is also runnable directly:

```bash
npm install -g a2a-claude
export ANTHROPIC_API_KEY=sk-ant-... WORKSPACE_DIR=/path/to/your/repo
a2a-claude --config agents/example/config.json
```

## Release Channels [#release-channels]

| Channel | Install                         | Cadence                                                                                                                                         |
| ------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Stable  | `npm install a2a-claude`        | Manually reviewed and approved — see [Release Process](https://github.com/shashikanth-gs/a2a-wrapper/blob/main/CONTRIBUTING.md#release-process) |
| Canary  | `npm install a2a-claude@canary` | Every merge to `main` that passes CI                                                                                                            |

Canary builds publish automatically after each merge, tagged like `a2a-claude@0.4.0-canary-20260812081117`. They're unreviewed and have no changelog entry — useful for trying a fix ahead of a stable release, not for production. `npm install a2a-claude` always resolves to the latest stable release regardless of how many canaries have shipped since.

## Authentication [#authentication]

`ANTHROPIC_API_KEY` is the primary and recommended authentication path — export it before starting the agent (or set it in `.env`, see `.env.example`).

Bedrock, Vertex, and related Claude authentication environment variables (e.g. `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, and related credentials) pass through untouched to the Claude Agent SDK — the wrapper never reads, validates, or stores them. Set whichever auth environment your deployment needs; a2a-claude only cares that the SDK's `query()` call can authenticate when invoked.

The API key (or any other credential) is **never** placed in `config.json`. Config fields that need a secret use `${ENV_VAR}` substitution instead (see MCP servers below).

## Configuration Reference [#configuration-reference]

All settings live in a single JSON config file. Priority order: **built-in defaults ← config file ← environment variables ← CLI flags**.

### `claude` block fields [#claude-block-fields]

Fields map 1:1 onto `@anthropic-ai/claude-agent-sdk` `Options` (source of truth: `src/config/types.ts`).

| Field                               | Type                                                                                               | Description                                                                                                                                                                                                       |
| ----------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workingDirectory`                  | `string`                                                                                           | Absolute path to the workspace Claude operates on. Required at runtime. Supports `${ENV_VAR}`.                                                                                                                    |
| `model`                             | `string`                                                                                           | Model (e.g. `claude-sonnet-5`). Supports `${CLAUDE_MODEL}`. SDK default when omitted.                                                                                                                             |
| `fallbackModel`                     | `string`                                                                                           | Fallback model when the primary is overloaded/unavailable.                                                                                                                                                        |
| `effort`                            | `"low" \| "medium" \| "high" \| "xhigh" \| "max"`                                                  | Reasoning effort level. Overridable with the `CLAUDE_EFFORT` env var. SDK default when omitted.                                                                                                                   |
| `thinking`                          | `{ type: "adaptive" }` \| `{ type: "enabled", budgetTokens?, display? }` \| `{ type: "disabled" }` | Extended thinking behavior. SDK default when omitted.                                                                                                                                                             |
| `outputFormat`                      | `{ type: "json_schema", schema: object }`                                                          | Request structured JSON output — see **Structured output** below. SDK default (freeform text) when omitted.                                                                                                       |
| `permissionMode`                    | `"acceptEdits" \| "dontAsk" \| "plan" \| "bypassPermissions"`                                      | Permission mode. `"default"` and `"auto"` are rejected — see **Permission modes** below. Default: `"acceptEdits"`.                                                                                                |
| `allowedTools`                      | `string[]`                                                                                         | Tools auto-allowed without prompting.                                                                                                                                                                             |
| `disallowedTools`                   | `string[]`                                                                                         | Tools removed from the model's context entirely.                                                                                                                                                                  |
| `systemPromptAppend`                | `string`                                                                                           | Appended to the `claude_code` preset system prompt.                                                                                                                                                               |
| `customSystemPrompt`                | `string`                                                                                           | Full system prompt replacement. Mutually exclusive with `systemPromptAppend`.                                                                                                                                     |
| `settingSources`                    | `Array<"user" \| "project" \| "local">`                                                            | Filesystem settings sources to load. Default `[]` = full isolation from host `~/.claude` and project settings. Include `"project"` to load workspace `CLAUDE.md`.                                                 |
| `marketplaces`                      | `Record<string, { source: { source: string, ... } }>`                                              | Plugin marketplaces to register, keyed by marketplace id. The SDK fetches and installs the plugins itself — see **Marketplace plugins** below. Every string field in `source` supports `${ENV_VAR}` substitution. |
| `enabledPlugins`                    | `Record<string, boolean>`                                                                          | Plugins to enable, keyed `"<plugin-id>@<marketplace-id>"`. The marketplace id must appear in `marketplaces`. Startup fails if an enabled plugin does not load.                                                    |
| `maxTurns`                          | `number`                                                                                           | Max conversation turns per query (runaway protection).                                                                                                                                                            |
| `maxBudgetUsd`                      | `number`                                                                                           | Max budget in USD per query.                                                                                                                                                                                      |
| `additionalDirectories`             | `string[]`                                                                                         | Additional directories Claude can access. Supports `${ENV_VAR}` per entry.                                                                                                                                        |
| `sandbox`                           | `object`                                                                                           | Opaque SDK sandbox settings passthrough (OS-level command sandboxing).                                                                                                                                            |
| `executablePathOverride`            | `string`                                                                                           | Override the path to the Claude executable.                                                                                                                                                                       |
| `dangerouslyAllowBypassPermissions` | `boolean`                                                                                          | Must be `true` when `permissionMode` is `"bypassPermissions"`.                                                                                                                                                    |
| `contextFile`                       | `string`                                                                                           | Filename for the pre-built domain context file within `workingDirectory`. Default `"context.md"`.                                                                                                                 |
| `contextPrompt`                     | `string`                                                                                           | Default prompt used when `buildContext()` is called without an explicit prompt.                                                                                                                                   |

### Reasoning effort and extended thinking [#reasoning-effort-and-extended-thinking]

`effort` and `thinking` are independent knobs that both affect how much the model reasons before answering.

```json
{
  "claude": {
    "effort": "high",
    "thinking": { "type": "adaptive" }
  }
}
```

**`thinking`** selects the mode:

| Value                                         | Behavior                                                      |
| --------------------------------------------- | ------------------------------------------------------------- |
| `{ "type": "adaptive" }`                      | Claude decides when and how much to think. Newer models only. |
| `{ "type": "enabled", "budgetTokens": 4096 }` | Fixed thinking token budget. `budgetTokens` is optional.      |
| `{ "type": "disabled" }`                      | No extended thinking.                                         |

`adaptive` and `enabled` also accept `display`, either `"summarized"` or `"omitted"`.

**`effort`** guides thinking depth, trading latency and token spend against answer quality. Models that do not support the requested level silently downgrade it, and `"max"` is available on select models only — so a level accepted at startup is not a guarantee that the model honors it.

#### Thinking summaries are requested automatically [#thinking-summaries-are-requested-automatically]

The SDK leaves `display` at `"omitted"`, which makes the model emit thinking blocks whose text is an **empty string**. Nothing reaches the sideband, and `features.emitThinkingEvents` looks broken even though it is on.

So whenever `emitThinkingEvents` is enabled — the default — the wrapper asks for summaries:

| Your `claude.thinking`                         | Sent to the SDK                                                        |
| ---------------------------------------------- | ---------------------------------------------------------------------- |
| unset                                          | `{ "type": "adaptive", "display": "summarized" }`                      |
| `{ "type": "adaptive" }`                       | `{ "type": "adaptive", "display": "summarized" }`                      |
| `{ "type": "enabled", "budgetTokens": 8000 }`  | `{ "type": "enabled", "budgetTokens": 8000, "display": "summarized" }` |
| `{ "type": "adaptive", "display": "omitted" }` | unchanged — an explicit `display` always wins                          |
| `{ "type": "disabled" }`                       | unchanged                                                              |

Note the first row: with thinking events on and no `thinking` block of your own, the wrapper turns adaptive thinking **on**, which costs thinking tokens. Set `"thinking": { "type": "disabled" }` or `"emitThinkingEvents": false` to opt out.

Two more things worth knowing:

* `{ "type": "disabled" }` means no `thinking` sideband events can ever fire, regardless of `features.emitThinkingEvents`.
* Both values are validated at startup. An unsupported effort level or a malformed `thinking` object fails `initialize()` with a message naming the allowed values.

### Structured output [#structured-output]

* **`claude.outputFormat`** (object, optional) — Request structured JSON output.
  Maps 1:1 onto the Claude Agent SDK's `Options.outputFormat`. Shape:
  `{ "type": "json_schema", "schema": { ... } }`, where `schema` is a JSON
  Schema the model's output is constrained to match. When set, the parsed
  object is published as an `application/json` **data part** on the `response`
  artifact, alongside the usual text part (the text part is still emitted, so
  text-only clients are unaffected). Omit for freeform text (the SDK default).

### Full config reference [#full-config-reference]

> `agentCard.protocolVersion` below is optional and kept only for backward
> compatibility with older config files — the server negotiates A2A v1.0 vs.
> v0.3.x automatically per request and no longer reads this field. See
> [Protocol Versions](https://github.com/shashikanth-gs/a2a-wrapper/blob/main/packages/core/README.md#protocol-versions) for how
> negotiation works.

```json
{
  "agentCard": {
    "name": "Claude Workspace Engineer",
    "description": "...",
    "version": "1.0.0",
    "protocolVersion": "0.3.0",
    "streaming": true,
    "defaultInputModes": ["text"],
    "defaultOutputModes": ["text"],
    "skills": [
      {
        "id": "workspace-engineering",
        "name": "Workspace Engineering",
        "description": "Inspect, modify, and validate code within the configured workspace.",
        "tags": ["code", "repository", "tests", "refactoring"]
      }
    ]
  },

  "server": {
    "port": 3030,
    "hostname": "0.0.0.0",
    "advertiseHost": "localhost",
    "advertiseProtocol": "http"
  },

  "claude": {
    "workingDirectory": "${WORKSPACE_DIR}",
    "model": "${CLAUDE_MODEL}",
    "effort": "high",
    "thinking": { "type": "adaptive" },
    "permissionMode": "acceptEdits",
    "settingSources": [],
    "additionalDirectories": [],
    "systemPromptAppend": "Operate only within the configured workspace."
  },

  "session": {
    "reuseByContext": true,
    "ttl": 0
  },

  "features": {
    "streamArtifactChunks": false,
    "emitThinkingEvents": true,
    "emitToolEvents": true,
    "emitFileChangeEvents": true,
    "emitTodoEvents": true,
    "emitRateLimitEvents": true,
    "holdTaskForBackgroundWork": true,
    "emitBackgroundTaskEvents": true
  },

  "timeouts": {
    "prompt": 600000
  },

  "logging": {
    "level": "info"
  }
}
```

### Prompt timeout [#prompt-timeout]

`timeouts.prompt` bounds one A2A task from start to terminal state, in milliseconds (default `600000`, ten minutes). When it elapses the task is aborted and published as `failed`.

The timer is armed once, when the task starts, and is never re-armed. With `features.holdTaskForBackgroundWork` on (the default) a task can span several SDK turns, and this one budget covers all of them — including the idle gaps while Claude's background work runs elsewhere. See [Background tasks → Caveats](#caveats) before choosing a value.

Set it to `0` — or any value `<= 0` — to disable the bound entirely and let a task run until it completes. This is the right setting for agents whose turns legitimately run for hours.

Disabling it has one consequence worth knowing: turns are serialized per context, so a turn that never finishes holds its context's queue indefinitely and every later turn on the same `contextId` blocks behind it. Cancelling the task (`tasks/cancel`) still aborts the running turn and is the escape hatch. With background-task holding on, that escape hatch is the *only* release — see [Background tasks → Caveats](#caveats).

### Session lifetime [#session-lifetime]

`session.ttl` controls expiry of the `contextId` → Claude session mapping. It
defaults to `0`, which disables expiry: a conversation resumes the same Claude
session indefinitely. A positive value expires a session that many milliseconds
after it was **created** — measured from the conversation's first message, not
its last — so an active conversation is reset on a fixed schedule rather than
after a period of inactivity. When a session is evicted, the next turn on that
`contextId` starts a brand-new Claude session with no memory of the conversation
so far.

`session.cleanupInterval` is a background sweep that reclaims expired sessions
without waiting for their context to be used again. It also defaults to `0`
(disabled), and it is only consulted at all when `ttl > 0` — with expiry off
there is nothing to sweep. Setting `ttl` alone gives you expiry via the lazy
check, but a context that is never touched again holds its record until the
process exits; set `cleanupInterval` as well if you want that memory reclaimed.
Both disabled states are logged at startup.

### Permission modes [#permission-modes]

The SDK's `permissionMode` controls whether tool calls are auto-approved. Headless A2A execution cannot show an interactive approval prompt to a human, so two of the SDK's four modes are rejected at startup:

| Mode                | Behaviour                                             | Headless-safe                                                |
| ------------------- | ----------------------------------------------------- | ------------------------------------------------------------ |
| `default`           | Interactive approval per tool call                    | **Rejected** — no human in the loop                          |
| `auto`              | Heuristic/classifier-based approval                   | **Rejected** — not supported for headless A2A                |
| `acceptEdits`       | Auto-approve file edits; other guardrails still apply | Yes (default)                                                |
| `dontAsk`           | Never prompt; broadest auto-approval short of bypass  | Yes                                                          |
| `plan`              | Read/analyze only — no mutating tool calls            | Yes                                                          |
| `bypassPermissions` | Unrestricted tool access                              | Yes, **only** with `dangerouslyAllowBypassPermissions: true` |

Setting `permissionMode: "bypassPermissions"` without `dangerouslyAllowBypassPermissions: true` throws at startup. When bypass is enabled, the executor logs a loud warning on every startup as a reminder that Claude has unrestricted tool access — only use it inside an isolated container or VM.

### `settingSources` isolation [#settingsources-isolation]

`settingSources` defaults to `[]`, meaning the SDK loads **no** host `~/.claude` user settings and **no** project `CLAUDE.md` / `.claude/settings.json` — every session starts from a clean, isolated slate driven entirely by `config.json`. Add `"project"` to `settingSources` to let Claude read the workspace's `CLAUDE.md` and project-level settings (useful when the target repository already documents its own conventions). Add `"user"` to load the host user's `~/.claude` settings — only do this in trusted, single-tenant deployments, since it pulls in configuration outside the agent's config file.

`strictMcpConfig` is always enabled internally (not user-configurable) — Claude is only allowed to use MCP servers explicitly declared in `config.json`, never ones discovered from ambient settings.

### Marketplace plugins [#marketplace-plugins]

`marketplaces` + `enabledPlugins` let the SDK fetch and install plugins itself, so nothing has to be baked into the image:

```json
"claude": {
  "marketplaces": {
    "superpowers-marketplace": {
      "source": { "source": "github", "repo": "obra/superpowers-marketplace", "ref": "v6.1.1" }
    }
  },
  "enabledPlugins": { "superpowers@superpowers-marketplace": true }
}
```

`source` is passed through to the SDK unchanged, so every source kind it supports works — `github` (`repo`), `git` (`url`), `git-subdir` (`url` + `path`, for monorepos), `url` (direct `marketplace.json`), `npm` (`package`), and local `directory`. Each accepts `ref` or `sha`.

**Pin every marketplace by `ref` or `sha`.** Plugin hooks and bundled MCP servers execute as code at the session's permission mode, so an unpinned marketplace means two sessions of the same agent can run different code.

These map to the SDK's *flag-tier* `settings`, which composes with `settingSources` rather than competing with it — marketplace plugins load even under the default `settingSources: []` isolation, and combining them with `["project"]` for `CLAUDE.md` works as expected.

Private marketplaces are cloned by `git`, so credentials must be supplied through the process environment (e.g. `GIT_ASKPASS`, or a configured credential helper) rather than per-source config.

#### Startup preflight [#startup-preflight]

The SDK installs marketplace plugins **asynchronously by default** — with `CLAUDE_CODE_SYNC_PLUGIN_INSTALL` unset it installs nothing, reports no error, and loads zero plugins on every subsequent run too. The wrapper therefore sets that flag for sessions with marketplaces configured, and `initialize()` runs a preflight that verifies each enabled plugin actually loaded:

```
Configured plugin(s) did not load: superpowers@superpowers-marketplace. Check that each
marketplace source and ref/sha is correct and that the plugin name exists in that
marketplace's manifest.
```

The check diffs the session init message's plugin list, **not** the `plugin_install` events — those report per-marketplace status, so a marketplace that clones cleanly but contains no plugin by the configured name still reports `installed` and `completed`. The probe costs no tokens (the init message precedes any model call) and warms the plugin cache so the first task doesn't pay the clone.

The preflight has no timeout of its own: the SDK already bounds marketplace fetches (an unreachable host fails in \~75s), and the session still reaches init afterwards, so a fetch failure surfaces as the precise "did not load" error above rather than a generic timeout. Tune `CLAUDE_CODE_PLUGIN_GIT_TIMEOUT_MS` or `CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS` if you need different bounds.

### Rate limits [#rate-limits]

When Claude reports a rate-limit rejection, the wrapper ends the turn
immediately rather than waiting out the reset or letting the request decay into
a prompt timeout. The task is published as `failed`, with a message naming the
limit and its reset time, for example:

> Rate limit reached (5-hour limit). Resets at 2026-08-11T18:00:00.000Z. Retry
> on the same contextId to continue this conversation.

The same status carries machine-readable metadata for orchestrators that
schedule their own retry:

```json
{
  "reason": "rate_limit",
  "source": "rate_limit_event",
  "rateLimitType": "five_hour",
  "resetsAt": 1786471200000,
  "resetsAtIso": "2026-08-11T18:00:00.000Z",
  "utilization": 1
}
```

The task is terminal, and deliberately so. The Claude Agent SDK cannot resume an
interrupted turn — a follow-up message is a new prompt appended to the
conversation either way — so holding the task open with `input-required` would
promise a continuation that never happens, and would misuse a state that means
"I need information from you" for what is really exhausted quota.

Continuity does not come from the task. It comes from the `contextId` → Claude
session mapping, which is unaffected by how a task ends. Once the limit resets,
the client starts a **new task on the same `contextId`**: the Claude session is
resumed and no context is lost.

Rate limit details (`resetsAt`, `rateLimitType`) are only available under
claude.ai subscription auth. Under API key, Bedrock, or Vertex the wrapper still
ends the turn cleanly, but the message omits the reset time because the SDK does
not report one. Details are never invented: a limit type or reset time is
carried onto a later rejection only if the SDK had already reported that limit
under pressure, and a reset time that is not in the future is dropped rather
than printed.

A rejection whose overage window is still open (`overageStatus` of `allowed` or
`allowed_warning`) is treated as a warning, not a rejection — the request may
well proceed on overage credits, and if it does not, the assistant's own
`rate_limit` error still ends the turn. When the SDK reports
`errorCode: "credits_required"`, no reset will restore capacity, so the message
says additional credits are required and omits the reset clause entirely.

Warnings — approaching a limit, or an internal retry after a 429 — do not end
the turn. They surface as `rate_limit` sideband events, which can be disabled
with `features.emitRateLimitEvents: false`; that flag only suppresses the
sideband events, and the turn still ends on a rejection either way.

The SDK retries 429s internally before giving up, and the wrapper does not
shorten that: those retries still run to exhaustion inside the same
`timeouts.prompt` window. They are at least visible now, as `rate_limit`
sideband events with `action: "retrying"` carrying the SDK's `attempt`,
`maxRetries`, and `delayMs`. Set a lower `timeouts.prompt` if a retry storm
burning the window matters more to you than the retries succeeding.

### Background tasks [#background-tasks]

Claude can start work that outlives a single SDK turn — a background shell
process, a long-running build — and end its turn saying it's waiting on the
result. Left alone, an A2A Task has no way to represent that: the Task reaches
a terminal state the moment the turn ends, and A2A gives an agent no way to
open a new turn against a terminal Task, so the eventual follow-up report
would have nowhere to land.

`features.holdTaskForBackgroundWork` (default `true`) keeps the Task in
`working` for as long as Claude reports background work in flight, instead of
completing it at the first SDK result. Each SDK turn — a "round" — publishes
its own `response` artifact plus a non-final `working` status update whose
`metadata.backgroundTasks` lists what's still running (`taskId`, `type`, and
`description`, mirrored from the SDK's own `background_tasks_changed`
message). The Task only reaches a terminal state once a round ends with that
set empty. A chain of any length works as rounds of one Task rather than a
string of separate ones — check the build, kick off a deploy, report the
result.

Set `holdTaskForBackgroundWork: false` to complete the Task at the first SDK
result, as before, regardless of what Claude reports is still running.

The flag governs that completion decision and nothing else. Queries are issued
in streaming-input mode either way: with a plain string prompt the SDK closes
the CLI subprocess's stdin on the first result and the process exits, so
streaming input is what makes a second round possible at all. There is no
setting that reverts it.

`features.emitBackgroundTaskEvents` (default `true`) publishes a
`background_tasks` sideband event each time the live set changes, carrying the
same `taskId`/`type`/`description` list plus a `count`. See
[Sideband Events](#sideband-events).

#### Caveats [#caveats]

Four things worth knowing before relying on this.

**`claude.maxTurns` now spans rounds.** A held-open Task accumulates SDK turns
across every round it takes, so a chain that used to run as several Tasks
under several separate budgets is now one Task under one budget. A
`maxTurns` that was comfortable before can be exhausted mid-chain, ending the
Task with `error_max_turns`.

**Further messages on the same `contextId` queue behind a held-open Task.**
Turns are serialized per context — see [Prompt timeout](#prompt-timeout) —
so a Task that's waiting on background work blocks every later message on
that context, the same as any other slow turn would. `cancelTask`
(`tasks/cancel`) is currently the only way to release the queue early, and
there's a sharp edge worth calling out: the remedy a user would naturally
reach for — sending another message on the same context — is exactly what's
blocked.

**`timeouts.prompt` bounds the whole Task, not one round.** The timer is
armed once at turn start and is never re-armed, so it also covers the idle
gaps between rounds while Claude's background work runs elsewhere. If you run
with a non-zero prompt timeout, raise it: the ten-minute default is usually
too low for a chain that holds the Task open, and a Task that runs out the
budget ends `failed` even if every round up to that point succeeded.

**With `timeouts.prompt: 0`, a held-open Task has no automatic release.**
This is the sharpest edge of the four, and it applies directly to any
deployment that disables the prompt timeout. If the background-task set
never empties — and the SDK's `background_tasks_changed` level is the only
settle signal available, with no wake-up turn guaranteed to ever follow — the
Task stays in `working` indefinitely and holds its context's queue open with
it. `cancelTask` is the only escape. This is a known limitation of the
current design; a non-timeout release mechanism is planned. Operators running
with the prompt timeout disabled should monitor for Tasks stuck in `working`.

## Example Agents [#example-agents]

| Config                                  | Port   | Permission mode                                            | Description                                           |
| --------------------------------------- | ------ | ---------------------------------------------------------- | ----------------------------------------------------- |
| `agents/example/config.json`            | `3030` | `acceptEdits`                                              | Workspace engineer — read + write access              |
| `agents/read-only-reviewer/config.json` | `3031` | `plan` (+ `Write`/`Edit`/`NotebookEdit`/`Bash` disallowed) | Code reviewer — never modifies files or runs commands |

Each agent directory bundles a `start.sh` lifecycle script and a `workspace/` placeholder directory:

```bash
# Start in the background
agents/example/start.sh start

# Check status / health
agents/example/start.sh status

# Tail logs
agents/example/start.sh logs

# Stop
agents/example/start.sh stop

# Run in the foreground (useful for debugging / Docker)
agents/example/start.sh foreground
```

Point either agent at a real repository by overriding `WORKSPACE_DIR` before calling `start.sh`:

```bash
WORKSPACE_DIR=/path/to/repo agents/read-only-reviewer/start.sh start
```

Copy a directory to create your own agent: `cp -r agents/example agents/my-agent`.

## MCP Servers [#mcp-servers]

MCP configuration is baked at SDK construction time — all servers must be declared in `config.json` before the agent starts. Only `stdio` and Streamable `http` transports are supported (SSE-only servers are rejected at startup).

```json
{
  "mcp": {
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "${WORKSPACE_DIR}"]
    },
    "linear": {
      "type": "http",
      "url": "https://mcp.linear.app/mcp",
      "headers": { "Authorization": "Bearer ${LINEAR_API_KEY}" }
    }
  }
}
```

The key **`a2a-subagents`** is reserved for the sub-agent bridge described below — using it for a user-defined MCP server fails validation at startup.

## Sub-agents [#sub-agents]

Any `a2a-claude` agent can delegate to other A2A agents by declaring them under `subAgents` in `config.json`. The wrapper bootstraps [`a2a-mcp-skillmap`](https://www.npmjs.com/package/a2a-mcp-skillmap) as a stdio MCP server registered under the reserved `a2a-subagents` key, and each remote skill becomes a callable tool for Claude (`{name}__{skillId}`). MCP calls to `a2a-subagents` are enriched with `toolKind: "a2a_subagent"` and `delegation: true` in sideband events.

See the [root README's Sub-Agents section](https://github.com/shashikanth-gs/a2a-wrapper/blob/main/README.md#calling-other-a2a-agents-sub-agents) for the full `subAgents` schema, auth modes, and a runnable example.

## Sideband Events [#sideband-events]

Sideband events are published through `AgentEventEmitter` for every Claude Agent SDK message. Use them for observability, tracing, and orchestration. Secrets are redacted (API keys, tokens, passwords, etc.) and tool output is truncated at 10,000 characters; file contents are never emitted, only path + operation kind.

| Event                                    | Emitted when                                                                                                | Notes                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_started`                          | SDK `system`/`init` message                                                                                 | Includes `backend: "claude"` and the resolved model; emitted once per A2A Task, even across a held-open Task's several rounds — the SDK re-emits `init` on every background-task wake, so this bookend is deduplicated per Task rather than per SDK turn                                                                                                          |
| `thinking`                               | Assistant `thinking` content block                                                                          | Controlled by `features.emitThinkingEvents`                                                                                                                                                                                                                                                                                                                       |
| `tool_call_start` / `tool_call_end`      | Assistant `tool_use` block / matching `tool_result`                                                         | `toolKind` is `"shell"` (Bash), `"mcp"`, `"a2a_subagent"` (mcp server `a2a-subagents`), or `"builtin"`; controlled by `features.emitToolEvents`                                                                                                                                                                                                                   |
| `decision` (`kind: "file_change"`)       | `Edit` / `Write` / `NotebookEdit` tool call                                                                 | Path and change kind only — never file contents; controlled by `features.emitFileChangeEvents`                                                                                                                                                                                                                                                                    |
| `decision` (`kind: "todo_list"`)         | `TodoWrite` tool call                                                                                       | Controlled by `features.emitTodoEvents`                                                                                                                                                                                                                                                                                                                           |
| `decision` (`kind: "permission_denied"`) | SDK `system`/`permission_denied` message                                                                    | Tool name + sanitized message                                                                                                                                                                                                                                                                                                                                     |
| `agent_finished`                         | SDK `result`/`success` message                                                                              | Includes sanitized `usage`, `totalCostUsd`, `numTurns`; emitted once per A2A Task, on the round that finally completes it — not on every intermediate round of a held-open Task                                                                                                                                                                                   |
| `agent_error`                            | SDK `result` failure subtypes / `error` message                                                             | Sanitized error message; reason mapped from the SDK's failure subtype (e.g. max turns, max budget)                                                                                                                                                                                                                                                                |
| `rate_limit`                             | SDK `rate_limit_event`, `system`/`api_retry` with `error: "rate_limit"`, or an assistant `rate_limit` error | `action` is `"ended_turn"` (rejection — the turn stops), `"retrying"` (SDK internal retry, with the `retry` counters), or `"warning"`; carries `status` plus `rateLimitType` / `resetsAt` / `utilization` when the SDK reports them. Controlled by `features.emitRateLimitEvents`                                                                                 |
| `background_tasks`                       | SDK `system`/`background_tasks_changed` message                                                             | Level signal with replace semantics — each event carries the full live set (`taskId` / `type` / `description`) plus `count`, and is only emitted when membership actually changes. On the default `a2a` transport it arrives as a `trace.background_tasks` artifact. See [Background tasks](#background-tasks). Controlled by `features.emitBackgroundTaskEvents` |

> **Note:** `rate_limit` (and `context_window`) have no A2A trace-artifact
> mapping, so on the default `a2a` transport they are dropped rather than
> delivered. They are observable on the `http` transport or a custom one. This
> is a pre-existing gap, tracked separately.

## Docker [#docker]

Build from the **monorepo root** (the image needs local `@a2a-wrapper/core` source):

```bash
docker build -f a2a-claude/Dockerfile -t a2a-claude:latest .
```

Run with an API key:

```bash
docker run -p 3030:3030 \
  -e ANTHROPIC_API_KEY=sk-ant-... \
  -e WORKSPACE_DIR=/workspace \
  -v /host/path/to/repo:/workspace \
  a2a-claude:latest
```

## Manual E2E Verification [#manual-e2e-verification]

The automated test suite (`npm test -w a2a-claude`) uses a fake SDK client and never calls the real Anthropic API. To validate the full stack against a real backend, run this one-shot check with a real `ANTHROPIC_API_KEY`:

```bash
export ANTHROPIC_API_KEY=sk-ant-... WORKSPACE_DIR=/path/to/scratch-repo
npm run dev -- --config agents/example/config.json &
curl -s -X POST http://localhost:3030/a2a/jsonrpc -H 'content-type: application/json' -d '{
  "jsonrpc": "2.0", "id": "1", "method": "message/send",
  "params": { "message": { "kind": "message", "messageId": "m1", "role": "user",
    "parts": [{ "kind": "text", "text": "List the files in this repository and summarize what it does." }] } }
}'
```

This is a manual, documented step — it is **not** part of automated CI and requires a funded Anthropic API key.

## Phase 2 Roadmap [#phase-2-roadmap]

The following are explicitly out of scope for this release and tracked as future work:

* hooks configuration
* native Claude subagents (`agents` option)
* skills
* richer usage/cost telemetry
* session forking
* `canUseTool` policy engine
* file checkpointing/rewind
* plan-mode review workflows
