Remove the stdio agent

This commit is contained in:
Tianyi Cui
2026-07-20 19:26:04 +08:00
parent 42f19edd67
commit 4cadf096ce
162 files changed
+1301 -3920

No files matched your search

+12 -21
View File
@@ -1,34 +1,25 @@
# echo-agent
Runnable demo: stdin chat with a scripted mock model and an echo tool. The all-mock skeleton — "swap the backend, keep the app".
Network-free Headless demo with a scripted mock model and an echo tool.
## What it shows
This example is just a leaf `cordis.yml`: it loads the [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo) app (which bundles the whole [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) spine, JSONL persistence, the TTY-selected `dsh-tui`/`dsh-stdio` front doors, and a pre-created `main` agent), and swaps in two example-local backends plus `hmr`:
The leaf loads [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), which supplies the shared spine, JSONL persistence, one fresh `main` agent, and the one-shot CLI driver. Two local plugins provide the demo behavior:
- `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'], …)`.
- `echo-tool.ts` — a tool registered via `ctx.tools.register(defineTool(…))` with typed `execute` args; echoes text back uppercased.
- `mock-llm.ts` registers a scripted `LlmAdapter`; a task beginning with `echo ` requests the tool.
- `echo-tool.ts` registers a typed tool that returns the input uppercased.
Swapping `mock-llm` for the real `llm-deepseek` adapter is all that separates this from `repl-agent` — the same app, a different backend.
## Plugin files
| File | Role | Key patterns demonstrated |
|---|---|---|
| `src/mock-llm.ts` | `LlmAdapter` registration | `ctx.llm.registerAdapter(['mock-echo'], …)`, streaming chunks with the proper `block-start`/`block-end` protocol |
| `src/echo-tool.ts` | Tool registration | `ctx.tools.register(defineTool(…))` with typed `execute` args, returning `ContentBlock[]` |
| `cordis.yml` | Leaf wiring | the two backends + `hmr` + one `@deepseek-ai/dsh-stdio-demo` entry carrying the app config |
The spine, UI, persistence, and boot glue all live in `@deepseek-ai/dsh-stdio-demo` and the bundle it loads — this folder holds only the demo-specific mocks and the leaf wiring.
| File | Role |
|---|---|
| `src/mock-llm.ts` | Streaming mock adapter |
| `src/echo-tool.ts` | Model-facing echo tool |
| `cordis.yml` | Mock plugins, local providers, and one `@deepseek-ai/dsh-cli-demo` entry |
## Run
```sh
pnpm run demo:echo
# or:
node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/echo-agent/cordis.yml
pnpm run demo:echo "echo hello world"
pnpm run demo:echo --output-format stream-json -- "echo hello world"
```
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).
The session is persisted under `.sessions/` relative to the directory you launch the demo from. `pnpm run demo:echo` runs from the repo root, so the logs land in `<repo-root>/.sessions/cwd-<hash>/` (one `.jsonl` log per session). Clean up with: `rm -rf .sessions`
The first command prints the final canned response. `stream-json` also exposes the canonical `tool/call` and `tool/result` events. Sessions persist under `.sessions/` relative to the launch directory; remove that generated directory when finished.
+7 -10
View File
@@ -3,13 +3,11 @@
# Echo Agent App Composition
The echo demo swaps in a local mock LLM and teaching echo tool, then loads the stdio app package for the shared spine and terminal front door.
The echo demo swaps in a local mock LLM and teaching echo tool, then loads the headless one-shot app package.
```mermaid
flowchart LR
cfg["examples/echo-agent<br/>cordis.yml"]
plugin_echo_hmr["hmr<br/>@cordisjs/plugin-hmr"]
cfg --> plugin_echo_hmr
plugin_echo_mock_llm["mock-llm<br/>./src/mock-llm.ts"]
cfg --> plugin_echo_mock_llm
plugin_echo_echo_tool["echo-tool<br/>./src/echo-tool.ts"]
@@ -18,11 +16,11 @@ flowchart LR
cfg --> plugin_echo_bash
plugin_echo_fs_local["fs-local<br/>@deepseek-ai/dsh-fs-local"]
cfg --> plugin_echo_fs_local
plugin_echo_stdio_agent["stdio-agent<br/>@deepseek-ai/dsh-stdio-demo"]
cfg --> plugin_echo_stdio_agent
plugin_echo_stdio_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"]
plugin_echo_stdio_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
plugin_echo_stdio_agent --> frontdoor_stdio["dsh-tui (TTY) / dsh-stdio (pipes)<br/>pre-created main agent"]
plugin_echo_cli_agent["cli-agent<br/>@deepseek-ai/dsh-cli-demo"]
cfg --> plugin_echo_cli_agent
plugin_echo_cli_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"]
plugin_echo_cli_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
plugin_echo_cli_agent --> frontdoor_cli["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]
bundle_agent_core --> spine_llm["ctx.llm"]
bundle_agent_core --> spine_sessions["ctx.sessions"]
bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
@@ -31,12 +29,11 @@ flowchart LR
| Plugin id | Package / module |
| --- | --- |
| `hmr` | `@cordisjs/plugin-hmr` |
| `mock-llm` | `./src/mock-llm.ts` |
| `echo-tool` | `./src/echo-tool.ts` |
| `bash` | `@deepseek-ai/dsh-bash-local` |
| `fs-local` | `@deepseek-ai/dsh-fs-local` |
| `stdio-agent` | `@deepseek-ai/dsh-stdio-demo` |
| `cli-agent` | `@deepseek-ai/dsh-cli-demo` |
Source config: [`examples/echo-agent/cordis.yml`](cordis.yml).
+4 -21
View File
@@ -1,43 +1,26 @@
# Stdio agent with the network-free `mock-echo` adapter and example-local `echo`
# tool. The app bundle supplies the spine; this leaf selects backends, HMR, and app config.
# No API key: the `mock-echo` adapter never touches the network.
# Headless agent with the network-free `mock-echo` adapter and example-local
# `echo` tool. No API key is needed because the adapter never touches the network.
# Hot-module reload for the dev/demo loop (a leaf entry, not baked into
# dsh-stdio-demo — it needs `node --expose-internals`, which `demo:echo` passes).
- id: hmr
name: '@cordisjs/plugin-hmr'
config:
root: ['.']
# Example-local model and tool plugins resolve relative to this file.
- id: mock-llm
name: './src/mock-llm.ts'
- id: echo-tool
name: './src/echo-tool.ts'
# Local bash executor: agent-spine-demo ships the `tool-bash` consumer schema, so the
# leaf provides the executor it runs on (the echo demo doesn't drive bash, but
# the tool is part of the shared spine).
- id: bash
name: '@deepseek-ai/dsh-bash-local'
# Local filesystem provider for agent-spine-demo's workspace-context loader. This
# does not expose model-facing read/write/edit tools in the echo demo.
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:
cwd: !!js process.cwd()
# The app pre-creates `main` on the mock model and supplies persistence plus
# TTY-selected `dsh-tui`/`dsh-stdio` front doors; readline mode also owns logging.
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-demo'
- id: cli-agent
name: '@deepseek-ai/dsh-cli-demo'
config:
provider: mock
model: mock-echo
persona: 'You are echo-agent, a demo agent.'
welcome: 'echo-agent ready. Type a message ("echo <text>" triggers the tool).'
persistenceRoot: './.sessions'
workspaceContext:
maxBytes: 65536
+1 -1
View File
@@ -3,5 +3,5 @@
"private": true,
"version": "0.0.1",
"type": "module",
"description": "Runnable demo: stdin chat with a scripted mock model + echo tool"
"description": "Runnable headless demo: scripted mock model + echo tool"
}
+16 -22
View File
@@ -1,43 +1,37 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
/**
* Keyless-by-nature Loader-path coverage for examples/echo-agent. The real
* tree uses its deterministic mock model, so this suite is both the boot smoke
* and the complete behavior proof for the example.
*/
const binScript = fileURLToPath(new URL('../../../packages/examples/stdio-demo/src/bin.ts', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
async function runEcho(stdinLines: readonly string[]): Promise<string> {
async function runEcho(task: string, outputFormat: 'text' | 'stream-json' = 'text'): Promise<string> {
const { stdout } = await runLoaderSmoke({
label: 'echo-agent',
tempDirPrefix: 'echo-smoke-',
binScript,
configPath,
binArgs: ['--config', configPath, '--output-format', outputFormat, task],
tsconfigPath,
stdinLines,
})
return stdout
}
describe('echo-agent keyless smoke (real cordis.yml via the Loader)', () => {
it('boots, prints its welcome banner, and exits cleanly on stdin EOF', async () => {
expect(await runEcho([])).toContain('echo-agent ready.')
describe('echo-agent keyless smoke (Headless through the real Loader tree)', () => {
it('runs the echo tool round-trip and exposes both events in stream-json', async () => {
const lines = (await runEcho('echo hello world', 'stream-json'))
.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
const events = lines.slice(0, -1).map(line => line['event'] as SessionEvent)
expect(events.some(event => event.type === 'tool/call' && event.data.name === 'echo')).toBe(true)
expect(JSON.stringify(events.find(event => event.type === 'tool/result'))).toContain('ECHO: HELLO WORLD')
expect(lines.at(-1)).toMatchObject({ type: 'result', success: true })
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('runs the echo tool round-trip for an "echo …" line', async () => {
const stdout = await runEcho(['echo hello world'])
expect(stdout).toContain('[tool call] echo')
expect(stdout).toContain('[tool result] ECHO: HELLO WORLD')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('streams a direct canned reply for a non-echo line', async () => {
const stdout = await runEcho(['just chatting'])
expect(stdout).toContain('just chatting')
expect(stdout).not.toContain('[tool call]')
it('prints the final canned reply for a direct one-shot task', async () => {
const stdout = await runEcho('just chatting')
expect(stdout).toContain('You said: "just chatting"')
expect(stdout).not.toContain('tool/call')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})
@@ -8,12 +8,11 @@
- id: time-context
name: '@deepseek-ai/dsh-time-context'
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-demo'
- id: cli-agent
name: '@deepseek-ai/dsh-cli-demo'
config:
provider: mock
model: mock-echo
persona: 'Test the time-context plugin.'
welcome: 'time-context e2e ready.'
persistenceRoot: './.sessions'
workspaceContext: false
@@ -0,0 +1,16 @@
#!/usr/bin/env node
/** Test driver that sends two turns through one headless Loader composition. */
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
const configPath = process.argv[2]
if (configPath === undefined) throw new Error('time-context driver requires a config path')
const ctx = await boot('time-context-e2e', resolveConfigPath(configPath, undefined))
try {
await runOneShot(ctx, { task: 'first' })
await runOneShot(ctx, { task: 'second' })
} finally {
await ctx.fiber.dispose()
}