Remove the Echo agent

This commit is contained in:
Tianyi Cui
2026-07-20 20:16:41 +08:00
parent 2191d45ece
commit 44ad2ef073
64 changed files with 230 additions and 483 deletions

View File

@@ -11,8 +11,6 @@ Each example has both:
- **Keyless:** boot the real `cordis.yml` through the Loader, drive it, and assert output and clean exit. Catches Loader/export-shape failures hand-mounted tests miss ([postmortem](../docs/postmortem/0001-acp-default-export-drops-inject.md)).
- **With-key:** send a live-model prompt and verify external state, not the model's claim. Self-skip without `DEEPSEEK_API_KEY`; see [testing.md](../docs/testing.md).
Mock-only examples require only the keyless tier; state that exception in the test.
Keyless process smokes use `@deepseek-ai/dsh-loader-smoke` for Loader launch resolution; terminal tests wrap that launch in a pseudo-terminal. Tests supply paths, environment, input, and assertions. Every checked-in test Cordis config lives under its corresponding `examples/<agent>/` leaf. Map a package-owned config to `examples/<agent>/tests/fixtures/<group>/<package>/cordis.yml`, keep its driver and assertions package-local, and declare every package it names in both root `tsconfig.json` references and `examples/package.json`.
Do not inventory example tests here; the `tests/` trees and root scripts are authoritative.

View File

@@ -1,17 +1,6 @@
# Examples
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is a **thin leaf**: a `cordis.yml` that picks swappable backends, loads one app package, and may add optional product tools or demo-only mocks. The composition and boot glue live in [`@deepseek-ai/dsh-tui-demo`](../packages/examples/tui-demo), [`@deepseek-ai/dsh-cli-demo`](../packages/examples/cli-demo), [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo), and their shared [`@deepseek-ai/dsh-agent-spine-demo`](../packages/examples/agent-spine-demo) bundle. There is no `start.ts`; the `demo:*` scripts invoke each app package's bin.
## echo-agent
A mock model + echo tool on the headless one-shot app — the all-mock skeleton. It demonstrates:
- A thin leaf `cordis.yml` loading the `@deepseek-ai/dsh-cli-demo` app
- Registering a mock `LlmAdapter` (streaming scripted responses)
- Registering a tool via `ctx.tools.register()`
- A network-free Headless task with text or DSH-native JSON output
Run with: `pnpm run demo:echo "echo hello"`. The task prefix `echo ` triggers a tool-call round trip.
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is a **thin leaf**: a `cordis.yml` that picks swappable backends, loads one app package, and may add optional product tools. The composition and boot glue live in [`@deepseek-ai/dsh-tui-demo`](../packages/examples/tui-demo), [`@deepseek-ai/dsh-cli-demo`](../packages/examples/cli-demo), [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo), and their shared [`@deepseek-ai/dsh-agent-spine-demo`](../packages/examples/agent-spine-demo) bundle. There is no `start.ts`; the `demo:*` scripts invoke each app package's bin.
## headless-agent

View File

@@ -1,25 +0,0 @@
# echo-agent
Network-free Headless demo with a scripted mock model and an echo tool.
## What it shows
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` registers a scripted `LlmAdapter`; a task beginning with `echo ` requests the tool.
- `echo-tool.ts` registers a typed tool that returns the input uppercased.
| 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 "echo hello world"
pnpm run demo:echo --output-format stream-json -- "echo hello world"
```
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.

View File

