Files
claude-agent-acp/docs/goal-extension.md
nikita-ashihmin 8b31dea11b feat: expose provider-neutral ACP goal extension (#964)
## Summary

- advertise a provider-neutral goal extension in
`InitializeResponse._meta.goal`
- map Claude SDK `active_goal` messages to
`session_info_update._meta.goal`
- expose the shared `_session/goal` control method with the truthfully
supported `clear` action
- document the extension as a candidate shape for a future first-class
ACP goal API

## Architecture

A goal is persistent **session state**, not the lifecycle of one
`session/prompt` request. `goal.status === "active"` says that the
objective may drive more work; it does not say that a prompt is
executing now.

This keeps two independent axes:

1. **Goal state** is published asynchronously as session metadata and
may survive multiple prompt turns.
2. **Prompt activity** follows the backend's actual running/quiescent
boundaries. While a turn runs, clients use advertised steering or prompt
queueing. Once the backend is quiescent, an ordinary `session/prompt` is
valid even if the goal remains active.

The metadata is deliberately provider-neutral:

```json
{
  "_meta": {
    "goal": {
      "version": 1,
      "controlMethod": "_session/goal",
      "actions": ["clear"]
    }
  }
}
```

State is emitted as `session_info_update._meta.goal`; `null` clears it.
The common snapshot vocabulary supports `objective`, common statuses,
timestamps in Unix milliseconds, budgets/usage, iteration count, and a
continuation reason. Each implementation emits only fields it actually
knows. This shape is intentionally close to a future typed ACP
capability/update so promotion out of `_meta` would not require a
provider-specific migration.

Control actions are capability-negotiated. The shared vocabulary
reserves `set`, `pause`, `resume`, and `clear`, but an agent advertises
only the subset it can implement with correct semantics.

## Claude mapping

The Claude runtime already emits internal `active_goal` messages,
although that message is not currently included in the public
`SDKMessage` union. The adapter handles it before the exhaustive switch:

- `condition` → `objective`
- `iterations` → `iterations`
- `set_at` → `createdAt`
- `last_reason` → `lastReason`
- `value: null` → `goal: null`

Provider bookkeeping such as `tokens_at_start` is intentionally not
exposed. Goal updates do not settle, extend, or otherwise mutate ACP
prompt lifecycle. Claude's existing Stop hook remains authoritative for
whether the backend itself continues working.

Only `clear` is advertised for out-of-band control. It submits `/goal
clear` through the existing session prompt queue, preserving the
adapter's established ordering and turn ownership. Setting a goal
remains available through `/goal <condition>` but is not advertised as a
control action because it starts the long-running Stop-hook workflow.

The companion Codex implementation is agentclientprotocol/codex-acp#371.

## Verification

- `npm run test:run` — 692 passed, 20 skipped
- `npm run build`
- `npm run check`
2026-08-06 17:01:56 +04:00

3.0 KiB

Goal extension

This document defines a provider-neutral experimental ACP extension implemented by claude-agent-acp. It is intentionally shaped like a possible future first-class ACP API: implementations publish _meta.goal, not provider-specific metadata such as _meta.claudeCode.goal.

Capability negotiation

An agent advertises support in its initialize response:

{
  "_meta": {
    "goal": {
      "version": 1,
      "controlMethod": "_session/goal",
      "actions": ["clear"]
    }
  }
}

actions is the implementation-supported subset of set, pause, resume, and clear. Clients must not infer support for an action that is not advertised. The control request contains sessionId and action; a future version may add action-specific fields such as an objective for set.

Session state

The current snapshot is published in session_info_update._meta.goal. Clearing a goal publishes goal: null.

{
  "objective": "Ship the change",
  "status": "active",
  "iterations": 3,
  "lastReason": "Tests still need work",
  "createdAt": 1710000000123,
  "controlMethod": "_session/goal"
}

Common statuses are active, paused, blocked, limited, and complete. Optional fields allow implementations to report budgets, usage, iteration count, and the last continuation reason. Timestamps are Unix milliseconds.

Lifecycle architecture

A goal belongs to the ACP session, not to an individual session/prompt request. Goal activity and prompt activity are independent:

  • status: active means the persistent objective can drive more work; it does not mean an ACP prompt is currently executing.
  • A prompt completes when its current backend turn reaches a quiescent boundary, even when the goal remains active.
  • A later autonomous cycle may publish more session updates outside that completed prompt.
  • While a turn is running, clients use steering or prompt queueing when advertised. While the session is quiescent, clients may send an ordinary session/prompt.

This separation prevents a persistent goal from monopolizing the session's prompt slot and lets clients model “working now” independently from “objective remains active.”

Claude mapping

Claude's session-scoped /goal command installs a Stop hook. The runtime emits internal active_goal messages when that hook updates or clears its state; the adapter maps them into the neutral snapshot before the exhaustive SDK message switch. condition, iterations, set_at, and last_reason become objective, iterations, createdAt, and lastReason. Provider-only bookkeeping such as tokens_at_start is not exposed.

Only clear is advertised for out-of-band control. The adapter submits /goal clear through the existing session prompt queue, preserving the same ordering and lifecycle rules as other Claude commands. Setting a goal remains available through /goal <condition> and is not advertised as an extension action because it starts Claude's long-running Stop-hook workflow.