Flat config with two layers. Correctness (type-checked): the headline rules for this codebase are no-floating-promises / no-misused-promises (a lost promise in the agent loop is our primary bug class), switch-exhaustiveness-check (we switch over merge-extensible unions everywhere), no-unnecessary-condition, require-await, and no-explicit-any. Style (@stylistic): 2-space, no semicolons, single quotes, trailing commas, max-len 140 — the existing house style, now enforced instead of drifting between agents. vendor/ is excluded (vendored source keeps upstream style); tests relax the rules that fight test ergonomics (non-null assertions after expects, async mock signatures, non-Error throws). Code adjusted to pass: registry disposers wrap ctx.effect's promise-returning disposer behind a sync () => void (our public API), BlockAssembler gains an invariant-checking mustGet instead of non-null assertions, lastTurnNumber uses findLast, waterfall tails return Promise.resolve instead of async-without-await arrows, and the two deliberate suppressions (non-exhaustive derivation switch, unbound execute pass-through) carry justification comments. yarn lint / yarn lint:fix added.
dsh-tools
Tool registry and execution waterfall. Tool plugins register their schemas and
executors; the agent loop executes calls through the tools/execute waterfall.
Service: ToolRegistry (ctx key: tools)
Public API
ctx.tools.register(definition: ToolDefinition): () => voidRegister a tool. Disposed with the calling fiber.ctx.tools.get(name: string): ToolDefinition | undefinedctx.tools.schemas(): ToolSchema[]Schemas of all registered tools (without theexecutefunctions).ctx.tools.execute(exec: ToolExecution): Promise<ToolExecutionResult>Execute one tool call through thetools/executewaterfall.
Injected services
SystemPrompt — the registry automatically feeds its tool schemas into the
system-prompt assembly via ctx.systemPrompt.tools().
Events
| Event | Mode | Purpose |
|---|---|---|
tools/execute |
waterfall | Wrap/veto tool execution (sandbox, permission, hooks, plan mode) |
tools/change |
emit | A tool was registered or unregistered |
Key types
ToolDefinition—ToolSchema+execute(args, exec): Promise<ContentBlock[]>.ToolExecution— one pending tool call:{ callId, name, arguments, agent?, signal? }.ToolExecutionResult— outcome:{ callId, content, isError }.
Extension points
- Tool plugins call
ctx.tools.register()— schemas flow into the assembly automatically. - The
tools/executewaterfall is the single seam for sandbox, permission, hooks, and plan-mode plugins to wrap or veto a call. Listeners receive(exec, next): callnext()to proceed, or return a result without callingnext()to short-circuit (veto). - MCP servers: one plugin per server, discover tools, call
ctx.tools.register()with the server's schemas.
Typed tool parameter schemas
First-party plugin authors can use the defineTool() helper (exported from this
package) for typed tool parameter schemas:
import { defineTool } from '@deepseek-ai/dsh-tools'
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read a file from disk.',
parameters: {
path: { type: 'string', required: true, description: 'Absolute file path' },
offset: { type: 'number' },
limit: { type: 'number' },
},
async execute(args, exec) {
// args is typed: { path: string; offset?: number; limit?: number }
const text = await readFile(args.path, 'utf8')
return [{ type: 'text', text }]
},
}))
The helper converts the author-facing SchemaSpec (with required: true as a
per-property boolean) to standard JSON Schema for the wire format. Raw
JSON-Schema tool definitions (from MCP servers) are still accepted by the
registry directly.
See defineTool, SchemaSpec, InferArgs, and schemaSpecToJsonSchema in the
public API for details.
What is NOT here (TODO)
- Tool shapes review — when real tools land (e.g. a concurrency-safety hint for parallel execution); phase 1 executes tool calls sequentially.
- Parallel execution — the loop currently iterates tool calls sequentially.