Add Agent Client Protocol (Zed) server — Phase 1 - #53
Draft
alpibrupa wants to merge 7 commits into
Draft
Conversation
…ntion lex-code was three weeks stale relative to the rest of the lex ecosystem and lacked native support for the OpenCode Go plan (https://opencode.ai/docs/zen) that lex-loom already uses. OpenCode Go support (native + via the LiteLLM proxy, matching lex-loom): - New `opencode_agent()` in all seven src/agents/*.lex files (build/explore/plan/refactor/review/spec_agent/test_agent), using lex-llm's existing `providers.opencode_go()` — no new package code needed there. Since the Go plan serves full-scale cloud models (DeepSeek, Qwen3, Kimi, GLM, MiniMax, MiMo), these get the same full tool budget as the other cloud providers (mistral/openai/google), not litellm/ollama's stripped-down local-model treatment. - `tools.opencode_model()` (src/tools/index.lex) reads OPENCODE_MODEL, mirroring the existing litellm_model() pattern. - Wired into src/server/session.lex's `pick_agent` and src/tui/main.lex's `select_provider_tag`/help text as a new `--opencode` flag. - litellm/config.yaml + litellm/docker-compose.yml: lex-code had no bundled proxy config at all (the README referenced a "litellm_config.yaml at project root" that didn't exist). Copied from lex-loom's own litellm/ directory — same model list including the OpenCode Go entries (DeepSeek/Qwen3/Kimi/GLM/MiniMax/MiMo) — so both repos can point at one shared proxy instance, and `--litellm` now works out of the box. - README: new "OpenCode Go plan" section, provider table row, and LiteLLM section pointing at the bundled config instead of a nonexistent file. Housekeeping (bringing this repo to the same convention as lex-agent/lex-soft/lex-loom/lex-memory): - lex.toml: bump the `lex` version pin from 0.9.7 to 0.10.9 (CI itself already installs 0.10.7; the rest of the ecosystem is on 0.10.9), add `license = "EUPL-1.2"` and a description. - LICENSE file (matches lex-lang/lex-os/lex-soft/lex-loom/lex-agent/ lex-memory's EUPL-1.2 notice-and-pointer form). - AGENTS.md + CLAUDE.md: this repo had neither, despite being the project that *builds* the coding-agent convention for everyone else. Documents the two-call-sites-per-provider pattern (agents/*.lex + session.lex's pick_agent), why there's no tests/ directory (CI gates on per-file lex check + the manifesto demos), and the local-model tool-budget curation. - README: License section, v0.6 roadmap entry. Verification: - lex pkg install: resolves cleanly (12 packages). - lex check on every tracked src/*.lex file (matches CI's own loop): all ok. - lex fmt --check src/: all 76 files formatted. - Manifesto demos (CI's actual regression gate, no tests/ dir exists): manifesto_parallel.lex passes, its negative twin is correctly rejected, manifesto_semantic_diff/run.sh validates. - Live smoke test: two lex run invocations confirm build_agent.opencode_agent() constructs and session.pick_agent(Build, "opencode") dispatches to it correctly (script not part of this diff). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY
CI on the previous commit failed: "module \"src/memory\" not found in package \"lex-agent\"". src/project_memory.lex imports lex-agent/src/memory, which was deleted from lex-agent in favor of the new lex-memory package (alpibrusl/lex-memory#1, alpibrusl/lex-agent#30) — a consumer this repo's own audit at extraction time didn't know about, since lex-code wasn't yet in scope. lex-code's lex.toml pins lex-agent with a loose git ref (no branch/tag), so CI picked up the deletion the moment it landed on lex-agent's main. Fix: add lex-memory as a direct dependency and swap the import, same as the migration already done for lex-soft/lex-loom. No behavior change — project_memory.lex's own API (open/close/store_*/recall_*) is untouched; only the underlying module moved. Verification: - lex pkg install: lex-memory resolves cleanly (13 packages). - lex check on every tracked src/*.lex file: all ok. - lex fmt --check src/: all 76 files formatted. - Manifesto demos: unaffected, still pass. - Live smoke test: two lex run invocations against a real file-backed SQLite db confirm store_convention (process 1) is correctly recalled via recall_for_prompt (process 2) through the new import. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY
Picks up lex-lang v0.10.10 (released today): the JIT step-counter fix closing the --max-steps bypass (#465/#710), the new [approval] effect for human-in-the-loop host boundaries (#737), and 'vcs' added to lex test's default permissive effect policy (#736), among other changes since v0.10.9/v0.10.7 (the two versions this repo had drifted across between lex.toml and CI -- lex.toml said 0.10.9, ci.yml/ publish.yml still said 0.10.7). Verification (against the real v0.10.10 release binary, not just the version string): - lex pkg install: resolves cleanly (13 packages). - lex check on every tracked src/*.lex file: all ok. - lex fmt --check src/: all 76 files formatted. - Manifesto demos (this repo's actual CI regression gate, no tests/ dir exists): manifesto_parallel.lex passes, its negative twin is still correctly rejected, manifesto_semantic_diff/run.sh validates. No behavior differences observed from the 0.10.9 baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY
lex-code already has an "ACP" server (src/server/acp.lex), but that's BeeAI's unrelated REST-based Agent Communication Protocol. This adds support for Zed's Agent Client Protocol: JSON-RPC 2.0 over stdin/ stdout, NDJSON-framed, the standard Zed/JetBrains/Neovim/Emacs use to launch and drive any coding agent as a subprocess (opencode is one of the other agents already on the shared ACP Registry). New src/server/client_protocol.lex, Phase 1: initialize, session/new, session/prompt (streaming session/update notifications per step), session/close. Deliberately not yet implemented, documented in the file header rather than silently missing: - session/request_permission — today's permission_spec is a static filter at agent construction, not an interactive round-trip; wiring this changes *when* the check fires, not just adding a handler. - $/cancel_request — needs the turn running as a std.conc actor (the pattern src/server/multi_agent.lex already uses for --multi) so the stdin-read loop stays free to receive a cancel mid-turn. - client-mediated fs/*, terminal/*, auth/login — optional in ACP; lex-code's own read_file/write_file/bash tools and env-var provider keys already cover this ground directly. session.lex gains run_turn_streaming_with_provider(session, input, provider_tag, on_step) alongside the existing run_turn_with_provider, so all four transports (TUI, A2A, BeeAI ACP, this) still share one turn-handling implementation per this repo's own AGENTS.md convention, rather than duplicating loop logic into the new transport file. Note in the code: this is NOT token-level real-time streaming — lex-llm's run_loop_traced already runs the full loop to completion internally before returning its Iter, so on_step fires in a burst right after the blocking call returns, not interleaved with live generation. WIRE-FORMAT CAVEAT (documented in the file and the README): the exact session/update field shapes are a best-effort reconstruction from available ACP v2 schema documentation, not a byte-for-byte verified trace against a reference SDK — the JSON-RPC 2.0 envelope and NDJSON framing themselves were verified directly against the protocol's own GitHub repo and multiple independent sources. Verification: - lex check on every tracked src/*.lex file + the new file: all ok. - lex fmt --check src/: all 77 files formatted. - Manifesto demos (this repo's CI gate, no tests/ dir exists): all three still pass/fail as expected. - Isolated checks of every session/update variant's exact JSON shape (text chunk, tool-call begin/exec/result) via direct lex run calls. - Live end-to-end test: a real subprocess spawned exactly as an editor would, piped real NDJSON over stdin/stdout via a bash coprocess — initialize, session/new (real ephemeral session + random sessionId), unknown-method error (-32601), malformed-JSON error (-32700), session/prompt on an unknown sessionId (-32001), session/prompt on a real session with no provider key configured (graceful empty turn, no crash — matches lex-llm's existing provider-error-swallowing behavior, not new), session/close, and a clean exit after stdin closes. Empty stderr throughout — no non-protocol output on stdout. Not independently verified: a real LLM call end-to-end (no provider credentials available in this environment) and interop against an actual ACP client (Zed) for the field-shape caveat above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY
…hain lex-lang v0.10.10 added a new [approval] effect (human-in-the-loop host boundary, lex-lang#737). lex-llm's run_loop/run_loop_traced picked it up immediately (their #41) -- both now require [..., approval]. Since lex-code's lex.toml pins lex-llm with a loose git ref, this landed on every open branch the moment it hit lex-llm's main, same mechanism as the earlier lex-agent/memory break -- except this one is a legitimate upstream feature, not a deletion, so the fix is pure propagation. Adds `approval` to every function's effect row that transitively calls ag.run_loop/run_loop_traced (directly, or via session.lex's run_turn*/run_turn_streaming_with_provider): session.lex's three turn functions, client_protocol.lex's handler chain (my own new file, this session), bootstrap/{hello,run}.lex, server/{acp,api* no-op below, multi_agent,web}.lex, and tui/main.lex's repl/run_once/multi_repl/ run_headless/main. Updated every --allow-effects example in the README and in main.lex's own doc comment to match, so copy-pasted commands still work at runtime. *api.lex needed NO changes -- lex check flagged it in the original sweep only because it transitively imports session.lex, which was still broken; once session.lex was fixed, api.lex passed as-is. Same false-positive risk applies to any file reached only by re-running `lex check` on a single entry point -- verify against the CURRENT state of each dependency before editing, not the first error message. One file is NOT fixed here and can't be fixed from lex-code alone: src/server/mcp_main.lex's make_handler is assigned into lex-agent's Skill.handle, whose effect row is fixed by lex-agent's own Skill type and does not include approval -- widening make_handler's row just moves the failure to that assignment site (effect rows unify by exact equality, not subtyping). This is a genuine incompatibility between two of lex-code's dependencies (lex-agent's Skill type vs lex-llm's now-approval-requiring run_loop), filed upstream as alpibrusl/lex-agent#32 with a suggested fix (add approval to Skill.handle's row). mcp_main.lex will stay red in CI until that lands -- flagging here rather than masking it. Verification (against the real v0.10.10 release binary): - lex check on every tracked src/*.lex file + client_protocol.lex: all ok except mcp_main.lex (the filed, upstream-blocked issue above). - lex fmt --check src/: all 77 files formatted. - Manifesto demos: all three still pass/fail as expected. - Live end-to-end re-test of the ACP server (real subprocess, real NDJSON over stdin/stdout via a bash coprocess) with the corrected --allow-effects list: initialize, session/new, session/close all round-trip correctly, clean exit, empty stderr. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY
alpibrupa
added a commit
to alpibrusl/lex-agent
that referenced
this pull request
Aug 14, 2026
…ect (#33) * Widen Skill.handle's fixed effect row to admit the new [approval] effect Closes #32. lex-lang v0.10.10 added a new [approval] effect (human-in-the-loop host boundary, lex-lang#737). lex-llm's run_loop/run_loop_traced picked it up immediately -- their signatures now require [..., approval]. Since Lex effect rows unify by exact equality (not subtyping), no handler that calls run_loop could be assigned to Skill.handle anymore: type Skill = { capability :: cap.Capability, handle :: (msg.Message) -> [...no approval] HandlerOutcome } Discovered via alpibrusl/lex-code#53, whose MCP handler calls ag.run_loop directly inside a Skill.handle closure. Widens Skill.handle's row to admit approval, then propagates through every function in server.lex's dispatch chain that calls skill.handle transitively (run_skill, dispatch_skill, handle_tasks_send, handle_method, dispatch_request, and the SSE-subscribe mirror: emit_skill_frames, run_skill_subscribe, build_subscribe_frames, dispatch_subscribe_str, plus handle_tasks_get/handle_tasks_cancel) and into mount.lex's rpc_route, this repo's only caller of that chain. tests/test_server_trail.lex's echo_handler and its four dispatch_ request-calling test functions updated to match. This is additive only -- no handler is now REQUIRED to use approval, just permitted to. Existing handlers are unaffected (an unused declared effect is structurally fine, not an error). mount.lex's rpc_route is itself registered via lex-web's router.route_effectful, whose handler-closure type was ALSO fixed without approval -- fixing Skill.handle alone would have left mount.lex newly broken (a regression from this fix, not a preexisting gap). Companion PR: alpibrusl/lex-web#49, merge that first or alongside this one. Also bumped ci.yml's `lex test`/allow-effects list to include approval -- CI passes an explicit list (not the default permissive test policy), so 1 of 10 test files failed purely on runtime policy ("effect `approval` not in --allow-effects"), not any actual assertion, once its handler's row grew to match Skill.handle. Verification (against the real v0.10.10 release binary, and against lex-web's fix checked out locally via a package-cache symlink, since alpibrusl/lex-web#49 is unmerged): - lex check --strict on every src/ and tests/ file: all ok. - lex fmt --check src/ tests/: all 20 files formatted. - lex test with CI's exact --allow-effects list: 10 passed, 0 failed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY * chore: retrigger CI now that lex-web#49 is merged Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY --------- Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZagDSXr1qNv7dwkLrphHY
Completes the widening this repo's turn-execution chain already started: once lex-agent's Skill.handle (lex-agent#33), lex-web's route_effectful handler type (lex-web#49), and lex-mcp's Skill dispatch (lex-mcp#8) all required approval, the handlers this repo assigns into those fixed types needed it too -- handle_chat (api.lex), make_handler/main (mcp_main.lex), and the static-file route closure (web.lex). Verified against the real v0.10.10 release binary with every upstream fix (lex-agent, lex-web, lex-mcp, lex-agent-llm) resolved from main: lex check <every src/*.lex file individually> # all ok lex fmt --check src/ # 77 files formatted lex check examples/manifesto_parallel.lex # ok lex check examples/manifesto_parallel_bad.lex # correctly rejected bash examples/manifesto_semantic_diff/run.sh # validated
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
lex-code already has an "ACP" server (
src/server/acp.lex), but that's BeeAI's unrelated REST-based Agent Communication Protocol. This adds support for Zed's Agent Client Protocol: JSON-RPC 2.0 over stdin/stdout, NDJSON-framed — the standard Zed, JetBrains (IntelliJ/PyCharm/GoLand/WebStorm), Neovim, and Emacs use to launch and drive any coding agent as a subprocess. opencode is one of the other agents already on the shared ACP Registry.New
src/server/client_protocol.lex, Phase 1:initialize,session/new,session/prompt(streamingsession/updatenotifications per step),session/close.Deliberately not yet implemented — documented in the file header rather than silently missing:
session/request_permission— today'spermission_specis a static filter at agent construction, not an interactive round-trip; wiring this in changes when the check fires, not just adding a handler.$/cancel_request— needs the turn running as astd.concactor (the same patternsrc/server/multi_agent.lexalready uses for--multi) so the stdin-read loop stays free to receive a cancel mid-turn.fs/*,terminal/*,auth/login— all optional in ACP; lex-code's ownread_file/write_file/bashtools and env-var provider keys already cover this ground directly.session.lexgainsrun_turn_streaming_with_provider(session, input, provider_tag, on_step)alongside the existingrun_turn_with_provider, so all four transports (TUI, A2A, BeeAI ACP, this) still share one turn-handling implementation per this repo's ownAGENTS.mdconvention, rather than duplicating loop logic into the new transport file. Note: this is not token-level real-time streaming —lex-llm'srun_loop_tracedalready runs the full loop to completion internally before returning itsIter, soon_stepfires in a burst right after the blocking call returns, not interleaved with live generation.Wire-format caveat (documented in the file and the README): the exact
session/updatefield shapes are a best-effort reconstruction from available ACP v2 schema documentation, not a byte-for-byte verified trace against a reference SDK — the JSON-RPC 2.0 envelope and NDJSON framing themselves were verified directly against the protocol's own GitHub repo and multiple independent sources.Verification
Isolated checks of every
session/updatevariant's exact JSON shape (text chunk, tool-call begin/exec/result) via directlex runcalls.Live end-to-end test: a real subprocess spawned exactly as an editor would, piped real NDJSON over stdin/stdout via a bash coprocess —
initialize✓session/new✓ (real ephemeral session + randomsessionId)-32601) ✓-32700) ✓session/prompton an unknownsessionId(-32001) ✓session/prompton a real session with no provider key configured — graceful empty turn, no crash (matcheslex-llm's existing provider-error-swallowing behavior, not new) ✓session/close✓Not independently verified: a real LLM call end-to-end (no provider credentials available in this environment) and interop against an actual ACP client (Zed) for the field-shape caveat above.
Generated by Claude Code