Files
deepseek-harness/packages/core/agent-core
Tianyi Cui e2bde2902c 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.
2026-06-21 12:06:38 +08:00
..

@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, dsh-acp-agent) 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 raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door.

Config

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.