6.5 KiB
Agent Note: Extract example apps into packages
Status: implemented
Problem
An example folder is supposed to be thin — the variable wiring of a demo, not the demo's machinery. Before this change it was thick. Each example carried a hand-rolled start.ts boot bootstrap, an infra preamble (timer, and — for the stdio demos — logger + hmr), nested includes of three shared YAML fragments (base.yml / base-core.yml / acp-agent/acp-tail.yml), and per-example agent-loop/persistence/system-prompt config. The actual app — the spine of services every agent needs — was spread across the leaf and those includes.
The leaf configs also owned coupled front doors. ACP requires stdout purity and creates agents through session/new; terminal and Headless apps pre-create main but have different process I/O contracts. Prose warnings were the only guard against combining these incorrectly, while three start.ts files duplicated the Loader bootstrap and lifecycle code.
Decision
Each example is now mostly an invocation of an app package, splitting the wiring along the existing interface / implementation / consumer seam: the app package owns the composition, the leaf cordis.yml owns only the swappable choices (which LLM adapter, which bash executor, model, prompt, persistence root).
@deepseek-ai/dsh-agent-spine-demo(packages/examples/agent-spine-demo) composes the providerless, executor-less, UI-less spine and forwards the loop's agent-list config. Its dependency on the concrete loop is intentional because this package composes the spine rather than extending it; swapping the loop means supplying another bundle.@deepseek-ai/dsh-tui-demo,@deepseek-ai/dsh-cli-demo, and@deepseek-ai/dsh-acp-demobake in their process roles. TUI includes the full-screen UI and a pre-createdmain; Headless includes the one-shot driver and a pre-createdmain; ACP includes the bridge and no pre-created agent. All three include JSONL persistence and omit stdout loggers.start.tsis gone. Each app package exposes a bin; thedemo:*scripts invoke it. Loader boot,.envloading, and fail-loud guards live in the shared@deepseek-ai/dsh-app-bootpackage (unit-tested under the per-file coverage gate — see share the app bins' boot glue); the thin self-executing entries are driven by keyless Loader-path tests.- Each leaf
cordis.ymlcollapses to backends, optional product tools, and one app entry carrying the app config. TUI and Headless route model/session choices onto a pre-created agent; ACP routes the initial provider/model onto its bridge. base.yml,base-core.yml, andacp-agent/acp-tail.ymlare retired — the spine they shared now lives indsh-agent-spine-demo.
bash-local and the LLM adapter stay leaf choices: the bundle ships tool-bash (the consumer schema), the leaf picks the executor implementation, so a sandboxed executor or replay adapter swaps in without touching the app.
Amendment on implementation: hmr stays a leaf entry
The proposal listed hmr among the interactive app's baked-in front-door cluster. Validating against the code, baking hmr into the app package fights Cordis in two ways, so it ships as a leaf cordis.yml entry instead:
@cordisjs/plugin-hmris a Loader-only, subprocess-only dev plugin — its constructor throws withoutnode --expose-internals+ a liveloaderservice, so it can only run in the realdemo:*/bin subprocess, never in the in-process unit/coverage tier.- The in-process test tier (vitest) cannot even import the vendored
hmrmodule (its class-decorator@Injectform fails under Vite's transform), so a package whoseapplystatically imported it could never satisfy the per-file 100% coverage gate on its headline function.
Crucially, hmr is not a stdout-purity footgun: a stray entry in the ACP config does not corrupt JSON-RPC frames. Every shipped app omits a stdout console logger; the app or protocol driver alone owns stdout.
Alternatives considered
Why not keep the wiring in shared YAML includes?
The old base*.yml/acp-tail.yml includes already deduped the config, but a YAML include cannot encapsulate the front-door coupling — it can only describe it in a comment and trust every leaf to obey. It also cannot own a bin, so the boot glue stayed copied across three start.ts files. A package turns "the ACP app never logs to stdout" from a prose warning into a property of the artifact: there is no logger entry in the leaf to get wrong.
Verification
- Example directories contain only their config, README, and tests:
start.ts, the infrastructure preamble, and the shared YAML includes are gone. demo:tui,demo:headless, anddemo:acpinvoke the app-package bins.- Each new package has a README and per-file 100% coverage; each app package also has a keyless real-Loader-path bin smoke that catches export-shape failures described in postmortem 0001.
- The ACP replay suite boots through the app-package bin, so protocol wiring and assembled backend behavior cross the real Loader boundary.
Consequences
- The bare-plugin-tree pedagogy. The spine lives behind a bundle, so seeing the whole tree means opening
dsh-agent-spine-demo. The app package's README carries that teaching weight. - A layer of indirection. "What does this demo load?" becomes a package read, not a single YAML scan.
Related
- Supersedes Make the shared example base providerless: renaming
base.ymlto the providerless core is moot once the spine moves intodsh-agent-spine-demoand thebase*.ymlfiles are deleted. - Builds on the capability-seams interface/implementation/consumer split — backends and presentation stay leaf choices; the spine is the shared bundle.
- Complements Reorganize packages into a modular hierarchy: the new app/core packages slot into existing groups under that hierarchy (
corefor the reusable spine bundle,uifor the app-specific front doors). - The later redundant-agent removal owns the final TUI/Headless split and removes the line-oriented and mock-only leaves.