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,7 +1,6 @@
|
||||
# Cookbook: adding a workspace package
|
||||
|
||||
The file-by-file checklist for a new `@deepseek-ai/dsh-<name>` package.
|
||||
(Verified by the bash and adapter packages; if it drifts, fix it here.)
|
||||
The file-by-file checklist for a new `@deepseek-ai/dsh-<name>` package. (Verified by the bash and adapter packages; if it drifts, fix it here.)
|
||||
|
||||
## 1. Create the package
|
||||
|
||||
@@ -16,11 +15,7 @@ packages/<name>/
|
||||
README.md # service API, events, extension points, design notes
|
||||
```
|
||||
|
||||
package.json invariants (enforced by `yarn constraints` / yarn.config.cjs):
|
||||
`private: true`, `version: 0.0.1`, `type: module`, `cordis` in BOTH
|
||||
peerDependencies and devDependencies (same range). Mirror every dsh peer
|
||||
dependency in devDependencies. `schemastery` goes in `dependencies` (it is a
|
||||
runtime validator), matching agent-loop.
|
||||
package.json invariants (enforced by `yarn constraints` / yarn.config.cjs): `private: true`, `version: 0.0.1`, `type: module`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop.
|
||||
|
||||
## 2. Register it in the root configs
|
||||
|
||||
@@ -32,14 +27,11 @@ runtime validator), matching agent-loop.
|
||||
| `scripts/publint-all.ts` | add `'packages/<name>'` to the array |
|
||||
| `knip.json` | only if the package has non-`*.spec.ts` entries (e.g. `*.e2e.ts` → add a per-workspace override like `packages/llm-deepseek`) |
|
||||
|
||||
Covered automatically by globs — no edits needed: root `package.json`
|
||||
workspaces, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`.
|
||||
Covered automatically by globs — no edits needed: root `package.json` workspaces, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`.
|
||||
|
||||
## 3. Decide the package topology
|
||||
|
||||
For a swappable capability, split interface / implementation / consumer into
|
||||
separate packages (see docs/architecture.md § "Capability seams" — the bash
|
||||
trio is the template). A single-purpose plugin stays one package.
|
||||
For a swappable capability, split interface / implementation / consumer into separate packages (see docs/architecture.md § "Capability seams" — the bash trio is the template). A single-purpose plugin stays one package.
|
||||
|
||||
## 4. Verify
|
||||
|
||||
@@ -50,6 +42,4 @@ yarn test:coverage # 100% per-file over src (types.ts exempt)
|
||||
yarn build && yarn knip && yarn publint
|
||||
```
|
||||
|
||||
Test expectations: every registry/registration needs an HMR-safety test
|
||||
(register from a child fiber, dispose it, assert cleanup). Excessive tests
|
||||
are welcome — see AGENTS.md.
|
||||
Test expectations: every registry/registration needs an HMR-safety test (register from a child fiber, dispose it, assert cleanup). Excessive tests are welcome — see AGENTS.md.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Cookbook: adding a tool
|
||||
|
||||
How to give the model a new capability. Reference implementations:
|
||||
`examples/echo-agent/src/echo-tool.ts` (minimal) and
|
||||
`packages/tool-bash` (production-grade, three-package seam).
|
||||
How to give the model a new capability. Reference implementations: `examples/echo-agent/src/echo-tool.ts` (minimal) and `packages/tool-bash` (production-grade, three-package seam).
|
||||
|
||||
## The minimal shape
|
||||
|
||||
@@ -30,33 +28,18 @@ export function apply(ctx: Context) {
|
||||
}
|
||||
```
|
||||
|
||||
Registration is effect-based: disposing the plugin fiber unregisters the
|
||||
tool (write the HMR test). Schemas flow into the system-prompt assembly
|
||||
automatically.
|
||||
Registration is effect-based: disposing the plugin fiber unregisters the tool (write the HMR test). Schemas flow into the system-prompt assembly automatically.
|
||||
|
||||
## Rules of the execute() contract
|
||||
|
||||
- **Validate args at runtime.** `defineTool`'s `InferArgs` typing is
|
||||
compile-time only; at runtime `arguments` is whatever JSON the model
|
||||
emitted. Check every field; throw a descriptive Error for bad input.
|
||||
- **Throwing means isError.** The registry catches anything `execute()`
|
||||
throws and returns `{isError: true}` to the model. Use that for
|
||||
infrastructure failures (bad input, spawn errors, aborts) — but REPORT
|
||||
domain failures in the result text instead (e.g. tool-bash returns
|
||||
`[exit code: 9]` with `isError: false`: the model decides what a failing
|
||||
command means).
|
||||
- **Validate args at runtime.** `defineTool`'s `InferArgs` typing is compile-time only; at runtime `arguments` is whatever JSON the model emitted. Check every field; throw a descriptive Error for bad input.
|
||||
- **Throwing means isError.** The registry catches anything `execute()` throws and returns `{isError: true}` to the model. Use that for infrastructure failures (bad input, spawn errors, aborts) — but REPORT domain failures in the result text instead (e.g. tool-bash returns `[exit code: 9]` with `isError: false`: the model decides what a failing command means).
|
||||
- **Honor `exec.signal`.** Cancel in-flight work when it fires.
|
||||
- **Use `exec.agent` for async notifications.** `agent.inject(content,
|
||||
{source: {kind: 'plugin', plugin: '<name>'}})` appends durable context the
|
||||
NEXT model request sees — it is not a wake-up (an idle agent stays idle).
|
||||
Guard against disposed agents (try/catch).
|
||||
- **Use `exec.agent` for async notifications.** `agent.inject(content, {source: {kind: 'plugin', plugin: '<name>'}})` appends durable context the NEXT model request sees — it is not a wake-up (an idle agent stays idle). Guard against disposed agents (try/catch).
|
||||
|
||||
## Long-running work
|
||||
|
||||
Follow tool-bash's background pattern: a `run_in_background` flag returns a
|
||||
task id immediately; companion tools poll incrementally and kill; completion
|
||||
notices arrive via `agent.inject()`. Bound buffers and spill full output to
|
||||
disk so nothing is silently lost.
|
||||
Follow tool-bash's background pattern: a `run_in_background` flag returns a task id immediately; companion tools poll incrementally and kill; completion notices arrive via `agent.inject()`. Bound buffers and spill full output to disk so nothing is silently lost.
|
||||
|
||||
> TODO: each tool reimplements this background pattern by hand today. At some
|
||||
> point we need a generic long-running-tool layer that handles task ids,
|
||||
@@ -64,14 +47,8 @@ disk so nothing is silently lost.
|
||||
|
||||
## Permissions / sandboxing
|
||||
|
||||
Prefer not to build policy into the tool. The seam is the `tools/execute` waterfall
|
||||
(veto or wrap — see the permission-gate example in docs/architecture.md), or
|
||||
a sandboxing implementation behind the tool's executor seam.
|
||||
Prefer not to build policy into the tool. The seam is the `tools/execute` waterfall (veto or wrap — see the permission-gate example in docs/architecture.md), or a sandboxing implementation behind the tool's executor seam.
|
||||
|
||||
## Tests every tool needs
|
||||
|
||||
Arg-validation rejections, result shaping for every outcome, the HMR
|
||||
disposal test, and — for tools with side effects — an integration spec that
|
||||
drives the tool through the agent loop with a scripted `MockAdapter`
|
||||
(`packages/agent-loop/tests/mock-adapter.ts`), asserting the `tool/call` /
|
||||
`tool/result` session events.
|
||||
Arg-validation rejections, result shaping for every outcome, the HMR disposal test, and — for tools with side effects — an integration spec that drives the tool through the agent loop with a scripted `MockAdapter` (`packages/agent-loop/tests/mock-adapter.ts`), asserting the `tool/call` / `tool/result` session events.
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# Cookbook: adding an LLM adapter
|
||||
|
||||
How to connect a new model provider. Reference implementations:
|
||||
`packages/llm-deepseek` (hand-rolled HTTP/SSE) and `packages/llm-pi-ai`
|
||||
(wrapping an LLM library). Read the `StreamChunk` doc in
|
||||
`packages/llm/src/types.ts` first — it records the protocol conventions both
|
||||
adapters were verified against.
|
||||
How to connect a new model provider. Reference implementations: `packages/llm-deepseek` (hand-rolled HTTP/SSE) and `packages/llm-pi-ai` (wrapping an LLM library). Read the `StreamChunk` doc in `packages/llm/src/types.ts` first — it records the protocol conventions both adapters were verified against.
|
||||
|
||||
## The shape
|
||||
|
||||
@@ -22,54 +18,26 @@ export function apply(ctx: Context, config: Config) {
|
||||
}
|
||||
```
|
||||
|
||||
Registration is effect-based (HMR-safe); one adapter per model name —
|
||||
duplicates throw. Secrets are cordis-native: schemastery Config with env
|
||||
fallbacks, fed from cordis.yml via `!!js process.env.MY_KEY`. Never read
|
||||
ad-hoc key files in code.
|
||||
Registration is effect-based (HMR-safe); one adapter per model name — duplicates throw. Secrets are cordis-native: schemastery Config with env fallbacks, fed from cordis.yml via `!!js process.env.MY_KEY`. Never read ad-hoc key files in code.
|
||||
|
||||
## Protocol obligations (the contract two implementations verified)
|
||||
|
||||
- Emit `usage` BEFORE `finish`; emit NOTHING after `finish`. The robust way:
|
||||
buffer finish/usage until the provider's end-of-stream marker, then flush
|
||||
(handles providers that send trailing usage-only chunks).
|
||||
- Tool-call `arguments` are RAW JSON strings end-to-end; stream fragments as
|
||||
`argumentsDelta`. If your provider hands back parsed objects, re-stringify
|
||||
at `block-end`.
|
||||
- Allocate block `index`es in first-seen stream order; reuse the index for
|
||||
every delta of the same block.
|
||||
- Errors have exactly two sanctioned paths: THROW from `stream()` (transport
|
||||
and protocol failures — use `LlmError` with a stable code), or end the
|
||||
stream with `finish {kind: 'error' | 'aborted'}` (provider in-band
|
||||
failures). Consumers handle both; pick per failure class and document it.
|
||||
- Emit `usage` BEFORE `finish`; emit NOTHING after `finish`. The robust way: buffer finish/usage until the provider's end-of-stream marker, then flush (handles providers that send trailing usage-only chunks).
|
||||
- Tool-call `arguments` are RAW JSON strings end-to-end; stream fragments as `argumentsDelta`. If your provider hands back parsed objects, re-stringify at `block-end`.
|
||||
- Allocate block `index`es in first-seen stream order; reuse the index for every delta of the same block.
|
||||
- Errors have exactly two sanctioned paths: THROW from `stream()` (transport and protocol failures — use `LlmError` with a stable code), or end the stream with `finish {kind: 'error' | 'aborted'}` (provider in-band failures). Consumers handle both; pick per failure class and document it.
|
||||
- Honor `options.signal` (pass it to fetch / your SDK).
|
||||
- `prefill` and other unsupported `GenerateOptions` fields: throw
|
||||
`LlmError(..., 'UNSUPPORTED')` rather than silently dropping.
|
||||
- `prefill` and other unsupported `GenerateOptions` fields: throw `LlmError(..., 'UNSUPPORTED')` rather than silently dropping.
|
||||
|
||||
Provider-specific request knobs (thinking modes, effort levels) belong in
|
||||
the ADAPTER's Config, not in `GenerateOptions` — the core vocabulary stays
|
||||
provider-neutral.
|
||||
Provider-specific request knobs (thinking modes, effort levels) belong in the ADAPTER's Config, not in `GenerateOptions` — the core vocabulary stays provider-neutral.
|
||||
|
||||
## Structure that worked
|
||||
|
||||
Split the adapter into testable stages (llm-deepseek's layout): wire types
|
||||
(`types.ts`, coverage-exempt) → request serializer → SSE/transport parser →
|
||||
chunk-translation state machine → a thin adapter class wiring them. Each
|
||||
stage gets its own unit suite.
|
||||
Split the adapter into testable stages (llm-deepseek's layout): wire types (`types.ts`, coverage-exempt) → request serializer → SSE/transport parser → chunk-translation state machine → a thin adapter class wiring them. Each stage gets its own unit suite.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit: mock the provider, not the harness.** A scripted `node:http`
|
||||
server speaking the provider's wire format covers happy paths, every error
|
||||
status, malformed payloads, premature closes, and aborts — no network, and
|
||||
it drives the 100% per-file coverage gate. Works for SDK-backed adapters
|
||||
too (point the SDK's baseURL at the mock).
|
||||
- **Hostile framing tests.** Split stream payloads at arbitrary byte
|
||||
positions (including mid-UTF-8) — real networks do.
|
||||
- **E2E: `tests/*.e2e.ts`** under `yarn test:e2e`, gated with
|
||||
`describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green.
|
||||
Cover each model × each provider mode you map (thinking on/off, effort
|
||||
levels), a tool-call round trip INCLUDING the follow-up turn with results
|
||||
in history, and loose assertions only (substring/structure, bounded
|
||||
maxTokens — real models are nondeterministic).
|
||||
- Register the e2e file pattern in `knip.json` (per-workspace `entry`
|
||||
override) or knip flags it unused.
|
||||
- **Unit: mock the provider, not the harness.** A scripted `node:http` server speaking the provider's wire format covers happy paths, every error status, malformed payloads, premature closes, and aborts — no network, and it drives the 100% per-file coverage gate. Works for SDK-backed adapters too (point the SDK's baseURL at the mock).
|
||||
- **Hostile framing tests.** Split stream payloads at arbitrary byte positions (including mid-UTF-8) — real networks do.
|
||||
- **E2E: `tests/*.e2e.ts`** under `yarn test:e2e`, gated with `describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green. Cover each model × each provider mode you map (thinking on/off, effort levels), a tool-call round trip INCLUDING the follow-up turn with results in history, and loose assertions only (substring/structure, bounded maxTokens — real models are nondeterministic).
|
||||
- Register the e2e file pattern in `knip.json` (per-workspace `entry` override) or knip flags it unused.
|
||||
|
||||
Reference in New Issue
Block a user