A field-tested integration guide — architecture, full source code, and eleven pitfalls we hit so you don’t have to.
- Components: Hermes Agent (open-source agent runtime) · CodeBuddy Code CLI 2.180.0 (Tencent’s Claude-Code-style coding CLI) · systemd user services
- Scope: Replace an agent’s direct model-API calls with a local, resident CodeBuddy CLI reached over ACP (Agent Client Protocol) — without touching the agent’s core loop.
- Status: Running in production on our side; every command and code block below is verbatim from the working deployment.
1. Why this integration
Hermes Agent already ships a complete agent loop: conversation history, tool execution, permissions, memory, retries. CodeBuddy CLI ships a full coding-agent runtime: its own model access, streaming, session management, and an ACP server.
The naive options are both bad:
- Call the model API directly — you pay for (or configure) yet another upstream provider and duplicate what CodeBuddy already provides.
- Treat CodeBuddy as a second agent — two autonomous agents executing tools on the same task means duplicated side effects, double spend, and diverging history.
What we wanted: CodeBuddy becomes a “model brain” only. Hermes keeps 100% ownership of tools, permissions, and history. CodeBuddy is reduced to a resident inference endpoint that speaks JSON-RPC over HTTP.
1 | Hermes AIAgent / conversation loop |
Result: a full multi-turn, tool-using Hermes session where every inference round-trips through a local CodeBuddy. Streaming time-to-first-token ~1–2 s on warm sessions; one-command rollback to the previous provider.
2. Five design decisions (and why)
codebuddy --serve, notcodebuddy daemon start.
In 2.180.0,daemon startaccepts only--host/--port— it has no--toolsflag.--serveruns the same HTTP surface (REST + ACP over HTTP SSE) on a fixed port with password auth, and honors--tools ""(disable all built-in tools). One harmless leak: the model may issue aToolSearchprobe, find nothing, and move on — semantically tools stay disabled.User-level provider plugin, zero repo patches.
Hermes loads plugins from$HERMES_HOME/plugins/model-providers/<name>/and later registrations override bundled profiles. Everything lives outside the agent’s source tree — upgrades never force a rebase.A fresh ACP connection + session per completion.
Hermes is the single source of truth for conversation history. A CodeBuddy session represents exactly one inference call; the resident server is the only long-lived thing. This eliminates history duplication, retry forks, and cross-session bleed.--tools ""— CodeBuddy executes nothing.
Hermes’ tool schemas travel into the prompt as text (Hermes’ ACP bridge already does this), and the model’s<tool_call>{...}</tool_call>output is parsed back into OpenAItool_calls. Two agents executing tools is the single biggest semantic hazard; we removed it structurally, plus the client auto-denies anyinterruption_request(tool-permission prompt) with a feedback message.One turn at a time per server.
Measured: CodeBuddy serve serializes agent work per account and silently drops the loser’s final result under concurrency. A module-level lock perbase_urlfixes it; sequential agent turns pay nothing.
3. Prerequisites
- A Linux box with ≥ 4 GB RAM strongly recommended (our 2 GB VM OOM-killed things until we trimmed services — see §9).
codebuddyCLI installed and signed in (verify:codebuddy -p "Reply with exactly: OK"printsOK).- Node ≥ 18; git;
systemduser services withLinger=yesfor the running user. - Hermes Agent installed at
$HERMES_HOME/hermes-agent, running under a non-root user (call itsvcbelow). - Tag the Hermes repo before you start (we change nothing in it, but leave a trail):
git -C $HERMES_HOME/hermes-agent tag poc-baseline-$(date +%Y%m%d)
4. Step 1 — Make the CLI usable for the service user
The trap that cost us the most time: CodeBuddy’s sign-in state is not in ~/.codebuddy. Find it with strace:
1 | strace -f -e trace=openat -o /tmp/st.log timeout 90 \ |
Copy the three pieces to the service user (here: svc, home /home/svc):
1 | # 1) the actual credential (XDG data dir!) |
⚠️ Copying only
settings.json/user-state.json/local_storageis not enough — the credential lives in the XDG data dir. This single fact cost us a full debugging loop.
5. Step 2 — The resident service (systemd user unit)
/home/svc/.config/systemd/user/codebuddy-acp.service:
1 | [Unit] |
1 | loginctl show-user svc | grep Linger # want: Linger=yes (else: loginctl enable-linger svc) |
First start auto-generates the Web password into ~/.codebuddy/settings.json under gateway.password (the log prints where auth comes from). Feed it to the plugin via Hermes’ env file:
1 | grep -q CODEBUDDY_DAEMON_PASSWORD $HERMES_HOME/.env || \ |
ℹ️ Pure
--acpmode (without--serve) is password-free but binds a random port —--portis ignored there — which makes the fixed systemd URL impossible. That’s why we chose--serve.
6. Step 3 — The provider plugin (full source)
Directory layout (all under $HERMES_HOME/plugins/model-providers/codebuddy-daemon/):
1 | plugin.yaml |
6.1 plugin.yaml
1 | name: codebuddy-daemon |
6.2 __init__.py
1 | """CodeBuddy daemon provider profile: the resident CodeBuddy CLI reached over local HTTP ACP.""" |
6.3 daemon_client.py
The whole file (445 lines) verbatim. Nine non-negotiable rules are encoded here — see §6.4 for the reasoning:
- reuse Hermes’ own ACP text bridge and tool-call parser (
agent.acp_client,agent.acp_openai_bridge) — never hand-roll prompt rendering; - lifecycle per completion:
login → connect → initialize → session/new → (set_model / set thinking) → prompt → DELETE connection; never stop the server; - every request carries
X-CodeBuddy-Request: 1(fixed value) andAccept: application/json, text/event-stream; - the POST’s response body is itself an SSE stream — parse
data:lines; the JSON-RPC result and allsession/updatenotifications arrive inline there; - map
agent_message_chunk → delta.content,agent_thought_chunk → delta.reasoning_content, auto-denyinterruption_request; - tool-free requests stream live; tool-bearing requests buffer the full text first (never stream half a tool-call JSON);
- usage is always the zero-value shape (see §9, pitfall 10);
- one transparent auth retry only before the first delta; after that, errors propagate;
- a per-
base_urllock wraps the whole turn.
1 | """CodeBuddy resident-CLI ACP client: an OpenAI ``chat.completions`` facade over CodeBuddy's |
6.4 Regression scripts
Two standalone scripts validated the client before and after cutover (plain text; tool bridge emitting get_time with correct JSON arguments; live-stream TTFT; two concurrent streams; wrong-password and unreachable-server error paths). Keep them — they are your contract tests for every CLI upgrade.
7. Step 4 — Cutover & verification
1 | # 1) back up config |
Verification ladder (cheap → production):
- Client level (in-process, no gateway): plain text reply; a tool schema that must come back as
tool_callswith valid arguments;stream=Truewith sub-2s TTFT. - Hermes one-shot:
hermes --provider codebuddy-daemon -m <model> -z "Reply with exactly: PONG". - Production: a real message to the bot; confirm in
journalctl --user -u hermes-gatewaywhich provider served the turn.
🚨 Never run
hermesone-shots while the gateway is running on a small box. The CLI detects the running gateway and hands the turn over — you end up with two full agent processes (≈1 GB combined). On a 2 GB VM this OOM-kills the gateway. We did this three times so you don’t have to. Production validation = real messages through the gateway process itself.
8. Operations runbook
1 | # daily |
Monitoring: gateway RSS (>550 MB warn), host memory available (<300 MB warn), OOM in dmesg, turn error rate, non-end_turn stopReasons, TTFT, and a credits reconciliation channel on the CodeBuddy side (client-side usage is deliberately zero — see below).
9. Eleven pitfalls, compressed
| # | Symptom | Root cause | Fix |
|---|---|---|---|
| 1 | spawn node EACCES as the service user |
sudo/ssh inherited cwd the user can’t enter | cd into the user’s home first |
| 2 | “Authentication required” after copying settings | credential lives in ~/.local/share/CodeBuddyExtension/... |
copy the XDG auth file (§4) |
| 3 | Every API call: Missing required header |
CSRF-style design | send X-CodeBuddy-Request: 1 |
| 4 | POST returns an empty body | response is an SSE stream (streamable-http semantics) | parse data: lines; require the SSE Accept header |
| 5 | session/new: “Server not initialized” |
ACP state machine | initialize first, per connection |
| 6 | daemon start can’t disable tools |
no --tools flag on the daemon subcommand |
use --serve + --tools "" |
| 7 | Concurrent turns: loser’s result vanishes | serve serializes per account | client-side lock per base_url |
| 8 | Gateway OOM-killed | 2 GB box, two agent processes | right-size the VM; never oneshot-while-gateway-runs |
| 9 | CLI updates itself, protocol drifts | auto-updater on | DISABLE_AUTOUPDATER=1; contract-test before upgrades |
| 10 | Treating usage_update as billing |
it’s context occupancy (used/size), not tokens |
report zero-value usage; reconcile credits separately |
| 11 | Plugin “not loaded” | plugins load at gateway start | install plugin before starting/restarting the gateway |
10. Closing notes
- What this is not: it does not make inference free or local — CodeBuddy still runs the model in its cloud, under its quota and terms. The resident server removes process startup cost, nothing else.
- Stability: CodeBuddy’s HTTP API is labeled Beta; pin the CLI version, keep the two regression scripts as contract tests, and upgrade deliberately.
- Measured results on our deployment: text round-trips 5–20 s (thinking-heavy), warm-session one-shots ~2–5 s, streaming TTFT 1.2–2.3 s, full multi-turn tool loop through Hermes with zero core-loop patches.
If this guide saved you a weekend, the credit goes to the Hermes Agent and CodeBuddy teams — we just wired the plugs together.