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.
This commit is contained in:
Tianyi Cui
2026-06-19 09:41:10 +08:00
parent ec73e11c9b
commit 679aaacfc4
9 changed files with 117 additions and 164 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
examples/base-core.yml Normal file
View File

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

View File

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