feat: add ZCode as a BYOA engine via the zcode-acp-server ACP bridge - #247
Conversation
|
Thanks — I read through this one. The shape is right for a new engine: it goes in through the adapter/registry seam rather than around it, and I specifically want to note that adding It cannot merge right now: it conflicts with Two things to check while rebasing, both new since you branched:
Also worth a line in the PR description: what the ACP bridge process is, whether it is spawned by the daemon or expected to be already running, and what it listens on. For a new engine that is the part I most need to understand before merging, and it is not in the description today. CI has not run on this yet because of the conflict — push the rebase and it will. |
Drive the zcode CLI through the zcode-acp-server bridge's standard ACP stdio surface (initialize / session/new / session/prompt), reusing the GrokSession wire pattern for both a persistent per-agent session and the one-shot run/classify/probe shape. The bridge resolves at spawn time: CUMORA_ZCODE_ACP_BIN, then a require.resolve next to the daemon, then npx -y zcode-acp-server. The engine's model pin rides session/set_config_option once per session (the bridge's session/new is lazy, so the pin lands at the first prompt boundary) and falls back to the operator's zcode default when rejected. There is no out-of-band standing-prompt channel, so carriesStandingPrompt stays false and the daemon inlines the scaffold per turn. Zcode stays a compatibility engine (CUMORA_BYOA_ALLOW_UNSANDBOXED=1 opt-in): Cumora cannot impose a verified fail-closed boundary on the bridge + app-server pair, which runs on the operator's own zcode login and permission configuration. Persona ships as AGENTS.md plus .zcode/skills/; version probing uses zcode --version.
Add ZCode to the engine lists in the README BYOA bullet and throughout BYOA.md: the compatibility table (persistent session via the bridge, inlined standing prompt, AGENTS.md + .zcode/skills/), the wake-path diagram, the per-agent home tree, and a paragraph covering the bridge resolution chain and why Zcode stays behind the unsandboxed opt-in.
- Bridge resolution: the npm-published package is the source of truth — the daemon spawns 'npx -y zcode-acp-server' by default, so bridge fixes reach operators without a daemon release and no local checkout can shadow it. CUMORA_ZCODE_ACP_BIN remains the explicit pin for an offline or version-locked copy. Also resolve 'npx' (not 'npx.cmd') through resolveSpawn so the Windows PATHEXT lookup finds the shim. - seedHome: zcode's skill discovery scans PROJECT skills from .agents/skills/ (shared with Antigravity), not .zcode/skills/ — the seeded persona now points at the directory the engine actually reads. - ZcodeSession: stop re-sending the model pin on every wake once the bridge has accepted it; a rejected pin still retries on the next wake since the lazy session/new can reject before the backend session is live. - Docs and the missing-engine hint updated to match.
Review follow-up for the v0.17.0 rebase (yetone#231, yetone#235): - Failure classification: every zcode settle path flows through the shared classifyEngineResult vocabulary. The bridge surfaces a missing backend session with the literal "Session is not active" (documented in its own dispatch code), which the generic resume patterns don't match, so a resumed session that lost its backend maps through an adapter-aware rule to resume-not-found and runWithSessionRecovery gives zcode the same drop-and-fresh-retry the other engines get. Load-time stale resumes still self-heal into a fresh session before any turn runs. - Restructure: the wire machinery (spawn, JSON-RPC framing, handshake, session new/load, death) collapses into one AcpRpcConnection, and ZcodeSession keeps only turn settlement, the lazy model pin, and the bridge-specific mapping — plain composition rather than inheritance, since the codebase shares via helpers. The one-shot turn and probeWake reuse the same connection instead of hand-rolling the handshake a third and fourth time. - Contract tests behind a fake bridge injected through CUMORA_ZCODE_ACP_BIN: pin-once semantics, pin retry on rejection, stale-resume self-heal, resume-not-found mapping, a bridge dying MID-TURN must resolve a failure (never reject), and probeWake / one-shot round trips.
a36f1c0 to
1b4ae44
Compare
|
Thanks for the detailed read — all points addressed. Pushed the rebase plus a follow-up commit (
While rebasing I also restructured the session internals: the handshake had ended up hand-rolled in four places (persistent session, one-shot, two probes), so the wire plumbing now lives in one small CI should pick this up now that the conflict is gone. |
|
Merging. This is a clean adapter — the ACP plumbing ( Two things I checked specifically, since they're the ones that bite silently:
One note, not a blocker: Verified locally on top of current |
What
Adds ZCode (the
zcodeCLI) as a BYOA engine. ZCode has no ACP surface of its own that the daemon can speak directly, so this drives it through thezcode-acp-servernpm bridge, which wraps ZCode's headless app-server in the standard ACP agent methods (initialize/session/new/session/promptover stdio) — the same wire protocol the Grok adapter already speaks.The ACP bridge process
The bridge is a small npm package (
zcode-acp-server, Apache-2.0) that presents a standard ACP agent: JSON-RPC over stdio (newline-delimited), no sockets, no listener, and nothing expected to be already running. The daemon spawns it per agent session and speaks client-side ACP to it over stdin/stdout; the bridge in turn spawns the actualzcodeprocess and owns its lifecycle (its PATH lookup orZCODE_BINdecides which CLI runs — Cumora only probeszcodeto decide whether the engine is installed). The npm-published package is the source of truth: the daemon runsnpx -y zcode-acp-server, so bridge fixes reach operators without a daemon release, andCUMORA_ZCODE_ACP_BINpins an explicit copy for offline/version-locked setups. The bridge is a spawn-type dependency — it cannot be esbuild-bundled into the single-file daemon because its whole job is to be a separate process tree.Design decisions
CUMORA_BYOA_ALLOW_UNSANDBOXED=1and is never auto-selected.AcpRpcConnectionnow owns spawn / JSON-RPC framing / initialize / session new|load / death, andZcodeSessionkeeps only turn settlement, the lazy model pin, and the bridge-specific failure mapping — plain composition (no inheritance), since this codebase shares via helpers. The one-shot turn (run/classify/probe) andprobeWakereuse the same connection;probeWakepasses when a real handshake settles, so doctor exercises the production path.classifyEngineResult. The bridge reports a missing backend session with the literal "Session is not active" (documented in its dispatch code), which the generic resume patterns don't match — a resumed session that lost its backend maps through an adapter-aware rule toresume-not-found, sorunWithSessionRecoverygives zcode the same drop-and-fresh-retry as the other engines. Load-time stale resumes self-heal into a fresh session before any turn runs (second layer, not a substitute).session/set_config_optionat the first prompt boundary (itssession/newis lazy), retries on rejection, and stops once accepted.carriesStandingPrompt = false; the daemon inlines the scaffold per turn (the Cursor/OpenCode contract). Persona ships asAGENTS.md+.agents/skills/(the directory the bridge's skill discovery actually scans).byoa-zcodeledger/triage sources, auth hint, triage model, UI label/picker, en/zh-CN strings, docs.guard-engine-registrypasses.Verification
guard-engine-registry,guard-big-brain,tsc --noEmit,biome lintgreen. Full unit suite diffed against a pristinemaincheckout: identical failures (all pre-existing env-gated tests), zero new.agents-computer-engine-zcode.test.ts) against a fake bridge injected throughCUMORA_ZCODE_ACP_BIN: pin-once semantics, pin retry on rejection, stale-resume self-heal, the resume-not-found mapping, a bridge dying MID-TURN must resolve a failure (never reject), and probeWake/one-shot round trips.--engine zcode, DM → wake (SSE) → local triage → persistent zcode session → model calls thecumora replytool → reply lands in the conversation; second wake reuses the live session;llm_callsrecordsbyoa-zcode;--doctor: big ✓ small ✓ wake path ✓; daemon-restart resume viasession/loadverified.Known limitation
The bridge does not put per-turn token usage in the
session/promptresult, sollm_callsrows for zcode carry0input/output tokens (hop rows and model attribution are still correct). Bridge-side follow-up.Docs updated in
README.mdanddocs/BYOA.md.