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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, ...).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user