Files
deepseek-harness/examples/echo-agent/src/stdio-chat.ts
T
Tianyi Cui 7f024a1a9d Document the codebase thoroughly and tighten type safety
Docs: per-folder README.md for packages/ (family overview + one per
package: service, events, API, extension points, TODOs), examples/,
and examples/echo-agent/; folder-level AGENTS.md (+ CLAUDE.md
symlinks) for packages/ and vendor/; module-level doc comments in
every packages/*/src file; richer JSDoc on all exported API
(event side effects, disposal contracts, error behavior). Root
AGENTS.md gains a "Type Safety and Documentation" policy section:
the codebase aims to be very type-safe and well documented; type
gymnastics are acceptable in core packages when they improve
plugin-author DX; verbose docs are fine as long as they stay strictly
in sync with the code.

Type safety: removed the upstream-inherited "noImplicitAny": false
from tsconfig.base.json — packages/* now compile under full strict
mode; vendor/loader and vendor/include set it locally (vendor/cordis
already did). Eliminated every `: any` / `as any` from packages and
examples (catch clauses use unknown + a CodedError narrowing type;
event data access uses discriminated-union narrowing).

Typed tool schemas: new @deepseek-ai/dsh-tools schema DSL —
SchemaSpec with per-property `required: true` booleans, type-level
InferArgs<S>, a runtime SchemaSpec → JSON Schema converter, and
defineTool() so first-party tools get typed execute(args) with zero
casts (raw JSON Schema still accepted for MCP interop; chosen over
schemastery because it targets JSON Schema generation directly).
echo-tool and all test tools migrated; +7 tests.
2026-06-11 13:01:00 +08:00

61 lines
2.0 KiB
TypeScript

import { createInterface } from 'node:readline'
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-agent'
export const name = 'stdio-chat'
export const inject = ['agents']
/**
* Minimal UI plugin: reads lines from stdin → agent.send(); renders the
* agent's stream chunks and tool activity to stdout. Demonstrates that a UI
* is "just a plugin" — it only consumes the agent/* event taxonomy.
*/
export function apply(ctx: Context) {
ctx.on('agent/stream-chunk', (agent, _turn, _step, chunk) => {
if (chunk.type === 'text-delta') process.stdout.write(chunk.text)
})
ctx.on('agent/turn-start', (agent, turn) => {
process.stdout.write(`\n[${agent.id} turn ${turn}] `)
})
ctx.on('agent/turn-end', () => {
process.stdout.write('\n> ')
})
ctx.on('session/event', (_session, event) => {
if (event.type === 'tool/call') {
const { name: toolName, arguments: args } = event.data
process.stdout.write(`\n [tool call] ${toolName}(${args})`)
} else if (event.type === 'tool/result') {
const { content } = event.data
const text = content.filter(b => b.type === 'text').map(b => b.text).join('')
process.stdout.write(`\n [tool result] ${text}\n `)
}
})
ctx.effect(() => {
const reader = createInterface({ input: process.stdin })
reader.on('line', (line) => {
const text = line.trim()
if (!text) return
const agent = ctx.agents.get('main')
if (!agent) {
console.error('agent "main" is not running')
return
}
if (agent.status === 'running') {
agent.steer([{ type: 'text', text }])
} else {
agent.send([{ type: 'text', text }])
}
})
reader.on('close', () => {
// allow the process to exit when stdin ends (piped input)
setTimeout(() => process.exit(0), 200)
})
process.stdout.write('echo-agent ready. Type a message ("echo <text>" triggers the tool).\n> ')
return () => reader.close()
}, 'stdio-chat')
}