Files
deepseek-harness/packages/examples/stdio-demo
Turtle 1119c537d0 feat(ui): add dedicated TUI package
Move the interactive pi-tui front door into @deepseek-ai/dsh-tui while keeping @deepseek-ai/dsh-stdio line-oriented for pipes. Select the terminal package in the demo app, preserve logger ownership, and cover the production Loader composition with a PTY smoke test.
2026-07-17 12:01:37 +08:00
..
2026-07-17 12:01:37 +08:00
2026-07-17 12:01:37 +08:00
2026-07-17 12:01:37 +08:00

@deepseek-ai/dsh-stdio-demo

The terminal chat app: a Cordis app plugin that composes the default agent spine (@deepseek-ai/dsh-agent-spine-demo) with JSONL persistence, human interaction, a pre-created main agent, and a TTY-selected pi-tui/readline terminal front door. Its bin boots a leaf cordis.yml.

It is the terminal counterpart to @deepseek-ai/dsh-acp-demo: both consume the same spine, while ACP reserves stdout for JSON-RPC and creates sessions from the client.

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
@deepseek-ai/dsh-agent-spine-demo the spine, pre-creating a main agent from this app's model with process.cwd() as the fresh session cwd and carrying its persona
@deepseek-ai/dsh-session-persistence-jsonl durable JSONL session log under persistenceRoot
@deepseek-ai/dsh-user-interaction the human question/answer seam used by confirmation tools
@deepseek-ai/dsh-tool-ask-user the model-facing ask_user_question tool
@cordisjs/plugin-logger-console readline diagnostics for non-TTY operation; omitted from the fullscreen TUI path
@deepseek-ai/dsh-stdio the line-oriented terminal channel, bound to main for pipes and automation
@deepseek-ai/dsh-tui the interactive pi-tui channel, bound to main for TTY pairs

@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 whose constructor needs node --expose-internals plus a live loader. The demo:echo / demo:repl 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. 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
persona the deployment persona template (may reference {{model}}/{{cwd}}), routed to dsh-system-prompt
toolOrder explicit model-facing tool order (a name list with one '<unlisted-tools>' rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to dsh-system-prompt
tools { mode: 'native' } tool-registry presentation config (native / code / both), routed through dsh-agent-spine-demo
skills owner defaults registry-cache, local-provider, and model-facing skill-tool config, routed through dsh-agent-spine-demo
toolBash owner defaults model-facing bash config routed through dsh-agent-spine-demo, including bash's producer-local enableRunInBackground
toolTasks owner defaults generic task_output wait bounds routed through dsh-agent-spine-demo
persistenceRoot ./.sessions the JSONL backend's root directory
welcome ready. terminal banner / TUI subtitle
ui owner defaults app mode selection and nested dsh-tui presentation config
resumeSessionId resume a persisted session id instead of starting fresh (sourced from an env var in the leaf)

Fresh stdio sessions use the process launch directory as session.header.cwd, so project-scoped features such as skill discovery and default bash workdir follow the directory where dsh-stdio-demo was started. Resumed sessions keep the cwd stored in the persisted session header.

The bin

dsh-stdio-demo [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 and awaits the whole plugin tree before returning. Run it under node --expose-internals, or install the Loader's optional node-addon-require-builtin fallback, so the Loader can resolve the config's bare plugin specifiers (@deepseek-ai/dsh-*, npm packages). The demo:echo / demo:repl scripts use --expose-internals.

Example leaf cordis.yml

# A coding-agent demo: 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-demo'
  config:
    model: deepseek-v4-flash
    persona: 'You are a coding assistant powered by the {{model}} model.'
    ui:
      mode: auto

Swap llm-deepseek for a mock-llm leaf plugin and you have the echo demo — "swap the backend, keep the app".

Model Experience

Composed terminal agent request

What the model sees: Through dsh-agent-spine-demo, the main agent receives the harness identity, configured persona, skill catalog, and visible tools; this app also composes the generated ask_user_question schema. Each terminal submission becomes a user message; submissions made while the agent runs steer the active turn.

Token effect: Child prompt and schema costs repeat per request; user input and tool history grow until compaction. The TUI/readline banners and rendered transcripts are terminal-only and add zero model tokens.

Human-answer result

What the model sees: Through dsh-tool-ask-user, successful terminal answers use that package's exact compact JSON shape. Interruption becomes exactly Error: ask_user_question was interrupted before the user answered; a closed stdin becomes Error: ask_user_question cannot be answered because stdin is closed.

Token effect: Only a completed or failed tool call adds retained result tokens; prompts printed while waiting are terminal-only.

Known Limitations and Deferred Work

  • One pre-created main agent drives the terminal UI — there is no multi-session or concurrent-agent surface in this app; a run is one conversation.
  • The front-door cluster is fixed in code — the JSONL persistence backend and the ask-user tooling are baked; a different composition is a leaf-level sibling entry or another app package.
  • The question tool is not an approval answerer — this app mounts user-interaction and ask_user_question, but not ctx.approval; a tools/pre-execute ask therefore fails closed unless the leaf composes an approval service and terminal answerer.