feat(providers): add ACP Client Protocol provider#2542
Open
joka-7 wants to merge 11 commits into
Open
Conversation
Adds acp-client provider implementing agentclientprotocol.com — a JSON-RPC 2.0 protocol where NanoClaw acts as the editor/client, spawning an AI coding agent subprocess or connecting via TCP. Protocol flow: initialize → session/new → session/prompt → collect session/update streaming chunks → result on stopReason=done. Includes host-side container config, container-side provider with LineReader/JsonRpcDispatcher/transport abstraction, fs/ request serving from /workspace, and a Python test agent (stdio + TCP modes). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Installs the ACP Client Protocol provider from the providers branch, creates the agent group, wires a trigger pattern, and sets up a Groq test agent in subprocess or TCP mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace fixed trigger-pattern step with three options: trigger prefix, default agent (engage_mode=always), or dedicated channel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… idle timeout - Honor input.continuation: skip session/new and reuse the existing sessionId. Works for TCP (stateful server); subprocess mode will return a session-not-found error that isSessionInvalid() handles correctly. - Forward input.systemContext.instructions: prepend as <system>…</system> block before the prompt text (mirrors opencode's wrapPromptWithContext). - Implement multi-turn via push(): pending queue + waitKick pattern keeps the connection open and calls session/prompt again on the same session. - Add idle timeout (ACP_CLIENT_IDLE_TIMEOUT_MS, default 60 s): setInterval tracks lastEventAt (reset on every session/update notification), closes the transport if the agent goes silent mid-turn. Check interval scales with the configured timeout so short values work correctly in tests. - abort() now closes the active transport so any in-flight rpc.request() unblocks immediately instead of waiting for the pump loop to drain. - Add 8 new tests: session resume (skips session/new, preserves sessionId), systemContext forwarding (with/without), multi-turn push (2 turns same transport, same sessionId, independent chunk accumulation per turn), abort mid-wait, idle timeout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… test server Container-side provider (acp-client.ts): - Honor input.continuation: skip session/new, reuse existing sessionId - Forward input.systemContext.instructions as <system>…</system> wrapper - abort() closes active transport so in-flight rpc.request() unblocks immediately - 8 new tests: session resume, systemContext with/without, abort mid-wait container/Dockerfile: - Pin pnpm@9 via corepack to restore global bin symlink behavior (pnpm 10+ creates wrapper scripts; claude-agent-sdk requires native binary) scripts/test-acp-client-server.py: - Add User-Agent header (Groq blocks Python-urllib default agent) - Extract <system> block and pass as Groq system message - Strip acp: prefix to send only the actual user question to Groq Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- initialize: clientCapabilities.fs (not fileSystem), terminal boolean, authMethods (not authenticationMethods), add title to clientInfo - initialize: validate protocolVersion in response - session/new: add required mcpServers:[] field - session/prompt: rename content → prompt (spec field name) - session/update: accept sessionUpdate (spec) and kind (legacy) discriminator - fs/read_text_file + fs/write_text_file: accept path (spec) and uri (legacy), add sessionId handling, support line/limit slicing - stopReason: handle end_turn, max_tokens, max_turn_requests, refusal, done; previously only cancelled and done were recognised - session/close: send best-effort close before disconnecting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…te types - Warn if agent did not declare fs capabilities after initialize - Log plan/tool_call/tool_call_update session/update types instead of silently dropping them Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Strip NanoClaw XML conversation format before sending prompt to external agent (same as BeeAI ACP provider) — non-Claude agents don't understand the XML wrapper and were producing generic responses - Fix double init event on stale session resume: emit init exactly once with the final sessionId, whether resumed or freshly created - Log pumpLoop closure instead of silently swallowing it - Add comment on session/close best-effort intent - Add comment explaining agentCapabilities.fs check rationale - Remove debug file-write artifacts from handle() (stdio mode) - Fix missing blank line in SKILL.md between Option C and next section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests were checking params.content but session/prompt now correctly sends params.prompt per the ACP Client Protocol spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eferences - Ask user upfront: primary agent or trigger prefix, and which prefix - Remove broken test-server section (script not in branch) - Remove doc reference (docs/acp-client.md deleted from branch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Option A: pattern update now handles both fresh installs (simple '.'
pattern converts to negative lookahead) and existing installs (appends
to existing lookahead). Old replace(')') silently did nothing on '.'.
Option B: remove false claim that lower priority = fallback. Router fans
out to all matching agents so both would reply. Skill now instructs user
to delete the existing Claude wiring instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an ACP Client Protocol provider that lets NanoClaw act as the
editor/client side of the Agent Client Protocol
(JSON-RPC 2.0 over TCP or stdio). Any external AI coding agent that speaks
ACP can be wired to an agent group — no container image rebuild required.
Protocol overview
ACP Client Protocol defines a JSON-RPC 2.0 exchange between an editor
(NanoClaw) and an agent (any ACP-compliant AI). The wire format is one
JSON object per line (
\n-delimited), over either a TCP socket or stdin/stdout.Handshake
editor → agent: initialize
← agent: { protocolVersion, agentCapabilities, meta, authMethods }
editor → agent: session/new { cwd, mcpServers }
← agent: { sessionId }
Per-message flow
editor → agent: session/prompt { sessionId, prompt: [{ type: "text", text }] }
← agent: session/update (notification, 0..N)
{ method: "session/update",
params: { sessionId, update: { sessionUpdate: "agent_message_chunk",
content: { role, content: [{ type, text }] } } } }
← agent: { content: [], stopReason: "end_turn" } (RPC response)
NanoClaw collects all
agent_message_chunktext blocks, joins them, and deliversthe assembled message back through the originating channel.
File callbacks (optional)
If the agent declares
agentCapabilities.fs.readTextFile / writeTextFile,it may call back into NanoClaw mid-prompt:
agent → editor (RPC request): fs/read_text_file { path }
← editor: { content: "..." }
agent → editor (RPC request): fs/write_text_file { path, content }
← editor: {}
All paths are resolved under
/workspace; requests outside that boundaryare rejected.
Session resume
sessionIdis stored inoutbound.dbafter each successful exchange.On the next message NanoClaw sends the same
sessionIdinsession/prompt.If the agent returns a session-not-found error, NanoClaw automatically opens
a new session and retries transparently.
Connection modes
commandhost+portFiles
container/agent-runner/src/providers/acp-client.tscontainer/agent-runner/src/providers/acp-client.test.tssrc/providers/acp-client.ts.claude/skills/add-acp-client-agent/SKILL.mdExample configuration
Subprocess mode (
groups/my-acp-agent/acp-client.json):{ "command": ["python3", "/path/to/my-agent.py"] } TCP mode: { "host": "host.docker.internal", "port": 7787 } container.json (set "provider": "acp-client"): { "provider": "acp-client", "groupName": "My ACP Agent", "assistantName": "My ACP Agent", "agentGroupId": "ag-..." }Testing
All 33 container tests pass:
cd container/agent-runner && bun test src/providers/acp-client.test.ts
A minimal echo-mode test server (scripts/test-acp-client-server.py) is
included for manual end-to-end verification — run it untracked alongside
a live NanoClaw instance.