a2a-wrapper logoa2a-wrapper
ReferencePackages

a2a-claude

Reference for a2a-claude: expose Claude as an A2A agent, with permission modes, sessions, rate limits, and background tasks.

This page is generated from 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, using the @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 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

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:

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

The CLI is also runnable directly:

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

ChannelInstallCadence
Stablenpm install a2a-claudeManually reviewed and approved — see Release Process
Canarynpm install a2a-claude@canaryEvery merge to main that passes CI

Canary builds publish automatically after each merge, tagged like [email protected]. 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

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

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

claude block fields

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

FieldTypeDescription
workingDirectorystringAbsolute path to the workspace Claude operates on. Required at runtime. Supports ${ENV_VAR}.
modelstringModel (e.g. claude-sonnet-5). Supports ${CLAUDE_MODEL}. SDK default when omitted.
fallbackModelstringFallback 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".
allowedToolsstring[]Tools auto-allowed without prompting.
disallowedToolsstring[]Tools removed from the model's context entirely.
systemPromptAppendstringAppended to the claude_code preset system prompt.
customSystemPromptstringFull system prompt replacement. Mutually exclusive with systemPromptAppend.
settingSourcesArray<"user" | "project" | "local">Filesystem settings sources to load. Default [] = full isolation from host ~/.claude and project settings. Include "project" to load workspace CLAUDE.md.
marketplacesRecord<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.
enabledPluginsRecord<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.
maxTurnsnumberMax conversation turns per query (runaway protection).
maxBudgetUsdnumberMax budget in USD per query.
additionalDirectoriesstring[]Additional directories Claude can access. Supports ${ENV_VAR} per entry.
sandboxobjectOpaque SDK sandbox settings passthrough (OS-level command sandboxing).
executablePathOverridestringOverride the path to the Claude executable.
dangerouslyAllowBypassPermissionsbooleanMust be true when permissionMode is "bypassPermissions".
contextFilestringFilename for the pre-built domain context file within workingDirectory. Default "context.md".
contextPromptstringDefault prompt used when buildContext() is called without an explicit prompt.

Reasoning effort and extended thinking

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

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

thinking selects the mode:

ValueBehavior
{ "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

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.thinkingSent 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

  • 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

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 for how negotiation works.

{
  "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

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 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.

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

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:

ModeBehaviourHeadless-safe
defaultInteractive approval per tool callRejected — no human in the loop
autoHeuristic/classifier-based approvalRejected — not supported for headless A2A
acceptEditsAuto-approve file edits; other guardrails still applyYes (default)
dontAskNever prompt; broadest auto-approval short of bypassYes
planRead/analyze only — no mutating tool callsYes
bypassPermissionsUnrestricted tool accessYes, 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 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

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

"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

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

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:

{
  "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

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.

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 — 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

ConfigPortPermission modeDescription
agents/example/config.json3030acceptEditsWorkspace engineer — read + write access
agents/read-only-reviewer/config.json3031plan (+ 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:

# 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:

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 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).

{
  "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

Any a2a-claude agent can delegate to other A2A agents by declaring them under subAgents in config.json. The wrapper bootstraps 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 for the full subAgents schema, auth modes, and a runnable example.

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.

EventEmitted whenNotes
agent_startedSDK system/init messageIncludes 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
thinkingAssistant thinking content blockControlled by features.emitThinkingEvents
tool_call_start / tool_call_endAssistant tool_use block / matching tool_resulttoolKind is "shell" (Bash), "mcp", "a2a_subagent" (mcp server a2a-subagents), or "builtin"; controlled by features.emitToolEvents
decision (kind: "file_change")Edit / Write / NotebookEdit tool callPath and change kind only — never file contents; controlled by features.emitFileChangeEvents
decision (kind: "todo_list")TodoWrite tool callControlled by features.emitTodoEvents
decision (kind: "permission_denied")SDK system/permission_denied messageTool name + sanitized message
agent_finishedSDK result/success messageIncludes 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_errorSDK result failure subtypes / error messageSanitized error message; reason mapped from the SDK's failure subtype (e.g. max turns, max budget)
rate_limitSDK rate_limit_event, system/api_retry with error: "rate_limit", or an assistant rate_limit erroraction 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_tasksSDK system/background_tasks_changed messageLevel 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. 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

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

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

Run with an API key:

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

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:

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

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

On this page