diff --git a/AGENTS.md b/AGENTS.md index 8e5fb22a29..84d4297b12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,9 @@ examples/ Runnable demos (not workspaces). echo-agent = mock model + echo (pnpm run demo:coding, needs DEEPSEEK_API_KEY). acp-agent = the coding agent exposed as an ACP server over JSON-RPC stdio (pnpm run demo:acp, needs DEEPSEEK_API_KEY). - base.yml = shared provider/tool core both real demos include. + base.yml = shared provider/tool core both real demos include + (= base-core.yml, the providerless core, + the llm-deepseek adapter; + base-core.yml is reused by the acp-agent snapshot-replay config). docs/ architecture.md — the design doc. module-graph.md — generated inter-package dependency graph (Mermaid; `pnpm run gen-module-graph`). rfc/ — design decisions and proposals, one kind of doc grouped by diff --git a/docs/rfc/implemented/2026-06-19-acp-snapshot-tests.md b/docs/rfc/implemented/2026-06-19-acp-snapshot-tests.md index 470e1baf74..8433e131f2 100644 --- a/docs/rfc/implemented/2026-06-19-acp-snapshot-tests.md +++ b/docs/rfc/implemented/2026-06-19-acp-snapshot-tests.md @@ -46,7 +46,7 @@ Replay is positional: the Nth `stream()` call serves the Nth `ReplayEntry`. This Recording runs the scenario with the real `llm-deepseek` adapter and the JSONL persistence backend, then copies the produced `.jsonl` into the scenario dir. Per-event appends are durable, but the harness shuts the subprocess down gracefully (close stdin → `await ctx.dispose()`) before harvesting so the final events are flushed. `llm-replay` itself does no recording — it is replay-only. -`examples/base.yml` always loads `@deepseek-ai/dsh-llm-deepseek`, whose `apply` throws when no API key is present ([packages/llm-deepseek/src/index.ts](../../../packages/llm-deepseek/src/index.ts)). So replay cannot reuse the normal config — it uses a dedicated `examples/acp-agent/cordis.snapshot.yml` that omits `llm-deepseek` and installs `llm-replay` in its place. Recording uses a config that loads the real adapter (no `llm-replay`). In replay mode `start.ts` also skips `.env` loading so a stray key cannot trigger a live call. +`examples/base.yml` always loads `@deepseek-ai/dsh-llm-deepseek`, whose `apply` throws when no API key is present ([packages/llm-deepseek/src/index.ts](../../../packages/llm-deepseek/src/index.ts)). So replay cannot reuse the normal config — it uses a dedicated `examples/acp-agent/cordis.snapshot.yml` that installs `llm-replay` in place of the adapter. To avoid duplicating the rest of the tree, the providerless core is factored into `examples/base-core.yml` (shared by `base.yml = base-core + llm-deepseek` and the replay config = `base-core + llm-replay`), and the agent-loop/persistence/ACP-bridge tail into `examples/acp-agent/acp-tail.yml` (shared by `cordis.yml` and the replay config). Recording reuses the normal `cordis.yml` (real adapter) — its persistence root reads `$DSH_SNAPSHOT_SESSIONS_ROOT` when the harness sets it — so there is no separate record config. In replay mode `start.ts` skips `.env` loading so a stray key cannot trigger a live call. ### Two goldens: normalize, then snapshot diff --git a/examples/acp-agent/acp-tail.yml b/examples/acp-agent/acp-tail.yml new file mode 100644 index 0000000000..ce58343add --- /dev/null +++ b/examples/acp-agent/acp-tail.yml @@ -0,0 +1,33 @@ +# The acp-agent "tail" shared by every acp-agent config (the normal demo, the +# snapshot RECORD path which reuses cordis.yml, and the snapshot REPLAY config): +# agent-loop (no pre-created agents — ACP session/new creates them on demand), +# JSONL session persistence, and the ACP bridge with its system prompt. The +# providerless core + an LLM adapter are included BEFORE this tail by each +# config; nothing here loads an adapter, so the tail is provider-agnostic. +# +# Persistence root: $DSH_SNAPSHOT_SESSIONS_ROOT when the snapshot harness sets +# it (so it can harvest / isolate the log), else ./.sessions for the demo. + +- id: agent-loop + name: '@deepseek-ai/dsh-agent-loop' + config: + agents: [] + +- id: session-persistence + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions' + +- id: acp + name: '@deepseek-ai/dsh-acp' + config: + model: deepseek-v4-flash + systemPrompt: | + You are a coding assistant driven over the Agent Client Protocol. + + Your only tools are bash (plus bash_output/bash_kill for background + tasks). Do ALL file operations through bash: read with cat/sed/head, + search with grep, write with heredocs (cat <<'EOF' > file), edit with + sed or a rewrite. Each bash call runs in a fresh shell — pass workdir + instead of cd. Check the [exit code: N] marker; verify your work. Keep + answers brief and factual. diff --git a/examples/acp-agent/cordis.snapshot-record.yml b/examples/acp-agent/cordis.snapshot-record.yml deleted file mode 100644 index 7a4a431f0a..0000000000 --- a/examples/acp-agent/cordis.snapshot-record.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Snapshot-test RECORD config: a real run whose persisted session JSONL is -# harvested into a scenario fixture. Identical to cordis.yml (real llm-deepseek -# adapter + JSONL persistence) — recording must exercise the REAL model so the -# recorded log is a genuine product of the system. Needs DEEPSEEK_API_KEY. -# -# It is a separate file (rather than reusing cordis.yml) only so the snapshot -# harness selects it explicitly via $DSH_SNAPSHOT=record and so its persistence -# root can be pointed at the harness's harvest directory by the same env the -# replay path uses. The graceful-shutdown path in start.ts flushes persistence -# before exit so the harvested log is complete. - -- id: timer - name: '@cordisjs/plugin-timer' - -# Shared provider/tool core, INCLUDING the real llm-deepseek adapter. -- id: base - name: '@cordisjs/plugin-include' - config: - path: '../base.yml' - -- id: agent-loop - name: '@deepseek-ai/dsh-agent-loop' - config: - agents: [] - -- id: session-persistence - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT - -- id: acp - name: '@deepseek-ai/dsh-acp' - config: - model: deepseek-v4-flash - systemPrompt: | - You are a coding assistant driven over the Agent Client Protocol. - - Your only tools are bash (plus bash_output/bash_kill for background - tasks). Do ALL file operations through bash: read with cat/sed/head, - search with grep, write with heredocs (cat <<'EOF' > file), edit with - sed or a rewrite. Each bash call runs in a fresh shell — pass workdir - instead of cd. Check the [exit code: N] marker; verify your work. Keep - answers brief and factual. diff --git a/examples/acp-agent/cordis.snapshot.yml b/examples/acp-agent/cordis.snapshot.yml index ed5690db18..2dadd066c4 100644 --- a/examples/acp-agent/cordis.snapshot.yml +++ b/examples/acp-agent/cordis.snapshot.yml @@ -1,10 +1,11 @@ # Snapshot-test REPLAY config: the acp-agent plugin tree with the model replaced # by llm-replay (serves a recorded session JSONL — no API key, no network). # -# This does NOT include ../base.yml: base.yml always loads -# @deepseek-ai/dsh-llm-deepseek, whose apply() throws without DEEPSEEK_API_KEY, -# so a keyless replay run would die at boot. We inline the providerless core -# instead and install llm-replay where the adapter would be. +# It reuses ../base-core.yml (the providerless core) + ./acp-tail.yml (agent- +# loop + persistence + the ACP bridge), the SAME pieces cordis.yml shares — only +# the LLM adapter differs: llm-replay here, llm-deepseek there. It can't reuse +# ../base.yml because that loads llm-deepseek, whose apply() throws without +# DEEPSEEK_API_KEY, killing a keyless replay run at boot. # # stdout is reserved for the ACP JSON-RPC protocol — no stdout logger (see # cordis.yml). The replay fixture path comes from $DSH_SNAPSHOT_FILE (and an @@ -13,57 +14,19 @@ - id: timer name: '@cordisjs/plugin-timer' -# Providerless core (everything base.yml has EXCEPT llm-deepseek). -- id: llm - name: '@deepseek-ai/dsh-llm' - -- id: sessions - name: '@deepseek-ai/dsh-session' - -- id: system-prompt - name: '@deepseek-ai/dsh-system-prompt' - -- id: tools - name: '@deepseek-ai/dsh-tools' - -- id: agents - name: '@deepseek-ai/dsh-agent' - -- id: invariants - name: '@deepseek-ai/dsh-invariants' - -- id: bash - name: '@deepseek-ai/dsh-bash-local' +# Providerless core (everything base.yml has EXCEPT the llm-deepseek adapter). +- id: base-core + name: '@cordisjs/plugin-include' config: - timeoutMs: 60000 + path: '../base-core.yml' -- id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' - -# The replay adapter: short-circuits llm/stream with the recorded log's chunks. +# The replay adapter: short-circuits llm/stream with the recorded log's chunks, +# in place of llm-deepseek. - id: llm-replay name: './src/llm-replay.ts' -- id: agent-loop - name: '@deepseek-ai/dsh-agent-loop' +# agent-loop + persistence + the ACP bridge — shared with cordis.yml. +- id: acp-tail + name: '@cordisjs/plugin-include' config: - agents: [] - -- id: session-persistence - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT - -- id: acp - name: '@deepseek-ai/dsh-acp' - config: - model: deepseek-v4-flash - systemPrompt: | - You are a coding assistant driven over the Agent Client Protocol. - - Your only tools are bash (plus bash_output/bash_kill for background - tasks). Do ALL file operations through bash: read with cat/sed/head, - search with grep, write with heredocs (cat <<'EOF' > file), edit with - sed or a rewrite. Each bash call runs in a fresh shell — pass workdir - instead of cd. Check the [exit code: N] marker; verify your work. Keep - answers brief and factual. + path: './acp-tail.yml' diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index b21b661851..384330cc9d 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -1,4 +1,6 @@ -# The acp-agent plugin tree, loaded via @cordisjs/plugin-include. +# The acp-agent plugin tree, loaded via @cordisjs/plugin-include. Also the +# snapshot RECORD config (start.ts selects it for DSH_SNAPSHOT=record): a real +# llm-deepseek run whose persisted log the snapshot harness harvests. # # CRITICAL: this example loads NO stdout logger (no @cordisjs/plugin-logger- # console, no stdio-chat). stdout is reserved for the ACP JSON-RPC protocol — @@ -12,38 +14,17 @@ - id: timer name: '@cordisjs/plugin-timer' -# Shared provider/tool core (llm, sessions, system-prompt, tools, agents, -# invariants, llm-deepseek, bash-local, tool-bash). Nested include resolved -# relative to THIS file's directory. +# Shared provider/tool core, INCLUDING the real llm-deepseek adapter. Nested +# include resolved relative to THIS file's directory. - id: base name: '@cordisjs/plugin-include' config: path: '../base.yml' -# agent-loop with NO pre-created agents: ACP `session/new` creates them on -# demand (unlike coding-agent, which pre-creates `main`). -- id: agent-loop - name: '@deepseek-ai/dsh-agent-loop' +# agent-loop (no pre-created agents) + JSONL persistence + the ACP bridge. +# Shared with the snapshot REPLAY config (cordis.snapshot.yml) so the three +# acp-agent configs don't drift. +- id: acp-tail + name: '@cordisjs/plugin-include' config: - agents: [] - -# Durable session persistence — required by the ACP bridge for `session/load`. -- id: session-persistence - name: '@deepseek-ai/dsh-session-persistence-jsonl' - config: - root: './.sessions' - -# The ACP bridge: wires AgentSideConnection to stdin/stdout. -- id: acp - name: '@deepseek-ai/dsh-acp' - config: - model: deepseek-v4-flash - systemPrompt: | - You are a coding assistant driven over the Agent Client Protocol. - - Your only tools are bash (plus bash_output/bash_kill for background - tasks). Do ALL file operations through bash: read with cat/sed/head, - search with grep, write with heredocs (cat <<'EOF' > file), edit with - sed or a rewrite. Each bash call runs in a fresh shell — pass workdir - instead of cd. Check the [exit code: N] marker; verify your work. Keep - answers brief and factual. + path: './acp-tail.yml' diff --git a/examples/acp-agent/start.ts b/examples/acp-agent/start.ts index 028308cb4a..f2ae6f60ba 100644 --- a/examples/acp-agent/start.ts +++ b/examples/acp-agent/start.ts @@ -6,13 +6,12 @@ import Loader from '@cordisjs/plugin-loader' // DSH_SNAPSHOT=replay — load cordis.snapshot.yml (providerless; llm-replay // serves a recorded session log). Skip .env so a stray // key can never trigger a live model call. -// DSH_SNAPSHOT=record — load cordis.snapshot-record.yml (the real adapter + -// persistence) so a real run can be harvested. +// DSH_SNAPSHOT=record — load the normal cordis.yml (the real llm-deepseek +// adapter + persistence) so a real run can be harvested +// (the persistence root is redirected by env). // Absent — the normal demo (cordis.yml), driven by a real editor. const snapshotMode = process.env.DSH_SNAPSHOT -const configPath = snapshotMode === 'replay' ? './cordis.snapshot.yml' - : snapshotMode === 'record' ? './cordis.snapshot-record.yml' - : './cordis.yml' +const configPath = snapshotMode === 'replay' ? './cordis.snapshot.yml' : './cordis.yml' // Load DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL from a gitignored repo-root .env // (Node native). Absent file is fine — the environment may already carry them. diff --git a/examples/base-core.yml b/examples/base-core.yml new file mode 100644 index 0000000000..15282c8ff6 --- /dev/null +++ b/examples/base-core.yml @@ -0,0 +1,37 @@ +# Providerless provider/tool core — everything the model and tools need EXCEPT +# an LLM adapter. Split out of base.yml so two consumers can share it: +# - base.yml = base-core.yml + the real llm-deepseek adapter (the demos). +# - acp-agent/cordis.snapshot.yml = base-core.yml + llm-replay (keyless +# snapshot replay — base.yml can't be reused there because llm-deepseek's +# apply() throws without DEEPSEEK_API_KEY). +# +# Plugin entries use package names (resolved from node_modules), so they are +# insensitive to the baseUrl reset that plugin-include performs per file. + +- id: llm + name: '@deepseek-ai/dsh-llm' + +- id: sessions + name: '@deepseek-ai/dsh-session' + +- id: system-prompt + name: '@deepseek-ai/dsh-system-prompt' + +- id: tools + name: '@deepseek-ai/dsh-tools' + +- id: agents + name: '@deepseek-ai/dsh-agent' + +# Dev-mode event-contract assertions + session-log freeze (off in prod). +- id: invariants + name: '@deepseek-ai/dsh-invariants' + +# Bash execution: the local executor implementation + the tool schemas. +- id: bash + name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 60000 + +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' diff --git a/examples/base.yml b/examples/base.yml index 992f6a2c8b..897cef725d 100644 --- a/examples/base.yml +++ b/examples/base.yml @@ -1,7 +1,12 @@ # Shared provider/tool core for the example agents, loaded via a nested -# @cordisjs/plugin-include from each example's cordis.yml. Contains everything -# the model and tools need; each example adds its own infra (logger/timer/hmr), -# its agent-loop config (the examples disagree — see below), and its UI plugin. +# @cordisjs/plugin-include from each example's cordis.yml. This is +# base-core.yml (the providerless core: llm, sessions, system-prompt, tools, +# agents, invariants, bash-local, tool-bash) PLUS the real llm-deepseek adapter. +# +# The providerless core lives in base-core.yml so the keyless snapshot-replay +# config (acp-agent/cordis.snapshot.yml) can reuse it with llm-replay in place +# of the adapter — it can't reuse THIS file, because llm-deepseek's apply() +# throws without DEEPSEEK_API_KEY. # # Deliberately EXCLUDES: # - the console logger: it writes to stdout, which the acp-agent reserves for @@ -12,28 +17,13 @@ # pre-create NONE (ACP session/new creates agents on demand). So each example # declares agent-loop with its own `agents` list. # -# Plugin entries here use package names (resolved from node_modules), so they -# are insensitive to the baseUrl reset that plugin-include performs per file. # Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) in the env. -- id: llm - name: '@deepseek-ai/dsh-llm' - -- id: sessions - name: '@deepseek-ai/dsh-session' - -- id: system-prompt - name: '@deepseek-ai/dsh-system-prompt' - -- id: tools - name: '@deepseek-ai/dsh-tools' - -- id: agents - name: '@deepseek-ai/dsh-agent' - -# Dev-mode event-contract assertions + session-log freeze (off in prod). -- id: invariants - name: '@deepseek-ai/dsh-invariants' +# The providerless core (resolved relative to THIS file's directory). +- id: base-core + name: '@cordisjs/plugin-include' + config: + path: './base-core.yml' # The DeepSeek adapter. Swap to '@deepseek-ai/dsh-llm-pi-ai' for the pi-ai-backed # twin (same config shape; `reasoning: high` replaces thinking/reasoningEffort). @@ -45,12 +35,3 @@ models: - deepseek-v4-flash - deepseek-v4-pro - -# Bash execution: the local executor implementation + the tool schemas. -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - timeoutMs: 60000 - -- id: tool-bash - name: '@deepseek-ai/dsh-tool-bash'