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:
28
AGENTS.md
28
AGENTS.md
@@ -52,6 +52,9 @@ packages/ Harness packages, grouped by role at packages/<group>/<pkg>/.
|
|||||||
tools/ tool registry + tools/execute waterfall
|
tools/ tool registry + tools/execute waterfall
|
||||||
agent/ Agent interface, registry, agent/* event vocabulary
|
agent/ Agent interface, registry, agent/* event vocabulary
|
||||||
agent-loop/ THE concrete plugin: ReactLoopAgent + the loop driver
|
agent-loop/ THE concrete plugin: ReactLoopAgent + the loop driver
|
||||||
|
agent-core/ bundle plugin: the providerless/executor-less/UI-less spine
|
||||||
|
(timer+llm+sessions+system-prompt+tools+agents+invariants+
|
||||||
|
tool-bash+agent-loop) as code; forwards agent-loop's `agents`
|
||||||
llm/ LLM capability family
|
llm/ LLM capability family
|
||||||
llm/ abstract LLM service + content-block vocabulary
|
llm/ abstract LLM service + content-block vocabulary
|
||||||
llm-deepseek/ DeepSeek API adapter (hand-rolled fetch/SSE)
|
llm-deepseek/ DeepSeek API adapter (hand-rolled fetch/SSE)
|
||||||
@@ -67,21 +70,28 @@ packages/ Harness packages, grouped by role at packages/<group>/<pkg>/.
|
|||||||
ui/ product integration surfaces
|
ui/ product integration surfaces
|
||||||
acp/ Agent Client Protocol bridge: drive the agent from an ACP
|
acp/ Agent Client Protocol bridge: drive the agent from an ACP
|
||||||
editor (Zed) over JSON-RPC stdio
|
editor (Zed) over JSON-RPC stdio
|
||||||
|
stdio-agent/ stdio chat APP: agent-core spine + console logger + readline
|
||||||
|
UI + a pre-created main agent + a bin (the demo:echo/coding
|
||||||
|
front door)
|
||||||
|
acp-agent/ ACP server APP: agent-core spine + JSONL persistence + the
|
||||||
|
acp bridge, NO stdout logger + a bin (the demo:acp front door)
|
||||||
support/ dev/test/example infrastructure (lower compat expectations)
|
support/ dev/test/example infrastructure (lower compat expectations)
|
||||||
invariants/ dev-mode event-contract invariants + session-log freeze
|
invariants/ dev-mode event-contract invariants + session-log freeze
|
||||||
ui-stdio/ minimal stdio (readline) UI plugin: renders agent/* events,
|
ui-stdio/ minimal stdio (readline) UI plugin: renders agent/* events,
|
||||||
feeds stdin lines to the agent (shared by the demos)
|
feeds stdin lines to the agent (shared by the demos)
|
||||||
llm-replay/ record/replay adapter: short-circuits llm/stream from a
|
llm-replay/ record/replay adapter: short-circuits llm/stream from a
|
||||||
recorded session JSONL (keyless snapshot tests)
|
recorded session JSONL (keyless snapshot tests)
|
||||||
examples/ Runnable demos (not workspaces; see examples/AGENTS.md). echo-agent
|
examples/ Runnable demos (not workspaces; see examples/AGENTS.md). Each is a
|
||||||
= mock model + echo tool + stdio UI + JSONL persistence, wired via
|
THIN leaf cordis.yml: it picks the swappable backends (an LLM adapter,
|
||||||
cordis.yml. coding-agent = the real thing: DeepSeek V4 + bash tools
|
a bash executor) and loads ONE app package (dsh-stdio-agent or
|
||||||
(pnpm run demo:coding, needs DEEPSEEK_API_KEY).
|
dsh-acp-agent), which bundles the agent-core spine + front-door
|
||||||
acp-agent = the coding agent exposed as an ACP server over
|
cluster + boot glue (a bin). No start.ts. echo-agent = mock model +
|
||||||
JSON-RPC stdio (pnpm run demo:acp, needs DEEPSEEK_API_KEY).
|
echo tool on dsh-stdio-agent (pnpm run demo:echo, no key).
|
||||||
base.yml = shared provider/tool core both real demos include
|
coding-agent = the real thing: DeepSeek V4 + bash tools on the same
|
||||||
(= base-core.yml, the providerless core, + the llm-deepseek adapter;
|
app (pnpm run demo:coding, needs DEEPSEEK_API_KEY). acp-agent = the
|
||||||
base-core.yml is reused by the acp-agent snapshot-replay config).
|
coding agent as an ACP server on dsh-acp-agent (pnpm run demo:acp,
|
||||||
|
needs DEEPSEEK_API_KEY). cordis.snapshot.yml = the acp leaf with
|
||||||
|
llm-replay for keyless snapshot replay.
|
||||||
docs/ architecture.md — the design doc. module-graph.md — generated
|
docs/ architecture.md — the design doc. module-graph.md — generated
|
||||||
inter-package dependency graph (Mermaid; `pnpm run gen-module-graph`).
|
inter-package dependency graph (Mermaid; `pnpm run gen-module-graph`).
|
||||||
rfc/ — design decisions and proposals, one kind of doc grouped by
|
rfc/ — design decisions and proposals, one kind of doc grouped by
|
||||||
|
|||||||
@@ -83,4 +83,4 @@ export function apply(ctx: Context) {
|
|||||||
|
|
||||||
## Runnable wirings
|
## 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 --> bash
|
||||||
tool-bash --> llm
|
tool-bash --> llm
|
||||||
tool-bash --> tools
|
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 |
|
| Package | Depends on |
|
||||||
@@ -72,3 +88,6 @@ graph TD
|
|||||||
| `acp` | `agent`, `llm`, `session`, `session-persistence`, `tools` |
|
| `acp` | `agent`, `llm`, `session`, `session-persistence`, `tools` |
|
||||||
| `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` |
|
| `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` |
|
||||||
| `tool-bash` | `agent`, `bash`, `llm`, `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 |
|
| [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 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
|
### 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 |
|
| [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 |
|
| [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 |
|
| [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
|
### 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.
|
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
|
### 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
|
# 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
|
## 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
|
## 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.
|
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
|
## Acceptance criteria
|
||||||
|
|
||||||
- [examples/base.yml](../../../../examples/base.yml) is providerless.
|
- `examples/base.yml` is providerless.
|
||||||
- [examples/base-core.yml](../../../../examples/base-core.yml) is deleted.
|
- `examples/base-core.yml` is deleted.
|
||||||
- Real demo configs explicitly add the DeepSeek adapter.
|
- Real demo configs explicitly add the DeepSeek adapter.
|
||||||
- Snapshot replay config includes the same providerless base and its replay 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".
|
- The [examples README](../../../../examples/README.md), example-specific READMEs, and RFC references stop explaining "base = base-core plus adapter".
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Runnable demos that show how the harness is wired. **Examples are NOT workspaces** — each `examples/*/package.json` is a private, dependency-free stub with no build. They are booted as unbuilt `tsx` subprocesses via the cordis Loader reading a `cordis.yml`; the `@deepseek-ai/dsh-*` plugin names in those YAML files resolve through the root `tsconfig.json` `paths` map, not through `node_modules`.
|
Runnable demos that show how the harness is wired. **Examples are NOT workspaces** — each `examples/*/package.json` is a private, dependency-free stub with no build. They are booted as unbuilt `tsx` subprocesses via the cordis Loader reading a `cordis.yml`; the `@deepseek-ai/dsh-*` plugin names in those YAML files resolve through the root `tsconfig.json` `paths` map, not through `node_modules`.
|
||||||
|
|
||||||
Because examples are not under the `packages/*/src` coverage gate, an example that grows real, reusable *logic* should extract it into a `packages/` package (where it gets the per-file 100% gate and a README). Keep only example-specific glue here: `start.ts`, the `cordis.yml` wiring, demo-only mocks/teaching artifacts, and the e2e/snapshot scenarios.
|
Because examples are not under the `packages/*/src` coverage gate, an example that grows real, reusable *logic* should extract it into a `packages/` package (where it gets the per-file 100% gate and a README). Keep only example-specific glue here: the `cordis.yml` wiring, demo-only mocks/teaching artifacts, and the e2e/snapshot scenarios. There is no `start.ts` — the boot glue (Loader tail, `.env` load, snapshot-mode selection, stdin-dispose lifecycle) lives in each app package's `bin` (`@deepseek-ai/dsh-stdio-agent`, `@deepseek-ai/dsh-acp-agent`), which the `demo:*` scripts invoke against the leaf `cordis.yml`.
|
||||||
|
|
||||||
## Every example ships e2e smokes (keyless + with-key)
|
## Every example ships e2e smokes (keyless + with-key)
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
Runnable demos (not workspaces) that showcase how the harness is wired.
|
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is now a **thin leaf**: a `cordis.yml` that picks the swappable backends (an LLM adapter, a bash executor) and loads ONE app package, plus any demo-only mocks. The composition — the spine, the front-door cluster, and the boot glue — lives in the app packages ([`@deepseek-ai/dsh-stdio-agent`](../packages/ui/stdio-agent), [`@deepseek-ai/dsh-acp-agent`](../packages/ui/acp-agent)) and the [`@deepseek-ai/dsh-agent-core`](../packages/core/agent-core) bundle they share. There is no `start.ts`; the `demo:*` scripts invoke each app package's `bin`.
|
||||||
|
|
||||||
## echo-agent
|
## echo-agent
|
||||||
|
|
||||||
A mock model + echo tool + stdio UI + JSONL persistence demo. Demonstrates:
|
A mock model + echo tool on the stdio chat app — the all-mock skeleton. The leaf swaps `dsh-stdio-agent`'s LLM backend to a local `mock-echo` adapter and adds a local `echo` tool. Demonstrates:
|
||||||
|
|
||||||
- Loading plugins from a `cordis.yml` via `@cordisjs/plugin-loader` + `@cordisjs/plugin-include`
|
- A thin leaf `cordis.yml` loading the `@deepseek-ai/dsh-stdio-agent` app
|
||||||
- Registering a mock `LlmAdapter` (streaming scripted responses)
|
- Registering a mock `LlmAdapter` (streaming scripted responses)
|
||||||
- Registering a tool via `ctx.tools.register()`
|
- Registering a tool via `ctx.tools.register()`
|
||||||
- Persisting session events to JSONL via the `session/event` + `session/flush` pattern
|
- "Swap the backend, keep the app" — the only difference from `coding-agent` is the adapter
|
||||||
- A minimal stdio UI consuming `agent/stream-chunk` and session events
|
|
||||||
|
|
||||||
Run with: `pnpm run demo:echo`
|
Run with: `pnpm run demo:echo`. When prompted, type "echo <something>" to trigger a tool call round-trip.
|
||||||
|
|
||||||
When prompted, type "echo <something>" to trigger a tool call round-trip.
|
|
||||||
|
|
||||||
## coding-agent
|
## coding-agent
|
||||||
|
|
||||||
The real thing: DeepSeek V4 + the bash tool suite + stdio chat + JSONL persistence, wired from `cordis.yml`. Where echo-agent proves the skeleton with mocks, this is a usable coding assistant.
|
The real thing: DeepSeek V4 + the bash tool suite on the same `@deepseek-ai/dsh-stdio-agent` app. Where echo-agent proves the skeleton with mocks, this is a usable coding assistant.
|
||||||
|
|
||||||
Run with: `pnpm run demo:coding` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details.
|
Run with: `pnpm run demo:coding` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details.
|
||||||
|
|
||||||
|
## acp-agent
|
||||||
|
|
||||||
|
The same coding agent exposed as an **Agent Client Protocol (ACP)** server over JSON-RPC stdio, via the [`@deepseek-ai/dsh-acp-agent`](../packages/ui/acp-agent) app — drive it from Zed or any other ACP client. Also the home of the keyless snapshot tests.
|
||||||
|
|
||||||
|
Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`). See [acp-agent/README.md](acp-agent/README.md) for the Zed setup and the snapshot-test design.
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ The DeepSeek Harness coding agent exposed as an **Agent Client Protocol (ACP)**
|
|||||||
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
|
pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env)
|
||||||
```
|
```
|
||||||
|
|
||||||
This boots `@deepseek-ai/dsh-acp` over the shared provider/tool core (`../base.yml`), with `agent-loop` configured with **no pre-created agents** (ACP `session/new` creates them on demand) and JSONL session persistence (so `session/load` works).
|
This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-acp-agent`](../../packages/ui/acp-agent) app (which bundles the [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) spine, JSONL session persistence, and the `@deepseek-ai/dsh-acp` bridge — with **no pre-created agents**, since ACP `session/new` creates them on demand) plus the two swappable backends (`llm-deepseek`, `bash-local`). The app package bakes in the no-stdout-logger cluster, so the stdout-purity guarantee is a property of the artifact, not a leaf convention.
|
||||||
|
|
||||||
## stdout is the protocol
|
## stdout is the protocol
|
||||||
|
|
||||||
This example loads **no stdout logger** — `stdout` carries the JSON-RPC frames, and any other write corrupts them. Do not add `@cordisjs/plugin-logger-console` or a stdio UI here. Use a stderr exporter if you need logs.
|
This example loads **no stdout logger** — `stdout` carries the JSON-RPC frames, and any other write corrupts them. `@deepseek-ai/dsh-acp-agent` contains no logger entry, so the footgun is structurally unreachable from this leaf. Use a stderr exporter if you need logs.
|
||||||
|
|
||||||
## Zed configuration
|
## Zed configuration
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,32 +1,39 @@
|
|||||||
# Snapshot-test REPLAY config: the acp-agent plugin tree with the model replaced
|
# Snapshot-test REPLAY config: the acp-agent plugin tree with the model backend
|
||||||
# by llm-replay (serves a recorded session JSONL — no API key, no network).
|
# swapped to llm-replay (serves a recorded session JSONL — no API key, no
|
||||||
|
# network). The dsh-acp-agent bin selects this file for DSH_SNAPSHOT=replay.
|
||||||
#
|
#
|
||||||
# It reuses ../base-core.yml (the providerless core) + ./acp-tail.yml (agent-
|
# Same app as cordis.yml (@deepseek-ai/dsh-acp-agent: the agent-core spine +
|
||||||
# loop + persistence + the ACP bridge), the SAME pieces cordis.yml shares — only
|
# JSONL persistence + the ACP bridge) — only the LLM backend differs: llm-replay
|
||||||
# the LLM adapter differs: llm-replay here, llm-deepseek there. It can't reuse
|
# here, llm-deepseek there. It can't reuse the real adapter because llm-deepseek's
|
||||||
# ../base.yml because that loads llm-deepseek, whose apply() throws without
|
# apply() throws without DEEPSEEK_API_KEY, killing a keyless replay run at boot.
|
||||||
# DEEPSEEK_API_KEY, killing a keyless replay run at boot.
|
|
||||||
#
|
#
|
||||||
# stdout is reserved for the ACP JSON-RPC protocol — no stdout logger (see
|
# stdout is reserved for the ACP JSON-RPC protocol — no stdout logger (the app
|
||||||
# cordis.yml). The replay fixture path comes from $DSH_SNAPSHOT_FILE (and an
|
# package omits it). The replay fixture path comes from $DSH_SNAPSHOT_FILE (and
|
||||||
# optional $DSH_SNAPSHOT_OVERRIDE sidecar), set by the snapshot harness.
|
# an optional $DSH_SNAPSHOT_OVERRIDE sidecar), set by the snapshot harness.
|
||||||
|
|
||||||
- id: timer
|
|
||||||
name: '@cordisjs/plugin-timer'
|
|
||||||
|
|
||||||
# Providerless core (everything base.yml has EXCEPT the llm-deepseek adapter).
|
|
||||||
- id: base-core
|
|
||||||
name: '@cordisjs/plugin-include'
|
|
||||||
config:
|
|
||||||
path: '../base-core.yml'
|
|
||||||
|
|
||||||
# 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.
|
# in place of llm-deepseek.
|
||||||
- id: llm-replay
|
- id: llm-replay
|
||||||
name: '@deepseek-ai/dsh-llm-replay'
|
name: '@deepseek-ai/dsh-llm-replay'
|
||||||
|
|
||||||
# agent-loop + persistence + the ACP bridge — shared with cordis.yml.
|
# Local bash executor (the agent's only tool, via agent-core's tool-bash schema).
|
||||||
- id: acp-tail
|
- id: bash
|
||||||
name: '@cordisjs/plugin-include'
|
name: '@deepseek-ai/dsh-bash-local'
|
||||||
config:
|
config:
|
||||||
path: './acp-tail.yml'
|
timeoutMs: 60000
|
||||||
|
|
||||||
|
# The ACP server app — identical to cordis.yml's entry.
|
||||||
|
- id: acp-agent
|
||||||
|
name: '@deepseek-ai/dsh-acp-agent'
|
||||||
|
config:
|
||||||
|
model: deepseek-v4-flash
|
||||||
|
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||||
|
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.
|
||||||
|
|||||||
@@ -1,30 +1,48 @@
|
|||||||
# The acp-agent plugin tree, loaded via @cordisjs/plugin-include. Also the
|
# The acp-agent plugin tree: the ACP server. Also the snapshot RECORD config
|
||||||
# snapshot RECORD config (start.ts selects it for DSH_SNAPSHOT=record): a real
|
# (the dsh-acp-agent bin selects it for DSH_SNAPSHOT=record): a real llm-deepseek
|
||||||
# llm-deepseek run whose persisted log the snapshot harness harvests.
|
# run whose persisted log the snapshot harness harvests. Just the two swappable
|
||||||
|
# backends — the DeepSeek adapter and the local bash executor — plus the ACP
|
||||||
|
# server app (@deepseek-ai/dsh-acp-agent), which bundles the agent-core spine,
|
||||||
|
# JSONL persistence, and the ACP bridge.
|
||||||
#
|
#
|
||||||
# CRITICAL: this example loads NO stdout logger (no @cordisjs/plugin-logger-
|
# CRITICAL: this tree loads NO stdout logger and NO hmr — stdout is reserved for
|
||||||
# console, no stdio-chat). stdout is reserved for the ACP JSON-RPC protocol —
|
# the ACP JSON-RPC protocol (see packages/ui/acp). That guarantee is now a
|
||||||
# anything else written there corrupts the frames (see packages/acp, RFC 010 §
|
# property of @deepseek-ai/dsh-acp-agent (it contains no logger entry), not a
|
||||||
# Risks). Use a stderr exporter if you need logging. The timer plugin is loaded
|
# leaf convention: there is no logger here to get wrong.
|
||||||
# (no stdout writes); hmr is omitted (an editor manages the subprocess).
|
|
||||||
#
|
#
|
||||||
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) in the
|
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) — the
|
||||||
# environment — start.ts loads the gitignored repo-root .env first.
|
# dsh-acp-agent bin loads the gitignored repo-root .env first (on STDERR only).
|
||||||
|
|
||||||
- id: timer
|
# The DeepSeek adapter.
|
||||||
name: '@cordisjs/plugin-timer'
|
- id: llm-deepseek
|
||||||
|
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||||
# 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:
|
config:
|
||||||
path: '../base.yml'
|
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||||
|
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||||
|
models:
|
||||||
|
- deepseek-v4-flash
|
||||||
|
- deepseek-v4-pro
|
||||||
|
|
||||||
# agent-loop (no pre-created agents) + JSONL persistence + the ACP bridge.
|
# Local bash executor (the agent's only tool, via agent-core's tool-bash schema).
|
||||||
# Shared with the snapshot REPLAY config (cordis.snapshot.yml) so the three
|
- id: bash
|
||||||
# acp-agent configs don't drift.
|
name: '@deepseek-ai/dsh-bash-local'
|
||||||
- id: acp-tail
|
|
||||||
name: '@cordisjs/plugin-include'
|
|
||||||
config:
|
config:
|
||||||
path: './acp-tail.yml'
|
timeoutMs: 60000
|
||||||
|
|
||||||
|
# The ACP server app: the agent-core spine + JSONL persistence + the ACP bridge.
|
||||||
|
# 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: acp-agent
|
||||||
|
name: '@deepseek-ai/dsh-acp-agent'
|
||||||
|
config:
|
||||||
|
model: deepseek-v4-flash
|
||||||
|
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||||
|
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.
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
||||||
import { Context } from 'cordis'
|
|
||||||
import Loader from '@cordisjs/plugin-loader'
|
|
||||||
|
|
||||||
// Snapshot-test modes (set by the snapshot harness via env):
|
|
||||||
// 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 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' : './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.
|
|
||||||
// In REPLAY mode we deliberately skip this: replay must never reach the network,
|
|
||||||
// so we don't want a present .env to enable a live call.
|
|
||||||
//
|
|
||||||
// IMPORTANT: this server speaks ACP JSON-RPC on stdout. Do NOT add any
|
|
||||||
// stdout logging here or in cordis.yml — it would corrupt the protocol frames.
|
|
||||||
// A present-but-unreadable/malformed .env is a real misconfiguration: surface
|
|
||||||
// it on STDERR (never stdout) rather than silently running with the wrong env.
|
|
||||||
if (snapshotMode !== 'replay') {
|
|
||||||
try {
|
|
||||||
process.loadEnvFile(new URL('../../.env', import.meta.url).pathname)
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') {
|
|
||||||
process.stderr.write(`acp-agent: failed to load .env: ${String(error)}\n`)
|
|
||||||
}
|
|
||||||
// ENOENT (no .env) is fine — rely on the ambient environment.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve relative cordis.yml paths from the repo root no matter where the
|
|
||||||
// editor launches this demo command.
|
|
||||||
process.chdir(fileURLToPath(new URL('../..', import.meta.url)))
|
|
||||||
|
|
||||||
const ctx = new Context()
|
|
||||||
ctx.baseUrl = pathToFileURL(import.meta.dirname).href + '/'
|
|
||||||
|
|
||||||
await ctx.plugin(Loader)
|
|
||||||
await ctx.loader.create({
|
|
||||||
name: '@cordisjs/plugin-include',
|
|
||||||
config: {
|
|
||||||
path: configPath,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Graceful shutdown for snapshot runs (both replay and record): when the client
|
|
||||||
// closes our stdin (it is done driving the session), dispose the whole context.
|
|
||||||
// Disposal awaits the agent-loop teardown and the persistence backend's final
|
|
||||||
// `session/flush`, so the session `.jsonl` is fully written before the process
|
|
||||||
// exits and the harness harvests it (and the subprocess exits cleanly so the
|
|
||||||
// harness's waitForExit resolves). (In a normal editor session stdin stays open
|
|
||||||
// for the connection's lifetime; the editor kills the process, so this never
|
|
||||||
// fires.)
|
|
||||||
if (snapshotMode !== undefined) {
|
|
||||||
process.stdin.on('end', () => {
|
|
||||||
void ctx.fiber.dispose().then(() => { process.exit(0) })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,11 @@ import {
|
|||||||
* WITHOUT a key, since it only needs the server to boot and answer initialize.
|
* WITHOUT a key, since it only needs the server to boot and answer initialize.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const startScript = fileURLToPath(new URL('../start.ts', import.meta.url))
|
// The dsh-acp-agent bin (the demo:acp entry) and this example's cordis.yml. The
|
||||||
|
// bin resolves its config-path arg from CWD; the subprocess runs from a temp
|
||||||
|
// workdir, so pass the example config's ABSOLUTE path.
|
||||||
|
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
|
||||||
|
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||||
// Resolve tsx's loader to an ABSOLUTE path: the subprocess runs with cwd set to
|
// Resolve tsx's loader to an ABSOLUTE path: the subprocess runs with cwd set to
|
||||||
// a temp workdir (this test launches there and uses it as the session cwd; the
|
// a temp workdir (this test launches there and uses it as the session cwd; the
|
||||||
// bridge no longer requires cwd === the launch dir, but a temp dir keeps the
|
// bridge no longer requires cwd === the launch dir, but a temp dir keeps the
|
||||||
@@ -55,7 +59,7 @@ interface Spawned {
|
|||||||
function spawnAcpAgent(cwd: string, env: NodeJS.ProcessEnv = process.env): Spawned {
|
function spawnAcpAgent(cwd: string, env: NodeJS.ProcessEnv = process.env): Spawned {
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
['--import', tsxLoader, startScript],
|
['--import', tsxLoader, binScript, configPath],
|
||||||
{ cwd, env: { ...env, TSX_TSCONFIG_PATH: repoTsconfig }, stdio: ['pipe', 'pipe', 'pipe'] },
|
{ cwd, env: { ...env, TSX_TSCONFIG_PATH: repoTsconfig }, stdio: ['pipe', 'pipe', 'pipe'] },
|
||||||
)
|
)
|
||||||
const stderr: string[] = []
|
const stderr: string[] = []
|
||||||
@@ -101,7 +105,7 @@ describe('acp-agent over real stdio (no key required)', () => {
|
|||||||
// A dummy key lets the deepseek adapter APPLY (it only checks the key is
|
// A dummy key lets the deepseek adapter APPLY (it only checks the key is
|
||||||
// present at boot, not valid — the key is used only on a real model call,
|
// present at boot, not valid — the key is used only on a real model call,
|
||||||
// which this purity test never triggers). So this runs WITHOUT real creds.
|
// which this purity test never triggers). So this runs WITHOUT real creds.
|
||||||
const child = spawn(process.execPath, ['--import', tsxLoader, startScript], {
|
const child = spawn(process.execPath, ['--import', tsxLoader, binScript, configPath], {
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
env: { ...process.env, DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot', TSX_TSCONFIG_PATH: repoTsconfig },
|
env: { ...process.env, DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot', TSX_TSCONFIG_PATH: repoTsconfig },
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ import {
|
|||||||
type SessionNotification,
|
type SessionNotification,
|
||||||
} from '@agentclientprotocol/sdk'
|
} from '@agentclientprotocol/sdk'
|
||||||
|
|
||||||
const startScript = fileURLToPath(new URL('../start.ts', import.meta.url))
|
// The dsh-acp-agent bin (the demo:acp entry) and this example's cordis.yml.
|
||||||
|
// The bin resolves its config-path arg from CWD and, under DSH_SNAPSHOT=replay,
|
||||||
|
// swaps it for the sibling cordis.snapshot.yml. The child's cwd is a temp dir
|
||||||
|
// OUTSIDE the repo, so pass the example config's ABSOLUTE path.
|
||||||
|
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
|
||||||
|
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||||
// The repo-root tsconfig: dev/test run UNBUILT and the `@deepseek-ai/dsh-*`
|
// The repo-root tsconfig: dev/test run UNBUILT and the `@deepseek-ai/dsh-*`
|
||||||
// imports resolve through its `paths` map. The child's cwd is a temp dir
|
// imports resolve through its `paths` map. The child's cwd is a temp dir
|
||||||
@@ -130,7 +135,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise
|
|||||||
|
|
||||||
child = spawn(
|
child = spawn(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
['--import', tsxLoader, startScript],
|
['--import', tsxLoader, binScript, configPath],
|
||||||
{ cwd, env, stdio: ['pipe', 'pipe', 'pipe'] },
|
{ cwd, env, stdio: ['pipe', 'pipe', 'pipe'] },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# 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'
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Shared provider/tool core for the example agents, loaded via a nested
|
|
||||||
# @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
|
|
||||||
# the JSON-RPC protocol (see packages/acp). Each example loads logging itself.
|
|
||||||
# - agent-loop: AgentLoop pre-creates its configured `agents` in its
|
|
||||||
# constructor, and the examples disagree — coding-agent needs a pre-created
|
|
||||||
# `main` (its stdio-chat calls ctx.agents.get('main')), while acp-agent must
|
|
||||||
# pre-create NONE (ACP session/new creates agents on demand). So each example
|
|
||||||
# declares agent-loop with its own `agents` list.
|
|
||||||
#
|
|
||||||
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) in the env.
|
|
||||||
|
|
||||||
# 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).
|
|
||||||
- id: llm-deepseek
|
|
||||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
|
||||||
config:
|
|
||||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
|
||||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
|
||||||
models:
|
|
||||||
- deepseek-v4-flash
|
|
||||||
- deepseek-v4-pro
|
|
||||||
@@ -1,59 +1,59 @@
|
|||||||
# The coding-agent plugin tree, loaded via @cordisjs/plugin-include.
|
# The coding-agent plugin tree: the real coding agent. The two swappable
|
||||||
# Infra (logger/timer/hmr) first, then the shared provider/tool core (nested
|
# backends — the DeepSeek adapter and the local bash executor — plus `hmr` for
|
||||||
# include of ../base.yml), then this example's agent-loop config + UI.
|
# the dev/demo reload loop, then the stdio chat app (@deepseek-ai/dsh-stdio-
|
||||||
|
# agent), which bundles the whole agent-core spine (timer, llm, sessions,
|
||||||
|
# system-prompt, tools, agents, invariants, tool-bash, agent-loop), the console
|
||||||
|
# logger, JSONL persistence, the readline UI, and a pre-created `main` agent.
|
||||||
#
|
#
|
||||||
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) in the
|
# `hmr` is a leaf entry (not baked into dsh-stdio-agent): it is a Loader-only
|
||||||
# environment — start.ts loads the gitignored repo-root .env first.
|
# dev plugin that needs `--expose-internals` — the `demo:coding` script passes
|
||||||
|
# it. Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) in the
|
||||||
- id: logger
|
# environment — the dsh-stdio-agent bin loads the gitignored repo-root .env
|
||||||
name: '@cordisjs/plugin-logger-console'
|
# first. cordis.yml reads them via the `!!js` tag.
|
||||||
|
|
||||||
- id: timer
|
|
||||||
name: '@cordisjs/plugin-timer'
|
|
||||||
|
|
||||||
|
# Hot-module reload for the dev/demo loop (needs `node --expose-internals`).
|
||||||
- id: hmr
|
- id: hmr
|
||||||
name: '@cordisjs/plugin-hmr'
|
name: '@cordisjs/plugin-hmr'
|
||||||
config:
|
config:
|
||||||
root: ['.']
|
root: ['.']
|
||||||
|
|
||||||
# Shared provider/tool core (llm, sessions, system-prompt, tools, agents,
|
# The DeepSeek adapter. Swap to '@deepseek-ai/dsh-llm-pi-ai' for the pi-ai-backed
|
||||||
# invariants, llm-deepseek, bash-local, tool-bash). Nested include: the path is
|
# twin (same config shape; `reasoning: high` replaces thinking/reasoningEffort).
|
||||||
# resolved relative to THIS file's directory.
|
- id: llm-deepseek
|
||||||
- id: base
|
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||||
name: '@cordisjs/plugin-include'
|
|
||||||
config:
|
config:
|
||||||
path: '../base.yml'
|
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||||
|
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||||
|
models:
|
||||||
|
- deepseek-v4-flash
|
||||||
|
- deepseek-v4-pro
|
||||||
|
|
||||||
# agent-loop is per-example (NOT in base.yml): coding-agent pre-creates a `main`
|
# Local bash executor (the model's only tool, via agent-core's tool-bash schema).
|
||||||
# agent its stdio-chat drives via ctx.agents.get('main').
|
- id: bash
|
||||||
- id: agent-loop
|
name: '@deepseek-ai/dsh-bash-local'
|
||||||
name: '@deepseek-ai/dsh-agent-loop'
|
|
||||||
config:
|
config:
|
||||||
agents:
|
timeoutMs: 60000
|
||||||
- id: main
|
|
||||||
model: deepseek-v4-flash
|
|
||||||
# Set RESUME_SESSION_ID to continue a prior persisted session (the ids
|
|
||||||
# live under ./.sessions); unset starts a fresh session each run.
|
|
||||||
resumeSessionId: !!js process.env.RESUME_SESSION_ID
|
|
||||||
systemPrompt: |
|
|
||||||
You are coding-agent, a CLI coding assistant.
|
|
||||||
|
|
||||||
Your only tools are bash (plus bash_output/bash_kill for background
|
# The stdio chat app: the whole spine + front-door cluster, configured for a
|
||||||
tasks). Do ALL file operations through bash: read with cat/sed/head,
|
# real coding agent driving a pre-created `main` agent.
|
||||||
search with grep, write with heredocs (cat <<'EOF' > file), edit
|
- id: stdio-agent
|
||||||
with sed or a rewrite. Each bash call runs in a fresh shell — pass
|
name: '@deepseek-ai/dsh-stdio-agent'
|
||||||
workdir instead of cd, and never rely on shell state between calls.
|
|
||||||
|
|
||||||
Check the [exit code: N] marker on every command; investigate
|
|
||||||
failures before moving on. Verify your work by running the code or
|
|
||||||
tests. Keep answers brief and factual.
|
|
||||||
|
|
||||||
- id: session-persistence
|
|
||||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
|
||||||
config:
|
|
||||||
root: './.sessions'
|
|
||||||
|
|
||||||
- id: stdio-chat
|
|
||||||
name: '@deepseek-ai/dsh-ui-stdio'
|
|
||||||
config:
|
config:
|
||||||
|
model: deepseek-v4-flash
|
||||||
|
# Set RESUME_SESSION_ID to continue a prior persisted session (the ids live
|
||||||
|
# under ./.sessions); unset starts a fresh session each run.
|
||||||
|
resumeSessionId: !!js process.env.RESUME_SESSION_ID
|
||||||
|
persistenceRoot: './.sessions'
|
||||||
welcome: 'coding-agent ready. Give it a coding task (bash is its only tool).'
|
welcome: 'coding-agent ready. Give it a coding task (bash is its only tool).'
|
||||||
|
systemPrompt: |
|
||||||
|
You are coding-agent, a CLI coding assistant.
|
||||||
|
|
||||||
|
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, and never rely on shell state between calls.
|
||||||
|
|
||||||
|
Check the [exit code: N] marker on every command; investigate
|
||||||
|
failures before moving on. Verify your work by running the code or
|
||||||
|
tests. Keep answers brief and factual.
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { pathToFileURL } from 'node:url'
|
|
||||||
import { Context } from 'cordis'
|
|
||||||
import Loader from '@cordisjs/plugin-loader'
|
|
||||||
|
|
||||||
// Load DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL from a gitignored repo-root .env
|
|
||||||
// (Node >= 21.7 native). Absent file is fine — the environment may already
|
|
||||||
// carry the variables; cordis.yml reads them via the `!!js` tag. A
|
|
||||||
// present-but-unreadable/malformed .env is a real misconfiguration: surface it
|
|
||||||
// rather than silently running with the wrong environment.
|
|
||||||
try {
|
|
||||||
process.loadEnvFile(new URL('../../.env', import.meta.url).pathname)
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') {
|
|
||||||
process.stderr.write(`coding-agent: failed to load .env: ${String(error)}\n`)
|
|
||||||
}
|
|
||||||
// ENOENT (no .env) is fine — rely on the ambient environment.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boot a Cordis app from this example's cordis.yml — the same shape as the
|
|
||||||
// upstream `cordis` bin, pinned to this directory.
|
|
||||||
const ctx = new Context()
|
|
||||||
ctx.baseUrl = pathToFileURL(import.meta.dirname).href + '/'
|
|
||||||
|
|
||||||
await ctx.plugin(Loader)
|
|
||||||
await ctx.loader.create({
|
|
||||||
name: '@cordisjs/plugin-include',
|
|
||||||
config: {
|
|
||||||
path: './cordis.yml',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -7,21 +7,28 @@ import { afterEach, describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Keyless Loader-path smoke for examples/coding-agent: boot the REAL example
|
* Keyless Loader-path smoke for examples/coding-agent: boot the REAL example
|
||||||
* through its `cordis.yml` (the cordis Loader, `unwrapExports`, the full plugin
|
* through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the
|
||||||
* tree incl. the extracted `@deepseek-ai/dsh-ui-stdio`), then close stdin with
|
* cordis Loader, `unwrapExports`, the full plugin tree incl. the
|
||||||
* no prompt and assert the ready banner + a clean exit.
|
* `@deepseek-ai/dsh-agent-core` bundle and the extracted
|
||||||
|
* `@deepseek-ai/dsh-ui-stdio`), then close stdin with no prompt and assert the
|
||||||
|
* ready banner + a clean exit.
|
||||||
*
|
*
|
||||||
* No prompt is ever sent, so the model is NEVER called — this is why it runs
|
* No prompt is ever sent, so the model is NEVER called — this is why it runs
|
||||||
* without a real key. coding-agent's `cordis.yml` loads `llm-deepseek`, whose
|
* without a real key. coding-agent's `cordis.yml` loads `llm-deepseek`, whose
|
||||||
* `apply()` only requires a key to be PRESENT (it does not validate it and only
|
* `apply()` only requires a key to be PRESENT (it does not validate it and only
|
||||||
* uses it when a stream actually starts), so a dummy key lets the tree boot
|
* uses it when a stream actually starts), so a dummy key lets the tree boot
|
||||||
* while the absence of any prompt guarantees no network call. The value is the
|
* while the absence of any prompt guarantees no network call. The value is the
|
||||||
* real-Loader-path guard for the shared UI plugin's export shape (a broken
|
* real-Loader-path guard for the app + bundle + UI plugin export shapes (a broken
|
||||||
* `export default` that drops `inject` would crash here — see postmortem 0001),
|
* `export default` that drops `inject`/`Config` would crash here — see postmortem
|
||||||
* complementing coding-agent's with-key e2e suites which prove the real product.
|
* 0001), complementing coding-agent's with-key e2e suites which prove the real
|
||||||
|
* product.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const startScript = fileURLToPath(new URL('../start.ts', import.meta.url))
|
// The dsh-stdio-agent bin (the demo:coding entry) and this example's cordis.yml.
|
||||||
|
// The bin resolves its config-path arg from CWD; the test spawns from a temp
|
||||||
|
// cwd, so we pass the example config's ABSOLUTE path.
|
||||||
|
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||||
|
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||||
// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig
|
// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig
|
||||||
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
||||||
@@ -45,7 +52,7 @@ async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
|||||||
const proc = spawn(
|
const proc = spawn(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
// --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:coding).
|
// --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:coding).
|
||||||
['--expose-internals', '--import', tsxLoader, startScript],
|
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||||
{
|
{
|
||||||
cwd,
|
cwd,
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
# echo-agent
|
# echo-agent
|
||||||
|
|
||||||
Runnable demo: stdin chat with a scripted mock model and an echo tool.
|
Runnable demo: stdin chat with a scripted mock model and an echo tool. The all-mock skeleton — "swap the backend, keep the app".
|
||||||
|
|
||||||
## What it shows
|
## What it shows
|
||||||
|
|
||||||
- A complete Cordis app loaded from `cordis.yml` — the standard "stack of plugins" pattern
|
This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-stdio-agent`](../../packages/ui/stdio-agent) app (which bundles the whole [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) spine, the console logger, JSONL persistence, the readline UI, and a pre-created `main` agent), and swaps in two example-local backends plus `hmr`:
|
||||||
- `mock-llm.ts` — a mock `LlmAdapter` that streams scripted responses and calls the `echo` tool when the user types "echo <something>"
|
|
||||||
- `echo-tool.ts` — a tool registered via `ctx.tools.register()` that echoes text back uppercased
|
- `mock-llm.ts` — a mock `LlmAdapter` that streams scripted responses and calls the `echo` tool when the user types "echo <something>". Registered with `ctx.llm.registerAdapter(['mock-echo'], …)`.
|
||||||
- `@deepseek-ai/dsh-session-persistence-jsonl` — the durable JSONL persistence backend (loaded from `cordis.yml`, `root: ./.sessions`): append-only event log per session with crash-safe atomic writes, replacing the old write-only example plugin
|
- `echo-tool.ts` — a tool registered via `ctx.tools.register(defineTool(…))` with typed `execute` args; echoes text back uppercased.
|
||||||
- `stdio-chat.ts` — a minimal UI plugin: reads stdin lines and `send`/`steer`s the agent, renders stream deltas, tool calls, and tool results
|
|
||||||
|
Swapping `mock-llm` for the real `llm-deepseek` adapter is all that separates this from `coding-agent` — the same app, a different backend.
|
||||||
|
|
||||||
## Plugin files
|
## Plugin files
|
||||||
|
|
||||||
| File | Role | Key patterns demonstrated |
|
| File | Role | Key patterns demonstrated |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `mock-llm.ts` | `LlmAdapter` registration | `ctx.llm.registerAdapter(['mock-echo'], …)`, streaming chunks with proper `block-start`/`block-end` protocol |
|
| `src/mock-llm.ts` | `LlmAdapter` registration | `ctx.llm.registerAdapter(['mock-echo'], …)`, streaming chunks with the proper `block-start`/`block-end` protocol |
|
||||||
| `echo-tool.ts` | Tool registration | `ctx.tools.register(defineTool(…))` with typed `execute` args, tool execution returning `ContentBlock[]` |
|
| `src/echo-tool.ts` | Tool registration | `ctx.tools.register(defineTool(…))` with typed `execute` args, returning `ContentBlock[]` |
|
||||||
| `stdio-chat.ts` | UI | `agent/stream-chunk`, `session/event` (tool/*), stdin→send/steer |
|
| `cordis.yml` | Leaf wiring | the two backends + `hmr` + one `@deepseek-ai/dsh-stdio-agent` entry carrying the app config |
|
||||||
| `start.ts` | Bootstrap | `Context` + `Loader` + `plugin-include` wired to `cordis.yml` |
|
|
||||||
|
|
||||||
Persistence is the shared `@deepseek-ai/dsh-session-persistence-jsonl` plugin (not a per-example file).
|
The spine, UI, persistence, and boot glue all live in `@deepseek-ai/dsh-stdio-agent` and the bundle it loads — this folder holds only the demo-specific mocks and the leaf wiring.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run demo:echo
|
pnpm run demo:echo
|
||||||
# or:
|
# or:
|
||||||
node --expose-internals --import tsx examples/echo-agent/start.ts
|
node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Type a message and press Enter. "echo <text>" triggers a tool call round-trip (the mock model requests the `echo` tool, which echoes the text uppercased, and the next model step acknowledges it).
|
Type a message and press Enter. "echo <text>" triggers a tool call round-trip (the mock model requests the `echo` tool, which echoes the text uppercased, and the next model step acknowledges it).
|
||||||
|
|||||||
@@ -1,57 +1,38 @@
|
|||||||
# The echo-agent plugin tree, loaded via @cordisjs/plugin-include.
|
# The echo-agent plugin tree: the stdio chat app with its LLM backend swapped to
|
||||||
# Core services first, then the demo plugins, then the agent itself.
|
# the local `mock-echo` mock and the local `echo` tool added. The clean
|
||||||
|
# demonstration of "swap the backend, keep the app" — every service the agent
|
||||||
- id: logger
|
# needs lives in @deepseek-ai/dsh-stdio-agent (which bundles @deepseek-ai/dsh-
|
||||||
name: '@cordisjs/plugin-logger-console'
|
# agent-core); this leaf only picks the backends, `hmr`, and the app config.
|
||||||
|
#
|
||||||
- id: timer
|
# No API key: the `mock-echo` adapter never touches the network.
|
||||||
name: '@cordisjs/plugin-timer'
|
|
||||||
|
|
||||||
|
# Hot-module reload for the dev/demo loop (a leaf entry, not baked into
|
||||||
|
# dsh-stdio-agent — it needs `node --expose-internals`, which `demo:echo` passes).
|
||||||
- id: hmr
|
- id: hmr
|
||||||
name: '@cordisjs/plugin-hmr'
|
name: '@cordisjs/plugin-hmr'
|
||||||
config:
|
config:
|
||||||
root: ['.']
|
root: ['.']
|
||||||
|
|
||||||
- id: llm
|
# The mock model (registers the `mock-echo` adapter) and the demo `echo` tool —
|
||||||
name: '@deepseek-ai/dsh-llm'
|
# example-local teaching plugins, resolved relative to THIS file's directory.
|
||||||
|
|
||||||
- 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;
|
|
||||||
# on here so the demo smoke test exercises the contract).
|
|
||||||
- id: invariants
|
|
||||||
name: '@deepseek-ai/dsh-invariants'
|
|
||||||
|
|
||||||
- id: agent-loop
|
|
||||||
name: '@deepseek-ai/dsh-agent-loop'
|
|
||||||
config:
|
|
||||||
agents:
|
|
||||||
- id: main
|
|
||||||
model: mock-echo
|
|
||||||
systemPrompt: 'You are echo-agent, a demo agent.'
|
|
||||||
|
|
||||||
- id: mock-llm
|
- id: mock-llm
|
||||||
name: './src/mock-llm.ts'
|
name: './src/mock-llm.ts'
|
||||||
|
|
||||||
- id: echo-tool
|
- id: echo-tool
|
||||||
name: './src/echo-tool.ts'
|
name: './src/echo-tool.ts'
|
||||||
|
|
||||||
- id: session-persistence
|
# Local bash executor: agent-core ships the `tool-bash` consumer schema, so the
|
||||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
# leaf provides the executor it runs on (the echo demo doesn't drive bash, but
|
||||||
config:
|
# the tool is part of the shared spine).
|
||||||
root: './.sessions'
|
- id: bash
|
||||||
|
name: '@deepseek-ai/dsh-bash-local'
|
||||||
|
|
||||||
- id: stdio-chat
|
# The stdio chat app: console logger + the agent-core spine (pre-creating the
|
||||||
name: '@deepseek-ai/dsh-ui-stdio'
|
# `main` agent on the mock model) + JSONL persistence + the readline UI.
|
||||||
|
- id: stdio-agent
|
||||||
|
name: '@deepseek-ai/dsh-stdio-agent'
|
||||||
config:
|
config:
|
||||||
|
model: mock-echo
|
||||||
|
systemPrompt: 'You are echo-agent, a demo agent.'
|
||||||
welcome: 'echo-agent ready. Type a message ("echo <text>" triggers the tool).'
|
welcome: 'echo-agent ready. Type a message ("echo <text>" triggers the tool).'
|
||||||
|
persistenceRoot: './.sessions'
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { pathToFileURL } from 'node:url'
|
|
||||||
import { Context } from 'cordis'
|
|
||||||
import Loader from '@cordisjs/plugin-loader'
|
|
||||||
|
|
||||||
// Boot a Cordis app from this example's cordis.yml — the same shape as the
|
|
||||||
// upstream `cordis` bin, pinned to this directory.
|
|
||||||
const ctx = new Context()
|
|
||||||
ctx.baseUrl = pathToFileURL(import.meta.dirname).href + '/'
|
|
||||||
|
|
||||||
await ctx.plugin(Loader)
|
|
||||||
await ctx.loader.create({
|
|
||||||
name: '@cordisjs/plugin-include',
|
|
||||||
config: {
|
|
||||||
path: './cordis.yml',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -7,22 +7,29 @@ import { afterEach, describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Keyless Loader-path smoke for examples/echo-agent: boot the REAL example
|
* Keyless Loader-path smoke for examples/echo-agent: boot the REAL example
|
||||||
* through its `cordis.yml` (the cordis Loader, `unwrapExports`, the whole
|
* through the `@deepseek-ai/dsh-stdio-agent` bin against this example's
|
||||||
* plugin tree), pipe a script of stdin lines, and assert the rendered stdout.
|
* `cordis.yml` (the cordis Loader, `unwrapExports`, the whole plugin tree),
|
||||||
|
* pipe a script of stdin lines, and assert the rendered stdout.
|
||||||
*
|
*
|
||||||
* This is the guard the per-file unit suite structurally cannot be: it drives
|
* This is the guard the per-file unit suite structurally cannot be: it drives
|
||||||
* the extracted `@deepseek-ai/dsh-ui-stdio` plugin AND the example-local
|
* the `@deepseek-ai/dsh-stdio-agent` app plugin, the `@deepseek-ai/dsh-agent-core`
|
||||||
* `mock-llm.ts` / `echo-tool.ts` through their REAL load path, so a broken
|
* bundle it loads, the extracted `@deepseek-ai/dsh-ui-stdio` plugin, AND the
|
||||||
* plugin export shape (a stray `export default` that `unwrapExports` would
|
* example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path, so
|
||||||
* collapse, dropping `inject`) fails here even though hand-mounted unit tests
|
* a broken plugin export shape (a stray `export default` that `unwrapExports`
|
||||||
* stay green (see docs/postmortem/0001). It needs no API key — the `mock-echo`
|
* would collapse, dropping `inject`/`Config`) fails here even though hand-mounted
|
||||||
* adapter never touches the network — so it runs in the default e2e gate.
|
* unit tests stay green (see docs/postmortem/0001). It needs no API key — the
|
||||||
|
* `mock-echo` adapter never touches the network — so it runs in the default e2e
|
||||||
|
* gate.
|
||||||
*
|
*
|
||||||
* Both branches of mock-llm.ts are exercised: an `echo …` line (the tool
|
* Both branches of mock-llm.ts are exercised: an `echo …` line (the tool
|
||||||
* round-trip → `ECHO: …`) and a plain line (the direct canned reply).
|
* round-trip → `ECHO: …`) and a plain line (the direct canned reply).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const startScript = fileURLToPath(new URL('../start.ts', import.meta.url))
|
// The dsh-stdio-agent bin (the demo:echo entry) and this example's cordis.yml.
|
||||||
|
// The bin resolves its config-path arg from CWD; the test spawns from a temp
|
||||||
|
// cwd, so we pass the example config's ABSOLUTE path.
|
||||||
|
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||||
|
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||||
// Dev/test run UNBUILT: `@deepseek-ai/dsh-*` imports resolve through the root
|
// Dev/test run UNBUILT: `@deepseek-ai/dsh-*` imports resolve through the root
|
||||||
// tsconfig `paths` map, which tsx finds by searching UP from cwd. We spawn from
|
// tsconfig `paths` map, which tsx finds by searching UP from cwd. We spawn from
|
||||||
@@ -53,8 +60,8 @@ async function runEcho(lines: string[]): Promise<{ stdout: string; code: number
|
|||||||
process.execPath,
|
process.execPath,
|
||||||
// --expose-internals: the example's cordis.yml loads the HMR plugin, which
|
// --expose-internals: the example's cordis.yml loads the HMR plugin, which
|
||||||
// requires it (mirrors the `demo:echo` script). The whole point is to boot
|
// requires it (mirrors the `demo:echo` script). The whole point is to boot
|
||||||
// the example EXACTLY as it really runs, through the Loader.
|
// the example EXACTLY as it really runs, through the bin + Loader.
|
||||||
['--expose-internals', '--import', tsxLoader, startScript],
|
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||||
{ cwd, env: { ...process.env, TSX_TSCONFIG_PATH: repoTsconfig }, stdio: ['pipe', 'pipe', 'pipe'] },
|
{ cwd, env: { ...process.env, TSX_TSCONFIG_PATH: repoTsconfig }, stdio: ['pipe', 'pipe', 'pipe'] },
|
||||||
)
|
)
|
||||||
child = proc
|
child = proc
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
"packages/llm/llm-pi-ai": {
|
"packages/llm/llm-pi-ai": {
|
||||||
"entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
|
"entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
|
||||||
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||||
|
},
|
||||||
|
"packages/ui/acp-agent": {
|
||||||
|
"entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
|
||||||
|
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
||||||
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
|
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
|
||||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints",
|
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints",
|
||||||
"demo:echo": "node --expose-internals --import tsx examples/echo-agent/start.ts",
|
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
|
||||||
"demo:coding": "node --expose-internals --import tsx examples/coding-agent/start.ts",
|
"demo:coding": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
|
||||||
"demo:acp": "node --expose-internals --import tsx examples/acp-agent/start.ts",
|
"demo:acp": "node --import tsx packages/ui/acp-agent/src/bin.ts examples/acp-agent/cordis.yml",
|
||||||
"postinstall": "node scripts/install-lefthook.mjs"
|
"postinstall": "node scripts/install-lefthook.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ dsh-invariants ← dsh-llm, dsh-session, dsh-agent (dev-mode contract checks)
|
|||||||
dsh-acp ← dsh-agent, dsh-llm, dsh-session, dsh-session-persistence (ACP JSON-RPC bridge)
|
dsh-acp ← dsh-agent, dsh-llm, dsh-session, dsh-session-persistence (ACP JSON-RPC bridge)
|
||||||
dsh-ui-stdio ← dsh-agent, dsh-llm, dsh-session (stdio readline UI plugin)
|
dsh-ui-stdio ← dsh-agent, dsh-llm, dsh-session (stdio readline UI plugin)
|
||||||
dsh-llm-replay ← dsh-llm, dsh-session (record/replay adapter for keyless snapshot tests)
|
dsh-llm-replay ← dsh-llm, dsh-session (record/replay adapter for keyless snapshot tests)
|
||||||
|
dsh-agent-core ← timer, dsh-llm, dsh-session, dsh-system-prompt, dsh-tools, dsh-agent, dsh-invariants, dsh-tool-bash, dsh-agent-loop (the providerless spine, as one bundle plugin)
|
||||||
|
dsh-stdio-agent ← dsh-agent-core, dsh-ui-stdio, dsh-session-persistence-jsonl, dsh-agent, dsh-session (stdio chat APP + bin)
|
||||||
|
dsh-acp-agent ← dsh-agent-core, dsh-acp, dsh-session-persistence-jsonl (ACP server APP + bin)
|
||||||
```
|
```
|
||||||
|
|
||||||
The rule: plugins depend on interfaces, never on the concrete loop. `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)).
|
The rule: plugins depend on interfaces, never on the concrete loop. `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)).
|
||||||
@@ -49,6 +52,7 @@ The rule: plugins depend on interfaces, never on the concrete loop. `dsh-agent-l
|
|||||||
| `tools/` | `core` | Tool registry + `tools/execute` waterfall | `ctx.tools` |
|
| `tools/` | `core` | Tool registry + `tools/execute` waterfall | `ctx.tools` |
|
||||||
| `agent/` | `core` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
|
| `agent/` | `core` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
|
||||||
| `agent-loop/` | `core` | THE concrete loop plugin: `ReactLoopAgent` + the loop driver | `ctx.agentLoop` |
|
| `agent-loop/` | `core` | THE concrete loop plugin: `ReactLoopAgent` + the loop driver | `ctx.agentLoop` |
|
||||||
|
| `agent-core/` | `core` | Bundle plugin: the providerless/executor-less/UI-less spine as code (forwards `agent-loop`'s `agents`) | (loads the spine) |
|
||||||
| `bash/` | `bash` | Abstract bash executor seam (interface + vocabulary) | `ctx.bash` |
|
| `bash/` | `bash` | Abstract bash executor seam (interface + vocabulary) | `ctx.bash` |
|
||||||
| `bash-local/` | `bash` | Local-subprocess `BashExecutor` implementation | (registers `ctx.bash`) |
|
| `bash-local/` | `bash` | Local-subprocess `BashExecutor` implementation | (registers `ctx.bash`) |
|
||||||
| `tool-bash/` | `bash` | Model-facing `bash`/`bash_output`/`bash_kill` tool schemas | (registers on `ctx.tools`) |
|
| `tool-bash/` | `bash` | Model-facing `bash`/`bash_output`/`bash_kill` tool schemas | (registers on `ctx.tools`) |
|
||||||
@@ -59,6 +63,8 @@ The rule: plugins depend on interfaces, never on the concrete loop. `dsh-agent-l
|
|||||||
| `session-persistence-sqlite/` | `session-persistence` | SQLite persistence backend | (registers `ctx.sessionPersistence`) |
|
| `session-persistence-sqlite/` | `session-persistence` | SQLite persistence backend | (registers `ctx.sessionPersistence`) |
|
||||||
| `invariants/` | `support` | Dev-mode event-contract invariants + session-log freeze | (listens on `session/*`, `agent/*`) |
|
| `invariants/` | `support` | Dev-mode event-contract invariants + session-log freeze | (listens on `session/*`, `agent/*`) |
|
||||||
| `acp/` | `ui` | Agent Client Protocol bridge: serves the agent to an ACP editor over JSON-RPC stdio | (drives `ctx.agents`/`ctx.sessions`) |
|
| `acp/` | `ui` | Agent Client Protocol bridge: serves the agent to an ACP editor over JSON-RPC stdio | (drives `ctx.agents`/`ctx.sessions`) |
|
||||||
|
| `stdio-agent/` | `ui` | Terminal stdio chat APP: agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) |
|
||||||
|
| `acp-agent/` | `ui` | ACP server APP: agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) |
|
||||||
| `ui-stdio/` | `support` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) |
|
| `ui-stdio/` | `support` | Minimal stdio (readline) UI plugin: renders `agent/*` events, feeds stdin lines to the agent | (drives `ctx.agents`) |
|
||||||
| `llm-replay/` | `support` | Record/replay adapter: short-circuits `llm/stream` with chunks from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) |
|
| `llm-replay/` | `support` | Record/replay adapter: short-circuits `llm/stream` with chunks from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) |
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,8 @@ The packages every harness build is assembled from: the session log, the system-
|
|||||||
| `tools/` | Tool registry + `tools/execute` waterfall | `ctx.tools` |
|
| `tools/` | Tool registry + `tools/execute` waterfall | `ctx.tools` |
|
||||||
| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
|
| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
|
||||||
| `agent-loop/` | The concrete loop plugin: `ReactLoopAgent` + the loop driver | `ctx.agentLoop` |
|
| `agent-loop/` | The concrete loop plugin: `ReactLoopAgent` + the loop driver | `ctx.agentLoop` |
|
||||||
|
| `agent-core/` | Bundle plugin: the providerless/executor-less/UI-less spine as code | (loads the spine) |
|
||||||
|
|
||||||
`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; everything else in `core/` is interface/vocabulary. Plugins depend on the `agent` vocabulary, never on `agent-loop` directly, so the loop stays swappable.
|
`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; everything else in `core/` is interface/vocabulary. Plugins depend on the `agent` vocabulary, never on `agent-loop` directly, so the loop stays swappable.
|
||||||
|
|
||||||
|
`agent-core` is the composition counterpart: one bundle plugin that loads the whole providerless spine (`timer` + `llm` + sessions + system-prompt + tools + agents + invariants + `tool-bash` + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. App packages (`ui/stdio-agent`, `ui/acp-agent`) consume it and add only a front door; a leaf adds only the swappable backends. It lives in `core/` because it composes exclusively `core/` + interface packages and ships no provider, executor, or UI of its own.
|
||||||
|
|||||||
44
packages/core/agent-core/README.md
Normal file
44
packages/core/agent-core/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# @deepseek-ai/dsh-agent-core
|
||||||
|
|
||||||
|
The **providerless, executor-less, UI-less agent spine** as ONE Cordis bundle plugin. It loads the fixed set of services every harness agent needs and forwards the loop's `agents` list as its own config — so an app package composes a working agent by adding only a front door and the swappable backends.
|
||||||
|
|
||||||
|
This is the package to read to see **the whole plugin tree at once** — the teaching role the inlined `echo-agent` `cordis.yml` used to play before the spine moved behind this bundle.
|
||||||
|
|
||||||
|
## The tree it loads
|
||||||
|
|
||||||
|
`apply(ctx, config)` mounts each of these as a child of the bundle fiber:
|
||||||
|
|
||||||
|
```
|
||||||
|
@cordisjs/plugin-timer timer service (writes nothing to stdout)
|
||||||
|
@deepseek-ai/dsh-llm abstract LLM service + content-block vocabulary
|
||||||
|
@deepseek-ai/dsh-session event-sourced session log + store
|
||||||
|
@deepseek-ai/dsh-system-prompt prompt-section + tool-schema assembly
|
||||||
|
@deepseek-ai/dsh-tools tool registry + tools/execute waterfall
|
||||||
|
@deepseek-ai/dsh-agent agent registry + agent/* event vocabulary
|
||||||
|
@deepseek-ai/dsh-invariants dev-mode event-contract assertions
|
||||||
|
@deepseek-ai/dsh-tool-bash the model-facing bash/bash_output/bash_kill schemas
|
||||||
|
@deepseek-ai/dsh-agent-loop THE concrete loop (gets the forwarded `agents`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## What it deliberately leaves OUTSIDE the bundle
|
||||||
|
|
||||||
|
The spine is everything COMMON to every front door. The swappable and front-door-coupled pieces stay out, picked by whatever loads the bundle:
|
||||||
|
|
||||||
|
- **the LLM adapter** — the bundle ships the abstract `llm` service; the leaf registers a concrete adapter on `ctx.llm` (`llm-deepseek`, `llm-pi-ai`, `llm-replay`).
|
||||||
|
- **the bash executor** — the bundle ships `tool-bash` (the consumer schema); the leaf provides `ctx.bash` (`bash-local` or a sandboxed impl).
|
||||||
|
- **presentation + per-app infra** — the stdio UI / ACP bridge, a console logger, `hmr`. These form the coupled "front-door cluster" that the app packages ([`dsh-stdio-agent`](../../ui/stdio-agent/README.md), [`dsh-acp-agent`](../../ui/acp-agent/README.md)) bake in. `timer` is in the spine (common to both, stdout-silent); a console logger is NOT (it writes to stdout, which the ACP bridge reserves for JSON-RPC).
|
||||||
|
|
||||||
|
This is the [interface/implementation/consumer seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Config } from '@deepseek-ai/dsh-agent-core'
|
||||||
|
// Config === AgentLoop.Config — the `agents` list, default [].
|
||||||
|
```
|
||||||
|
|
||||||
|
The bundle FORWARDS `agent-loop`'s `agents` list as its own (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`). Forwarding the list is exactly why the loop can live in the shared spine even though the apps disagree on which agents to pre-create.
|
||||||
|
|
||||||
|
## Why a code bundle, not a shared YAML include
|
||||||
|
|
||||||
|
A YAML include can dedupe the config, but it cannot OWN a `bin`, and it can only *describe* the front-door coupling in a comment and trust each leaf to obey. Moving the spine into a package, and the front-door cluster into the app packages, turns "the ACP app never logs to stdout" from a prose warning into a property of the artifact. Services register in the root store keyed by their isolate symbol, so a child loaded here is visible to the bundle's siblings (the leaf's adapter and executor) exactly as a nested `plugin-include` subtree's services were — cordis gates every read on `inject`, never on load order.
|
||||||
46
packages/core/agent-core/package.json
Normal file
46
packages/core/agent-core/package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-agent-core",
|
||||||
|
"description": "The providerless/executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + agents + invariants + tool-bash + agent-loop)",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cordisjs/plugin-timer": "^1.1.2",
|
||||||
|
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-tool-bash": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||||
|
"cordis": "^4.0.0-rc.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cordisjs/plugin-timer": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-session": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||||
|
"cordis": "^4.0.0-rc.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
88
packages/core/agent-core/src/index.ts
Normal file
88
packages/core/agent-core/src/index.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* The providerless, executor-less, UI-less agent spine as ONE bundle plugin.
|
||||||
|
*
|
||||||
|
* Loads the fixed set of services every harness agent needs — `timer`, the LLM
|
||||||
|
* service, the session store, system-prompt assembly, the tool registry, the
|
||||||
|
* agent registry, the dev-mode invariants, the model-facing `bash` tool
|
||||||
|
* schemas, and the concrete `agent-loop` — and forwards the loop's `agents`
|
||||||
|
* list as its OWN config (default `[]`), so each app supplies its own
|
||||||
|
* pre-created agents.
|
||||||
|
*
|
||||||
|
* It is deliberately NOT the whole app: the swappable choices stay OUTSIDE the
|
||||||
|
* bundle, picked by whatever loads it.
|
||||||
|
* - the LLM ADAPTER (`llm-deepseek`/`llm-pi-ai`/`llm-replay`) — the bundle
|
||||||
|
* ships the abstract `llm` service + `tool-bash` consumer schema; the leaf
|
||||||
|
* registers a concrete adapter on `ctx.llm`.
|
||||||
|
* - the bash EXECUTOR (`bash-local` or a sandboxed impl) — the bundle ships
|
||||||
|
* the `bash` tool consumer; the leaf provides `ctx.bash`.
|
||||||
|
* - the PRESENTATION (stdio UI / ACP bridge / a logger) and the per-app infra
|
||||||
|
* (a console logger, `hmr`) — these are the coupled "front-door cluster" the
|
||||||
|
* app packages ({@link @deepseek-ai/dsh-stdio-agent},
|
||||||
|
* {@link @deepseek-ai/dsh-acp-agent}) bake in, NOT the shared spine.
|
||||||
|
*
|
||||||
|
* This is the interface/implementation/consumer seam at the composition level:
|
||||||
|
* the bundle owns the shared spine, the leaf owns the backends, the app package
|
||||||
|
* owns the front door. `timer` is in the spine (common to every front door — it
|
||||||
|
* writes nothing to stdout); the console logger is NOT (it writes to stdout,
|
||||||
|
* which the ACP bridge reserves for its JSON-RPC channel).
|
||||||
|
*
|
||||||
|
* Services register in the root store keyed by their isolate symbol, so a child
|
||||||
|
* loaded here via `ctx.plugin(...)` is visible to the bundle's SIBLINGS (the
|
||||||
|
* leaf's adapter and executor) exactly as a nested `plugin-include` subtree's
|
||||||
|
* services were before this bundle existed — cordis gates every read on
|
||||||
|
* `inject`, never on load order, so the fixed child set resolves regardless of
|
||||||
|
* which entry loads first.
|
||||||
|
*
|
||||||
|
* Plugin export shape: named `name`/`Config`/`apply`, NO default export — the
|
||||||
|
* cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray
|
||||||
|
* default would collapse the module to the bare `apply` function and drop the
|
||||||
|
* `Config` schema (see docs/postmortem/0001). The keyless Loader-path smokes in
|
||||||
|
* the app packages guard this end-to-end.
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-agent-core
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Context } from 'cordis'
|
||||||
|
import Timer from '@cordisjs/plugin-timer'
|
||||||
|
import LlmService from '@deepseek-ai/dsh-llm'
|
||||||
|
import SessionStore from '@deepseek-ai/dsh-session'
|
||||||
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||||
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||||
|
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||||
|
import * as invariants from '@deepseek-ai/dsh-invariants'
|
||||||
|
import * as toolBash from '@deepseek-ai/dsh-tool-bash'
|
||||||
|
import AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agent-loop'
|
||||||
|
|
||||||
|
export const name = 'agent-core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle config: the agent-loop `agents` list, forwarded verbatim. Default `[]`
|
||||||
|
* — an app that pre-creates no agents (the ACP bridge creates them on demand at
|
||||||
|
* `session/new`) simply omits it; an app that needs a pre-created `main` (the
|
||||||
|
* stdio chat) supplies one. This IS {@link AgentLoopConfig}, so the schema and
|
||||||
|
* the forwarded shape can never drift.
|
||||||
|
*/
|
||||||
|
export type Config = AgentLoopConfig
|
||||||
|
|
||||||
|
/** Forward the loop's own schema so validation + defaulting stay identical. */
|
||||||
|
export const Config = AgentLoop.Config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the spine. Each `ctx.plugin(...)` mounts one child of the bundle fiber;
|
||||||
|
* `agent-loop` receives the forwarded `agents` list. Load order is irrelevant
|
||||||
|
* (cordis pends each fiber on its `inject` until the services it needs exist),
|
||||||
|
* but the listing mirrors the dependency layering for readability: the LLM
|
||||||
|
* vocabulary and core registries first, then the dev tripwire and the bash tool
|
||||||
|
* consumer, then the loop that drives them.
|
||||||
|
*/
|
||||||
|
export function apply(ctx: Context, config: Config): void {
|
||||||
|
ctx.plugin(Timer)
|
||||||
|
ctx.plugin(LlmService)
|
||||||
|
ctx.plugin(SessionStore)
|
||||||
|
ctx.plugin(SystemPrompt)
|
||||||
|
ctx.plugin(ToolRegistry)
|
||||||
|
ctx.plugin(AgentRegistry)
|
||||||
|
ctx.plugin(invariants)
|
||||||
|
ctx.plugin(toolBash)
|
||||||
|
ctx.plugin(AgentLoop, { agents: config.agents })
|
||||||
|
}
|
||||||
57
packages/core/agent-core/tests/agent-core.spec.ts
Normal file
57
packages/core/agent-core/tests/agent-core.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import * as agentCore from '../src/index.ts'
|
||||||
|
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit coverage for the @deepseek-ai/dsh-agent-core bundle: mounting it brings
|
||||||
|
* up the whole providerless spine in one `ctx.plugin`, and the forwarded
|
||||||
|
* `agents` config reaches the loop (default `[]`, or a pre-created agent).
|
||||||
|
*
|
||||||
|
* The bundle is exercised through `ctx.plugin(agentCore, …)` — the NAMESPACE
|
||||||
|
* import, the same shape the Loader builds from `unwrapExports`. The real
|
||||||
|
* Loader-path guard (export shape, `unwrapExports`) is the app packages' keyless
|
||||||
|
* bin smokes; here we assert the composition + config forwarding.
|
||||||
|
*/
|
||||||
|
async function mount(config?: agentCore.Config): Promise<Context> {
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(agentCore, config)
|
||||||
|
// The bundle mounts its children inside apply() (not awaited there); let their
|
||||||
|
// fibers settle so the spine services and any pre-created agent are ready.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50))
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dsh-agent-core bundle', () => {
|
||||||
|
it('brings up the full providerless spine', async () => {
|
||||||
|
const ctx = await mount()
|
||||||
|
// One service from each layer of the spine proves the children loaded.
|
||||||
|
expect(ctx.get('timer')).toBeDefined()
|
||||||
|
expect(ctx.get('llm')).toBeDefined()
|
||||||
|
expect(ctx.get('sessions')).toBeDefined()
|
||||||
|
expect(ctx.get('systemPrompt')).toBeDefined()
|
||||||
|
expect(ctx.get('tools')).toBeDefined()
|
||||||
|
expect(ctx.get('agents')).toBeDefined()
|
||||||
|
expect(ctx.get('agentLoop')).toBeDefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults the agents list to empty (no pre-created agents)', async () => {
|
||||||
|
const ctx = await mount()
|
||||||
|
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forwards a pre-created agent to the loop', async () => {
|
||||||
|
const ctx = await mount({
|
||||||
|
agents: [{ id: AgentId('main'), model: 'mock', systemPrompt: 'hi' }],
|
||||||
|
})
|
||||||
|
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-exports the loop config schema as its own', () => {
|
||||||
|
expect(agentCore.Config).toBeDefined()
|
||||||
|
expect(agentCore.name).toBe('agent-core')
|
||||||
|
})
|
||||||
|
})
|
||||||
42
packages/core/agent-core/tsconfig.json
Normal file
42
packages/core/agent-core/tsconfig.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/timer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../llm/llm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/session"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/system-prompt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/agent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/agent-loop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../support/invariants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../bash/tool-bash"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,5 +5,9 @@ Integrations that expose the agent to an external editor or client. These are **
|
|||||||
| Package | Role | ctx key |
|
| Package | Role | ctx key |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `acp/` | Agent Client Protocol bridge: serves the agent to an ACP editor (Zed) over JSON-RPC stdio | (drives `ctx.agents`/`ctx.sessions`) |
|
| `acp/` | Agent Client Protocol bridge: serves the agent to an ACP editor (Zed) over JSON-RPC stdio | (drives `ctx.agents`/`ctx.sessions`) |
|
||||||
|
| `stdio-agent/` | Terminal stdio chat APP: the agent-core spine + console logger + readline UI + a pre-created `main` agent, with a `bin` | (composition + `bin`) |
|
||||||
|
| `acp-agent/` | ACP server APP: the agent-core spine + JSONL persistence + the `acp` bridge (no stdout logger), with a `bin` | (composition + `bin`) |
|
||||||
|
|
||||||
A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The readline `ui-stdio` plugin is the unstructured analogue but lives in `support/` because it exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product.
|
A UI integration is a client-driver plugin, not a loop change and not a capability seam: it consumes the existing `agent/*` event taxonomy and the `dsh-agent` factory. The readline `ui-stdio` plugin is the unstructured analogue but lives in `support/` because it exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product.
|
||||||
|
|
||||||
|
`stdio-agent` and `acp-agent` are the two **app packages**: each composes the [`core/agent-core`](../core/agent-core/README.md) spine with its coupled front-door cluster (and owns the boot `bin`), so a leaf `cordis.yml` is just the swappable backends plus one app entry. They live in `ui/` because each IS a user-facing front door; the stdout-purity coupling (logger vs. no logger) becomes a property of the artifact rather than a leaf convention.
|
||||||
|
|||||||
39
packages/ui/acp-agent/README.md
Normal file
39
packages/ui/acp-agent/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# @deepseek-ai/dsh-acp-agent
|
||||||
|
|
||||||
|
The **ACP server app**: a Cordis app plugin that composes the providerless agent spine ([`@deepseek-ai/dsh-agent-core`](../../core/agent-core/README.md)) with the front-door cluster an [Agent Client Protocol](../acp/README.md) server needs, and a `bin` that boots a leaf `cordis.yml` speaking ACP JSON-RPC on stdio.
|
||||||
|
|
||||||
|
It is the structured counterpart to [`@deepseek-ai/dsh-stdio-agent`](../stdio-agent/README.md): both consume the same spine, but this one bakes in the OPPOSITE front-door cluster.
|
||||||
|
|
||||||
|
## What it bakes in — and what it deliberately omits
|
||||||
|
|
||||||
|
stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it LEAVES OUT as what it includes:
|
||||||
|
|
||||||
|
| Plugin | Why |
|
||||||
|
|---|---|
|
||||||
|
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating **no** agents (ACP `session/new` creates them on demand) |
|
||||||
|
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log (the bridge advertises `loadSession`) |
|
||||||
|
| `@deepseek-ai/dsh-acp` | the bridge that owns stdout for JSON-RPC |
|
||||||
|
| ~~console logger~~ | **omitted** — it writes to stdout and would corrupt the protocol frames ([the stdout-purity footgun](../acp/README.md)) |
|
||||||
|
| ~~`hmr`~~ | **omitted** — the editor owns the subprocess |
|
||||||
|
|
||||||
|
Because there is no logger entry in the package, the footgun is **structurally unreachable from the leaf**: a leaf author cannot wire a stdout logger into the ACP config, because the leaf only picks backends, not the front door.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
| Key | Default | Routed to |
|
||||||
|
|---|---|---|
|
||||||
|
| `model` | (required) | the per-session agent template the bridge creates agents from |
|
||||||
|
| `systemPrompt` | (required) | the per-session agent's system prompt |
|
||||||
|
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||||
|
|
||||||
|
The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the real model, `llm-replay` for keyless snapshot replay) and a bash executor (`bash-local`).
|
||||||
|
|
||||||
|
## The bin
|
||||||
|
|
||||||
|
`dsh-acp-agent [path-to-cordis.yml]` (default `./cordis.yml`):
|
||||||
|
|
||||||
|
- loads a gitignored `.env` from the cwd — **skipped** in snapshot REPLAY so a stray key can never trigger a live call;
|
||||||
|
- honors `DSH_SNAPSHOT=replay` by booting the sibling `cordis.snapshot.yml` (the keyless replay tree, `llm-replay` in place of `llm-deepseek`);
|
||||||
|
- in a snapshot run, disposes the context on stdin EOF so the session log is fully flushed before exit.
|
||||||
|
|
||||||
|
All diagnostics go to **stderr** — stdout is the protocol.
|
||||||
47
packages/ui/acp-agent/package.json
Normal file
47
packages/ui/acp-agent/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-acp-agent",
|
||||||
|
"description": "ACP server app: the agent-core spine + JSONL persistence + the ACP bridge (no stdout logger, no hmr, no pre-created agents), with a bin to boot a leaf cordis.yml over JSON-RPC stdio",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"dsh-acp-agent": "lib/bin.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./bin": {
|
||||||
|
"types": "./lib/bin.d.ts",
|
||||||
|
"default": "./lib/bin.js"
|
||||||
|
},
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cordisjs/plugin-include": "^1.0.4",
|
||||||
|
"@cordisjs/plugin-loader": "^1.0.0-rc.4",
|
||||||
|
"@deepseek-ai/dsh-acp": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-agent-core": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||||
|
"cordis": "^4.0.0-rc.6",
|
||||||
|
"schemastery": "^3.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cordisjs/plugin-include": "workspace:^",
|
||||||
|
"@cordisjs/plugin-loader": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-acp": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-agent-core": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||||
|
"cordis": "^4.0.0-rc.6",
|
||||||
|
"schemastery": "^3.17.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
100
packages/ui/acp-agent/src/bin.ts
Normal file
100
packages/ui/acp-agent/src/bin.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* The `dsh-acp-agent` bin: boot the ACP server from a leaf `cordis.yml` that
|
||||||
|
* loads the {@link @deepseek-ai/dsh-acp-agent} app plugin (plus an LLM adapter
|
||||||
|
* and a bash executor), speaking ACP JSON-RPC on stdio.
|
||||||
|
*
|
||||||
|
* Owns the ACP-specific boot glue the example's `start.ts` once held:
|
||||||
|
* - `.env` loading (`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`) — SKIPPED in
|
||||||
|
* snapshot REPLAY so a stray key can never trigger a live model call.
|
||||||
|
* - snapshot-mode config selection: `DSH_SNAPSHOT=replay` swaps the given
|
||||||
|
* `cordis.yml` for its sibling `cordis.snapshot.yml` (the keyless replay
|
||||||
|
* tree: `llm-replay` in place of `llm-deepseek`).
|
||||||
|
* - the stdin-dispose lifecycle: in a snapshot run the harness closes stdin
|
||||||
|
* when done, so dispose the context (flushing persistence) and exit cleanly.
|
||||||
|
*
|
||||||
|
* IMPORTANT: stdout is the ACP JSON-RPC channel. This bin writes diagnostics to
|
||||||
|
* STDERR only; the app plugin loads no stdout logger. A stray stdout write
|
||||||
|
* corrupts the protocol frames.
|
||||||
|
*
|
||||||
|
* Usage: `dsh-acp-agent [path-to-cordis.yml]` (default `./cordis.yml`).
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-acp-agent/bin
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { basename, dirname, resolve } from 'node:path'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import Loader from '@cordisjs/plugin-loader'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the config to boot, honoring snapshot REPLAY. Given the requested
|
||||||
|
* path, replay mode swaps a `cordis.yml` basename for `cordis.snapshot.yml` in
|
||||||
|
* the SAME directory (the keyless replay tree). Other modes use the path as-is.
|
||||||
|
* Returns an absolute path resolved from the cwd.
|
||||||
|
*/
|
||||||
|
export function resolveConfigPath(configPath: string, snapshotMode: string | undefined): string {
|
||||||
|
const absolute = resolve(process.cwd(), configPath)
|
||||||
|
if (snapshotMode !== 'replay') return absolute
|
||||||
|
const dir = dirname(absolute)
|
||||||
|
const replayName = basename(absolute).replace(/cordis\.ya?ml$/, 'cordis.snapshot.yml')
|
||||||
|
return resolve(dir, replayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL` from a gitignored `.env` in the
|
||||||
|
* cwd (Node native). Diagnostics go to STDERR (stdout is the protocol). In
|
||||||
|
* REPLAY mode the caller skips this entirely — replay must never reach the
|
||||||
|
* network, so a present `.env` must not enable a live call.
|
||||||
|
*/
|
||||||
|
function loadEnv(): void {
|
||||||
|
try {
|
||||||
|
process.loadEnvFile(resolve(process.cwd(), '.env'))
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') {
|
||||||
|
process.stderr.write(`dsh-acp-agent: failed to load .env: ${String(error)}\n`)
|
||||||
|
}
|
||||||
|
// ENOENT (no .env) is fine — rely on the ambient environment.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the Loader against `absoluteConfigPath`. `baseUrl` is pinned to the
|
||||||
|
* config's directory and the include gets only the basename, so the config's
|
||||||
|
* relative plugin/include paths resolve as the upstream `cordis` bin does.
|
||||||
|
* Returns the root context.
|
||||||
|
*/
|
||||||
|
export async function boot(absoluteConfigPath: string): Promise<Context> {
|
||||||
|
const ctx = new Context()
|
||||||
|
ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/'
|
||||||
|
await ctx.plugin(Loader)
|
||||||
|
await ctx.loader.create({
|
||||||
|
name: '@cordisjs/plugin-include',
|
||||||
|
config: { path: `./${basename(absoluteConfigPath)}` },
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point. Selects the config (snapshot-aware), loads `.env` outside replay,
|
||||||
|
* boots, and — in a snapshot run — disposes the context on stdin EOF so the
|
||||||
|
* session log is fully flushed before exit and the harness's `waitForExit`
|
||||||
|
* resolves. In a normal editor session stdin stays open for the connection's
|
||||||
|
* lifetime (the editor kills the process), so the EOF handler never fires.
|
||||||
|
*/
|
||||||
|
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
|
||||||
|
const snapshotMode = process.env.DSH_SNAPSHOT
|
||||||
|
const configPath = resolveConfigPath(argv[0] ?? './cordis.yml', snapshotMode)
|
||||||
|
if (snapshotMode !== 'replay') loadEnv()
|
||||||
|
const ctx = await boot(configPath)
|
||||||
|
if (snapshotMode !== undefined) {
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
void ctx.fiber.dispose().then(() => { process.exit(0) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v8 ignore start -- top-level CLI invocation; the testable core is
|
||||||
|
resolveConfigPath()/boot()/main(), driven by the keyless snapshot + Loader-path tests */
|
||||||
|
await main()
|
||||||
|
/* v8 ignore stop */
|
||||||
70
packages/ui/acp-agent/src/index.ts
Normal file
70
packages/ui/acp-agent/src/index.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* The ACP server app: the providerless agent spine ({@link
|
||||||
|
* @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster an ACP
|
||||||
|
* server needs — JSONL session persistence and the {@link @deepseek-ai/dsh-acp}
|
||||||
|
* bridge, and DELIBERATELY NOTHING that writes to stdout.
|
||||||
|
*
|
||||||
|
* The cluster is the OPPOSITE of {@link @deepseek-ai/dsh-stdio-agent}'s, and
|
||||||
|
* baking it in is the whole point: an ACP server speaks JSON-RPC on stdout, so
|
||||||
|
* a stray console logger would corrupt the protocol frames (the [stdout-purity
|
||||||
|
* footgun]). This package contains NO console-logger entry, NO `hmr` (the editor
|
||||||
|
* owns the subprocess), and pre-creates NO agents (ACP `session/new` creates
|
||||||
|
* them on demand) — so the footgun is structurally unreachable from the leaf:
|
||||||
|
* there is no logger entry to get wrong.
|
||||||
|
*
|
||||||
|
* The leaf supplies only the swappable backends: the LLM adapter (`llm-deepseek`
|
||||||
|
* for the real model, `llm-replay` for keyless snapshot replay) and the bash
|
||||||
|
* executor (`bash-local`). This app's {@link Config} (model, system prompt,
|
||||||
|
* persistence root) routes each value to where it is wired — model/prompt onto
|
||||||
|
* the bridge's per-session agent template, the root onto the JSONL backend.
|
||||||
|
*
|
||||||
|
* Plugin export shape: named `name`/`Config`/`apply`, NO default export — the
|
||||||
|
* cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray
|
||||||
|
* default would collapse the module to the bare `apply` and drop the `Config`
|
||||||
|
* namespace (see docs/postmortem/0001 — the exact bug that shipped here once).
|
||||||
|
* The keyless ACP snapshot/Loader-path tests guard this end-to-end.
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-acp-agent
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Context } from 'cordis'
|
||||||
|
import z from 'schemastery'
|
||||||
|
import * as acp from '@deepseek-ai/dsh-acp'
|
||||||
|
import * as agentCore from '@deepseek-ai/dsh-agent-core'
|
||||||
|
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||||
|
|
||||||
|
export const name = 'acp-agent'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App config: the swappable per-deployment values. `model`/`systemPrompt`
|
||||||
|
* configure the agent template the ACP bridge creates each session's agent from
|
||||||
|
* (NOT a pre-created agent — ACP creates agents at `session/new`);
|
||||||
|
* `persistenceRoot` is the JSONL backend's directory.
|
||||||
|
*/
|
||||||
|
export interface Config {
|
||||||
|
/** Model name for ACP-created agents (must have a registered adapter). */
|
||||||
|
model: string
|
||||||
|
/** Per-agent system prompt for ACP-created agents. */
|
||||||
|
systemPrompt: string
|
||||||
|
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||||
|
persistenceRoot?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Config: z<Config> = z.object({
|
||||||
|
model: z.string().required(),
|
||||||
|
systemPrompt: z.string().required(),
|
||||||
|
persistenceRoot: z.string().default('./.sessions'),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose the spine with the ACP front door. The agent-core bundle pre-creates
|
||||||
|
* NO agents (its `agents` list defaults to `[]`); the JSONL backend persists
|
||||||
|
* under `persistenceRoot`; the ACP bridge owns stdout for JSON-RPC and creates
|
||||||
|
* one agent per `session/new` from `model`/`systemPrompt`. No logger, no `hmr` —
|
||||||
|
* stdout stays pure.
|
||||||
|
*/
|
||||||
|
export function apply(ctx: Context, config: Config): void {
|
||||||
|
ctx.plugin(agentCore)
|
||||||
|
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
|
||||||
|
ctx.plugin(acp, { model: config.model, systemPrompt: config.systemPrompt })
|
||||||
|
}
|
||||||
52
packages/ui/acp-agent/tests/acp-agent.spec.ts
Normal file
52
packages/ui/acp-agent/tests/acp-agent.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import * as acpAgent from '../src/index.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-process unit coverage for the @deepseek-ai/dsh-acp-agent composition:
|
||||||
|
* mounting it brings up the agent-core spine + JSONL persistence + the ACP
|
||||||
|
* bridge in one `ctx.plugin`. Unlike the stdio app, this one loads NO
|
||||||
|
* Loader-only plugin (no hmr), so it mounts in a plain Context.
|
||||||
|
*
|
||||||
|
* The REAL Loader-path guard (export shape via `unwrapExports`, the headline
|
||||||
|
* ACP operations end-to-end) is the keyless bin smoke in `load-path.e2e.ts`;
|
||||||
|
* this spec asserts the composition and the persistenceRoot default branch.
|
||||||
|
*/
|
||||||
|
async function mount(config: acpAgent.Config): Promise<Context> {
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(acpAgent, config)
|
||||||
|
// The bundle mounts its children inside apply() (not awaited there); let their
|
||||||
|
// fibers settle so the spine services are ready.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50))
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dsh-acp-agent composition', () => {
|
||||||
|
it('brings up the spine + persistence + the ACP bridge', async () => {
|
||||||
|
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-acp-agent-test' })
|
||||||
|
expect(ctx.get('agents')).toBeDefined()
|
||||||
|
expect(ctx.get('sessions')).toBeDefined()
|
||||||
|
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||||
|
expect(ctx.get('agentLoop')).toBeDefined()
|
||||||
|
// No pre-created agents — ACP session/new creates them on demand.
|
||||||
|
expect(ctx.get('agents')!.list()).toHaveLength(0)
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults the persistence root when omitted', async () => {
|
||||||
|
// Exercises the `?? './.sessions'` fallback for a direct-apply caller that
|
||||||
|
// bypasses the schema's `.default(...)`: call `apply` directly (not via
|
||||||
|
// `ctx.plugin`, which validates+defaults the config first) with no
|
||||||
|
// persistenceRoot, so the runtime fallback is the one that fires.
|
||||||
|
const ctx = new Context()
|
||||||
|
acpAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi' })
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50))
|
||||||
|
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exposes its plugin shape', () => {
|
||||||
|
expect(acpAgent.name).toBe('acp-agent')
|
||||||
|
expect(acpAgent.Config).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
147
packages/ui/acp-agent/tests/load-path.e2e.ts
Normal file
147
packages/ui/acp-agent/tests/load-path.e2e.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||||
|
import { Readable, Writable } from 'node:stream'
|
||||||
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
ClientSideConnection,
|
||||||
|
ndJsonStream,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
type Agent as AcpAgent,
|
||||||
|
type Client,
|
||||||
|
type RequestPermissionRequest,
|
||||||
|
type RequestPermissionResponse,
|
||||||
|
type SessionNotification,
|
||||||
|
} from '@agentclientprotocol/sdk'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REAL-load-path smoke for @deepseek-ai/dsh-acp-agent: boot the app through its
|
||||||
|
* own `bin` (the demo:acp entry) as a subprocess, driving the cordis Loader and
|
||||||
|
* `unwrapExports` over a minimal `cordis.yml` that loads THIS package. This is
|
||||||
|
* the guard a hand-built `ctx.plugin({...})` mount structurally cannot be — that
|
||||||
|
* bypasses `unwrapExports`, the exact path that once dropped the bridge's
|
||||||
|
* `inject` and shipped (docs/postmortem/0001). It exercises the headline ACP
|
||||||
|
* operations end-to-end: `initialize` → `session/new` → `session/load`.
|
||||||
|
*
|
||||||
|
* KEYLESS: `session/new` and `session/load` reach the agent FACTORY but never
|
||||||
|
* the model (no prompt is sent), so no DEEPSEEK_API_KEY is needed. A dummy key
|
||||||
|
* lets `llm-deepseek`'s `apply()` (key-PRESENT check only) boot the tree.
|
||||||
|
*
|
||||||
|
* The config is written into a temp dir whose cwd IS the session workspace, so
|
||||||
|
* the bash workdir validation passes. We point tsx at the repo-root tsconfig
|
||||||
|
* (TSX_TSCONFIG_PATH) because the child's cwd is outside the repo and the
|
||||||
|
* unbuilt `paths` map is found by searching UP from cwd.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const binScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
|
||||||
|
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||||
|
// Repo root is four levels up from packages/ui/acp-agent/tests.
|
||||||
|
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
|
||||||
|
|
||||||
|
// A minimal leaf that loads this app + the two backends — the same shape as
|
||||||
|
// examples/acp-agent/cordis.yml, inlined so the package test owns its fixture.
|
||||||
|
const CORDIS_YML = `
|
||||||
|
- id: llm-deepseek
|
||||||
|
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||||
|
config:
|
||||||
|
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||||
|
models: [deepseek-v4-flash]
|
||||||
|
- id: bash
|
||||||
|
name: '@deepseek-ai/dsh-bash-local'
|
||||||
|
- id: acp-agent
|
||||||
|
name: '@deepseek-ai/dsh-acp-agent'
|
||||||
|
config:
|
||||||
|
model: deepseek-v4-flash
|
||||||
|
systemPrompt: 'You are a test agent.'
|
||||||
|
`
|
||||||
|
|
||||||
|
interface Spawned {
|
||||||
|
child: ChildProcessWithoutNullStreams
|
||||||
|
client: ClientSideConnection
|
||||||
|
stderr: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let spawned: Spawned | undefined
|
||||||
|
let workdir: string | undefined
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (spawned !== undefined) {
|
||||||
|
spawned.child.kill('SIGKILL')
|
||||||
|
spawned = undefined
|
||||||
|
}
|
||||||
|
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||||
|
workdir = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
async function boot(): Promise<Spawned & { cwd: string }> {
|
||||||
|
workdir = await mkdtemp(join(tmpdir(), 'acp-agent-pkg-'))
|
||||||
|
const cwd = workdir
|
||||||
|
const configPath = join(cwd, 'cordis.yml')
|
||||||
|
await writeFile(configPath, CORDIS_YML)
|
||||||
|
const child = spawn(
|
||||||
|
process.execPath,
|
||||||
|
['--import', tsxLoader, binScript, configPath],
|
||||||
|
{
|
||||||
|
cwd,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TSX_TSCONFIG_PATH: repoTsconfig,
|
||||||
|
// Key-present check only; no prompt is sent, so the model is never called.
|
||||||
|
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'keyless-acp-agent-smoke',
|
||||||
|
},
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const stderr: string[] = []
|
||||||
|
child.stderr.setEncoding('utf8')
|
||||||
|
child.stderr.on('data', (chunk: string) => stderr.push(chunk))
|
||||||
|
const stream = ndJsonStream(
|
||||||
|
Writable.toWeb(child.stdin) as WritableStream<Uint8Array>,
|
||||||
|
Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,
|
||||||
|
)
|
||||||
|
const makeClient = (_agent: AcpAgent): Client => ({
|
||||||
|
sessionUpdate(_params: SessionNotification): Promise<void> {
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
requestPermission(_params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
||||||
|
return Promise.resolve({ outcome: { outcome: 'cancelled' } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const client = new ClientSideConnection(makeClient, stream)
|
||||||
|
spawned = { child, client, stderr }
|
||||||
|
return { ...spawned, cwd }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dsh-acp-agent real-load-path smoke (bin + Loader, keyless)', () => {
|
||||||
|
it('boots via its bin and answers initialize → session/new → session/load', async () => {
|
||||||
|
const { client, cwd, stderr } = await boot()
|
||||||
|
// initialize: a broken export shape (collapsed bridge plugin, dropped inject)
|
||||||
|
// crashes the tree on the first service read here — see postmortem 0001.
|
||||||
|
const init = await client.initialize({
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
clientCapabilities: {},
|
||||||
|
})
|
||||||
|
expect(init.agentCapabilities?.loadSession).toBe(true)
|
||||||
|
|
||||||
|
// session/new reaches the agent FACTORY (create) without the model.
|
||||||
|
const { sessionId } = await client.newSession({ cwd, mcpServers: [] })
|
||||||
|
expect(sessionId).toBeTruthy()
|
||||||
|
|
||||||
|
// session/load reaches the resume FACTORY + persistence without the model:
|
||||||
|
// load an UNKNOWN id (loading the live `sessionId` would correctly reject as
|
||||||
|
// "already loaded"). The bridge consults `sessionPersistence.list()` then
|
||||||
|
// `agents.resume()`, both of which run from the JSON-RPC read loop OUTSIDE
|
||||||
|
// the bridge's inject scope — the exact path postmortem 0001 crashed. A
|
||||||
|
// healthy tree rejects with a not-found error; a broken export shape would
|
||||||
|
// instead throw "cannot get property … without inject" before reaching it.
|
||||||
|
const unknownId = '00000000-0000-4000-8000-000000000000'
|
||||||
|
await client.loadSession({ sessionId: unknownId, cwd, mcpServers: [] }).then(
|
||||||
|
() => { throw new Error('expected session/load of an unknown id to reject') },
|
||||||
|
(error: unknown) => { expect(String(error)).not.toContain('without inject') },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stderr.join('')).not.toContain('without inject')
|
||||||
|
}, 30_000)
|
||||||
|
})
|
||||||
30
packages/ui/acp-agent/tsconfig.json
Normal file
30
packages/ui/acp-agent/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/schemastery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/loader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../acp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/agent-core"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../session-persistence/session-persistence-jsonl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
packages/ui/acp-agent/tsdown.config.ts
Normal file
18
packages/ui/acp-agent/tsdown.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'tsdown'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* acp-agent ships TWO entries: the plugin (`index`) and the CLI `bin` (`bin`),
|
||||||
|
* the latter referenced by package.json `bin`/`exports["./bin"]`. The root
|
||||||
|
* tsdown builds only `src/index.ts`, so this override adds `bin.ts`.
|
||||||
|
* Declarations come from `tsc -b` (dts: false), matching every package.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts', 'src/bin.ts'],
|
||||||
|
outDir: 'lib',
|
||||||
|
format: ['esm'],
|
||||||
|
platform: 'node',
|
||||||
|
target: 'es2024',
|
||||||
|
fixedExtension: false,
|
||||||
|
dts: false,
|
||||||
|
clean: false,
|
||||||
|
})
|
||||||
60
packages/ui/stdio-agent/README.md
Normal file
60
packages/ui/stdio-agent/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# @deepseek-ai/dsh-stdio-agent
|
||||||
|
|
||||||
|
The **terminal stdio chat app**: a Cordis app plugin that composes the providerless agent spine ([`@deepseek-ai/dsh-agent-core`](../../core/agent-core/README.md)) with the front-door cluster a terminal chat needs, and a `bin` that boots a leaf `cordis.yml`.
|
||||||
|
|
||||||
|
It is the readline counterpart to [`@deepseek-ai/dsh-acp-agent`](../acp-agent/README.md): both consume the same spine, but each bakes in the OPPOSITE front-door cluster.
|
||||||
|
|
||||||
|
## What it bakes in
|
||||||
|
|
||||||
|
A terminal chat always wants the same cluster, so the package owns it rather than trusting each leaf to re-wire it:
|
||||||
|
|
||||||
|
| Plugin | Why it is here |
|
||||||
|
|---|---|
|
||||||
|
| `@cordisjs/plugin-logger-console` | the console logger — stdout is just the terminal here, so logging to it is correct (the ACP app must NOT have this) |
|
||||||
|
| `@deepseek-ai/dsh-agent-core` | the spine, pre-creating a `main` agent from this app's `model`/`systemPrompt` |
|
||||||
|
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log under `persistenceRoot` |
|
||||||
|
| `@deepseek-ai/dsh-ui-stdio` | the readline UI, bound to the `main` agent |
|
||||||
|
|
||||||
|
`@cordisjs/plugin-hmr` (the dev/demo edit-reload loop) is deliberately a **leaf** entry, NOT baked in here: it is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader`, and the in-process test tier cannot even import it (so a package whose `apply` statically pulled it in 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. The `demo:echo` / `demo:coding` leaves load it and pass `--expose-internals`.
|
||||||
|
|
||||||
|
The leaf `cordis.yml` supplies only the **swappable backends** — an LLM adapter (`llm-deepseek` for the real model, or the mock `mock-llm` for a demo) and a bash executor (`bash-local`) — `hmr`, plus this app's [`Config`](#config). The whole plugin tree a run loads is therefore: this app's cluster, the spine inside `agent-core`, `hmr`, and the two leaf backends.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
| Key | Default | Routed to |
|
||||||
|
|---|---|---|
|
||||||
|
| `model` | (required) | the pre-created `main` agent's model |
|
||||||
|
| `systemPrompt` | (required) | the `main` agent's system prompt |
|
||||||
|
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||||
|
| `welcome` | `ready.` | the stdin-chat banner |
|
||||||
|
| `resumeSessionId` | — | resume a persisted session id instead of starting fresh (sourced from an env var in the leaf) |
|
||||||
|
|
||||||
|
## The bin
|
||||||
|
|
||||||
|
`dsh-stdio-agent [path-to-cordis.yml]` (default `./cordis.yml`) loads a gitignored `.env` from the cwd (`DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`), then drives the cordis Loader against the config — the boot glue the `examples/*/start.ts` files once each duplicated. The `demo:echo` / `demo:coding` scripts invoke it.
|
||||||
|
|
||||||
|
## Example leaf `cordis.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# A real coding agent: hmr + the DeepSeek adapter + local bash, then this app.
|
||||||
|
- id: hmr
|
||||||
|
name: '@cordisjs/plugin-hmr'
|
||||||
|
config:
|
||||||
|
root: ['.']
|
||||||
|
- id: llm-deepseek
|
||||||
|
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||||
|
config:
|
||||||
|
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||||
|
models: [deepseek-v4-flash]
|
||||||
|
- id: bash
|
||||||
|
name: '@deepseek-ai/dsh-bash-local'
|
||||||
|
config:
|
||||||
|
timeoutMs: 60000
|
||||||
|
- id: stdio-agent
|
||||||
|
name: '@deepseek-ai/dsh-stdio-agent'
|
||||||
|
config:
|
||||||
|
model: deepseek-v4-flash
|
||||||
|
systemPrompt: 'You are a CLI coding assistant. Your only tools are bash…'
|
||||||
|
```
|
||||||
|
|
||||||
|
Swap `llm-deepseek` for a `mock-llm` leaf plugin and you have the echo demo — "swap the backend, keep the app".
|
||||||
53
packages/ui/stdio-agent/package.json
Normal file
53
packages/ui/stdio-agent/package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-stdio-agent",
|
||||||
|
"description": "Terminal stdio chat app: the agent-core spine + console logger + readline UI + a pre-created main agent, with a bin to boot a leaf cordis.yml",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"dsh-stdio-agent": "lib/bin.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./bin": {
|
||||||
|
"types": "./lib/bin.d.ts",
|
||||||
|
"default": "./lib/bin.js"
|
||||||
|
},
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cordisjs/plugin-include": "^1.0.4",
|
||||||
|
"@cordisjs/plugin-loader": "^1.0.0-rc.4",
|
||||||
|
"@cordisjs/plugin-logger-console": "^1.0.0",
|
||||||
|
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-agent-core": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-ui-stdio": "^0.0.1",
|
||||||
|
"cordis": "^4.0.0-rc.6",
|
||||||
|
"schemastery": "^3.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cordisjs/plugin-include": "workspace:^",
|
||||||
|
"@cordisjs/plugin-loader": "workspace:^",
|
||||||
|
"@cordisjs/plugin-logger-console": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-agent-core": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-session": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-ui-stdio": "workspace:^",
|
||||||
|
"cordis": "^4.0.0-rc.6",
|
||||||
|
"schemastery": "^3.17.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
70
packages/ui/stdio-agent/src/bin.ts
Normal file
70
packages/ui/stdio-agent/src/bin.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* The `dsh-stdio-agent` bin: boot a Cordis app from a leaf `cordis.yml` that
|
||||||
|
* loads the {@link @deepseek-ai/dsh-stdio-agent} app plugin (plus a backend LLM
|
||||||
|
* adapter and a bash executor). Owns the boot glue the three `examples/*` once
|
||||||
|
* duplicated in their `start.ts`: load the gitignored repo-root `.env`, then
|
||||||
|
* drive the cordis Loader against the config path (default `./cordis.yml`).
|
||||||
|
*
|
||||||
|
* Usage: `dsh-stdio-agent [path-to-cordis.yml]`. The `demo:echo` / `demo:coding`
|
||||||
|
* scripts invoke it with the example's config.
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-stdio-agent/bin
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { basename, dirname, resolve } from 'node:path'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import Loader from '@cordisjs/plugin-loader'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL` from a gitignored `.env` in the
|
||||||
|
* CURRENT WORKING DIRECTORY (Node native `process.loadEnvFile`). An absent file
|
||||||
|
* is fine — the environment may already carry the variables; the leaf
|
||||||
|
* `cordis.yml` reads them via the `!!js` tag. A present-but-unreadable/malformed
|
||||||
|
* `.env` is a real misconfiguration: surface it on stderr rather than silently
|
||||||
|
* running with the wrong environment. The mock-model demo (echo) ships no key
|
||||||
|
* and simply has no `.env`.
|
||||||
|
*/
|
||||||
|
function loadEnv(): void {
|
||||||
|
try {
|
||||||
|
process.loadEnvFile(resolve(process.cwd(), '.env'))
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') {
|
||||||
|
process.stderr.write(`dsh-stdio-agent: failed to load .env: ${String(error)}\n`)
|
||||||
|
}
|
||||||
|
// ENOENT (no .env) is fine — rely on the ambient environment.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the Loader against `configPath` (resolved from the CWD). `baseUrl` is
|
||||||
|
* pinned to the config's directory and the include is handed only the basename,
|
||||||
|
* so the config's relative plugin/include paths resolve exactly as the upstream
|
||||||
|
* `cordis` bin does. Returns the root context (the process owns its lifetime).
|
||||||
|
*/
|
||||||
|
export async function boot(configPath: string): Promise<Context> {
|
||||||
|
const absolute = resolve(process.cwd(), configPath)
|
||||||
|
const ctx = new Context()
|
||||||
|
ctx.baseUrl = pathToFileURL(dirname(absolute)).href + '/'
|
||||||
|
await ctx.plugin(Loader)
|
||||||
|
await ctx.loader.create({
|
||||||
|
name: '@cordisjs/plugin-include',
|
||||||
|
config: { path: `./${basename(absolute)}` },
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point: load `.env`, then boot the config named on argv (default
|
||||||
|
* `./cordis.yml`). Awaited at the module top level by the published bin
|
||||||
|
* (`#!/usr/bin/env node` shebang via the package's `bin` field).
|
||||||
|
*/
|
||||||
|
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
|
||||||
|
loadEnv()
|
||||||
|
await boot(argv[0] ?? './cordis.yml')
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v8 ignore start -- top-level CLI invocation; the testable core is boot()/main(), driven by the keyless Loader-path smoke */
|
||||||
|
await main()
|
||||||
|
/* v8 ignore stop */
|
||||||
98
packages/ui/stdio-agent/src/index.ts
Normal file
98
packages/ui/stdio-agent/src/index.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* The stdio chat app: the providerless agent spine ({@link
|
||||||
|
* @deepseek-ai/dsh-agent-core}) plus the coupled front-door cluster a terminal
|
||||||
|
* chat needs — a console logger, the readline `ui-stdio` UI, JSONL session
|
||||||
|
* persistence, and a pre-created `main` agent the UI drives.
|
||||||
|
*
|
||||||
|
* The cluster is BAKED IN, not left to the leaf: a stdio app always logs to the
|
||||||
|
* console (stdout is just the terminal) and always pre-creates the `main` agent
|
||||||
|
* `ui-stdio` sends to. The leaf supplies only the swappable backends (the LLM
|
||||||
|
* adapter, the bash executor), the optional `hmr` dev-reload plugin, and this
|
||||||
|
* app's {@link Config} (model, prompt, persistence root, welcome banner).
|
||||||
|
*
|
||||||
|
* `hmr` is deliberately a LEAF entry, not baked in here: it is a Loader-only,
|
||||||
|
* subprocess-only dev plugin (its constructor throws without `--expose-internals`
|
||||||
|
* + a live `loader`, and the in-process test tier cannot even import it), so a
|
||||||
|
* package whose `apply` statically pulled it in could never be unit-tested or
|
||||||
|
* 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, while
|
||||||
|
* baking the LOGGER in (the real coupling) keeps stdout-vs-no-stdout a property
|
||||||
|
* of the artifact.
|
||||||
|
*
|
||||||
|
* Counterpart to {@link @deepseek-ai/dsh-acp-agent}, which bakes in the OPPOSITE
|
||||||
|
* cluster (no stdout logger, no pre-created agents — the ACP bridge reserves
|
||||||
|
* stdout for JSON-RPC and creates agents on demand). Splitting the two front
|
||||||
|
* doors into two packages makes each cluster a property of the artifact: there
|
||||||
|
* is no logger entry in the ACP leaf to get wrong.
|
||||||
|
*
|
||||||
|
* Plugin export shape: named `name`/`Config`/`apply`, NO default export — the
|
||||||
|
* cordis Loader's `unwrapExports` does `exports.default ?? exports`, so a stray
|
||||||
|
* default would collapse the module to the bare `apply` and drop the `Config`
|
||||||
|
* namespace (see docs/postmortem/0001). The keyless Loader-path smoke in the
|
||||||
|
* echo example guards this end-to-end.
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-stdio-agent
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Context } from 'cordis'
|
||||||
|
import ConsoleExporter from '@cordisjs/plugin-logger-console'
|
||||||
|
import z from 'schemastery'
|
||||||
|
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||||
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||||
|
import * as agentCore from '@deepseek-ai/dsh-agent-core'
|
||||||
|
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||||
|
import * as uiStdio from '@deepseek-ai/dsh-ui-stdio'
|
||||||
|
|
||||||
|
export const name = 'stdio-agent'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App config: the swappable per-demo values, each routed to where the app wires
|
||||||
|
* it. `model`/`systemPrompt`/`resumeSessionId` configure the pre-created `main`
|
||||||
|
* agent (through {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list);
|
||||||
|
* `persistenceRoot` is the JSONL backend's directory; `welcome` is the UI banner.
|
||||||
|
*/
|
||||||
|
export interface Config {
|
||||||
|
/** Model name for the `main` agent (must have a registered adapter). */
|
||||||
|
model: string
|
||||||
|
/** System prompt for the `main` agent. */
|
||||||
|
systemPrompt: string
|
||||||
|
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||||
|
persistenceRoot?: string
|
||||||
|
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
|
||||||
|
welcome?: string
|
||||||
|
/**
|
||||||
|
* If set, the `main` agent RESUMES this persisted session id instead of
|
||||||
|
* starting fresh. Sourced from an env var in the leaf `cordis.yml`
|
||||||
|
* (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`).
|
||||||
|
*/
|
||||||
|
resumeSessionId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Config: z<Config> = z.object({
|
||||||
|
model: z.string().required(),
|
||||||
|
systemPrompt: z.string().required(),
|
||||||
|
persistenceRoot: z.string().default('./.sessions'),
|
||||||
|
welcome: z.string().default('ready.'),
|
||||||
|
resumeSessionId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose the spine with the stdio front door. The console logger comes first
|
||||||
|
* (infra), then the agent-core bundle pre-creating the `main` agent from this
|
||||||
|
* app's `model`/`systemPrompt`/`resumeSessionId`, then the JSONL backend, then
|
||||||
|
* the `ui-stdio` UI bound to `main`. The `hmr` dev-reload plugin is a leaf
|
||||||
|
* concern (see the module doc), so it is not mounted here.
|
||||||
|
*/
|
||||||
|
export function apply(ctx: Context, config: Config): void {
|
||||||
|
ctx.plugin(ConsoleExporter)
|
||||||
|
ctx.plugin(agentCore, {
|
||||||
|
agents: [{
|
||||||
|
id: AgentId('main'),
|
||||||
|
model: config.model,
|
||||||
|
systemPrompt: config.systemPrompt,
|
||||||
|
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
|
||||||
|
ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.', agent: 'main' })
|
||||||
|
}
|
||||||
71
packages/ui/stdio-agent/tests/stdio-agent.spec.ts
Normal file
71
packages/ui/stdio-agent/tests/stdio-agent.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||||
|
import * as stdioAgent from '../src/index.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit coverage for the @deepseek-ai/dsh-stdio-agent app plugin: mounting it
|
||||||
|
* composes the console logger, the agent-core spine (pre-creating the `main`
|
||||||
|
* agent from the app config), the JSONL backend, and the readline UI in one
|
||||||
|
* `ctx.plugin`. The forwarded `model`/`systemPrompt` reach the pre-created
|
||||||
|
* agent; `persistenceRoot`/`welcome`/`resumeSessionId` route to their backends.
|
||||||
|
*
|
||||||
|
* `hmr` is NOT part of this plugin (it is a leaf entry — a Loader-only dev
|
||||||
|
* plugin the in-process tier cannot import); the REAL Loader-path guard (export
|
||||||
|
* shape, `unwrapExports`, the whole subprocess tree incl. `hmr`) is the keyless
|
||||||
|
* echo smoke in `examples/echo-agent`. Here we assert the composition + config
|
||||||
|
* forwarding the unit tier can reach.
|
||||||
|
*/
|
||||||
|
async function mount(config: stdioAgent.Config): Promise<Context> {
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(stdioAgent, config)
|
||||||
|
// The app mounts its children inside apply() (not awaited there); let their
|
||||||
|
// fibers settle so the spine services + the pre-created agent are ready.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 80))
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dsh-stdio-agent app', () => {
|
||||||
|
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
|
||||||
|
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec' })
|
||||||
|
// The spine services (brought up by the agent-core bundle) are all present.
|
||||||
|
expect(ctx.get('agents')).toBeDefined()
|
||||||
|
expect(ctx.get('agentLoop')).toBeDefined()
|
||||||
|
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||||
|
// The pre-created `main` agent the UI drives.
|
||||||
|
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults persistenceRoot and welcome when omitted', async () => {
|
||||||
|
// Direct apply (NOT via ctx.plugin, which validates+defaults the config
|
||||||
|
// first) so the runtime `?? './.sessions'` / `?? 'ready.'` fallbacks on
|
||||||
|
// apply()'s last two lines are the ones that fire — covering a
|
||||||
|
// schema-bypassing direct-mount caller.
|
||||||
|
const ctx = new Context()
|
||||||
|
stdioAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi' })
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 80))
|
||||||
|
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||||
|
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forwards resumeSessionId onto the pre-created agent when set', async () => {
|
||||||
|
// A resume id defers agent creation until persistence loads; with no backing
|
||||||
|
// session the resume is contained + logged, so no `main` agent registers —
|
||||||
|
// the branch that maps resumeSessionId through is what this covers.
|
||||||
|
const ctx = await mount({
|
||||||
|
model: 'mock',
|
||||||
|
systemPrompt: 'hi',
|
||||||
|
persistenceRoot: '/tmp/dsh-stdio-agent-spec-resume',
|
||||||
|
resumeSessionId: 'no-such-session',
|
||||||
|
})
|
||||||
|
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
|
||||||
|
await ctx.fiber.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exposes its name and Config schema', () => {
|
||||||
|
expect(stdioAgent.name).toBe('stdio-agent')
|
||||||
|
expect(stdioAgent.Config).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
39
packages/ui/stdio-agent/tsconfig.json
Normal file
39
packages/ui/stdio-agent/tsconfig.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/schemastery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/loader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/logger-console"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/agent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/session"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../core/agent-core"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../session-persistence/session-persistence-jsonl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../support/ui-stdio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
packages/ui/stdio-agent/tsdown.config.ts
Normal file
18
packages/ui/stdio-agent/tsdown.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'tsdown'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stdio-agent ships TWO entries: the plugin (`index`) and the CLI `bin`
|
||||||
|
* (`bin`), the latter referenced by package.json `bin`/`exports["./bin"]`.
|
||||||
|
* The root tsdown builds only `src/index.ts`, so this override adds `bin.ts`.
|
||||||
|
* Declarations come from `tsc -b` (dts: false), matching every package.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts', 'src/bin.ts'],
|
||||||
|
outDir: 'lib',
|
||||||
|
format: ['esm'],
|
||||||
|
platform: 'node',
|
||||||
|
target: 'es2024',
|
||||||
|
fixedExtension: false,
|
||||||
|
dts: false,
|
||||||
|
clean: false,
|
||||||
|
})
|
||||||
98
pnpm-lock.yaml
generated
98
pnpm-lock.yaml
generated
@@ -133,6 +133,39 @@ importers:
|
|||||||
specifier: ^4.0.0-rc.6
|
specifier: ^4.0.0-rc.6
|
||||||
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
|
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
|
||||||
|
|
||||||
|
packages/core/agent-core:
|
||||||
|
devDependencies:
|
||||||
|
'@cordisjs/plugin-timer':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/timer
|
||||||
|
'@deepseek-ai/dsh-agent':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../agent
|
||||||
|
'@deepseek-ai/dsh-agent-loop':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../agent-loop
|
||||||
|
'@deepseek-ai/dsh-invariants':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../support/invariants
|
||||||
|
'@deepseek-ai/dsh-llm':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../llm/llm
|
||||||
|
'@deepseek-ai/dsh-session':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../session
|
||||||
|
'@deepseek-ai/dsh-system-prompt':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../system-prompt
|
||||||
|
'@deepseek-ai/dsh-tool-bash':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../bash/tool-bash
|
||||||
|
'@deepseek-ai/dsh-tools':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../tools
|
||||||
|
cordis:
|
||||||
|
specifier: ^4.0.0-rc.6
|
||||||
|
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
|
||||||
|
|
||||||
packages/core/agent-loop:
|
packages/core/agent-loop:
|
||||||
dependencies:
|
dependencies:
|
||||||
schemastery:
|
schemastery:
|
||||||
@@ -377,6 +410,63 @@ importers:
|
|||||||
specifier: ^4.0.0-rc.6
|
specifier: ^4.0.0-rc.6
|
||||||
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
|
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
|
||||||
|
|
||||||
|
packages/ui/acp-agent:
|
||||||
|
devDependencies:
|
||||||
|
'@cordisjs/plugin-include':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/include
|
||||||
|
'@cordisjs/plugin-loader':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/loader
|
||||||
|
'@deepseek-ai/dsh-acp':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../acp
|
||||||
|
'@deepseek-ai/dsh-agent-core':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../core/agent-core
|
||||||
|
'@deepseek-ai/dsh-session-persistence-jsonl':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../session-persistence/session-persistence-jsonl
|
||||||
|
cordis:
|
||||||
|
specifier: ^4.0.0-rc.6
|
||||||
|
version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader)
|
||||||
|
schemastery:
|
||||||
|
specifier: ^3.17.0
|
||||||
|
version: 3.18.0
|
||||||
|
|
||||||
|
packages/ui/stdio-agent:
|
||||||
|
devDependencies:
|
||||||
|
'@cordisjs/plugin-include':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/include
|
||||||
|
'@cordisjs/plugin-loader':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/loader
|
||||||
|
'@cordisjs/plugin-logger-console':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/logger-console
|
||||||
|
'@deepseek-ai/dsh-agent':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../core/agent
|
||||||
|
'@deepseek-ai/dsh-agent-core':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../core/agent-core
|
||||||
|
'@deepseek-ai/dsh-session':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../core/session
|
||||||
|
'@deepseek-ai/dsh-session-persistence-jsonl':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../session-persistence/session-persistence-jsonl
|
||||||
|
'@deepseek-ai/dsh-ui-stdio':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../support/ui-stdio
|
||||||
|
cordis:
|
||||||
|
specifier: ^4.0.0-rc.6
|
||||||
|
version: 4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader)
|
||||||
|
schemastery:
|
||||||
|
specifier: ^3.17.0
|
||||||
|
version: 3.18.0
|
||||||
|
|
||||||
packages/util/brand:
|
packages/util/brand:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
cordis:
|
cordis:
|
||||||
@@ -3893,6 +3983,14 @@ snapshots:
|
|||||||
'@cordisjs/plugin-include': 1.0.4(@cordisjs/plugin-loader@1.0.0-rc.4)(cordis@4.0.0-rc.6)
|
'@cordisjs/plugin-include': 1.0.4(@cordisjs/plugin-loader@1.0.0-rc.4)(cordis@4.0.0-rc.6)
|
||||||
'@cordisjs/plugin-loader': 1.0.0-rc.4(cordis@4.0.0-rc.6)
|
'@cordisjs/plugin-loader': 1.0.0-rc.4(cordis@4.0.0-rc.6)
|
||||||
|
|
||||||
|
cordis@4.0.0-rc.6(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader):
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
cosmokit: 1.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@cordisjs/plugin-include': link:vendor/include
|
||||||
|
'@cordisjs/plugin-loader': link:vendor/loader
|
||||||
|
|
||||||
cosmokit@1.8.1: {}
|
cosmokit@1.8.1: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
{ "path": "./packages/core/agent" },
|
{ "path": "./packages/core/agent" },
|
||||||
{ "path": "./packages/core/tools" },
|
{ "path": "./packages/core/tools" },
|
||||||
{ "path": "./packages/core/agent-loop" },
|
{ "path": "./packages/core/agent-loop" },
|
||||||
|
{ "path": "./packages/core/agent-core" },
|
||||||
{ "path": "./packages/bash/bash" },
|
{ "path": "./packages/bash/bash" },
|
||||||
{ "path": "./packages/llm/llm-deepseek" },
|
{ "path": "./packages/llm/llm-deepseek" },
|
||||||
{ "path": "./packages/llm/llm-pi-ai" },
|
{ "path": "./packages/llm/llm-pi-ai" },
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
{ "path": "./packages/bash/tool-bash" },
|
{ "path": "./packages/bash/tool-bash" },
|
||||||
{ "path": "./packages/support/invariants" },
|
{ "path": "./packages/support/invariants" },
|
||||||
{ "path": "./packages/ui/acp" },
|
{ "path": "./packages/ui/acp" },
|
||||||
|
{ "path": "./packages/ui/acp-agent" },
|
||||||
|
{ "path": "./packages/ui/stdio-agent" },
|
||||||
{ "path": "./packages/support/ui-stdio" },
|
{ "path": "./packages/support/ui-stdio" },
|
||||||
{ "path": "./packages/support/llm-replay" }
|
{ "path": "./packages/support/llm-replay" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ export default defineConfig({
|
|||||||
// executable code; vendor/ and examples/ are out of scope (examples are
|
// executable code; vendor/ and examples/ are out of scope (examples are
|
||||||
// exercised by the demo smoke test instead).
|
// exercised by the demo smoke test instead).
|
||||||
include: ['packages/*/*/src/**/*.ts'],
|
include: ['packages/*/*/src/**/*.ts'],
|
||||||
exclude: ['packages/*/*/src/types.ts'],
|
// Types-only files carry no executable code. `bin.ts` files are
|
||||||
|
// self-executing CLI entrypoints (a top-level `await main()`): a spec
|
||||||
|
// can't import one without booting it, so they are driven by the keyless
|
||||||
|
// Loader-path smoke (a real subprocess) instead of the in-process unit
|
||||||
|
// suite — the same reason `examples/start.ts` sat out of coverage scope.
|
||||||
|
exclude: ['packages/*/*/src/types.ts', 'packages/*/*/src/bin.ts'],
|
||||||
// 100% or it doesn't merge (AGENTS.md: excessive tests are welcome).
|
// 100% or it doesn't merge (AGENTS.md: excessive tests are welcome).
|
||||||
// Per-file so a well-covered big file can't subsidize a bare one.
|
// Per-file so a well-covered big file can't subsidize a bare one.
|
||||||
// Every v8 ignore comment must carry a reason — see AGENTS.md.
|
// Every v8 ignore comment must carry a reason — see AGENTS.md.
|
||||||
|
|||||||
Reference in New Issue
Block a user