Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 82 additions & 10 deletions .config/mise/tasks/acp-freshness
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,88 @@ crates_io() {
jq -r '.crate.max_stable_version // .crate.max_version'
}

# The SDK pins its schema crate exactly, so a newer schema on crates.io is only
# adoptable once the SDK itself moves to it. Resolve that pin to avoid reporting
# schema drift nobody can act on.
sdk_schema_pin() {
curl -sf --retry 3 -A "trogonai-acp-freshness" \
"https://crates.io/api/v1/crates/agent-client-protocol/$1/dependencies" |
jq -r '.dependencies[] | select(.crate_id == "agent-client-protocol-schema" and .kind == "normal") | .req' |
grep -oE '^=[0-9]+\.[0-9]+\.[0-9]+$' | tr -d '='
}

pinned="$(grep -oE 'agent-client-protocol = \{ version = "=?[0-9]+\.[0-9]+\.[0-9]+"' "$root/rsworkspace/Cargo.toml" |
grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
if [[ -z "$pinned" ]]; then
echo "Could not find pinned agent-client-protocol version in rsworkspace/Cargo.toml" >&2
exit 1
fi

bundled_schema="$(grep -A2 'name = "agent-client-protocol-schema"' "$root/rsworkspace/Cargo.lock" |
grep -oE 'version = "[0-9]+\.[0-9]+\.[0-9]+"' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
# Every resolved version, not just the first: cargo writes same-name packages
# oldest-first, so reading one entry hides a forked graph behind a healthy-looking
# version. Two schema crates is the failure this whole pin exists to prevent.
lock_package_versions() {
awk -F' = ' -v pkg="$1" '
/^\[\[package\]\]/ { inpkg = 1; name = ""; next }
/^\[/ { inpkg = 0 }
inpkg && /^name = / { name = $2; gsub(/"/, "", name); next }
inpkg && /^version = / { version = $2; gsub(/"/, "", version); if (name == pkg) print version; next }
' "$2"
}

mapfile -t schema_versions < <(lock_package_versions agent-client-protocol-schema "$root/rsworkspace/Cargo.lock" | sort -u -V)
schema_count="${#schema_versions[@]}"
if ((schema_count == 0)); then
bundled_schema="unknown"
else
bundled_schema="$(
IFS=,
echo "${schema_versions[*]}"
)"
bundled_schema="${bundled_schema//,/, }"
fi

latest_sdk="$(crates_io agent-client-protocol)"
latest_schema="$(crates_io agent-client-protocol-schema)"
latest_sdk_schema_pin="$(sdk_schema_pin "$latest_sdk" || true)"

# Adoptable schema level: whatever the latest SDK pins, falling back to the
# newest published schema when the SDK does not pin one exactly.
schema_target="${latest_sdk_schema_pin:-$latest_schema}"

sdk_drift=false
if [[ "$pinned" != "$latest_sdk" ]]; then
sdk_drift=true
fi
schema_forked=false
schema_drift=false
if ((schema_count > 1)); then
schema_forked=true
schema_drift=true
elif [[ "$bundled_schema" != "$schema_target" ]]; then
schema_drift=true
fi
Comment thread
cursor[bot] marked this conversation as resolved.
drift=false
if [[ "$pinned" != "$latest_sdk" || "${bundled_schema:-unknown}" != "$latest_schema" ]]; then
if [[ "$sdk_drift" == "true" || "$schema_drift" == "true" ]]; then
drift=true
fi

echo "ACP freshness:"
echo " SDK pinned=$pinned latest=$latest_sdk"
echo " schema bundled=${bundled_schema:-unknown} latest=$latest_schema"
echo " drift=$drift"
echo " schema resolved=$bundled_schema adoptable=$schema_target latest=$latest_schema"
echo " drift=$drift (sdk=$sdk_drift schema=$schema_drift forked=$schema_forked)"

if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
{
echo "pinned=$pinned"
echo "latest_sdk=$latest_sdk"
echo "bundled_schema=${bundled_schema:-unknown}"
echo "bundled_schema=$bundled_schema"
echo "schema_count=$schema_count"
echo "latest_schema=$latest_schema"
echo "schema_target=$schema_target"
echo "sdk_drift=$sdk_drift"
echo "schema_drift=$schema_drift"
echo "schema_forked=$schema_forked"
echo "drift=$drift"
} >>"$GITHUB_OUTPUT"
fi
Expand All @@ -46,16 +99,35 @@ if [[ "$drift" != "true" || "${ACP_FRESHNESS_FILE_ISSUE:-}" != "true" ]]; then
exit 0
fi

title="ACP drift: agent-client-protocol $pinned is behind $latest_sdk"
if [[ "$sdk_drift" == "true" ]]; then
title="ACP drift: agent-client-protocol $pinned is behind $latest_sdk"
lead="The pinned \`agent-client-protocol\` version has fallen behind crates.io."
elif [[ "$schema_forked" == "true" ]]; then
title="ACP drift: the graph carries $schema_count agent-client-protocol-schema versions ($bundled_schema)"
lead="\`Cargo.lock\` resolves more than one \`agent-client-protocol-schema\` version. Cargo unifies features per crate version, so the schema-level unstable flags the direct dependency turns on no longer reach the types \`agent-client-protocol\` re-exports, and the bridge silently drops every field they gate. Nothing fails to compile. Pin the direct dependency back to the single version the SDK requires."
else
# Direction-neutral: the resolved version can be ahead of the target as easily
# as behind it, and calling an ahead version "behind" sends the reader the wrong way.
title="ACP drift: agent-client-protocol-schema $bundled_schema does not match the $schema_target that agent-client-protocol $latest_sdk requires"
lead="The resolved \`agent-client-protocol-schema\` version is not the schema level \`agent-client-protocol\` $latest_sdk builds on."
fi

schema_note=""
if [[ "$schema_target" != "$latest_schema" ]]; then
schema_note="

\`agent-client-protocol\` $latest_sdk pins \`agent-client-protocol-schema\` at exactly $schema_target, so schema $latest_schema and anything it adds are not adoptable until the SDK moves. Track the gap in the conformance matrix rather than bumping the schema past the SDK's pin: a mismatched direct dependency puts two schema crates in the graph and the schema-level unstable flags stop reaching the SDK's re-exported types."
fi

body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
cat >"$body_file" <<EOF
The pinned \`agent-client-protocol\` version has fallen behind crates.io.
$lead

| | pinned | latest |
| | resolved | target |
| --- | --- | --- |
| SDK (\`agent-client-protocol\`) | $pinned | $latest_sdk |
| Schema (\`agent-client-protocol-schema\`) | ${bundled_schema:-unknown} | $latest_schema |
| Schema (\`agent-client-protocol-schema\`) | $bundled_schema | $schema_target |$schema_note

A version bump is never just a version change. Per the upgrade ritual in \`docs/architecture/acp-conformance.md\`, the bump PR must:

Expand Down
9 changes: 9 additions & 0 deletions docs/adr/0020-acp-sdk-1x-boundary-and-bridge-traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ delegation, per the zero-cost passthrough rule in `rsworkspace/crates/AGENTS.md`
> semantics are unchanged; the per-method surface lives only in the bridge
> trait and one match expression.

> Amended 2026-07-27: this decision survives the SDK 2.0 major bump unchanged.
> 2.0 rewrites the transport boundary around batch-aware frames and renames the
> response-router methods, but because the adapter delegates through the
> `ClientRequest`/`ClientNotification` enums rather than the low-level channel,
> the entire migration was one method rename. Inbound JSON-RPC batches now work
> through the bridge for free: the SDK splits them into independent dispatches
> and regroups the replies, so the NATS leg keeps carrying one message per
> subject message.

The adapters are shared by `acp-nats-server` (WebSocket and HTTP duplex) and
`acp-nats-stdio`, so they live in one place: the `boundary` module of
`acp-nats`. That module is the single SDK-connection-aware part of the crate;
Expand Down
23 changes: 14 additions & 9 deletions docs/architecture/acp-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ This document is the single source of truth for where this repository stands rel
| Fact | Value |
| --- | --- |
| Wire protocol | v1 |
| Pinned Rust SDK | `agent-client-protocol` 1.2.0 (`rsworkspace/Cargo.toml`) |
| Bundled schema (effective spec level) | 1.4.0 (plus direct `agent-client-protocol-schema` dependency for schema-only unstable flags) |
| Latest upstream SDK at last review | 1.2.0 (2026-07-07) |
| Latest upstream schema at last review | 1.4.0 (2026-07-06) |
| Last reviewed | 2026-07-08 |
| Pinned Rust SDK | `agent-client-protocol` 2.0.0 (`rsworkspace/Cargo.toml`) |
| Bundled schema (effective spec level) | 1.5.0 (plus direct `agent-client-protocol-schema` dependency for schema-only unstable flags) |
| Latest upstream SDK at last review | 2.0.0 (2026-07-23) |
| Latest upstream schema at last review | 1.6.0 (2026-07-21) |
| Highest adoptable schema | 1.5.0 — the SDK pins `agent-client-protocol-schema` at exactly `=1.5.0` |
| Last reviewed | 2026-07-27 |

Upstream repositories: [spec/schema](https://github.com/agentclientprotocol/agent-client-protocol), [Rust SDK](https://github.com/agentclientprotocol/rust-sdk).

The SDK's exact schema pin caps our effective spec level. The direct schema dependency exists only to turn on schema-level unstable flags the SDK facade does not forward, and it works because cargo unifies features on a single schema crate. Pinning it past the SDK's requirement would put two schema crates in the graph, at which point the flags no longer apply to the types the SDK actually re-exports, so schema releases above the SDK's pin are tracked as gaps here rather than adopted early.

## Policy

Opt in to unstable spec features ahead of stabilization. The default for every unstable feature is to enable the flag, wire the routing, and test it. Opting out is the exception and requires a rationale in the matrix below.
Expand All @@ -29,15 +32,15 @@ Status values: `implemented` (routed, typed, tested), `capabilities implemented`

### Agent-side methods (client to agent)

| Spec surface | Spec stage (schema 1.4.0) | Our status | Notes |
| Spec surface | Spec stage (schema 1.5.0) | Our status | Notes |
| --- | --- | --- | --- |
| `initialize` | stable | implemented | |
| `authenticate` | stable | implemented | `unstable_auth_methods` shapes enabled |
| `logout` | stable (0.13.3) | implemented | |
| `session/new` | stable | implemented | includes `additionalDirectories` |
| `session/load` | stable | implemented | includes `additionalDirectories` |
| `session/list` | stable | implemented | |
| `providers/list` | unstable (0.11.7) | unrepresentable | not a routing gap: bridge-owned `AgentHandler::list_providers` and NATS subject routing (`providers.list`) are implemented and tested, but `agent-client-protocol` 1.2.0 cannot express the request at the byte-stream boundary (no `unstable_llm_providers` feature, provider types omitted from its `JsonRpcRequest` registrations); blocked on upstream SDK support |
| `providers/list` | unstable (0.11.7) | unrepresentable | not a routing gap: bridge-owned `AgentHandler::list_providers` and NATS subject routing (`providers.list`) are implemented and tested, but `agent-client-protocol` 2.0.0 cannot express the request at the byte-stream boundary (no `unstable_llm_providers` feature, and the provider variants the schema does define are omitted from the SDK's `ClientRequest` method table); blocked on upstream SDK support |
| `providers/set` | unstable (0.11.7) | unrepresentable | see `providers/list`; `AgentHandler::set_provider` and NATS subject routing (`providers.set`) implemented and tested |
| `providers/disable` | unstable (0.11.7) | unrepresentable | see `providers/list`; `AgentHandler::disable_provider` and NATS subject routing (`providers.disable`) implemented and tested |
| `session/prompt` | stable | implemented | |
Expand All @@ -50,6 +53,7 @@ Status values: `implemented` (routed, typed, tested), `capabilities implemented`
| `session/close` | stable (0.12.2) | implemented | |
| `session/delete` | stable (0.13.6) | implemented | routed end to end with tests, span `acp.session.delete` |
| JSON-RPC request cancellation | stable (1.2.0) | implemented | boundary honors `$/cancel_request`: bridge-side work is dropped and the request answers with `request_cancelled` (tested); prompt-turn cancellation on the runner side remains `session/cancel` per spec |
| JSON-RPC batches (inbound) | stable (SDK 2.0.0) | implemented | the SDK splits an inbound batch into independent dispatches and regroups the replies into one response array, so every routed method works inside a batch and unrouted ones still answer `method_not_found`; round-trip tested at the boundary. NATS carries one JSON-RPC message per subject message, so a batch never reaches the runner as a batch, and the bridge never emits one: the SDK sends typed requests and notifications individually |
| `ext/*` (extension methods) | stable | implemented | passthrough |

### Client-side methods (agent to client)
Expand Down Expand Up @@ -82,8 +86,9 @@ Status values: `implemented` (routed, typed, tested), `capabilities implemented`
| NES (next edit suggestions) | unstable | capabilities implemented | capability payloads round-trip via schema-level flag; NES document sync methods are not routed (no runner demand yet, revisit with Phase 4 adoption cadence) |
| Plan operations | unstable (0.13.4) | implemented | `PlanUpdate`/`PlanRemoved` round-trip tested via schema-level flag |
| Providers | unstable (0.11.7) | unrepresentable | see `providers/list`/`providers/set`/`providers/disable` rows above |
| MCP-over-ACP message types | unstable (0.13.0) | implemented | `McpServer::Acp` and `McpCapabilities.acp` payload round-trip tested via schema-level and `unstable_mcp_over_acp` SDK flag; the `mcp/connect`, `mcp/message`, `mcp/disconnect` RPC methods are not routed (no runner demand yet, revisit with Phase 4 adoption cadence) |
| MCP-over-ACP message types | unstable (0.13.0) | implemented | `McpServer::Acp` and `McpCapabilities.acp` payload round-trip tested via schema-level and `unstable_mcp_over_acp` SDK flag; SDK 2.0.0 dropped its own wire types in favor of the schema-native ones the bridge already used; the `mcp/connect`, `mcp/message`, `mcp/disconnect` RPC methods are not routed (no runner demand yet, revisit with Phase 4 adoption cadence) |
| Elicitation enum option descriptions | unstable (1.4.0) | implemented | `EnumOption` descriptions on `StringPropertySchema.one_of` round-trip tested |
| Tool call `name` | unstable (1.6.0) | unrepresentable | `unstable_tool_call_name` adds `name` to `ToolCall` and `ToolCallUpdateFields`, but the flag only exists in schema 1.6.0 and the pinned SDK requires schema `=1.5.0`; enabling it early would fork the schema crate and silently disarm every other schema-level flag. Adopt in the SDK release that moves to schema 1.6.0; until then the field is dropped by typed re-encode |
| Protocol v2 | unstable, heavy churn | watch-only | adopt once upstream marks it preview; the freshness workflow surfaces every release it churns in |

## Upgrade ritual
Expand All @@ -93,7 +98,7 @@ A version bump of `agent-client-protocol` (or the schema it bundles) is never ju
1. Diff the schema changelog between the old and new pinned versions ([changelog](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/CHANGELOG.md)).
2. For each added or stabilized method: add subject mapping in `acp-nats/src/nats/parsing.rs`, a handler (bridge trait method plus its match arm in the boundary dispatch and [NATS](../glossary/nats) dispatch), and tests, or add a matrix row with an opt-out rationale. The byte-stream boundary no longer needs per-method registrations: `connect_agent_boundary` routes through the SDK's `ClientRequest`/`ClientNotification` enums, so a method missing from its match answers `method_not_found` instead of being silently unreachable (see [ADR#0020](../adr/0020-acp-sdk-1x-boundary-and-bridge-traits.md), amendment of 2026-07-09).
3. For each added field or `session/update` variant: add a round-trip test through the bridge. Typed re-encode means unmapped fields are silently dropped, so a green compile proves nothing about coverage.
4. For each new unstable flag: enable it per the opt-in policy and wire it.
4. For each new unstable flag: enable it per the opt-in policy and wire it. A flag that only exists in a schema release above the SDK's exact pin cannot be enabled; move the direct schema dependency in lockstep with the SDK's requirement and record the gap as a matrix row instead.
5. Update this document (matrix and spec position table) in the same PR.

The scheduled freshness workflow (`.github/workflows/acp-freshness.yml`) embeds this checklist in the issue it files when drift is detected.
Loading
Loading