@@ -1,40 +0,0 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. -->
# Echo Agent App Composition
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_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"]
cfg --> plugin_echo_echo_tool
plugin_echo_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
cfg --> plugin_echo_bash
plugin_echo_fs_local["fs-local<br/>@deepseek-ai/dsh-fs-local"]
cfg --> plugin_echo_fs_local
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"]
bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"]
```
| Plugin id | Package / module |
| --- | --- |
| `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` |
| `cli-agent` | `@deepseek-ai/dsh-cli-demo` |
Source config: [`examples/echo-agent/cordis.yml`](cordis.yml).
Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.

View File

@@ -1,26 +0,0 @@
# 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.
- id: mock-llm
name: './src/mock-llm.ts'
- id: echo-tool
name: './src/echo-tool.ts'
- id: bash
name: '@deepseek-ai/dsh-bash-local'
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:
cwd: !!js process.cwd()
- id: cli-agent
name: '@deepseek-ai/dsh-cli-demo'
config:
provider: mock
model: mock-echo
persona: 'You are echo-agent, a demo agent.'
persistenceRoot: './.sessions'
workspaceContext:
maxBytes: 65536

View File

@@ -1,7 +0,0 @@
{
"name": "echo-agent-example",
"private": true,
"version": "0.0.1",
"type": "module",
"description": "Runnable headless demo: scripted mock model + echo tool"
}

View File

@@ -1,19 +0,0 @@
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'echo-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'echo',
description: 'Echo the given text back, uppercased.',
parameters: {
text: { type: 'string', required: true },
},
async execute(args) {
// args is typed: { text: string }
return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }]
},
}))
}

View File

@@ -1,59 +0,0 @@
import type { Context } from 'cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
/**
* Demo adapter for the `mock-echo` model.
*
* Behavior: if the last user text starts with "echo ", it calls the `echo`
* tool with the rest of the line (exercising the tool round-trip), otherwise
* it streams a canned reply quoting the input.
*/
class MockEchoAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const lastUserText = [...options.messages].reverse()
.filter(message => message.role === 'user')
.flatMap(message => message.content)
.filter(block => block.type === 'text')
.map(block => block.text)
.find(text => !text.startsWith('<')) ?? ''
const hasToolResult = options.messages.at(-1)?.content.some(block => block.type === 'tool-result')
if (lastUserText.startsWith('echo ') && !hasToolResult) {
const payload = lastUserText.slice(5)
const args = JSON.stringify({ text: payload })
yield { type: 'block-start', index: 0, blockType: 'text' }
for (const char of 'Let me echo that for you.') {
yield { type: 'text-delta', index: 0, text: char }
await new Promise(resolve => setTimeout(resolve, 2))
}
yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Let me echo that for you.' } }
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 1, id: CallId('call-echo'), name: 'echo', argumentsDelta: args }
yield { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('call-echo'), name: 'echo', arguments: args } }
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
return
}
const reply = hasToolResult
? 'The echo tool has spoken.'
: `You said: "${lastUserText}". Try "echo <something>" to see a tool call.`
yield { type: 'block-start', index: 0, blockType: 'text' }
for (const char of reply) {
yield { type: 'text-delta', index: 0, text: char }
await new Promise(resolve => setTimeout(resolve, 2))
}
yield { type: 'block-end', index: 0, block: { type: 'text', text: reply } }
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: reply.length } }
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
export const name = 'mock-llm'
export const inject = ['llm']
export function apply(ctx: Context) {
ctx.llm.registerAdapter(['mock'], new MockEchoAdapter())
}

View File

@@ -1,37 +0,0 @@
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'
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(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,
})
return stdout
}
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('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)
})

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
/** Test driver that sends two turns through one headless Loader composition. */
/** 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'

View File

@@ -0,0 +1,22 @@
import type { Context } from 'cordis'
import { LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm'
/** Deterministic one-step adapter for the time-context Loader fixture. */
class TimeContextMockAdapter extends LlmAdapter {
async * stream(): AsyncIterable<StreamChunk> {
const text = 'time context sampled'
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text }
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
export const name = 'time-context-mock-llm'
export const inject = ['llm']
/** Register the test-only `time-context-mock` adapter. */
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['time-context-mock'], new TimeContextMockAdapter())
}

View File

@@ -1,6 +1,6 @@
# Test-only composition: keep time-context opt-in while exercising its real Loader/app path.
- id: mock-llm
name: '../../../../src/mock-llm.ts'
- id: time-context-mock-llm
name: './time-context-mock-llm.ts'
- id: bash
name: '@deepseek-ai/dsh-bash-local'
@@ -11,8 +11,8 @@
- id: cli-agent
name: '@deepseek-ai/dsh-cli-demo'
config:
provider: mock
model: mock-echo
provider: time-context-mock
model: time-context-mock
persona: 'Test the time-context plugin.'
persistenceRoot: './.sessions'
workspaceContext: false