docs: unwrap hard-wrapped Markdown to one line per paragraph

Hard line breaks mid-paragraph make docs harder to edit and diff — a
one-word change reflows and re-diffs the whole paragraph. Reflow all
tracked non-vendor Markdown (plus vendor/AGENTS.md) so each prose
paragraph is a single line; soft-wrapping is the editor's job. Fenced
code, tables, and list structure are preserved (wrapped list items fold
to one line per bullet). Documents the convention in AGENTS.md.
This commit is contained in:
Tianyi Cui
2026-06-13 18:39:20 +08:00
parent e98c1c5d42
commit 066f94c7e0
39 changed files with 348 additions and 1206 deletions

View File

@@ -1,30 +1,16 @@
# AGENTS.md — Harness Packages
This directory contains all `@deepseek-ai/dsh-*` harness packages. When editing
code here, follow these conventions:
This directory contains all `@deepseek-ai/dsh-*` harness packages. When editing code here, follow these conventions:
- **Effect-based registrations**: every contribution (tool, section, adapter,
agent, event listener) goes through `ctx.effect()` / `ctx.on()`, and
`register()` methods return disposers. Never use bare arrays or manual cleanup.
- **Declaration merging**: services declare their ctx key in
`declare module 'cordis' { interface Context { } }` and their events in
`interface Events`. Merge-extensible maps (`ContentBlockMap`,
`MessageSourceMap`, `FinishReasonMap`, `TurnTriggerMap`, `TurnEndReasonMap`,
`SessionEventMap`) are how plugins add new variants.
- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)`;
call `next()` to delegate, or return without it to short-circuit (veto). Never
call `next()` after returning.
- **Tests**: vitest in `packages/<name>/tests/*.spec.ts`. Every registry needs an
HMR-safety test (register a plugin, dispose its fiber, assert cleanup). Err on
the side of more tests — edge cases, error paths, event ordering, races.
- **Effect-based registrations**: every contribution (tool, section, adapter, agent, event listener) goes through `ctx.effect()` / `ctx.on()`, and `register()` methods return disposers. Never use bare arrays or manual cleanup.
- **Declaration merging**: services declare their ctx key in `declare module 'cordis' { interface Context { } }` and their events in `interface Events`. Merge-extensible maps (`ContentBlockMap`, `MessageSourceMap`, `FinishReasonMap`, `TurnTriggerMap`, `TurnEndReasonMap`, `SessionEventMap`) are how plugins add new variants.
- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)`; call `next()` to delegate, or return without it to short-circuit (veto). Never call `next()` after returning.
- **Tests**: vitest in `packages/<name>/tests/*.spec.ts`. Every registry needs an HMR-safety test (register a plugin, dispose its fiber, assert cleanup). Err on the side of more tests — edge cases, error paths, event ordering, races.
Naming notes:
- Files `src/index.ts` export the service default + all public types
- `src/types.ts` contain only types — no runtime code
- Tests live at package level under `tests/`, not `src/__tests__/`
- A package's README and module/JSDoc comments are part of the change: when you
alter behavior (config keys, defaults, error codes, wire fields), update them
in the same commit. CI has no doc-sync gate, so stale docs are on the author.
- A package's README and module/JSDoc comments are part of the change: when you alter behavior (config keys, defaults, error codes, wire fields), update them in the same commit. CI has no doc-sync gate, so stale docs are on the author.
Read the per-package README.md for package-specific details: service API,
events, extension points, TODOs.
Read the per-package README.md for package-specific details: service API, events, extension points, TODOs.

View File

@@ -1,10 +1,6 @@
# Packages
Harness packages, all under the `@deepseek-ai/dsh-*` scope. Each package is a
Cordis service (microkernel plugin-style): it exports a default `Service` class
that gets registered via `ctx.plugin()`, declares its ctx key and events through
declaration merging, and exposes extension points through `ctx.effect()`,
`ctx.on()`, and `ctx.waterfall()`.
Harness packages, all under the `@deepseek-ai/dsh-*` scope. Each package is a Cordis service (microkernel plugin-style): it exports a default `Service` class that gets registered via `ctx.plugin()`, declares its ctx key and events through declaration merging, and exposes extension points through `ctx.effect()`, `ctx.on()`, and `ctx.waterfall()`.
## Dependency graph
@@ -17,9 +13,7 @@ dsh-tools ← dsh-llm, dsh-system-prompt, dsh-agent
dsh-agent-loop ← dsh-llm, dsh-session, dsh-system-prompt, dsh-tools, dsh-agent
```
The rule: plugins depend on interfaces, never on the concrete loop.
`dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the
`dsh-agent` vocabulary if the loop is replaced.
The rule: plugins depend on interfaces, never on the concrete loop. `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced.
## What goes where
@@ -32,25 +26,13 @@ The rule: plugins depend on interfaces, never on the concrete loop.
| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
| `agent-loop/` | THE concrete plugin: `LoopAgent` + the loop driver | `ctx.agentLoop` |
Each package has its own `README.md` with purpose, service API, events,
extension points, and deliberate non-goals (TODOs).
Each package has its own `README.md` with purpose, service API, events, extension points, and deliberate non-goals (TODOs).
## Conventions (applied across all harness packages)
- **Registrations are effects**: every contribution (adapter, tool, section,
agent, event listener) goes through `ctx.effect()` / `ctx.on()`, so disposal
and HMR clean up automatically. Every `register()` returns the disposer.
- **Declaration merging for events and ctx**: services declare their events in
`declare module 'cordis' { interface Events { ... } }` and their ctx key in
`interface Context`.
- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)`
and MUST call `next()` to delegate; returning without it short-circuits (the
veto mechanism).
- **Extensible unions**: `ContentBlockMap`, `MessageSourceMap`,
`FinishReasonMap`, `TurnTriggerMap`, `TurnEndReasonMap`, and `SessionEventMap`
use the merge-extensible-map pattern so plugins can add variants via
declaration merging.
- **ESM everywhere**; imports use package names across package boundaries,
`.ts` extensions within a package.
- **Tests**: vitest, colocated under `packages/<name>/tests/*.spec.ts`. Every
registry needs an HMR-safety test. Err on the side of more tests.
- **Registrations are effects**: every contribution (adapter, tool, section, agent, event listener) goes through `ctx.effect()` / `ctx.on()`, so disposal and HMR clean up automatically. Every `register()` returns the disposer.
- **Declaration merging for events and ctx**: services declare their events in `declare module 'cordis' { interface Events { ... } }` and their ctx key in `interface Context`.
- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)` and MUST call `next()` to delegate; returning without it short-circuits (the veto mechanism).
- **Extensible unions**: `ContentBlockMap`, `MessageSourceMap`, `FinishReasonMap`, `TurnTriggerMap`, `TurnEndReasonMap`, and `SessionEventMap` use the merge-extensible-map pattern so plugins can add variants via declaration merging.
- **ESM everywhere**; imports use package names across package boundaries, `.ts` extensions within a package.
- **Tests**: vitest, colocated under `packages/<name>/tests/*.spec.ts`. Every registry needs an HMR-safety test. Err on the side of more tests.

View File

@@ -1,24 +1,18 @@
# dsh-agent-loop
THE concrete agent plugin: `LoopAgent` and the loop driver. Implements the
`Agent` interface and drives the session/turn/step lifecycle.
THE concrete agent plugin: `LoopAgent` and the loop driver. Implements the `Agent` interface and drives the session/turn/step lifecycle.
This is the only package in the harness that contains concrete loop logic.
Everything else is an abstract service or a plugin against extension seams —
new behavior goes into plugins, not here.
This is the only package in the harness that contains concrete loop logic. Everything else is an abstract service or a plugin against extension seams — new behavior goes into plugins, not here.
## Service: `AgentLoop` (ctx key: `agentLoop`)
### Public API
- `ctx.agentLoop.create(id: string, options?: AgentOptions): LoopAgent`
Create an agent, start its loop, and register it in `ctx.agents`. Disposed
with the calling fiber.
- `ctx.agentLoop.create(id: string, options?: AgentOptions): LoopAgent` Create an agent, start its loop, and register it in `ctx.agents`. Disposed with the calling fiber.
### Injected services
`agents`, `sessions`, `llm`, `tools`, `systemPrompt` — all five interface
services.
`agents`, `sessions`, `llm`, `tools`, `systemPrompt` — all five interface services.
### Configuration (schemastery)
@@ -36,11 +30,8 @@ Agents listed in config are auto-created at startup.
### Classes
- `LoopAgent` — the concrete `Agent` implementation. Owns the inbox (`Inbox`),
the per-step `AbortController`, and the loop driver. Everything observable
happens through session events and the `agent/*` event taxonomy.
- `Inbox` — per-agent queued + steering FIFOs (`enqueue`, `steer`, `drainQueued`,
`drainSteering`, `waitForQueued`).
- `LoopAgent` — the concrete `Agent` implementation. Owns the inbox (`Inbox`), the per-step `AbortController`, and the loop driver. Everything observable happens through session events and the `agent/*` event taxonomy.
- `Inbox` — per-agent queued + steering FIFOs (`enqueue`, `steer`, `drainQueued`, `drainSteering`, `waitForQueued`).
### Loop lifecycle (`loop.ts`)
@@ -68,13 +59,11 @@ forever:
idle unless more queued
```
Error containment: a throwing plugin ends the **turn**, never the loop. Dispose
mid-turn emits `agent/status('disposed')` and ends with reason `disposed`.
Error containment: a throwing plugin ends the **turn**, never the loop. Dispose mid-turn emits `agent/status('disposed')` and ends with reason `disposed`.
### What is NOT here
Everything that goes beyond "call the model, run the tools, repeat" belongs to
plugins listening on the event taxonomy:
Everything that goes beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy:
- Hooks: `agent/request`, `agent/step-result`, `tools/execute`, `agent/turn-continuation`
- Compaction: `agent/request`
- Sandbox, permission, plan mode: `tools/execute`

View File

@@ -1,25 +1,20 @@
# dsh-agent
Agent interface, registry, and `agent/*` event vocabulary. Every plugin (UI,
hooks, orchestrators) programs against the `Agent` handle defined here — it has
zero loop dependency, so the loop is swappable.
Agent interface, registry, and `agent/*` event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the `Agent` handle defined here — it has zero loop dependency, so the loop is swappable.
## Service: `AgentRegistry` (ctx key: `agents`)
Tracks live agents so UI, hook, and orchestrator plugins can find them without
importing the concrete loop package.
Tracks live agents so UI, hook, and orchestrator plugins can find them without importing the concrete loop package.
### Public API
- `ctx.agents.register(agent: Agent): () => void`
Register a live agent. Disposed with the calling fiber.
- `ctx.agents.register(agent: Agent): () => void` Register a live agent. Disposed with the calling fiber.
- `ctx.agents.get(id: string): Agent | undefined`
- `ctx.agents.list(): Agent[]`
### Events
The full `agent/*` event taxonomy is declared via declaration merging in
`dsh-agent` (not `dsh-agent-loop`), so plugins depend only on this package.
The full `agent/*` event taxonomy is declared via declaration merging in `dsh-agent` (not `dsh-agent-loop`), so plugins depend only on this package.
#### Lifecycle (emit)
@@ -34,12 +29,9 @@ The full `agent/*` event taxonomy is declared via declaration merging in
#### Interception seams (waterfall)
- `agent/request` — mutate `GenerateOptions` before the model call (hooks,
compaction, model switching, tool filtering)
- `agent/step-result` — post-process the assembled assistant message before tool
dispatch (validates what the log records)
- `agent/turn-continuation` — override the continue/stop decision
(force-continue /loop, force-stop budget guard)
- `agent/request` — mutate `GenerateOptions` before the model call (hooks, compaction, model switching, tool filtering)
- `agent/step-result` — post-process the assembled assistant message before tool dispatch (validates what the log records)
- `agent/turn-continuation` — override the continue/stop decision (force-continue /loop, force-stop budget guard)
#### Streaming + tool (emit)
@@ -52,20 +44,15 @@ The full `agent/*` event taxonomy is declared via declaration merging in
The handle every plugin programs against:
- `agent.send(content, options?)` — queue a message; starts a turn when idle
- `agent.steer(content, options?)` — steer a running turn (inject between steps);
behaves like `send` when idle
- `agent.inject(content, options?)` — inject in-session context without triggering
a turn (context/message event); next request sees it
- `agent.steer(content, options?)` — steer a running turn (inject between steps); behaves like `send` when idle
- `agent.inject(content, options?)` — inject in-session context without triggering a turn (context/message event); next request sees it
- `agent.abort(reason?)` — abort the in-flight step
- `agent.session`, `agent.status`, `agent.options`, `agent.id`
### Extension points
- Agent creation: `AgentLoop.create()` is the concrete implementation (in
`dsh-agent-loop`). Replace the loop by implementing `Agent` and registering
via `ctx.agents.register()`.
- Event listeners: all `agent/*` events are declared here — no dependency on the
loop package needed.
- Agent creation: `AgentLoop.create()` is the concrete implementation (in `dsh-agent-loop`). Replace the loop by implementing `Agent` and registering via `ctx.agents.register()`.
- Event listeners: all `agent/*` events are declared here — no dependency on the loop package needed.
### What is NOT here (TODO)

View File

@@ -1,9 +1,6 @@
# @deepseek-ai/dsh-bash-local
Local-subprocess implementation of the `@deepseek-ai/dsh-bash` executor seam:
`LocalBashExecutor` spawns `bash -c <command>` per call in its own process
group, collects bounded output with full-stream spill files, and escalates
kills SIGTERM→SIGKILL across the whole group.
Local-subprocess implementation of the `@deepseek-ai/dsh-bash` executor seam: `LocalBashExecutor` spawns `bash -c <command>` per call in its own process group, collects bounded output with full-stream spill files, and escalates kills SIGTERM→SIGKILL across the whole group.
## Config
@@ -19,33 +16,14 @@ kills SIGTERM→SIGKILL across the whole group.
## Behavior (and where it came from)
Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and
pi; the notable choices:
Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi; the notable choices:
- **Spawn per call, no shell state** — every call is a fresh non-login
`bash -c` (deterministic; no rc files). All four surveyed tools spawn per
call. `TODO(stateful-shell)` in `src/run.ts` records the two proven
stateful designs (Claude Code's cwd-only persistence; Codex's PTY exec
sessions) for when real workflows demand them.
- **Process-group kills with escalation** — children are spawned `detached`
(own process group); kills send SIGTERM to the group, then SIGKILL after a
3s grace (OpenCode's escalation; pipelines and subshells die with the
parent). ESRCH is tolerated; daemons that re-parent away from the group can
still survive — same caveat as the surveyed tools.
- **Tail-keep truncation + spill files** — output beyond `maxOutputBytes`
keeps the in-memory TAIL (errors/results cluster at the end — pi/OpenCode
rationale) while the FULL stream is appended to a temp file whose path is
reported. The model can `grep`/`tail` the spill file with bash itself.
- **Model-friendly env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat`
(Codex's hardcoded set) so pagers and ANSI color don't garble results.
- **Background tasks** — `start()` returns immediately, no timeout applies
(Claude Code detaches timeouts when backgrounding), `readOutput()` is
incremental with whole-stream byte offsets, and disposal kills everything.
- **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` (deterministic; no rc files). All four surveyed tools spawn per call. `TODO(stateful-shell)` in `src/run.ts` records the two proven stateful designs (Claude Code's cwd-only persistence; Codex's PTY exec sessions) for when real workflows demand them.
- **Process-group kills with escalation** — children are spawned `detached` (own process group); kills send SIGTERM to the group, then SIGKILL after a 3s grace (OpenCode's escalation; pipelines and subshells die with the parent). ESRCH is tolerated; daemons that re-parent away from the group can still survive — same caveat as the surveyed tools.
- **Tail-keep truncation + spill files** — output beyond `maxOutputBytes` keeps the in-memory TAIL (errors/results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a temp file whose path is reported. The model can `grep`/`tail` the spill file with bash itself.
- **Model-friendly env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` (Codex's hardcoded set) so pagers and ANSI color don't garble results.
- **Background tasks** — `start()` returns immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), `readOutput()` is incremental with whole-stream byte offsets, and disposal kills everything.
## Sandboxing
`TODO(permissions/sandbox)`: execution policy does NOT belong in this
package. Wrap the `tools/execute` waterfall (veto/ask) or implement a
sandboxing `BashExecutor` — see docs/architecture.md § plugin checklist.
Reference points: Claude Code wraps commands in sandbox-exec/bubblewrap;
Codex applies seatbelt/landlock plus an execpolicy prefix-rule engine.
`TODO(permissions/sandbox)`: execution policy does NOT belong in this package. Wrap the `tools/execute` waterfall (veto/ask) or implement a sandboxing `BashExecutor` — see docs/architecture.md § plugin checklist. Reference points: Claude Code wraps commands in sandbox-exec/bubblewrap; Codex applies seatbelt/landlock plus an execpolicy prefix-rule engine.

View File

@@ -1,11 +1,8 @@
# @deepseek-ai/dsh-bash
The **bash executor seam**: an abstract `BashExecutor` service (`ctx.bash`)
defining WHAT a bash backend does — run commands, manage background tasks —
without saying HOW.
The **bash executor seam**: an abstract `BashExecutor` service (`ctx.bash`) defining WHAT a bash backend does — run commands, manage background tasks — without saying HOW.
This package is one third of the bash capability, split so each concern can
evolve (and be swapped) independently:
This package is one third of the bash capability, split so each concern can evolve (and be swapped) independently:
| Package | Role |
|---|---|
@@ -13,11 +10,7 @@ evolve (and be swapped) independently:
| `@deepseek-ai/dsh-bash-local` | an implementation: local subprocesses |
| `@deepseek-ai/dsh-tool-bash` | the model-facing tool schemas over `ctx.bash` |
The split mirrors the LLM seam (`LlmService`/`LlmAdapter`) and the agent-tool
survey: pi hides execution behind a `BashOperations` interface (local shell /
SSH / VM backends), Codex behind an exec-server protocol. A future sandboxed,
containerized, or remote executor implements this interface and the tool
schemas don't change.
The split mirrors the LLM seam (`LlmService`/`LlmAdapter`) and the agent-tool survey: pi hides execution behind a `BashOperations` interface (local shell / SSH / VM backends), Codex behind an exec-server protocol. A future sandboxed, containerized, or remote executor implements this interface and the tool schemas don't change.
## Service API (`ctx.bash`)
@@ -30,13 +23,8 @@ schemas don't change.
| `kill(id)` | Kill a running task. Returns `false` when it already finished; throws for unknown ids. |
| `onTaskDone(listener)` | Completion listener (effect-based, disposer returned). Fires exactly once per task; never after the service is disposed. |
Implementations subclass `BashExecutor`, implement the abstract methods, and
call `notifyTaskDone(task)` on background completion. Disposal must kill every
running task (no orphan processes) — see the HMR-safety tests.
Implementations subclass `BashExecutor`, implement the abstract methods, and call `notifyTaskDone(task)` on background completion. Disposal must kill every running task (no orphan processes) — see the HMR-safety tests.
## Vocabulary
`BashExecSpec` (command, workdir?, timeoutMs?, signal?) →
`BashRunResult` (exitCode, signal, timedOut, aborted, timeoutMs, stdout/stderr
as `CollectedOutput`) and `BashTask`/`BashTaskRead` for the background side.
See `src/types.ts` for the full contracts.
`BashExecSpec` (command, workdir?, timeoutMs?, signal?) → `BashRunResult` (exitCode, signal, timedOut, aborted, timeoutMs, stdout/stderr as `CollectedOutput`) and `BashTask`/`BashTaskRead` for the background side. See `src/types.ts` for the full contracts.

View File

@@ -1,13 +1,8 @@
# @deepseek-ai/dsh-llm-deepseek
DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled
`fetch` + SSE translation from the official wire format (source of truth:
the API docs — guides/thinking_mode, guides/tool_calls,
api/create-chat-completion) into the `StreamChunk` protocol.
DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled `fetch` + SSE translation from the official wire format (source of truth: the API docs — guides/thinking_mode, guides/tool_calls, api/create-chat-completion) into the `StreamChunk` protocol.
A second, independent implementation of the same seam exists in
`@deepseek-ai/dsh-llm-pi-ai` (library-backed). Same Config shape — pick one
per context (registering both for the same model names throws by design).
A second, independent implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai` (library-backed). Same Config shape — pick one per context (registering both for the same model names throws by design).
## Config
@@ -22,62 +17,30 @@ per context (registering both for the same model names throws by design).
reasoningEffort: high # optional; high | max — omitted ⇒ not sent
```
`models` lists every model name this one adapter instance serves: the adapter
registers itself for each (the harness model name IS the wire `model` string),
so a `generate`/`stream` call routes to it whenever `options.model` is any of
them. Registering a second adapter for a name already taken throws
`LlmError('DUPLICATE_ADAPTER')` (the LLM service enforces one adapter per
model, all-or-nothing).
`models` lists every model name this one adapter instance serves: the adapter registers itself for each (the harness model name IS the wire `model` string), so a `generate`/`stream` call routes to it whenever `options.model` is any of them. Registering a second adapter for a name already taken throws `LlmError('DUPLICATE_ADAPTER')` (the LLM service enforces one adapter per model, all-or-nothing).
`reasoningEffort` is **omitted by default** — when unset, the `reasoning_effort`
wire field is not sent and the server applies its own default for the model.
The only accepted values are `high` and `max` (DeepSeek's official effort
levels). It is meaningful only with thinking enabled (the provider default).
`reasoningEffort` is **omitted by default** — when unset, the `reasoning_effort` wire field is not sent and the server applies its own default for the model. The only accepted values are `high` and `max` (DeepSeek's official effort levels). It is meaningful only with thinking enabled (the provider default).
`thinking`/`reasoningEffort` are adapter-level request defaults serialized as
the official top-level `thinking: {type}` / `reasoning_effort` wire fields.
They live in adapter config (not `GenerateOptions`) to keep the core
vocabulary provider-neutral.
`thinking`/`reasoningEffort` are adapter-level request defaults serialized as the official top-level `thinking: {type}` / `reasoning_effort` wire fields. They live in adapter config (not `GenerateOptions`) to keep the core vocabulary provider-neutral.
## Wire-format notes (verified live + against the official docs)
- Streaming only (`stream_options.include_usage` always on). `usage` may
arrive attached to the finish chunk or as a trailing usage-only chunk —
the translator defers both to `[DONE]`, so `usage` always precedes
`finish` and nothing follows `finish`.
- The first thinking-mode chunk carries `reasoning_content: ""` — handled
(no spurious reasoning block).
- **Reasoning passback rule**: on assistant turns that carried tool calls,
`reasoning_content` is serialized back in history (required by the API in
thinking mode); on tool-call-free turns it is dropped (ignored anyway —
saves tokens).
- `strict` on tool schemas passes through (officially Beta; the public API
wants the `/beta` base URL for it, the internal endpoint accepts it
directly).
- Cache accounting: `cacheReadTokens``prompt_cache_hit_tokens` /
`prompt_tokens_details.cached_tokens`; DeepSeek reports no cache-write
metric.
- Streaming only (`stream_options.include_usage` always on). `usage` may arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to `[DONE]`, so `usage` always precedes `finish` and nothing follows `finish`.
- The first thinking-mode chunk carries `reasoning_content: ""` — handled (no spurious reasoning block).
- **Reasoning passback rule**: on assistant turns that carried tool calls, `reasoning_content` is serialized back in history (required by the API in thinking mode); on tool-call-free turns it is dropped (ignored anyway — saves tokens).
- `strict` on tool schemas passes through (officially Beta; the public API wants the `/beta` base URL for it, the internal endpoint accepts it directly).
- Cache accounting: `cacheReadTokens``prompt_cache_hit_tokens` / `prompt_tokens_details.cached_tokens`; DeepSeek reports no cache-write metric.
## Limitations (MVP, documented deliberately)
- `prefill` throws `LlmError('UNSUPPORTED')` — DeepSeek's chat-prefix
completion is a Beta feature on the `/beta` base URL; future work.
- `prefill` throws `LlmError('UNSUPPORTED')` — DeepSeek's chat-prefix completion is a Beta feature on the `/beta` base URL; future work.
- `image` blocks are skipped (no vision support on these models).
- `tool_choice` is not mapped (not part of the core vocabulary).
## Errors
Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403),
`RATE_LIMIT` (429), `INVALID_REQUEST` (400), `SERVER` (5xx), `HTTP_<status>`
otherwise. Protocol violations throw `STREAM_CLOSED` (no `[DONE]`) or
`MALFORMED_RESPONSE` (bad JSON payload). Unknown wire `finish_reason`s
(e.g. `content_filter`, `insufficient_system_resource`) become
`finish {kind: 'error', code: <REASON>}` chunks.
Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `RATE_LIMIT` (429), `INVALID_REQUEST` (400), `SERVER` (5xx), `HTTP_<status>` otherwise. Protocol violations throw `STREAM_CLOSED` (no `[DONE]`) or `MALFORMED_RESPONSE` (bad JSON payload). Unknown wire `finish_reason`s (e.g. `content_filter`, `insufficient_system_resource`) become `finish {kind: 'error', code: <REASON>}` chunks.
## Testing
Unit suites run against a local `node:http` mock SSE server (no network).
Real-API coverage lives in `tests/adapter.e2e.ts` (`yarn test:e2e`,
key-gated): V4 Flash + V4 Pro across thinking enabled/disabled and both
official effort levels, including the thinking+tools round trip with
reasoning passback.
Unit suites run against a local `node:http` mock SSE server (no network). Real-API coverage lives in `tests/adapter.e2e.ts` (`yarn test:e2e`, key-gated): V4 Flash + V4 Pro across thinking enabled/disabled and both official effort levels, including the thinking+tools round trip with reasoning passback.

View File

@@ -1,33 +1,19 @@
# @deepseek-ai/dsh-llm-pi-ai
DeepSeek adapter for the harness LLM seam backed by
[`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai)
(the LLM library behind the pi agent).
DeepSeek adapter for the harness LLM seam backed by [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai) (the LLM library behind the pi agent).
## Why a second adapter exists
`@deepseek-ai/dsh-llm-deepseek` already talks to the same endpoint. This
package is its **design-verification twin**: same models, same wire
protocol, completely different internals — a unified LLM library with its
own event vocabulary versus hand-rolled fetch/SSE. Anything the harness
`StreamChunk` protocol cannot express for BOTH implementations is a
core-vocabulary bug. The differences it exercised on purpose:
`@deepseek-ai/dsh-llm-deepseek` already talks to the same endpoint. This package is its **design-verification twin**: same models, same wire protocol, completely different internals — a unified LLM library with its own event vocabulary versus hand-rolled fetch/SSE. Anything the harness `StreamChunk` protocol cannot express for BOTH implementations is a core-vocabulary bug. The differences it exercised on purpose:
- pi-ai hands back tool-call `arguments` as **parsed objects**; the harness
keeps raw JSON strings (re-stringified at `block-end`).
- pi-ai reports failures as **in-stream error events** (it never throws
mid-stream); these map to `finish {kind:'error'|'aborted'}` chunks — the
protocol's other sanctioned error path besides throwing (which
llm-deepseek uses).
- pi-ai folds reasoning tokens into `usage.output`; there is no separate
reasoning count to map.
- pi-ai's options omit stop sequences; `GenerateOptions.stop` is injected
via its `onPayload` hook.
- pi-ai hands back tool-call `arguments` as **parsed objects**; the harness keeps raw JSON strings (re-stringified at `block-end`).
- pi-ai reports failures as **in-stream error events** (it never throws mid-stream); these map to `finish {kind:'error'|'aborted'}` chunks — the protocol's other sanctioned error path besides throwing (which llm-deepseek uses).
- pi-ai folds reasoning tokens into `usage.output`; there is no separate reasoning count to map.
- pi-ai's options omit stop sequences; `GenerateOptions.stop` is injected via its `onPayload` hook.
## Config
Same shape as llm-deepseek (one-line swap in cordis.yml), with pi-ai's
thinking-level vocabulary:
Same shape as llm-deepseek (one-line swap in cordis.yml), with pi-ai's thinking-level vocabulary:
```yaml
- id: llm
@@ -41,20 +27,12 @@ thinking-level vocabulary:
## Dependency weight
pi-ai declares the openai/anthropic/google/mistral/AWS SDKs as install-time
dependencies. They are lazy-loaded — only the openai SDK actually loads for
this adapter — but they do land in `node_modules`. Accepted for a package
whose purpose is design verification.
pi-ai declares the openai/anthropic/google/mistral/AWS SDKs as install-time dependencies. They are lazy-loaded — only the openai SDK actually loads for this adapter — but they do land in `node_modules`. Accepted for a package whose purpose is design verification.
## Limitations
Same MVP contract as llm-deepseek: `prefill` throws `UNSUPPORTED`, images
are not representable, `tool_choice` is not mapped.
Same MVP contract as llm-deepseek: `prefill` throws `UNSUPPORTED`, images are not representable, `tool_choice` is not mapped.
## Testing
Unit suites run against a local `node:http` mock SSE server (pi-ai's openai
SDK happily talks to any base URL). Real-API coverage in
`tests/adapter.e2e.ts` (`yarn test:e2e`, key-gated): V4 Flash + V4 Pro across
all exposed reasoning levels (off/high/xhigh), the thinking+tools round trip,
and a cross-adapter structural-equivalence check against llm-deepseek.
Unit suites run against a local `node:http` mock SSE server (pi-ai's openai SDK happily talks to any base URL). Real-API coverage in `tests/adapter.e2e.ts` (`yarn test:e2e`, key-gated): V4 Flash + V4 Pro across all exposed reasoning levels (off/high/xhigh), the thinking+tools round trip, and a cross-adapter structural-equivalence check against llm-deepseek.

View File

@@ -1,24 +1,18 @@
# dsh-llm
Provider-neutral LLM vocabulary and abstract service. This package defines the
canonical language spoken by the agent loop, session logs, and every plugin.
Provider-neutral LLM vocabulary and abstract service. This package defines the canonical language spoken by the agent loop, session logs, and every plugin.
## Service: `LlmService` (ctx key: `llm`)
An adapter registry plus streaming / non-streaming call surfaces. Both call
surfaces are interceptable via waterfall events.
An adapter registry plus streaming / non-streaming call surfaces. Both call surfaces are interceptable via waterfall events.
### Public API
- `ctx.llm.registerAdapter(models: string[], adapter: LlmAdapter): () => void`
Register an adapter for the given model names. Disposed with the calling fiber.
- `ctx.llm.registerAdapter(models: string[], adapter: LlmAdapter): () => void` Register an adapter for the given model names. Disposed with the calling fiber.
- `ctx.llm.models(): string[]` — model names with a registered adapter.
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>`
Stream one model call as raw chunks (token-level deltas).
- `ctx.llm.streamBlocks(options: GenerateOptions): AsyncIterable<ContentBlock>`
Stream as completed content blocks (convenience view).
- `ctx.llm.generate(options: GenerateOptions): Promise<GenerateResult>`
One model call, fully assembled.
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` Stream one model call as raw chunks (token-level deltas).
- `ctx.llm.streamBlocks(options: GenerateOptions): AsyncIterable<ContentBlock>` Stream as completed content blocks (convenience view).
- `ctx.llm.generate(options: GenerateOptions): Promise<GenerateResult>` One model call, fully assembled.
### Events
@@ -30,35 +24,23 @@ surfaces are interceptable via waterfall events.
### Extension points
- Subclass `LlmAdapter` and call `ctx.llm.registerAdapter(models, adapter)`
to add a new model provider.
- Wrap `llm/stream` or `llm/generate` via `ctx.on()` waterfall listeners for
caching, retry, logging, rate-limiting, etc.
- Subclass `LlmAdapter` and call `ctx.llm.registerAdapter(models, adapter)` to add a new model provider.
- Wrap `llm/stream` or `llm/generate` via `ctx.on()` waterfall listeners for caching, retry, logging, rate-limiting, etc.
### Content-block vocabulary (`types.ts`)
Messages are arrays of typed content blocks: `text`, `reasoning`, `tool-call`,
`tool-result`, `image`. The union is derived from the merge-extensible
`ContentBlockMap`, so plugins can add block types via declaration merging.
Messages are arrays of typed content blocks: `text`, `reasoning`, `tool-call`, `tool-result`, `image`. The union is derived from the merge-extensible `ContentBlockMap`, so plugins can add block types via declaration merging.
Streaming is a raw chunk protocol (`block-start`, `text-delta`,
`reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`).
`BlockAssembler` is the single shared implementation that assembles chunks into
blocks/messages.
Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`). `BlockAssembler` is the single shared implementation that assembles chunks into blocks/messages.
### Classes
- `LlmAdapter` — abstract base class for provider adapters. The only required
method is `stream()`.
- `BlockAssembler` — incrementally assembles raw chunks into complete content
blocks and an assistant message. Used by the agent loop (raw chunks for replay
- `LlmAdapter` — abstract base class for provider adapters. The only required method is `stream()`.
- `BlockAssembler` — incrementally assembles raw chunks into complete content blocks and an assistant message. Used by the agent loop (raw chunks for replay
+ assembled for history) and by `streamBlocks()`/`generate()`.
- `LlmError` — typed error with a `code` string (`NO_ADAPTER`,
`DUPLICATE_ADAPTER`).
- `LlmError` — typed error with a `code` string (`NO_ADAPTER`, `DUPLICATE_ADAPTER`).
### What is NOT here (TODO)
- **DeepSeek V4 adapter** — the first real adapter lands in a later phase.
- **Streaming protocol review** — the chunk protocol has `TODO(review)` markers
and needs careful review before the first real adapter (DeepSeek V4 wire
format, partial JSON arguments, interleaved reasoning signatures, ...).
- **Streaming protocol review** — the chunk protocol has `TODO(review)` markers and needs careful review before the first real adapter (DeepSeek V4 wire format, partial JSON arguments, interleaved reasoning signatures, ...).

View File

@@ -1,20 +1,14 @@
# dsh-session
Event-sourced session log and in-memory store. A `Session` is the append-only
source of truth for an agent's whole interaction history — the LLM message
history is *derived* from it.
Event-sourced session log and in-memory store. A `Session` is the append-only source of truth for an agent's whole interaction history — the LLM message history is *derived* from it.
## Service: `SessionStore` (ctx key: `sessions`)
Creates and holds event-sourced `Session` instances. Persistence is intentionally
not implemented here — plugins subscribe to `session/event` and flush on
`session/flush`.
Creates and holds event-sourced `Session` instances. Persistence is intentionally not implemented here — plugins subscribe to `session/event` and flush on `session/flush`.
### Public API
- `ctx.sessions.create(id?: string, seed?: SessionEvent[]): Session`
Create a session. `seed` replays/forks an existing event log. Disposed with
the calling fiber.
- `ctx.sessions.create(id?: string, seed?: SessionEvent[]): Session` Create a session. `seed` replays/forks an existing event log. Disposed with the calling fiber.
- `ctx.sessions.get(id: string): Session | undefined`
- `ctx.sessions.list(): Session[]`
@@ -31,35 +25,24 @@ not implemented here — plugins subscribe to `session/event` and flush on
Plain class (not a Cordis Service). Create via `ctx.sessions.create()`.
- `session.append(type, data): SessionEvent` — synchronous, never blocks on I/O.
- `session.deriveMessages(): Message[]` — derive the LLM message history from
the event log. Raw `assistant/chunk` events are skipped; `context/message` and
`steering/message` render as tagged synthetic user messages.
- `session.deriveMessages(): Message[]` — derive the LLM message history from the event log. Raw `assistant/chunk` events are skipped; `context/message` and `steering/message` render as tagged synthetic user messages.
- `session.events`, `session.seq`, `session.id`
### Session event vocabulary (`types.ts`)
The append-only log: `turn/start`, `turn/end`, `step/start`, `step/end`,
`user/message`, `assistant/message`, `assistant/chunk`, `tool/call`,
`tool/result`, `steering/message`, `context/message`, `usage`, `error`.
The append-only log: `turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `assistant/message`, `assistant/chunk`, `tool/call`, `tool/result`, `steering/message`, `context/message`, `usage`, `error`.
Merge-extensible via `SessionEventMap` — a compaction plugin adds
`compaction/marker`, etc.
Merge-extensible via `SessionEventMap` — a compaction plugin adds `compaction/marker`, etc.
Also defines `TurnTriggerMap` and `TurnEndReasonMap` (merge-extensible sum types
for typed turn boundaries — `kind`-tagged instead of strings).
Also defines `TurnTriggerMap` and `TurnEndReasonMap` (merge-extensible sum types for typed turn boundaries — `kind`-tagged instead of strings).
### Extension points
- Persistence plugins: subscribe to `session/event` (write-behind) and drain on
`session/flush` (awaited) and fiber dispose. See
`examples/echo-agent/src/session-jsonl.ts` for the pattern.
- Replay/fork: `ctx.sessions.create(id, seed)` seeds a new session with an
existing event log.
- Persistence plugins: subscribe to `session/event` (write-behind) and drain on `session/flush` (awaited) and fiber dispose. See `examples/echo-agent/src/session-jsonl.ts` for the pattern.
- Replay/fork: `ctx.sessions.create(id, seed)` seeds a new session with an existing event log.
### What is NOT here (TODO)
- **Real persistence backends** (JSONL per session dir, sqlite) — future phase.
- **Session event vocabulary review** — `TODO(review)` once the loop and a
persistence plugin coexist.
- **Session branching/tree** (pi-style entry tree) — defered unless needed beyond
seed-based forking.
- **Session event vocabulary review** — `TODO(review)` once the loop and a persistence plugin coexist.
- **Session branching/tree** (pi-style entry tree) — defered unless needed beyond seed-based forking.

View File

@@ -1,19 +1,14 @@
# dsh-system-prompt
System prompt assembly registry. Plugins contribute ordered text sections and
tool-schema providers; the agent loop calls `assemble()` once per step.
System prompt assembly registry. Plugins contribute ordered text sections and tool-schema providers; the agent loop calls `assemble()` once per step.
## Service: `SystemPrompt` (ctx key: `systemPrompt`)
### Public API
- `ctx.systemPrompt.section(section: PromptSection): () => void`
Contribute a section. Disposed with the calling fiber.
- `ctx.systemPrompt.tools(provider: () => ToolSchema[]): () => void`
Contribute tool schemas (evaluated at each assembly). Disposed with the calling
fiber.
- `ctx.systemPrompt.assemble(): Promise<PromptAssembly>`
Assemble the current prompt. Runs through the `system-prompt/assemble` waterfall.
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. Disposed with the calling fiber.
- `ctx.systemPrompt.tools(provider: () => ToolSchema[]): () => void` Contribute tool schemas (evaluated at each assembly). Disposed with the calling fiber.
- `ctx.systemPrompt.assemble(): Promise<PromptAssembly>` Assemble the current prompt. Runs through the `system-prompt/assemble` waterfall.
### Events
@@ -24,24 +19,17 @@ tool-schema providers; the agent loop calls `assemble()` once per step.
### Key types
- `PromptSection``{ name, order, text: string | (() => string) }`. Sections
are concatenated in ascending `order`.
- `PromptAssembly``{ sections: PromptSection[], tools: ToolSchema[] }`.
Tool schemas are part of the assembly by design: "what the model is told it
can do" is one coherent thing, even though adapters transmit schemas as a
separate wire field.
- `PromptSection``{ name, order, text: string | (() => string) }`. Sections are concatenated in ascending `order`.
- `PromptAssembly``{ sections: PromptSection[], tools: ToolSchema[] }`. Tool schemas are part of the assembly by design: "what the model is told it can do" is one coherent thing, even though adapters transmit schemas as a separate wire field.
- `renderPrompt(assembly)` — joins section texts with blank lines.
Merge-extensible: plugins can declare extra fields on `PromptAssembly` via
declaration merging.
Merge-extensible: plugins can declare extra fields on `PromptAssembly` via declaration merging.
### Extension points
- Section providers: AGENTS.md reader, cwd notifier, persona config, etc.
- Tool schema providers: `ToolRegistry` registers itself as a tool provider
automatically.
- The `system-prompt/assemble` waterfall: mutate or replace the assembly
(system-prompt configurability, dynamic tool filtering).
- Tool schema providers: `ToolRegistry` registers itself as a tool provider automatically.
- The `system-prompt/assemble` waterfall: mutate or replace the assembly (system-prompt configurability, dynamic tool filtering).
### What is NOT here

View File

@@ -1,13 +1,8 @@
# @deepseek-ai/dsh-tool-bash
The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registered
over the `ctx.bash` executor seam (`@deepseek-ai/dsh-bash`). Pure schema +
text shaping; every process concern lives behind the seam, so sandboxed or
remote executor implementations swap in without changing what the model sees.
The model-facing bash tools — `bash`, `bash_output`, `bash_kill` — registered over the `ctx.bash` executor seam (`@deepseek-ai/dsh-bash`). Pure schema + text shaping; every process concern lives behind the seam, so sandboxed or remote executor implementations swap in without changing what the model sees.
Requires a loaded executor implementation (e.g.
`@deepseek-ai/dsh-bash-local`); the plugin stays pending until `ctx.bash`
exists (`inject: ['tools', 'bash']`).
Requires a loaded executor implementation (e.g. `@deepseek-ai/dsh-bash-local`); the plugin stays pending until `ctx.bash` exists (`inject: ['tools', 'bash']`).
## Tools
@@ -21,42 +16,22 @@ exists (`inject: ['tools', 'bash']`).
| `workdir` | string | Working directory for this call. |
| `run_in_background` | boolean | Return a task id immediately; no timeout applies. |
`command`, `workdir`, and `timeoutMs` are resolved against the executor's
config defaults via `ctx.bash.resolve()` before execution, so the executor
seam (`BashExecSpec`) receives explicit `workdir`/`timeoutMs` values.
`command`, `workdir`, and `timeoutMs` are resolved against the executor's config defaults via `ctx.bash.resolve()` before execution, so the executor seam (`BashExecSpec`) receives explicit `workdir`/`timeoutMs` values.
Result text: stdout, then a `[stderr]` section, then status markers —
`[timed out after Nms]` whenever the executor's timer fired (reported
independently of how the process ended, so a command that traps SIGTERM and
exits 0 still shows it), `[killed by signal: …]` for a signal death,
`[exit code: N]` for a non-zero exit (reported, **not** `isError`: the model
decides how to react), and `[output truncated; full output: <path>]` when the
tail was kept. Only infrastructure failures (spawn errors, aborts) surface as
`isError` results.
Result text: stdout, then a `[stderr]` section, then status markers — `[timed out after Nms]` whenever the executor's timer fired (reported independently of how the process ended, so a command that traps SIGTERM and exits 0 still shows it), `[killed by signal: …]` for a signal death, `[exit code: N]` for a non-zero exit (reported, **not** `isError`: the model decides how to react), and `[output truncated; full output: <path>]` when the tail was kept. Only infrastructure failures (spawn errors, aborts) surface as `isError` results.
### `bash_output`
`task_id` → output produced **since the previous `bash_output` call** plus a
status line (`running` / `completed, exit code: N` / `killed`). Reads that
lost data to buffer bounds say so and point at the full-output spill file.
`task_id` → output produced **since the previous `bash_output` call** plus a status line (`running` / `completed, exit code: N` / `killed`). Reads that lost data to buffer bounds say so and point at the full-output spill file.
### `bash_kill`
`task_id` → SIGTERM→SIGKILL on the task's process group. Killing an
already-finished task is a reported no-op; unknown ids are errors.
`task_id` → SIGTERM→SIGKILL on the task's process group. Killing an already-finished task is a reported no-op; unknown ids are errors.
## Background completion notices
When a background task finishes, a short notice is injected into the owning
agent's session (`agent.inject()`, source `{kind: 'plugin', plugin:
'tool-bash'}`). Injection is **durable context for the next model request,
not a wake-up** — an idle agent stays idle until something sends a message.
That's why the tool descriptions tell the model to poll with `bash_output`.
When a background task finishes, a short notice is injected into the owning agent's session (`agent.inject()`, source `{kind: 'plugin', plugin: 'tool-bash'}`). Injection is **durable context for the next model request, not a wake-up** — an idle agent stays idle until something sends a message. That's why the tool descriptions tell the model to poll with `bash_output`.
## Permissions
`TODO(permissions)`: commands run with the executor's full authority. The
permission/sandbox seam is the `tools/execute` waterfall (veto or ask) plus
sandboxing `BashExecutor` implementations — see docs/architecture.md.
`@cordisjs/plugin-capability` (a named-permission service with a session
`test()`) is a candidate building block for that work.
`TODO(permissions)`: commands run with the executor's full authority. The permission/sandbox seam is the `tools/execute` waterfall (veto or ask) plus sandboxing `BashExecutor` implementations — see docs/architecture.md. `@cordisjs/plugin-capability` (a named-permission service with a session `test()`) is a candidate building block for that work.

View File

@@ -1,24 +1,19 @@
# dsh-tools
Tool registry and execution waterfall. Tool plugins register their schemas and
executors; the agent loop executes calls through the `tools/execute` waterfall.
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): () => void`
Register a tool. Disposed with the calling fiber.
- `ctx.tools.register(definition: ToolDefinition): () => void` Register a tool. Disposed with the calling fiber.
- `ctx.tools.get(name: string): ToolDefinition | undefined`
- `ctx.tools.schemas(): ToolSchema[]`
Schemas of all registered tools (without the `execute` functions).
- `ctx.tools.execute(exec: ToolExecution): Promise<ToolExecutionResult>`
Execute one tool call through the `tools/execute` waterfall.
- `ctx.tools.schemas(): ToolSchema[]` Schemas of all registered tools (without the `execute` functions).
- `ctx.tools.execute(exec: ToolExecution): Promise<ToolExecutionResult>` Execute one tool call through the `tools/execute` waterfall.
### Injected services
`SystemPrompt` — the registry automatically feeds its tool schemas into the
system-prompt assembly via `ctx.systemPrompt.tools()`.
`SystemPrompt` — the registry automatically feeds its tool schemas into the system-prompt assembly via `ctx.systemPrompt.tools()`.
### Events
@@ -35,19 +30,13 @@ system-prompt assembly via `ctx.systemPrompt.tools()`.
### Extension points
- Tool plugins call `ctx.tools.register()` — schemas flow into the assembly
automatically.
- The `tools/execute` waterfall is the single seam for sandbox, permission,
hooks, and plan-mode plugins to wrap or veto a call. Listeners receive
`(exec, next)`: call `next()` to proceed, or return a result without calling
`next()` to short-circuit (veto).
- MCP servers: one plugin per server, discover tools, call
`ctx.tools.register()` with the server's schemas.
- Tool plugins call `ctx.tools.register()` — schemas flow into the assembly automatically.
- The `tools/execute` waterfall is the single seam for sandbox, permission, hooks, and plan-mode plugins to wrap or veto a call. Listeners receive `(exec, next)`: call `next()` to proceed, or return a result without calling `next()` 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:
First-party plugin authors can use the `defineTool()` helper (exported from this package) for typed tool parameter schemas:
```ts
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -68,16 +57,11 @@ ctx.tools.register(defineTool({
}))
```
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.
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.
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.
- **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.