feat(acp): serve Kannaka's memory as an ACP agent (kannaka-acp) - #660
Merged
Conversation
Adds `kannaka-acp`, an Agent Client Protocol agent that answers prompts by resonating them through the HRM. This lets Kannaka register as a Buzz harness via the upstream "bring your own harness" seam (block/buzz#2773) — a JSON definition in `<app_data>/custom_harnesses/`, so no fork of Buzz is required. Buzz Relay --WS--> buzz-acp --ACP/stdio--> kannaka-acp --> HRM (read-only) Layout: src/acp/protocol.rs JSON-RPC 2.0 / NDJSON framing src/acp/server.rs method dispatch behind a `MemorySource` trait src/acp/mod.rs transport loop + read-only HRM source src/bin/kannaka_acp.rs config/kannaka-acp-harness.json harness definition for the BYOH gallery Notes on the design: - Read-only is enforced twice — `KANNAKA_READONLY=1` and `HrmStore::set_readonly(true)`. The HRM is single-writer and neither alone closes the write path. Mirrors `attention serve` and `swarm`. - Transport is a blocking line loop, not async: this crate deliberately dropped tokio with the old MCP server, and ACP over stdio does not need it. - Dispatch is pure (`Inbound` in, frames out) and the substrate sits behind a trait, so the whole protocol surface is testable with no HRM on disk. - A failed recall is reported in-band with `stopReason: end_turn` rather than as a JSON-RPC error, because buzz-acp treats an error on `session/prompt` as an agent fault and recycles the process. - stdout carries frames only; every diagnostic goes to stderr. Known gap: buzz-acp only logs `agent_message_chunk` and never publishes it, so replies do not yet reach a Buzz channel. That needs an MCP client calling `send_message` on the server the harness injects into `session/new`. The desktop BYOH gallery renders replies itself and works today. 33 new tests; full lib suite 883 passed / 0 failed. Verified end-to-end against the live HRM, which was confirmed unmodified afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…limit `src/acp/server.rs` landed at 599 lines, over this crate's file-size rule. Two extractions, both of which also improve separation: - `src/acp/render.rs` — answer formatting, split from protocol dispatch so wording and wire format can change independently. Adds boundary tests for the 1h/24h age transitions and content trimming. - `tests/acp_dispatch.rs` — the dispatch tests, which drive only the public surface (decoded messages in, frames out), so they belong in `tests/` per the project layout rules. Adds `Agent::memory()` so those tests can assert which queries actually reached the substrate now that they sit outside the module. No behavior change. All files now under 500 lines (server.rs 599 -> 268). 36 acp tests preserved (20 lib + 16 integration) and re-verified end-to-end against the live HRM. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the round trip. `buzz-acp` logs `agent_message_chunk` but never
publishes it — its only two `publish_event` call sites are presence and
observer telemetry — so an agent's answer never reached the channel. The
harness base prompt names the `buzz` CLI as the agent's interface, so that is
the path taken here (not MCP; `--mcp-command` is optional extra tooling and
defaults to empty):
buzz messages send --channel <uuid> [--reply-to <event>] --content -
Credentials arrive by inheritance: buzz-acp spawns agents without
`env_clear()`, so BUZZ_RELAY_URL / BUZZ_PRIVATE_KEY are already present.
src/acp/buzz_cli.rs reply-target parsing + the CLI sink
src/acp/prompt.rs recovering the actual question from the prompt
Two bugs found while building this, both worth calling out:
- The harness renders the flag inside backticks — ``use `--reply-to <id>` `` —
so splitting on whitespace dragged a trailing backtick into the event id and
every reply silently lost its threading. Now takes the leading hex run.
- The recall query was the *entire* assembled prompt, `[Context]` scaffolding
included. That both let boilerplate dominate the resonance vector (recall
matching "Context/Channel/Event" instead of the question) and echoed harness
internals back to real users. `session_prompt` now keeps two views: `full`
for locating the reply destination, `query` for resonating. Only end-to-end
testing surfaced this — a mock sink accepts any string.
Security: the reply destination is parsed ONLY from the harness-authored
`[Context]` block, never the wider prompt, which carries untrusted participant
text. Scanning all of it would let anyone redirect this agent's replies by
typing `--reply-to` into a message. Channel UUID and event id are both
format-validated, with tests asserting an injected anchor in `[Event]` is
ignored.
A missing `buzz` CLI is probed once at startup and degrades to streaming only
— correct for the desktop harness gallery, which renders replies itself and
would otherwise show the answer twice.
61 acp tests (40 lib + 21 integration). Verified end-to-end against the live
HRM with a stub CLI: correct argv, body over stdin, and a clean query.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… `acp` arg
Handshaking `kannaka-acp` with the actual `buzz-acp` client (rather than
frames written by hand) failed twice. Both are compatibility bugs no amount of
mock-based testing would have surfaced.
1. `error: agent timed out (10s)`
Opening the medium reads and reconstructs the whole tensor — 15.9s for a
47 MB kannaka.hrm — and it happened before `initialize` could be answered.
ACP clients read silence as a dead agent, and buzz-acp's helper subcommands
allow 10s. The agent looked broken before it could say hello.
`HrmMemory` now holds `Option<KannakaMemorySystem>` and opens on first
recall, so the handshake never touches the substrate: 15.9s -> 2.5s, and a
client that only probes capabilities never pays the load at all. The first
prompt pays it (~17s, now logged with timing), well inside the 60s per-turn
idle timeout. `KANNAKA_READONLY` is still asserted up front, before any code
path can construct a store.
2. `unknown argument: acp` (exit 2)
`buzz-acp --agent-args` defaults to `"acp"` because goose is spawned as
`goose acp`. Its `normalize_agent_args` strips that token only for runtimes
it recognizes, so an unrecognized command receives it verbatim — meaning the
*default* invocation, the one everybody hits first, died immediately. A bare
`acp` token is now accepted and ignored; we are always in ACP mode.
Verified against the real client: `buzz-acp auth-methods` returns
`{"methods": []}` and `buzz-acp models` reads back agentInfo
`kannaka-acp 0.12.0`. Re-verified end-to-end with a stub CLI that the lazy open
still yields a clean query and a posted reply. 61 acp tests unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <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.
Adds
kannaka-acp, a binary that serves Kannaka's HRM as an ACP agent over stdio — so Kannaka can join a Buzz workspace as a first-class agent member.Why this shape
Buzz upstream shipped a "bring your own harness" seam (block/buzz#2773): a generic ACP runtime registered from a JSON definition. Speaking ACP therefore means Kannaka plugs into the Buzz desktop client with no fork and no upstream PR — the strategic reason to prefer it over carrying fork weight. This is Tier 1 of ADR-0052.
What's here
src/acp/— protocol (NDJSON framing, notContent-Length), server, dispatch, prompt parsing, rendering, and abuzzCLI reply sink. Plussrc/bin/kannaka_acp.rsand the harness definition.Details worth review
HrmMemory::opensetsKANNAKA_READONLY=1andHrmStore::set_readonly(true)— neither alone closes the write path.[Context],[Thread Context],[Agent Memory],[Event]). Querying the whole blob lets scaffolding dominate the resonance vector and echoes harness internals back to users, so we extract theContent:of the last event block.[Context]. The prompt carries untrusted participant text; scanning all of it for--reply-towould let anyone redirect the agent's replies.agent_message_chunk. The agent must send its own reply, and the path is thebuzzCLI, not MCP. The sink is opt-in by probe, which is correct for the desktop gallery (it renders chunks itself, so posting would double the answer).Verification
61 ACP tests; full lib suite 890 pass / 0 fail. Handshake verified against the real
buzz-acpclient —auth-methods→{"methods": []},models→ agent info — which is the only thing that caught the two bugs above.🤖 Generated with Claude Code