Remove the Echo agent
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()}` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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'
|
||||
22
examples/headless-agent/tests/fixtures/time-context-mock-llm.ts
vendored
Normal file
22
examples/headless-agent/tests/fixtures/time-context-mock-llm.ts
vendored
Normal 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())
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user