From 034779d761341e0d16ea89452aa4dbdd08725908 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:05:31 +0800 Subject: [PATCH 1/3] docs(rfc): propose extracting example apps into packages Add a proposed architecture RFC to make the examples folder thin: each example becomes mostly an invocation of an app package. A shared dsh-agent-core bundle owns the providerless spine; dsh-stdio-agent and dsh-acp-agent app packages bake in their coupled front-door cluster (UI + logger/hmr policy + agent pre-creation), turning the ACP stdout-purity footgun into a property of the artifact. Leaf cordis.yml shrinks to backends + config; start.ts is dropped in favor of a package bin. Supersedes the providerless-example-base RFC (cross-linked) and indexes the new RFC under Proposed -> Architecture. --- docs/rfc/README.md | 1 + ...2026-06-20-extract-example-app-packages.md | 44 +++++++++++++++++++ .../2026-06-20-providerless-example-base.md | 2 + 3 files changed, 47 insertions(+) create mode 100644 docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 6f295957ab..f3aea7cf2f 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -65,6 +65,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [Extract a generic long-running tool runtime](proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md) | 2026-06-20 | | [Make the shared example base providerless](proposed/architecture/2026-06-20-providerless-example-base.md) | 2026-06-20 | | [Reorganize packages into a modular hierarchy](proposed/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 | +| [Extract example apps into packages](proposed/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 | ### Process diff --git a/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md b/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md new file mode 100644 index 0000000000..c175d93e98 --- /dev/null +++ b/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md @@ -0,0 +1,44 @@ +# 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 (`logger`/`timer`/`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`. 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: `llm` + sessions + system-prompt + tools + agents + invariants + `tool-bash` + `agent-loop` (neutral `agents: []`). This is today's [base-core.yml](../../../../examples/base-core.yml) **minus** `bash-local`, **plus** the loop, as code instead of a YAML include. +- **`@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 model / systemPrompt / persistence root. ~4 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](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](2026-06-20-package-hierarchy.md): the new app/core packages slot into whatever hierarchy that RFC settles on. diff --git a/docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md b/docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md index 2061248596..fae44635d0 100644 --- a/docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md +++ b/docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md @@ -2,6 +2,8 @@ Status: proposed +> Superseded by [Extract example apps into packages](2026-06-20-extract-example-app-packages.md): moving the spine into a `dsh-agent-core` bundle and deleting the `base*.yml` files makes the rename below moot. Kept for the record. + ## 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`. From d9c9c5d403f2f4c4d3657e83e659291f3e82df0d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:25:11 +0800 Subject: [PATCH 2/3] docs(rfc): sharpen agent-loop, timer, and leaf-config accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review against the actual source surfaced three imprecisions: - agent-loop placement: the core bundle forwards agent-loop's `agents` list as its own config (default []), matching AgentLoop.Config, rather than hardcoding []. This is what lets a shared core coexist with stdio pre-creating `main` and acp pre-creating none — and it directly rebuts the reason base-core.yml gives today for keeping the loop out of core. - timer is universal and stdout-safe, so it lives in the shared spine, not the per-app front-door cluster (only logger + hmr are app-specific). - model/systemPrompt land in different places per app (stdio onto the pre-created agent, acp onto the bridge plugin), routed by the app package's own Config — not a single uniform bundle entry. --- .../2026-06-20-extract-example-app-packages.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md b/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md index c175d93e98..e129acf64e 100644 --- a/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md +++ b/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md @@ -4,18 +4,18 @@ 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 (`logger`/`timer`/`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. +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`. 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. +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: `llm` + sessions + system-prompt + tools + agents + invariants + `tool-bash` + `agent-loop` (neutral `agents: []`). This is today's [base-core.yml](../../../../examples/base-core.yml) **minus** `bash-local`, **plus** the loop, as code instead of a YAML include. +- **`@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/agent-loop/src/index.ts](../../../../packages/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 model / systemPrompt / persistence root. ~4 entries, no infra preamble. +- **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`. From 94a762658ff53a817de40504907fa6beafa81cd0 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:37:41 +0800 Subject: [PATCH 3/3] docs(rfc): reject the superseded providerless-example-base RFC git mv it from proposed/ to rejected/architecture/ (preserving history) and fold the supersede note into a one-line rejected Status. The extract-example-app-packages RFC subsumes it: once the spine moves into dsh-agent-core and the base*.yml files are deleted, there is no shared base YAML left to rename. Move its README row to Rejected -> Architecture and repoint the supersede cross-link to the new path. --- docs/rfc/README.md | 2 +- .../architecture/2026-06-20-extract-example-app-packages.md | 2 +- .../architecture/2026-06-20-providerless-example-base.md | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) rename docs/rfc/{proposed => rejected}/architecture/2026-06-20-providerless-example-base.md (88%) diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 83f4a2e6db..997683fab5 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -63,7 +63,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 | -| [Make the shared example base providerless](proposed/architecture/2026-06-20-providerless-example-base.md) | 2026-06-20 | | [Reorganize packages into a modular hierarchy](proposed/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 | | [Extract example apps into packages](proposed/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 | | [Branded IDs everywhere they belong](proposed/architecture/2026-06-20-branded-ids.md) | 2026-06-20 | @@ -163,3 +162,4 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | Title | First proposed | |---|---| | [Deep-readonly public surfaces](rejected/architecture/2026-06-11-immutable-public-surfaces.md) | 2026-06-11 | +| [Make the shared example base providerless](rejected/architecture/2026-06-20-providerless-example-base.md) | 2026-06-20 | diff --git a/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md b/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md index e129acf64e..a99fa80f86 100644 --- a/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md +++ b/docs/rfc/proposed/architecture/2026-06-20-extract-example-app-packages.md @@ -39,6 +39,6 @@ The `base*.yml`/`acp-tail.yml` includes already dedupe the *config*, but a YAML ## Related -- Supersedes [Make the shared example base providerless](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. +- 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](2026-06-20-package-hierarchy.md): the new app/core packages slot into whatever hierarchy that RFC settles on. diff --git a/docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md b/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md similarity index 88% rename from docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md rename to docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md index fae44635d0..c83824d21c 100644 --- a/docs/rfc/proposed/architecture/2026-06-20-providerless-example-base.md +++ b/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md @@ -1,8 +1,6 @@ # RFC: Make the shared example base providerless -Status: proposed - -> Superseded by [Extract example apps into packages](2026-06-20-extract-example-app-packages.md): moving the spine into a `dsh-agent-core` bundle and deleting the `base*.yml` files makes the rename below moot. Kept for the record. +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. ## Problem