ソースを参照

refactor(examples): DRY the acp-agent configs via base-core.yml + acp-tail.yml

The snapshot replay config duplicated most of base.yml + the acp tail just to
swap llm-deepseek → llm-replay. Factor the shared pieces:

- examples/base-core.yml: the providerless provider/tool core (llm, sessions,
  system-prompt, tools, agents, invariants, bash-local, tool-bash). base.yml is
  now base-core + the llm-deepseek adapter; the snapshot replay config is
  base-core + llm-replay. The replay config no longer hand-copies the core.
- examples/acp-agent/acp-tail.yml: agent-loop (no pre-created agents) +
  persistence + the ACP bridge/system-prompt, shared by cordis.yml and the
  replay config so the three acp-agent configs can't drift. Its persistence root
  is `$DSH_SNAPSHOT_SESSIONS_ROOT ?? ./.sessions`.
- Deleted cordis.snapshot-record.yml: recording now reuses the normal cordis.yml
  (real adapter), with the harness redirecting the persistence root via env.
  start.ts maps DSH_SNAPSHOT=record → cordis.yml.

Verified: snapshot replay 8/8 keyless; record path works through cordis.yml;
ACP e2e no-key boot green through the doubly-nested include (cordis.yml →
base.yml → base-core.yml); coding-agent boots clean; all gates pass.
Tianyi Cui 3 ヶ月 前
親
コミット
679aaacfc4

+ 3 - 1
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

+ 1 - 1
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
 

+ 33 - 0
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.

+ 0 - 43
examples/acp-agent/cordis.snapshot-record.yml

@@ -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.

+ 15 - 52
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'
-  config:
-    agents: []
-
-- id: session-persistence
-  name: '@deepseek-ai/dsh-session-persistence-jsonl'
+# agent-loop + persistence + the ACP bridge — shared with cordis.yml.
+- id: acp-tail
+  name: '@cordisjs/plugin-include'
   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'

+ 11 - 30
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'
-  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'
+# 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:
-    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'

+ 4 - 5
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.

+ 37 - 0
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'

+ 13 - 32
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'