Skip to content

feat(providers): add ACP Client Protocol provider#2542

Open
joka-7 wants to merge 11 commits into
nanocoai:providersfrom
joka-7:feature/acp-agent-client-protocol
Open

feat(providers): add ACP Client Protocol provider#2542
joka-7 wants to merge 11 commits into
nanocoai:providersfrom
joka-7:feature/acp-agent-client-protocol

Conversation

@joka-7
Copy link
Copy Markdown

@joka-7 joka-7 commented May 18, 2026

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_chunk text blocks, joins them, and delivers
the 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 boundary
are rejected.

Session resume

sessionId is stored in outbound.db after each successful exchange.
On the next message NanoClaw sends the same sessionId in session/prompt.
If the agent returns a session-not-found error, NanoClaw automatically opens
a new session and retries transparently.

Connection modes

Mode Config key How it works
Subprocess command NanoClaw spawns the agent as a child process; JSON-RPC over stdin/stdout
TCP host + port NanoClaw connects to a running agent server

Files

File Purpose
container/agent-runner/src/providers/acp-client.ts Core provider: transport, dispatcher, session lifecycle
container/agent-runner/src/providers/acp-client.test.ts 33 unit tests (bun:test)
src/providers/acp-client.ts Host-side config reader — injects env vars into the agent container
.claude/skills/add-acp-client-agent/SKILL.md Install + wiring skill

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

joka-7 and others added 11 commits May 17, 2026 22:40
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant