refactor(examples): extract the app spine into dsh-agent-core + app packages
Implements docs/rfc/.../2026-06-20-extract-example-app-packages.md. Each
example was thick — a hand-rolled start.ts, an infra preamble, nested
base.yml/base-core.yml/acp-tail.yml includes, and a coupled front-door
cluster enforced only by prose. This moves the composition into packages so
each example is a thin leaf cordis.yml: pick the swappable backends, load one
app package.
New packages:
- @deepseek-ai/dsh-agent-core (packages/core/agent-core): one bundle plugin
that loads the providerless/executor-less/UI-less spine (timer + llm +
sessions + system-prompt + tools + agents + invariants + tool-bash +
agent-loop) via ctx.plugin(...) inside apply(), and forwards agent-loop's
`agents` list as its own Config (export const Config = AgentLoop.Config,
default []).
- @deepseek-ai/dsh-stdio-agent (packages/ui/stdio-agent): terminal chat APP —
agent-core + console logger + readline UI + a pre-created `main` agent, with
a bin. The demo:echo/coding front door.
- @deepseek-ai/dsh-acp-agent (packages/ui/acp-agent): ACP server APP —
agent-core + JSONL persistence + the acp bridge, NO stdout logger, with a
bin. The stdout-purity footgun is structurally unreachable from the leaf.
Amendment to the RFC: hmr stays a LEAF cordis.yml entry, not baked into
dsh-stdio-agent. hmr is a Loader-only dev plugin (throws without
--expose-internals; the in-process test tier can't even import its decorator
form), so a package statically importing it could never carry the per-file
coverage gate. Unlike the console logger, a stray hmr is not a stdout-purity
footgun, so leaving it at the leaf costs no safety. With hmr out, all three new
packages carry in-process unit specs at 100%.
Boot glue (Loader tail, .env load, snapshot-mode selection, stdin-dispose
lifecycle) moves into each app's bin; start.ts and base.yml/base-core.yml/
acp-tail.yml are deleted. Each app package gets a keyless real-load-path test
that boots through its bin + the cordis Loader (guarding the unwrapExports
export-shape bug class, postmortem 0001). ACP snapshot replay stays green
against the existing committed goldens (pure boot restructuring). RFC moved
proposed->implemented with the amendment recorded; package/example/architecture
docs and the module graph updated.
This commit is contained in:
@@ -83,4 +83,4 @@ export function apply(ctx: Context) {
|
||||
|
||||
## Runnable wirings
|
||||
|
||||
Three complete examples load their plugin trees from `cordis.yml` with HMR: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite — the real thing, `pnpm run demo:coding`), and [`examples/acp-agent`](../../examples/acp-agent) (the same coding agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). The two real demos share their provider/tool core via [`examples/base.yml`](../../examples/base.yml).
|
||||
Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite — the real thing, `pnpm run demo:coding`), and [`examples/acp-agent`](../../examples/acp-agent) (the same coding agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is now just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-agent`](../../packages/ui/stdio-agent), the ACP demo loads [`@deepseek-ai/dsh-acp-agent`](../../packages/ui/acp-agent), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) bundle.
|
||||
|
||||
@@ -49,6 +49,22 @@ graph TD
|
||||
tool-bash --> bash
|
||||
tool-bash --> llm
|
||||
tool-bash --> tools
|
||||
agent-core --> agent
|
||||
agent-core --> agent-loop
|
||||
agent-core --> invariants
|
||||
agent-core --> llm
|
||||
agent-core --> session
|
||||
agent-core --> system-prompt
|
||||
agent-core --> tool-bash
|
||||
agent-core --> tools
|
||||
acp-agent --> acp
|
||||
acp-agent --> agent-core
|
||||
acp-agent --> session-persistence-jsonl
|
||||
stdio-agent --> agent
|
||||
stdio-agent --> agent-core
|
||||
stdio-agent --> session
|
||||
stdio-agent --> session-persistence-jsonl
|
||||
stdio-agent --> ui-stdio
|
||||
```
|
||||
|
||||
| Package | Depends on |
|
||||
@@ -72,3 +88,6 @@ graph TD
|
||||
| `acp` | `agent`, `llm`, `session`, `session-persistence`, `tools` |
|
||||
| `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` |
|
||||
| `tool-bash` | `agent`, `bash`, `llm`, `tools` |
|
||||
| `agent-core` | `agent`, `agent-loop`, `invariants`, `llm`, `session`, `system-prompt`, `tool-bash`, `tools` |
|
||||
| `acp-agent` | `acp`, `agent-core`, `session-persistence-jsonl` |
|
||||
| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl`, `ui-stdio` |
|
||||
|
||||
@@ -58,7 +58,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
|
||||
|---|---|
|
||||
| [Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)](proposed/architecture/2026-06-16-typed-event-schemas.md) | 2026-06-16 |
|
||||
| [Extract a generic long-running tool runtime](proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md) | 2026-06-20 |
|
||||
| [Extract example apps into packages](proposed/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 |
|
||||
|
||||
### Process
|
||||
|
||||
@@ -115,6 +114,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
|
||||
| [Agent lifecycle and ownership seams](implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md) | 2026-06-18 |
|
||||
| [Reorganize packages into a modular hierarchy](implemented/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 |
|
||||
| [Branded IDs everywhere they belong](implemented/architecture/2026-06-20-branded-ids.md) | 2026-06-20 |
|
||||
| [Extract example apps into packages](implemented/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 |
|
||||
|
||||
### Process
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# RFC: 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 deeper problem was a **coupled front-door cluster** that lived at the leaf with nothing enforcing it. Choosing the ACP bridge over `ui-stdio` was not one swappable line: an ACP server must **drop the stdout console logger** (stdout is the JSON-RPC channel — a stray log corrupts the frames) and pre-create **no** agents (ACP `session/new` creates them on demand), whereas the stdio app needs a console logger and a pre-created `main`. (`timer` is the one infra plugin common to both — it writes nothing to stdout — so it belongs in the shared spine, not the cluster.) That coupling was enforced only by prose warnings in the leaf YAML. A leaf that wired a console logger into the ACP config was a one-line, comment-only mistake away — exactly the [stdout-purity footgun](../feature/2026-06-18-acp-terminal-and-tool-rendering.md) the examples guarded by hand. The three `start.ts` files also duplicated the Loader-boot tail, the `.env` loader, and (for ACP) snapshot-mode branching and the stdin-dispose lifecycle.
|
||||
|
||||
## What shipped
|
||||
|
||||
Each example is now **mostly an invocation of an app package**, splitting the wiring along the existing [interface / implementation / consumer seam](2026-06-13-capability-seams.md): 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-core`** ([packages/core/agent-core](../../../../packages/core/agent-core)) — a Cordis bundle plugin for the providerless, executor-less, UI-less spine: `timer` + `llm` + sessions + system-prompt + tools + agents + invariants + `tool-bash` + `agent-loop`, mounted as child plugins inside its `apply(ctx)` via `ctx.plugin(...)`. This is the old `base-core.yml` **minus** `bash-local`, **plus** `timer` and the loop, as code instead of a YAML include. The bundle **forwards** `agent-loop`'s `agents` list as its own config (`export const Config = AgentLoop.Config`, default `[]`, the existing `AgentLoop.Config` shape in [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)) — so each app supplies its own pre-created agents. This is precisely the reason the old `base-core.yml` gave for keeping `agent-loop` *out* of the shared core ("the examples disagree — stdio needs a pre-created `main`, acp needs none"); forwarding the config dissolves that objection — the loop is shared, the agents list is per-app. The bundle children register into the root service store, so a leaf-mounted sibling (the adapter, the executor) sees them exactly as a nested `plugin-include` subtree's services were seen before.
|
||||
- **`@deepseek-ai/dsh-stdio-agent`** ([packages/ui/stdio-agent](../../../../packages/ui/stdio-agent)) and **`@deepseek-ai/dsh-acp-agent`** ([packages/ui/acp-agent](../../../../packages/ui/acp-agent)) — app packages, each consuming `dsh-agent-core` and **baking in its coupled front-door cluster**: stdio = `ui-stdio` + console logger + a pre-created `main`; acp = the `acp` bridge + JSONL persistence + **no stdout logger** + no pre-created agents. The coupling becomes structurally unreachable from the leaf. They land under the existing `ui` group alongside `acp`, so no new package group (and no `tsconfig`/`packages/README` group plumbing) was needed.
|
||||
- **`start.ts` is gone.** Each app package exposes a `bin` (`dsh-stdio-agent` / `dsh-acp-agent`); the `demo:*` scripts invoke it (e.g. `dsh-stdio-agent ./cordis.yml`). The Loader-boot tail, `.env` loading, snapshot-mode selection, and stdin-dispose lifecycle moved into that bin, owned by the app. The `bin.ts` files are coverage-excluded (a self-executing CLI entry, like the old `start.ts`) and driven by the keyless Loader-path tests.
|
||||
- **Each leaf `cordis.yml` collapses** to backends + config: the LLM adapter (`llm-deepseek` with apiKey/models, or `llm-replay`), the bash executor (`bash-local`), `hmr` for the stdio demos (see the amendment below), and one app entry carrying the app's config (model, system prompt, persistence root — surfaced as the app package's own `Config`, which routes each value to wherever the app wires it: stdio onto its pre-created agent, acp onto the bridge plugin).
|
||||
- **echo-agent folds onto `dsh-stdio-agent`**, swapping the LLM backend to the local `mock-llm` and adding the local `echo-tool` (plus `bash-local`, which the spine's `tool-bash` injects) at the leaf — the clean demonstration of "swap the backend, keep the app". `mock-llm.ts` / `echo-tool.ts` stay as example-local teaching plugins.
|
||||
- **`base.yml`, `base-core.yml`, and `acp-agent/acp-tail.yml` are retired** — the spine they shared now lives in `dsh-agent-core`.
|
||||
|
||||
`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 stdio app's baked-in front-door cluster. Validating against the code, baking `hmr` into the `dsh-stdio-agent` package fights cordis in two ways, so it ships as a **leaf `cordis.yml` entry** instead:
|
||||
|
||||
1. `@cordisjs/plugin-hmr` is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader` service, so it can only run in the real `demo:*`/bin subprocess, never in the in-process unit/coverage tier.
|
||||
2. The in-process test tier (vitest) cannot even *import* the vendored `hmr` module (its class-decorator `@Inject` form fails under Vite's transform), so a package whose `apply` statically imported it could never satisfy the per-file 100% coverage gate on its headline function.
|
||||
|
||||
Crucially, `hmr` is **not** a stdout-purity footgun the way the console logger is — a stray `hmr` in the ACP config would not corrupt the JSON-RPC frames — so leaving it at the leaf costs none of the safety the coupling argument is about. The **logger** (the real coupling) stays baked in: the stdio app has it, the ACP app structurally cannot.
|
||||
|
||||
## 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
|
||||
|
||||
- Each example directory is `cordis.yml` (+ the acp `cordis.snapshot.yml`) + `README.md` + tests only — no `start.ts`, no infra preamble; `base.yml`/`base-core.yml`/`acp-tail.yml` are gone.
|
||||
- `demo:echo` / `demo:coding` / `demo:acp` run via the app-package `bin`s.
|
||||
- The new packages carry the per-file 100% coverage gate and a README like every `@deepseek-ai/dsh-*`. Each app package has a keyless **real-load-path** smoke that boots it through its `bin` + the cordis Loader (not a hand-built `ctx.plugin({...})` mount), guarding the `unwrapExports` export-shape bug class ([postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)).
|
||||
- The ACP snapshot **replay** transcript is unchanged: the boot restructuring preserved the plugin set + load order, so `pnpm run test:snapshot` stays green against the committed goldens with no re-record.
|
||||
|
||||
## What we give up
|
||||
|
||||
- **The bare-plugin-tree pedagogy.** echo-agent's inlined `cordis.yml` showed every plugin at once; the spine now lives behind a bundle, so seeing the whole tree means opening `dsh-agent-core`. 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](../../rejected/architecture/2026-06-20-providerless-example-base.md): renaming `base.yml` to the providerless core is moot once the spine moves into `dsh-agent-core` and the `base*.yml` files are deleted.
|
||||
- Builds on the [capability-seams](2026-06-13-capability-seams.md) interface/implementation/consumer split — backends and presentation stay leaf choices; the spine is the shared bundle.
|
||||
- Complements [Reorganize packages into a modular hierarchy](2026-06-20-package-hierarchy.md): the new app/core packages slot into existing groups under that hierarchy (`core` for the reusable spine bundle, `ui` for the app-specific front doors).
|
||||
@@ -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/llm-deepseek/src/index.ts](../../../../packages/llm/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.
|
||||
The ACP server app loads `@deepseek-ai/dsh-llm-deepseek`, whose `apply` throws when no API key is present ([packages/llm/llm-deepseek/src/index.ts](../../../../packages/llm/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. The rest of the tree is not duplicated: both the normal `examples/acp-agent/cordis.yml` and the replay config load the same `@deepseek-ai/dsh-acp-agent` app entry (which bundles the agent-core spine + JSONL persistence + the ACP bridge), differing only in the LLM backend (`llm-deepseek` vs `llm-replay`) and the bash executor line. 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. The `dsh-acp-agent` bin selects `cordis.snapshot.yml` for `DSH_SNAPSHOT=replay` and skips `.env` loading in that mode so a stray key cannot trigger a live call.
|
||||
|
||||
### Two surfaces: normalize, then compare
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# RFC: Extract example apps into packages
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
An example folder is supposed to be *thin* — the variable wiring of a demo, not the demo's machinery. Today it is thick. Each example carries 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, and per-example `agent-loop`/persistence/system-prompt config. The actual app — the spine of services every agent needs — is spread across the leaf and the [base.yml](../../../../examples/base.yml) / [base-core.yml](../../../../examples/base-core.yml) / [acp-tail.yml](../../../../examples/acp-agent/acp-tail.yml) includes.
|
||||
|
||||
The deeper problem is a **coupled front-door cluster** that lives at the leaf with nothing enforcing it. Choosing the ACP bridge over `ui-stdio` is not one swappable line: an ACP server must **drop the stdout console logger** (stdout is the JSON-RPC channel — a stray log corrupts the frames), omit `hmr` (the editor owns the subprocess), and pre-create **no** agents (ACP `session/new` creates them on demand), whereas the stdio app needs a console logger, `hmr`, and a pre-created `main`. (`timer` is the one infra plugin common to both — it writes nothing to stdout — so it belongs in the shared spine, not the cluster.) Today that coupling is enforced only by prose warnings in [acp-agent/cordis.yml](../../../../examples/acp-agent/cordis.yml) and [base-core.yml](../../../../examples/base-core.yml). A leaf that wires a console logger into the ACP config is a one-line, comment-only mistake away — exactly the [stdout-purity footgun](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) the examples guard by hand. The three `start.ts` files also duplicate the Loader-boot tail, the `.env` loader, and (for ACP) snapshot-mode branching and the stdin-dispose lifecycle.
|
||||
|
||||
## Proposal
|
||||
|
||||
Make each example **mostly an invocation of an app package**, splitting the wiring along the existing [interface / implementation / consumer seam](../../implemented/architecture/2026-06-13-capability-seams.md): 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-core`** — a Cordis bundle plugin for the providerless, executor-less, UI-less spine: `timer` + `llm` + sessions + system-prompt + tools + agents + invariants + `tool-bash` + `agent-loop`. This is today's [base-core.yml](../../../../examples/base-core.yml) **minus** `bash-local`, **plus** `timer` and the loop, as code instead of a YAML include. The bundle **forwards** `agent-loop`'s `agents` list as its own config (default `[]`, exactly the existing `AgentLoop.Config` shape in [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)) — so each app supplies its own pre-created agents. This is precisely the reason [base-core.yml](../../../../examples/base-core.yml) gives today for keeping `agent-loop` *out* of the shared core ("the examples disagree — stdio needs a pre-created `main`, acp needs none"); forwarding the config dissolves that objection — the loop is shared, the agents list is per-app.
|
||||
- **`@deepseek-ai/dsh-stdio-agent`** and **`@deepseek-ai/dsh-acp-agent`** — app packages, each consuming `dsh-agent-core` and **baking in its coupled front-door cluster**: stdio = `ui-stdio` + console logger + `hmr` + a pre-created `main`; acp = the `acp` bridge + **no stdout logger** + no `hmr` + no pre-created agents. The coupling becomes structurally unreachable from the leaf.
|
||||
- **Drop `start.ts`.** Each app package exposes a `bin`; the `demo:*` scripts invoke it (e.g. `dsh-stdio-agent ./cordis.yml`). The Loader-boot tail, `.env` loading, snapshot-mode selection, and stdin-dispose lifecycle move into that bin, owned by the app.
|
||||
- **Collapse each leaf `cordis.yml`** to backends + config: the LLM adapter (`llm-deepseek` with apiKey/models, or `llm-replay`), the bash executor (`bash-local`), and one app-bundle entry carrying the app's config (model, system prompt, persistence root — surfaced as the app package's own `Config`, which routes each value to wherever the app wires it: stdio onto its pre-created agent, acp onto the bridge plugin). A handful of entries, no infra preamble.
|
||||
- **Fold echo-agent onto `dsh-stdio-agent`**, swapping the LLM backend to the local `mock-llm` and adding the local `echo-tool` at the leaf — the clean demonstration of "swap the backend, keep the app". `mock-llm.ts` / `echo-tool.ts` stay as example-local teaching plugins.
|
||||
- **Retire** [base.yml](../../../../examples/base.yml), [base-core.yml](../../../../examples/base-core.yml), and [acp-tail.yml](../../../../examples/acp-agent/acp-tail.yml) — the spine they shared now lives in `dsh-agent-core`.
|
||||
|
||||
`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.
|
||||
|
||||
## Why not keep the wiring in shared YAML includes?
|
||||
|
||||
The `base*.yml`/`acp-tail.yml` includes already dedupe 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 stays 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.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Each example directory is `cordis.yml` + `README.md` + tests only — no `start.ts`, no infra preamble; `base.yml`/`base-core.yml`/`acp-tail.yml` are gone.
|
||||
- `demo:echo` / `demo:coding` / `demo:acp` run via the app-package `bin`s.
|
||||
- `pnpm run test`, `pnpm run test:snapshot` (re-recorded), `pnpm run typecheck`, `pnpm run knip`, `pnpm run publint`, and `pnpm run doc-sync` are green; the new packages carry the per-file 100% coverage gate and a README like every `@deepseek-ai/dsh-*`.
|
||||
|
||||
## What we give up
|
||||
|
||||
- **The bare-plugin-tree pedagogy.** echo-agent's inlined `cordis.yml` showed every plugin at once; the spine now lives behind a bundle, so seeing the whole tree means opening `dsh-agent-core`. The app package's README must carry that teaching weight.
|
||||
- **A layer of indirection.** "What does this demo load?" becomes a package read, not a single YAML scan.
|
||||
- **Migration cost** (the implementing PR, not this one): three new packages, three leaf rewrites, the boot glue moved into bins, re-recorded ACP snapshots, and rewritten example READMEs + [examples/AGENTS.md](../../../../examples/AGENTS.md).
|
||||
|
||||
## Related
|
||||
|
||||
- Supersedes [Make the shared example base providerless](../../rejected/architecture/2026-06-20-providerless-example-base.md): renaming `base.yml` to the providerless core is moot once the spine moves into `dsh-agent-core` and the `base*.yml` files are deleted.
|
||||
- Builds on the [capability-seams](../../implemented/architecture/2026-06-13-capability-seams.md) interface/implementation/consumer split — backends and presentation stay leaf choices; the spine is the shared bundle.
|
||||
- Complements [Reorganize packages into a modular hierarchy](../../implemented/architecture/2026-06-20-package-hierarchy.md): the new app/core packages slot into a group under that hierarchy (a product group for the reusable core bundle, or alongside the examples for app-specific wiring).
|
||||
@@ -1,23 +1,23 @@
|
||||
# RFC: Make the shared example base providerless
|
||||
|
||||
Status: rejected — superseded by [Extract example apps into packages](../../proposed/architecture/2026-06-20-extract-example-app-packages.md), which moves the spine into a `dsh-agent-core` bundle and deletes the `base*.yml` files, so there is no shared base YAML left to rename.
|
||||
Status: rejected — superseded by [Extract example apps into packages](../../implemented/architecture/2026-06-20-extract-example-app-packages.md), which moves the spine into a `dsh-agent-core` bundle and deletes the `base*.yml` files, so there is no shared base YAML left to rename.
|
||||
|
||||
## Problem
|
||||
|
||||
The examples have two shared base files: [examples/base-core.yml](../../../../examples/base-core.yml) is providerless, while [examples/base.yml](../../../../examples/base.yml) includes that core plus the real `llm-deepseek` adapter. Snapshot replay needs the providerless core with `llm-replay`, because loading the real adapter without a key throws. The normal demos need the real adapter. The result is a naming inversion: the file named `base.yml` is not the reusable base for all examples, while the true base is `base-core.yml`.
|
||||
The examples had two shared base files: `examples/base-core.yml` was providerless, while `examples/base.yml` included that core plus the real `llm-deepseek` adapter. Snapshot replay needs the providerless core with `llm-replay`, because loading the real adapter without a key throws. The normal demos need the real adapter. The result was a naming inversion: the file named `base.yml` was not the reusable base for all examples, while the true base was `base-core.yml`.
|
||||
|
||||
The split is understandable, but it makes every config explanation longer. It also leads to awkward test setup like a keyless smoke test carrying a dummy API key so an adapter can boot even though the model is not called.
|
||||
The split was understandable, but it made every config explanation longer. It also led to awkward test setup like a keyless smoke test carrying a dummy API key so an adapter could boot even though the model is not called.
|
||||
|
||||
## Proposal
|
||||
|
||||
Rename the providerless core to [examples/base.yml](../../../../examples/base.yml) and make adapter selection explicit in each concrete example. The coding and ACP real configs add a tiny `llm-deepseek` include or local block; snapshot config adds `llm-replay`. Delete [examples/base-core.yml](../../../../examples/base-core.yml).
|
||||
Rename the providerless core to `examples/base.yml` and make adapter selection explicit in each concrete example. The coding and ACP real configs add a tiny `llm-deepseek` include or local block; snapshot config adds `llm-replay`. Delete `examples/base-core.yml`.
|
||||
|
||||
The shared base should contain only provider-neutral services and tools: `llm`, sessions, system prompt, tools, agents, invariants, bash executor, and bash tool schemas. Anything that chooses a model provider belongs at the leaf config.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [examples/base.yml](../../../../examples/base.yml) is providerless.
|
||||
- [examples/base-core.yml](../../../../examples/base-core.yml) is deleted.
|
||||
- `examples/base.yml` is providerless.
|
||||
- `examples/base-core.yml` is deleted.
|
||||
- Real demo configs explicitly add the DeepSeek adapter.
|
||||
- Snapshot replay config includes the same providerless base and its replay adapter.
|
||||
- The [examples README](../../../../examples/README.md), example-specific READMEs, and RFC references stop explaining "base = base-core plus adapter".
|
||||
|
||||
Reference in New Issue
Block a user