mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'master' into fetch-failed-diagnostics
This commit is contained in:
@@ -13,7 +13,7 @@ The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append
|
||||
Persistence is an abstract **capability seam** ([capability seams](2026-06-13-capability-seams.md), the `dsh-bash` template), not loop or core logic:
|
||||
|
||||
1. **Interface** (`dsh-session-persistence`, `ctx.sessionPersistence`) — an abstract `SessionPersistence` service: `create`/`append`/`load`/`list`. Its persisted unit IS the existing `SessionEvent` (`{ type, seq, time, data }`), reused verbatim — no conversion type.
|
||||
2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only JSONL log per session (a `SessionHeader` line then one `SessionEvent` per line, verbatim **including `assistant/chunk`**).
|
||||
2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session (a `SessionHeader` line then one `SessionEvent` per line, verbatim **including `assistant/chunk`**), encoded as [checksummed Zstandard frames by default](2026-07-19-zstandard-jsonl-session-logs.md) or raw lines by configuration.
|
||||
|
||||
Key choices recorded here because they are durable, contested, and surprising:
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Three seams: the queue-aware cancel, the `AgentHandle` disposer, and the bash ow
|
||||
|
||||
### 1. Queue-aware `Agent.cancel(reason?)`
|
||||
|
||||
A new `cancel()` verb on the `Agent` interface — the single public stop primitive. (It originally shipped alongside a narrower step-only `abort()`; that verb was later removed as unused, leaving `cancel()` the only public way to stop work.) It clears the inbox's queued + steering FIFOs, aborts the in-flight step if any, and drives a **turn-scoped cancellation marker** the driver loop checks at every turn-decision point — so a prompt that is queued-but-not-yet-started never runs, a cancel landing in the pre-step / continuation window drops the about-to-run turn (ending it `aborted`), and a later prompt cannot be batched into the cancelled turn. `whenIdle()` reaches post-cancel quiescence. ACP `session/cancel` maps to `cancel()`. The marker is armed ONLY when there is something to cancel, so an idle no-op cancel cannot strand the next prompt.
|
||||
A new `cancel()` verb on the `Agent` interface — the single public stop primitive. (It originally shipped alongside a narrower step-only `abort()`; that verb was later removed as unused, leaving `cancel()` the only public way to stop work.) It clears the inbox's queued + steering FIFOs, aborts the in-flight step if any, and drives a **turn-scoped cancellation marker** the driver loop checks at every turn-decision point — so a prompt that is queued-but-not-yet-started never runs, a cancel landing in the pre-step / continuation window drops the about-to-run turn (ending it `aborted`), and a later accepted prompt remains an independent queued turn. `whenIdle()` reaches post-cancel quiescence. ACP `session/cancel` maps to `cancel()`. The marker is armed ONLY when there is something to cancel, so an idle no-op cancel cannot strand the next prompt.
|
||||
|
||||
### 2. `AgentHandle` async disposer
|
||||
|
||||
@@ -29,7 +29,7 @@ Background-task ownership moved from a `tool-bash` plugin-local `Map<string, Age
|
||||
These invariants hold and are pinned by tests:
|
||||
|
||||
- ACP disconnect/session close leaves no registered agent AND no session-store entry for that session, even when `session/load` races teardown.
|
||||
- `session/cancel` before a queued prompt starts prevents that prompt from running and cannot batch the next prompt into the cancelled turn.
|
||||
- `session/cancel` before a queued prompt starts prevents that prompt from running; a later accepted prompt remains an independent queued turn.
|
||||
- A `tool-bash` HMR reload does NOT make an existing background task readable or killable by a different session (ownership survives on the executor).
|
||||
- Existing non-ACP demos still work without managing handles explicitly; config-created agents remain owned by the `AgentLoop` plugin fiber.
|
||||
|
||||
|
||||
@@ -68,3 +68,5 @@ Every surface-eligible event must carry `surfaceOp` or it would disappear from d
|
||||
- **`packages/session-persistence/session-persistence`**: Abstract interface unchanged.
|
||||
|
||||
The surface is the foundation for future history manipulation. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically.
|
||||
|
||||
A `tool/result` replacement may rewrite exactly one current `tool/result` and must preserve every data field except `content`. Session acceptance enforces this rule together with positional range and provenance validation, independent of optional diagnostic plugins.
|
||||
|
||||
@@ -28,7 +28,7 @@ Six methods (five required + an optional lifecycle hook) — the only seam betwe
|
||||
|
||||
### The opaque torn marker
|
||||
|
||||
The single design choice that keeps the seam clean: the crash-repair "where is the torn tail" token is OPAQUE to the coordinator. The coordinator computes the synthetic closers (it owns `interruptedTurnClosers` from `dsh-session`), but it only ever tests `tornMarker !== undefined` and passes the value straight back to `commitRepair` — it never inspects it. Each backend picks its own marker type: JSONL uses the byte offset to truncate to, SQLite the seq to delete from (both happen to be `number`). The JSONL backend folds its `committedBytes < buffer.byteLength` comparison INSIDE the hook so the returned marker is already `number | undefined`; without that fold the coordinator would have to know about byte lengths.
|
||||
The single design choice that keeps the seam clean: the crash-repair "where is the torn tail" token is OPAQUE to the coordinator. The coordinator computes the synthetic closers (it owns `interruptedTurnClosers` from `dsh-session`), but it only ever tests `tornMarker !== undefined` and passes the value straight back to `commitRepair` — it never inspects it. Each backend picks its own marker type: JSONL carries the byte offset to truncate to plus any complete events decoded from an incomplete final frame, while SQLite carries the seq to delete from. The coordinator therefore knows neither byte lengths nor frame recovery state.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: f1a1868cd00007fb24efb21779dcc94c098b54e2
|
||||
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: a4993de2830301610bb2a9b0d28e8bbdf0ed9c46
|
||||
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: d470cceaff68229b3872d0ade93d5fabc2e10c3f
|
||||
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: b7753fb226638b16b0f244b681cd2b9bcc9f25c2
|
||||
|
||||
@@ -16,9 +16,9 @@ Successful calls are not the only pressure signal. A provider can reject a reque
|
||||
|
||||
`agent/pre-step` is narrowed to `(agent, turn, step, signal)`. It remains a generic serial checkpoint before `step/start`, but it carries no compaction-only prompt or prefix fields.
|
||||
|
||||
The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A listener failure is an ordinary turn failure; it never enters model-request recovery.
|
||||
The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A propagated listener failure is an ordinary turn failure; it never enters model-request recovery. Compact-basic contains its expected operational failures as described below.
|
||||
|
||||
`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue with full history.
|
||||
`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue from the latest durable surface: full history before any replacement, or the pruned surface if pruning already landed.
|
||||
|
||||
### Request recovery is limited to the final model boundary
|
||||
|
||||
@@ -32,17 +32,17 @@ If cancellation lands after assistant tool calls are durable but before all call
|
||||
|
||||
`CompactService.compactIfNeeded(agent, trigger, signal)` accepts `trigger: 'pressure' | 'context-overflow'`. The interface gains no estimation methods or token types; `ctx.tokenMeter` remains the reusable accounting owner.
|
||||
|
||||
For `pressure`, compact-basic applies the service-wide threshold and retained-tail policy to one unified `ctx.tokenMeter.measure()` result. The same singleton meter owns range pricing, provenance, shadowed token counts, and non-shrinking-summary rejection. The common defaults remain threshold ratio `0.8`, retained history `floor(contextWindow × 0.16)`, summarization provider/model `''`, `maxTokens: 8192`, `compactionRetries: 1`, and `auto: true`.
|
||||
For `pressure`, compact-basic applies the service-wide threshold and retained-tail policy to one unified `ctx.tokenMeter.measure()` result. Below pressure it returns without pruning. Once pressure qualifies, optional `ctx.toolResultPrune` rewrites oversized current results and compact-basic remeasures through the same meter; safe pressure skips the model call, while remaining pressure selects and summarizes from the pruned surface. The same singleton meter owns range pricing, provenance, shadowed token counts, and non-shrinking-summary rejection. The common defaults remain threshold ratio `0.8`, retained history `floor(contextWindow × 0.16)`, summarization provider/model `''`, `maxTokens: 8192`, `compactionRetries: 1`, and `auto: true`.
|
||||
|
||||
For canonical overflow, compact-basic bypasses scalar pressure and the normal retained-token budget. It chooses the maximal tool-balanced head range while leaving the newest indivisible unit, then attempts exactly one shrinking compaction under the same signal. The automatic listener snapshots `session.surface.replaceGeneration` and returns `{ action: 'retry' }` only when compaction succeeds and the generation increases. A backend returning a result without replacement cannot authorize retry.
|
||||
For canonical overflow, compact-basic bypasses scalar pressure and the normal retained-token budget. It prunes first, then chooses the maximal tool-balanced head range while leaving the newest indivisible unit and attempts one shrinking summary compaction under the same signal when a range exists. The automatic listener snapshots `session.surface.replaceGeneration` and returns `{ action: 'retry' }` whenever pruning or summarization increases it. This remains true when pruning lands before later summary work throws; cancellation still wins. A backend returning a result without replacement cannot authorize retry, while pruning-only progress can authorize a retry without a `CompactionResult`.
|
||||
|
||||
`maxOverflowRetries` is optional and defaults to `1`; `0` disables overflow recovery without disabling pressure. `auto: false` registers neither automatic listener. Noncanonical errors, exhausted attempts, an already-aborted signal, a missing routed model, no safe range, no generation change, and recovery throws all delegate to the next listener. With no later recovery, the loop reports the original provider error object and code. Cancellation or disposal remains authoritative even if recovery work completes concurrently.
|
||||
`maxOverflowRetries` is optional and defaults to `1`; `0` disables overflow recovery without disabling pressure. `auto: false` registers neither automatic listener. Noncanonical errors, exhausted attempts, an already-aborted signal, a missing routed model, no safe range, no generation change, and recovery throws before any replacement all delegate to the next listener. With no later recovery, the loop reports the original provider error object and code. A recovery throw after generation advances authorizes retry from durable progress; cancellation or disposal remains authoritative even if recovery work completes concurrently.
|
||||
|
||||
The default summarizer resolves explicit configuration, then the latest logged route, then agent options. Because direct `llm/stream` middleware may reroute that auxiliary call, `compact/summary.{provider, model}` records the final mutable `GenerateOptions` target observed after dispatch rather than the pre-waterfall candidate.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests cover final-adapter failure provenance and identity, closed-step retry numbering and reset, cancellation and disposal, post-step ordering, routed-envelope pressure, balanced overflow reduction, generation proof, caps, delegation, and auxiliary-call routing. Real-loop tests cover thrown and in-band overflow through compaction to a reconstructed retry request.
|
||||
Unit tests cover final-adapter failure provenance and identity, closed-step retry numbering and reset, cancellation and disposal, post-step ordering, routed-envelope pressure, pressure-gated pruning, pruning-only relief, pruned-input summarization, balanced overflow reduction, durable prune progress before later failure, generation proof, caps, delegation, and auxiliary-call routing. Real-loop tests cover thrown and in-band overflow through pruning or summary compaction to a reconstructed retry request.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -54,8 +54,8 @@ Unit tests cover final-adapter failure provenance and identity, closed-step retr
|
||||
|
||||
## Consequences
|
||||
|
||||
Post-step pressure describes the completed routed request, including durable tool results and request-only prefix fields. Canonical overflow supplies the backstop when no successful usage anchor exists. Recovery is bounded, cancellation-owned, and monotonic: it retries only after a visible surface generation change.
|
||||
Post-step pressure describes the completed routed request, including durable tool results and request-only prefix fields. Optional model-free pruning removes predictable tool-output bulk before summary selection and can independently create retry-worthy progress. Canonical overflow supplies the backstop when no successful usage anchor exists. Recovery is bounded, cancellation-owned, and monotonic: it retries only after a visible surface generation change.
|
||||
|
||||
The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window or split one indivisible oversized message/tool unit.
|
||||
The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder remains oversized. The optional pruner can repair an otherwise indivisible tool pair when removable text-bearing tool-result content is the bulk.
|
||||
|
||||
This Agent Note supersedes only the pre-step automatic-trigger portion of the [compaction capability-seam Agent Note](../feature/2026-06-18-compaction-capability-seam.md). The service split, standalone token meter, balanced range contract, log-recorded lock, summary replacement, and sole `summarize()` subclass hook remain unchanged.
|
||||
|
||||
@@ -16,9 +16,9 @@ Status: implemented
|
||||
|
||||
`agent/pre-step` 收窄为 `(agent, turn, step, signal)`。它仍是 `step/start` 之前的通用串行检查点,但不再携带压缩专用的提示词或前缀字段。
|
||||
|
||||
循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。监听器失败属于普通 turn 失败,绝不会进入模型请求恢复。
|
||||
循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。向外传播的监听器失败属于普通 turn 失败,绝不会进入模型请求恢复;compact-basic 会按下文所述在内部处理其预期的操作性失败。
|
||||
|
||||
`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并继续使用完整历史。
|
||||
`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并从最新持久表层继续:任何替换发生前使用完整历史;若剪枝已经落盘,则使用已剪枝表层。
|
||||
|
||||
### 请求恢复只覆盖最终模型边界
|
||||
|
||||
@@ -32,17 +32,17 @@ Status: implemented
|
||||
|
||||
`CompactService.compactIfNeeded(agent, trigger, signal)` 接收 `trigger: 'pressure' | 'context-overflow'`。接口不增加估算方法或 token 类型;`ctx.tokenMeter` 继续作为可复用的核算所有者。
|
||||
|
||||
对于 `pressure`,compact-basic 把服务级阈值与保留尾部策略应用到一次统一的 `ctx.tokenMeter.measure()` 结果。范围定价、来源、被遮蔽 token 数与非缩小摘要拒绝也由同一个单例 meter 完成。通用默认值保持为阈值比例 `0.8`、保留历史 `floor(contextWindow × 0.16)`、摘要提供方/模型 `''`、`maxTokens: 8192`、`compactionRetries: 1` 与 `auto: true`。
|
||||
对于 `pressure`,compact-basic 把服务级阈值与保留尾部策略应用到一次统一的 `ctx.tokenMeter.measure()` 结果。低于压力时直接返回,不执行剪枝。压力达到条件后,可选的 `ctx.toolResultPrune` 会改写当前表层中过大的工具结果,compact-basic 再通过同一个 meter 重新计量;若压力恢复安全则跳过模型调用,否则从已剪枝表层选择范围并生成摘要。范围定价、来源、被遮蔽 token 数与非缩小摘要拒绝也由同一个单例 meter 完成。通用默认值保持为阈值比例 `0.8`、保留历史 `floor(contextWindow × 0.16)`、摘要提供方/模型 `''`、`maxTokens: 8192`、`compactionRetries: 1` 与 `auto: true`。
|
||||
|
||||
对于规范化溢出,compact-basic 绕过标量压力与普通保留 token 预算。它在保留最新不可分割单元的同时,选择最大的工具配对平衡头部范围,并在同一 signal 下只尝试一次缩小压缩。自动监听器先记录 `session.surface.replaceGeneration`,只有压缩成功且 generation 增加时才返回 `{ action: 'retry' }`。后端若只返回结果但没有替换表层,不能授权重试。
|
||||
对于规范化溢出,compact-basic 绕过标量压力与普通保留 token 预算。它先执行剪枝,再在保留最新不可分割单元的同时选择最大的工具配对平衡头部范围;存在范围时,才在同一 signal 下尝试一次缩小摘要压缩。自动监听器先记录 `session.surface.replaceGeneration`,剪枝或摘要让 generation 增加时就返回 `{ action: 'retry' }`。即使剪枝先落盘而后续摘要工作抛错,这条规则仍然成立;取消依然优先。后端若只返回结果但没有替换表层,不能授权重试;只有剪枝取得进展时,即使没有 `CompactionResult` 也可以授权重试。
|
||||
|
||||
`maxOverflowRetries` 可选且默认为 `1`;`0` 只禁用溢出恢复,不会禁用压力检查。`auto: false` 不注册任何自动监听器。非规范化错误、尝试耗尽、已经中止的 signal、缺失路由模型、没有安全范围、generation 未变化,以及恢复抛错都会委托给下一个监听器。若没有后续恢复,循环报告原始提供方错误对象与代码。即使恢复工作并发完成,取消或销毁仍具有最终优先级。
|
||||
`maxOverflowRetries` 可选且默认为 `1`;`0` 只禁用溢出恢复,不会禁用压力检查。`auto: false` 不注册任何自动监听器。非规范化错误、尝试耗尽、已经中止的 signal、缺失路由模型、没有安全范围、generation 未变化,以及在任何替换之前恢复抛错,都会委托给下一个监听器。若没有后续恢复,循环报告原始提供方错误对象与代码。generation 增加后的恢复抛错会基于持久进展授权重试;即使恢复工作并发完成,取消或销毁仍具有最终优先级。
|
||||
|
||||
默认摘要器依次解析显式配置、最近记录的路由与 agent options。因为直接 `llm/stream` 中间件可以重新路由该辅助调用,`compact/summary.{provider, model}` 记录分发后最终可变的 `GenerateOptions` 目标,而不是 waterfall 之前的候选值。
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试覆盖最终适配器失败的来源与身份、已关闭 step 的重试编号与重置、取消与销毁、post-step 顺序、已路由信封压力、平衡溢出缩减、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出,并验证压缩后的重试请求从替换表层重建。
|
||||
单元测试覆盖最终适配器失败的来源与身份、已关闭 step 的重试编号与重置、取消与销毁、post-step 顺序、已路由信封压力、压力门控剪枝、剪枝独立解除压力、从已剪枝输入生成摘要、平衡溢出缩减、后续失败前已落盘的剪枝进展、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出,并验证剪枝或摘要压缩后的重试请求从替换表层重建。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -54,8 +54,8 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
Post-step 压力描述已完成的路由请求,包括持久工具结果与仅请求前缀字段。当成功 usage 锚点不存在时,规范化溢出提供兜底路径。恢复有明确上限、以取消为准,并保持单调:只有模型可见的表层 generation 变化后才重试。
|
||||
Post-step 压力描述已完成的路由请求,包括持久工具结果与仅请求前缀字段。可选的无模型剪枝会在选择摘要前移除可预测的工具输出体积,也能独立产生足以重试的进展。当成功 usage 锚点不存在时,规范化溢出提供兜底路径。恢复有明确上限、以取消为准,并保持单调:只有模型可见的表层 generation 变化后才重试。
|
||||
|
||||
代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分单个不可分割的超大消息或工具单元。
|
||||
代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分不可分割的非工具节点,或修复非可剪枝剩余部分仍然过大的工具单元。若可移除的文本工具结果是主要体积,可选剪枝器仍可修复原本不可分割的工具配对。
|
||||
|
||||
本 Agent Note 只取代[压缩能力接缝 Agent Note](../feature/2026-06-18-compaction-capability-seam.md) 中的 pre-step 自动触发部分。服务拆分、独立 token meter、平衡范围契约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-zstandard-jsonl-session-logs.md: 09d30594fe31eed138a128dabc1947b15857808d
|
||||
2026-07-19-zstandard-jsonl-session-logs.zh.md: 131531d9dba7cb01407191bf937f8b0ee3c6860a
|
||||
@@ -0,0 +1,57 @@
|
||||
# Agent Note: Zstandard JSONL session logs
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-19-zstandard-jsonl-session-logs.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The JSONL persistence backend keeps every `SessionEvent` verbatim, including high-volume `assistant/chunk` records. Raw text makes logs inspectable but spends storage and I/O on repeated JSON keys and model text. Compression must retain the existing append/fsync commit boundary, collision-safe first materialization, crash repair, and metadata-only listing; rewriting a whole compressed file after every turn would discard those properties.
|
||||
|
||||
The encoding also has to remain explicit at the deployment boundary. Snapshot fixtures and external line readers require raw JSONL, while a backend cannot safely guess between compressed and raw artifacts in one root or silently migrate pre-release session data.
|
||||
|
||||
## Decision
|
||||
|
||||
### Configuration and suffix ownership
|
||||
|
||||
`dsh-session-persistence-jsonl` accepts `compression?: 'zstd' | 'none'` and explicitly resolves omission to `'zstd'`. Zstandard artifacts end in `.jsonl.zstd`; `'none'` retains the original newline-delimited UTF-8 `.jsonl` representation. `SessionLocation.kind` remains `'jsonl'`, because both encodings carry the same logical record format, and `SESSION_FORMAT_VERSION` remains `0` under the repository's pre-release reject-without-migration policy.
|
||||
|
||||
Each persistence root belongs to one encoding. A one-time discovery preflight rejects any opposite suffix, and targeted load, live-adoption, listing, and materialization paths repeat the relevant suffix check after an initially empty preflight. The error names the incompatible artifact and directs the deployment to the matching configuration or a separate root. There is no migration, dual read, dual write, or extension-based fallback.
|
||||
|
||||
### Frame and write path
|
||||
|
||||
The compressed artifact is a standard concatenation of independent [Zstandard frames](https://datatracker.ietf.org/doc/html/rfc8878): one checksummed frame containing exactly the header line, followed by one checksummed frame for every durable append batch. Normal loop batches are turn commits, so frame boundaries preserve the existing persistence checkpoint without making the storage layer depend on turn event types.
|
||||
|
||||
Compression uses Node's built-in [`zstdCompress` and `zstdDecompress`](https://nodejs.org/download/release/v22.19.0/docs/api/zlib.html), available at the repository's Node 22.19 floor. The backend enables `ZSTD_c_checksumFlag`, otherwise accepts Node's defaults, and exposes neither a compression-level knob nor a new dependency. The API is marked experimental by Node, so the Node 22.19, 24, and 26 compatibility gate exercises the exact helper.
|
||||
|
||||
First materialization compresses the two initial frames before opening the temporary file, then keeps the existing write, file `fsync`, collision-safe hard-link publication, and directory `fsync` sequence. Later batches are compressed before opening the destination and appended at EOF. A caught write or file-sync failure truncates to the prior byte length, syncs the rollback, and rethrows so the coordinator can retry the unchanged batch.
|
||||
|
||||
### Read, listing, and crash recovery
|
||||
|
||||
A frame-boundary scanner reads the standard magic, variable header fields, block headers and payload sizes, and optional checksum trailer. It does not interpret compressed blocks. Complete frames are decompressed independently and sequentially, which validates their checksums, and their plaintext is passed to the existing JSONL scanner. A checksum/decompression failure in any complete frame, a malformed complete-frame JSONL tail, or invalid frame structure is corruption and rejects.
|
||||
|
||||
Listing reads in bounded chunks only until the first complete frame is available, validates and decompresses that header frame, and never reads an event frame. The dedicated header frame therefore preserves metadata-only listing even for very large session logs.
|
||||
|
||||
EOF inside the final frame is a recoverable torn tail. Node's decoder is given the available frame prefix; every complete newline-terminated event it emits is retained. Repair truncates from that frame's starting byte and appends one new checksummed frame containing the recovered complete events followed by the coordinator's synthetic tool, step, and turn closers. If the tear occurs before any complete event is decodable, repair drops the partial frame and retains all prior complete frames.
|
||||
|
||||
### Consumers and verification
|
||||
|
||||
The CLI, ACP, and stdio app bundles expose symmetric `persistenceCompression` pass-through configuration. Snapshot recording and replay compositions select `'none'` explicitly because committed fixtures are raw JSONL inputs to replay and normalization; ordinary runtime compositions use the compressed default.
|
||||
|
||||
The shared persistence and coordinator contracts run against both encodings. Backend tests cover standard framing and checksum interoperability, header-only listing, append rollback, encoding mismatch rejection, complete-frame corruption, and final-frame tears through headers, blocks, and checksum trailers. Default runtime, built-bin, headless, ACP, and Python smokes assert the compressed suffix and Zstandard magic or decode the header; raw-content tests opt out explicitly.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **One frame per JSONL record** — rejected because it multiplies frame headers and checksums for high-volume chunk events and makes a physical boundary unrelated to the durable append batch.
|
||||
- **Rewrite one whole compressed stream after every append** — rejected because cost grows with log size and replacement would give up append/fsync rollback and the established collision-safe materialization mechanics.
|
||||
- **Use a streaming compressor across appends** — rejected because an interrupted encoder state does not leave independently checksummed append units, complicating bounded listing and frame-start repair.
|
||||
- **Add an external native Zstandard dependency** — rejected because the supported Node floor already provides the required codec; another native artifact would enlarge installation and executable-packaging risk without adding a required behavior.
|
||||
- **Expose compression level or keep raw JSONL as the default** — rejected because there is no deployment evidence for a second tuning policy, while `'none'` preserves the line-readable path for fixtures and integrations that need it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Ordinary session roots store `.jsonl.zstd` and retain append-only, fsync, rollback, and interrupted-turn recovery semantics.
|
||||
- Raw JSONL remains a deliberate configuration, but changing encoding requires a fresh/separate root or selecting the mode that matches existing artifacts.
|
||||
- One frame per durable batch adds bounded framing/checksum overhead and allows header-only listing plus repair from an exact append boundary.
|
||||
- External tools must understand concatenated Zstandard frames or consume raw-mode artifacts; generic one-shot Node decompression reads only the first independent frame, so backend reads walk frames explicitly.
|
||||
- The implementation depends on Node's experimental built-in Zstandard API without an npm dependency; the supported-version compatibility gate makes drift visible.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Agent Note: Zstandard JSONL 会话日志
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-19-zstandard-jsonl-session-logs.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
JSONL 持久化后端会逐字保留每个 `SessionEvent`,其中包括数量庞大的 `assistant/chunk` 记录。原始文本便于检查,但重复的 JSON 键和模型文本会增加存储与 I/O 开销。压缩编码必须保留既有的 append/fsync 提交边界、首次物化时的无冲突发布、崩溃修复以及仅元数据列举;如果每轮都重写整个压缩文件,就会失去这些属性。
|
||||
|
||||
编码还必须在部署边界上保持显式。快照 fixture 与外部逐行读取器需要原始 JSONL,而后端无法在同一根目录中安全猜测压缩产物与原始产物,也不能静默迁移预发布会话数据。
|
||||
|
||||
## 决策
|
||||
|
||||
### 配置与后缀归属
|
||||
|
||||
`dsh-session-persistence-jsonl` 接受 `compression?: 'zstd' | 'none'`,并将省略值显式解析为 `'zstd'`。Zstandard 产物使用 `.jsonl.zstd` 后缀;`'none'` 保留原有的换行分隔 UTF-8 `.jsonl` 表示。`SessionLocation.kind` 仍为 `'jsonl'`,因为两种编码承载同一逻辑记录格式;按照仓库的预发布拒绝且不迁移策略,`SESSION_FORMAT_VERSION` 仍为 `0`。
|
||||
|
||||
每个持久化根目录只归属于一种编码。一次性的发现预检会拒绝任何相反后缀,而针对性的加载、活跃采用、列举与物化路径会在最初空目录预检之后再次执行对应后缀检查。错误会指出不兼容产物,并要求部署选择匹配配置或单独根目录。系统不提供迁移、双重读取、双重写入或基于扩展名的兜底。
|
||||
|
||||
### 帧与写入路径
|
||||
|
||||
压缩产物是标准独立 [Zstandard 帧](https://datatracker.ietf.org/doc/html/rfc8878)的串联:第一个带校验和的帧只包含头部行,后续每个持久追加批次各占一个带校验和的帧。正常 agent loop 批次就是轮次提交,因此帧边界保留既有持久化检查点,同时不让存储层依赖轮次事件类型。
|
||||
|
||||
压缩使用 Node 内置的 [`zstdCompress` 与 `zstdDecompress`](https://nodejs.org/download/release/v22.19.0/docs/api/zlib.html),仓库最低支持的 Node 22.19 已提供这些 API。后端启用 `ZSTD_c_checksumFlag`,其余采用 Node 默认值,不公开压缩级别调节项,也不增加依赖。Node 将该 API 标记为实验性,因此 Node 22.19、24 与 26 兼容性门禁会执行同一个辅助实现。
|
||||
|
||||
首次物化会在打开临时文件之前压缩两个初始帧,然后保留既有的写入、文件 `fsync`、避免冲突的硬链接发布与目录 `fsync` 顺序。后续批次也会先压缩,再打开目标并在 EOF 追加。捕获到写入或文件同步失败时,后端会截断到原有字节长度,同步回滚结果,再重新抛出错误,让协调器重试未变化的批次。
|
||||
|
||||
### 读取、列举与崩溃恢复
|
||||
|
||||
帧边界扫描器会读取标准魔数、可变头字段、块头与负载长度,以及可选校验和尾部,但不会解释压缩块。后端独立且按顺序解压完整帧,由此验证各帧校验和,再把明文交给既有 JSONL 扫描器。任何完整帧的校验和或解压失败、完整帧中畸形的 JSONL 尾部,或者无效帧结构都属于损坏并拒绝加载。
|
||||
|
||||
列举只按有界分片读取到第一个完整帧可用为止,验证并解压该头部帧,绝不读取事件帧。因此,即使会话日志很大,专用头部帧仍能维持仅元数据列举。
|
||||
|
||||
最终帧内部遇到 EOF 属于可恢复的撕裂尾部。后端把已有帧前缀交给 Node 解码器,并保留其产出的每个完整、以换行结束的事件。修复从该帧起始字节截断,再追加一个新的带校验和帧,其中依次包含恢复出的完整事件,以及协调器生成的工具、步骤与轮次闭合事件。如果撕裂位置尚不足以解码任何完整事件,修复会丢弃该不完整帧并保留此前全部完整帧。
|
||||
|
||||
### 消费方与验证
|
||||
|
||||
CLI、ACP 与 stdio 应用包公开对称的 `persistenceCompression` 透传配置。快照录制与回放组合显式选择 `'none'`,因为提交的 fixture 是回放与规范化过程使用的原始 JSONL 输入;普通运行时组合使用压缩默认值。
|
||||
|
||||
共享持久化契约与协调器契约会针对两种编码运行。后端测试覆盖标准帧与校验和互操作性、仅头部列举、追加回滚、编码不匹配拒绝、完整帧损坏,以及横跨头部、块和校验和尾部的最终帧撕裂。默认运行时、构建后二进制、headless、ACP 与 Python 冒烟测试会断言压缩后缀与 Zstandard 魔数,或解码头部;读取原始内容的测试则显式退出压缩。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **每条 JSONL 记录一个帧**——不予采纳,因为它会让大量分片事件各自承担帧头与校验和开销,并让物理边界脱离持久追加批次。
|
||||
- **每次追加都重写一个完整压缩流**——不予采纳,因为成本会随日志大小增长,而且替换操作会放弃追加/fsync 回滚和既有的无冲突物化机制。
|
||||
- **跨追加使用流式压缩器**——不予采纳,因为编码器状态中断后不会留下可独立校验的追加单元,从而使有界列举与按帧起点修复更复杂。
|
||||
- **增加外部原生 Zstandard 依赖**——不予采纳,因为受支持的 Node 最低版本已经提供所需编解码器;另一个原生产物会增加安装与可执行文件打包风险,却不增加必需行为。
|
||||
- **公开压缩级别或继续默认使用原始 JSONL**——不予采纳,因为没有部署证据支持第二种调节策略,而 `'none'` 已为需要逐行读取的 fixture 与集成保留路径。
|
||||
|
||||
## 后果
|
||||
|
||||
- 普通会话根目录存储 `.jsonl.zstd`,并保留仅追加、fsync、回滚与中断轮次恢复语义。
|
||||
- 原始 JSONL 仍是显式配置,但切换编码需要使用全新或单独根目录,或者选择与既有产物匹配的模式。
|
||||
- 每个持久批次一个帧会增加有界的帧与校验和开销,同时支持仅头部列举和从精确追加边界开始修复。
|
||||
- 外部工具必须理解串联的 Zstandard 帧,或者消费原始模式产物;Node 通用的一次性解压只读取第一个独立帧,因此后端读取会显式遍历各帧。
|
||||
- 实现依赖 Node 的实验性内置 Zstandard API,但不增加 NPM 依赖;受支持版本兼容性门禁会暴露 API 漂移。
|
||||
@@ -18,7 +18,8 @@ Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seam
|
||||
|
||||
1. **Interface** — `@deepseek-ai/dsh-compact`: an abstract `CompactService` owning the `ctx.compact` key, the `CompactionResult` vocabulary, and the `compact/*` session events. It declares `compactIfNeeded()` and `compactRegion()` as **abstract** — the contract states *what* compaction does, not *how*.
|
||||
2. **Implementation** — `@deepseek-ai/dsh-compact-basic`: a concrete `BasicCompactService` that consumes `ctx.tokenMeter` and owns the tail→head retention walk, summarization via `ctx.llm.stream()`, the surface replacement, the lock, post-step pressure, and canonical context-overflow recovery. `summarize()` is its sole subclass hook; pricing and replay stay with the meter.
|
||||
3. **Consumer** — deferred. A `/compact` tool and slash command will `inject: ['compact']` and call the contract; they are intentionally out of scope here so the seam settles first.
|
||||
3. **Model-free companion** — `@deepseek-ai/dsh-compact-tool-result-prune`: a concrete optional service that rewrites oversized current `tool/result` nodes before the backend selects a summary range. It is not a second compaction implementation and does not implement `CompactService`.
|
||||
4. **Consumer** — deferred. A `/compact` tool and slash command will `inject: ['compact']` and call the contract; they are intentionally out of scope here so the seam settles first.
|
||||
|
||||
### The contract depends on `dsh-session` and `dsh-llm` — a deliberate deviation
|
||||
|
||||
@@ -34,9 +35,9 @@ An earlier draft put the full algorithm (the retention walk, token-summing, text
|
||||
|
||||
### Automatic pressure runs after successful durable step work
|
||||
|
||||
Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override.
|
||||
Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, optional `ctx.toolResultPrune` rewriting runs before summary selection; compact-basic remeasures the durable surface and skips summarization if pruning restores safe pressure.
|
||||
|
||||
Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic forces one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases; the loop then opens a new numbered step and reconstructs its request from the durable log. No range, no replacement, recovery failure, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
|
||||
Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic prunes before forcing one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists; the loop then opens a new numbered step and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
|
||||
|
||||
```
|
||||
assistant/message → tool/result/context/steering
|
||||
@@ -56,7 +57,7 @@ Auto-compaction checks after **every successful** step, not once per turn. This
|
||||
|
||||
A runaway turn thus compacts exactly like any other history: its early *closed* steps get summarized while its recent steps stay verbatim. When the only compactable content left is an un-splittable open tail step (its tool-calls have no results yet), compaction declines (`null`) and retries once that step closes.
|
||||
|
||||
**Single-unit overflow is out of scope, by design.** If a single retained unit — one closed step, or a large free entry such as a pasted `user/message` — *alone* exceeds the budget, compaction cannot help and the next model call may go out over-budget. Bounding an individual unit's size is a separate concern (output truncation), handled elsewhere; compaction makes no promise about it, and the harness without such a mechanism can still break on a single oversized unit. This is named honestly rather than papered over.
|
||||
**Some single-unit overflow remains out of scope.** Summary range selection cannot split an indivisible unit. The optional pruner can repair a closed tool pair when removable text-bearing tool-result content is the bulk and the pruned remainder fits. Envelope-only pressure, an oversized indivisible non-tool node such as a pasted `user/message`, and a tool unit whose non-prunable remainder is still oversized remain outside compaction; bounding those units is a separate concern.
|
||||
|
||||
### Head-anchoring: one auto checkpoint, always at the head
|
||||
|
||||
@@ -94,8 +95,8 @@ The `compact/start … compact/end` bracket is justified, in order of what now d
|
||||
|
||||
Two failure paths, both documented:
|
||||
|
||||
- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — the surface replacement never landed, so the full, uncompacted history derives correctly. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction.
|
||||
- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and leaves the surface untouched. Post-step pressure warns and continues; overflow recovery delegates so the original provider error remains authoritative.
|
||||
- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — no summary replacement lands. The derived surface remains the durable surface present at `compact/start`: full history when pruning made no replacement, or the already-pruned history when it did. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction.
|
||||
- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and lands no summary replacement. Post-step pressure warns and continues from the latest durable surface — full history if no replacement preceded the attempt, or the pruned surface if pruning already landed. Overflow recovery delegates only before any replacement; generation progress from earlier pruning authorizes a retry from that durable surface unless cancellation or disposal wins.
|
||||
|
||||
`compact/end` keeps its `error?` field (mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling). There is no separate `compact/error` event.
|
||||
|
||||
@@ -110,16 +111,16 @@ Two failure paths, both documented:
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Packages**: `packages/compact/compact` supplies the interface and `compact-basic` supplies the backend. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred.
|
||||
- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, and `compact-tool-result-prune` supplies optional deterministic rewriting. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred.
|
||||
- **Automatic seams**: `agent/post-step` (`@mode serial`) handles successful-call pressure and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. Generic `agent/pre-step` remains a four-argument checkpoint with no compaction-only prompt/prefix payload.
|
||||
- **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry.
|
||||
- **`dsh-compact`** owns `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)`, the cached surface-edge checks that `compactRegion` and `compactIfNeeded` use to avoid splitting a tool-call/result pair. The cache validates current membership by seq and answers both edges from one per-cut balance sequence; stale or missing seqs and orphan results reject. `dsh-session` continues to own the surface `replace` operation, ordered event sequences, and rewrite generation.
|
||||
- **`dsh-invariants`** drops its `surface replace: start must be <= end` assertion: a head-anchored compaction lands a high-seq replacement entry at an older range's *position*, so `start > end` numerically is normal and valid (the range is positional, validated by the surface's `indexOf` checks that remain). The turn-enclosure invariant is reused unchanged.
|
||||
- **Wiring**: `examples/repl-agent/cordis.yml` loads zero-config `dsh-token-meter` before `dsh-compact-basic`; the service-wide window and compact defaults make the pair usable without repeated numeric policy.
|
||||
- **`dsh-compact`** owns `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)`, the cached surface-edge checks that `compactRegion` and `compactIfNeeded` use to avoid splitting a tool-call/result pair. The cache validates current membership by seq and answers both edges from one per-cut balance sequence; stale or missing seqs and orphan results reject.
|
||||
- **`dsh-session`** validates positional replacement, complete provenance, and content-only single-node `tool/result` rewrites through its one surface manager. `dsh-invariants` treats fresh appended tool results as executions that require an open step and pending call; validated replacements remain turn-enclosed rewrites.
|
||||
- **Wiring**: `examples/repl-agent/cordis.yml` loads zero-config `dsh-token-meter`, `dsh-compact-tool-result-prune`, then `dsh-compact-basic`; service-wide defaults make the composition usable without repeated numeric policy.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit:** Real Loader and invariant plugins cover whole-unit retention, convergence failure, both `compact/end` outcomes, head anchoring, open-tail refusal, inert crash orphans, forced below-threshold overflow, generation proof, caps, and original-error preservation.
|
||||
- **Unit:** Real Loader and invariant plugins cover whole-unit retention, pruning configuration and replay, rich-block ordering, metadata preservation, convergence, both `compact/end` outcomes, open-tail refusal, pruning-only and summarized overflow recovery, generation proof, caps, and original-error preservation.
|
||||
- **Loop:** Tests pin post-step after durable tool results and before `step/end`, actual `agent/request` routing, closed failed steps, fresh retry numbering, and complete thrown/in-band overflow → compaction → reconstructed retry composition.
|
||||
- **With-key e2e:** A real model and bash session with lowered limits triggers compaction, records a complete `compact/start…end` pair, shrinks the surface, and finishes the task.
|
||||
- **Snapshot gap:** Runaway-turn compaction cannot yet replay because the summarization call records no `assistant/chunk` events or `sessionId`; interleaved summarization-call replay remains follow-up work.
|
||||
|
||||
@@ -14,7 +14,7 @@ The canonical surface separates transformable policy, around-dispatch control, a
|
||||
|
||||
**Agent events** (`dsh-agent`):
|
||||
- `agent/session-start(agent, source)` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`.
|
||||
- `agent/prompt-submit(agent, content, source, next) → PromptDecision` — waterfall, fired per drained queued message inside the open turn, before the `user/message` append. `allow` (optionally rewriting the prompt `content` or attaching separately sourced `additionalContexts[]`) or `block` (dropping the prompt; the loop appends a durable `prompt/blocked` in its place — see the dispatch note below).
|
||||
- `agent/prompt-submit(agent, content, source, next) → PromptDecision` — waterfall, fired for the turn's single claimed queued message before the `user/message` append. `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`; `block` appends a durable `prompt/blocked` and rejects that zero-step turn.
|
||||
|
||||
**`agent/turn-continuation`** receives and returns a `ContinuationDecision`. A `{action:'continue', reason?}` may carry model-facing content and source recorded as next-step steering in the same turn — the typed twin of the `/goal` step-end-steer pattern. It is not a `context/message`, so its type does not offer durable context metadata.
|
||||
|
||||
@@ -30,11 +30,11 @@ Every call follows `tools/pre-execute` → guards → `tools/execute` → dispat
|
||||
|
||||
Core dispatch and the tool body sit inside normalization boundaries, so tool, listener, malformed-result, non-JSON result, and identity-shape failures resolve as JSON-safe `isError` results rather than escaping the turn. A post-execute listener can therefore inspect a thrown tool, and a final observer sees exactly what the caller receives and the session log can persist.
|
||||
|
||||
**`TurnEndReason.rejected`** (`dsh-session`): a turn whose entire prompt batch was blocked by `prompt-submit`.
|
||||
**`TurnEndReason.rejected`** (`dsh-session`): a zero-step turn whose claimed prompt was blocked by `prompt-submit`.
|
||||
|
||||
### Three load-bearing loop decisions
|
||||
|
||||
1. **Open the turn before prompt policy.** A fully blocked batch becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. Every veto also records `prompt/blocked` with the original prompt and reason, so mixed batches retain blocked inputs. Every allowed `additionalContexts` entry is injected into the open turn.
|
||||
1. **Open the turn before prompt policy.** A blocked prompt becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. The veto records `prompt/blocked` with the original prompt and reason, while every allowed `additionalContexts` entry is injected into the open turn. Each claimed ordinary-send item is the sole message in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md); a pre-start drop creates no turn.
|
||||
|
||||
2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate `context/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision.
|
||||
|
||||
|
||||
@@ -49,9 +49,11 @@ The global registry remains live. A deny-only filter admits a later global name
|
||||
|
||||
The depth limit bounds recursive delegation independently of tool visibility. A top-level agent has depth zero; an in-process child has its parent's validated depth plus one. `maxDepth` is an absolute non-negative safe integer, and a start rejects before child ownership begins when the derived child depth is greater than the cap.
|
||||
|
||||
Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. Omitting the cap leaves depth unbounded by this mechanism.
|
||||
The effective parent depth is the greater of durable `SessionHeader.delegationDepth` and runtime `AgentOptions.subagentDepth`. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count.
|
||||
|
||||
A deployment can combine depth and filtering. For example, it may keep the delegation tool visible at depth one but set `maxDepth: 1`, or deny the delegation tool entirely in children. Neither choice changes the provider's conversation-history behavior.
|
||||
Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the [SDK helper's generated subagent entries](../../../../packages/sdk/helper/src/features/builtin/index.ts) and [JSON-RPC example](../../../../examples/jsonrpc-agent/cordis.yml) use that general policy, while the shipped interactive ACP, headless, and REPL examples pin one. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`.
|
||||
|
||||
A deployment can combine depth and filtering, but the numeric cap does not synthesize a filter. The delegation tool stays visible at the cap because authorization may depend on runtime state; every attempted start checks the calling agent's current durable and runtime depth, and a rejected start returns an errored tool result without publishing a child. A deployment may separately deny delegation tools in children when its visibility policy is static. Neither choice changes the provider's conversation-history behavior.
|
||||
|
||||
### Capability gating keeps providers honest
|
||||
|
||||
@@ -83,10 +85,10 @@ A security design would need a separate authority representation, propagation ru
|
||||
|
||||
**Hide only tool schemas.** Presentation-only filtering lets the model execute a tool that the prompt says does not exist through Code Mode or a forged call. One resolver governs both presentation and execution instead.
|
||||
|
||||
**Use only tool filtering to stop recursion.** Removing the delegation tool is useful but provider-specific and does not protect direct service callers or alternate delegation tools. Absolute depth is an independent structural bound.
|
||||
**Encode the depth cap as an automatic tool filter.** A creation-time filter snapshots a decision that may depend on runtime state, affects only one configured tool name, and does not protect direct service callers or alternate delegation tools. The provider instead enforces the absolute cap at every start.
|
||||
|
||||
## Consequences
|
||||
|
||||
Contributors can configure child role, visible global tools, and recursion without defining new providers. Capability checks fail before ownership starts, unpublished setup makes the first request consistent, and one tool resolver prevents presentation/execution drift.
|
||||
|
||||
The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation.
|
||||
The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. A model may call a visible delegation tool after the current depth policy forbids another child and receive an error. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-02-bilingual-docs-and-pairing-gate.md: 45c6edff41a7bc21c76aeeaf14d16af824c601de
|
||||
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 91ba7523705d1500150efe0eac9085ea980e80d6
|
||||
2026-07-02-bilingual-docs-and-pairing-gate.md: 3be1d5d8fd9dba20cfca34c79cb01d89fad8097a
|
||||
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: a8aa8812934e755fe0175c8f3f20d194e4d24b4a
|
||||
|
||||
@@ -13,7 +13,7 @@ This repo's README and docs tree are read by people and agents inside and outsid
|
||||
- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).
|
||||
- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.
|
||||
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical), excluded (generated or bilingual-by-construction) files stay unpaired, and date-named documents on or after the manifest's `requiredSince` cutoff have complete pairs. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows.
|
||||
- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth.
|
||||
- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. The skill directs the orchestrating agent to delegate translation writing to a subagent.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Status: implemented
|
||||
- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。
|
||||
- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。
|
||||
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:required 的配对必须存在;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)不得配对;凡文件名以日期开头且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,也必须有完整配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单只进不退:每个合并的翻译批次将自己的文件加入其中,覆盖面只增不减。
|
||||
- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。
|
||||
- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。该 skill 要求编排 agent 把翻译写作委派给 subagent。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Project canonical documentation into the website
|
||||
|
||||
Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub.
|
||||
|
||||
## Decision
|
||||
|
||||
Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths.
|
||||
|
||||
`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl.
|
||||
|
||||
`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout.
|
||||
|
||||
Locale home projections retain only the canonical YAML frontmatter. The repository-facing body can keep its H1 and bilingual source links, while the VitePress home theme owns the rendered hero and features and the site navigation owns locale switching.
|
||||
|
||||
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
|
||||
|
||||
Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception.
|
||||
|
||||
Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative.
|
||||
|
||||
**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer.
|
||||
|
||||
**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order.
|
||||
|
||||
**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments.
|
||||
|
||||
**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists.
|
||||
|
||||
## Consequences
|
||||
|
||||
Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection.
|
||||
|
||||
The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-20-generated-cordis-core-api.md: 848dec2dba6f432c706798c40abe98e8937da651
|
||||
2026-07-20-generated-cordis-core-api.zh.md: c40a480224f4e1387b71ade9264458cd84403584
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Generate the Cordis core API reference
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-20-generated-cordis-core-api.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Plugin authors need the detailed Cordis APIs behind `ctx`, event dispatch, fibers, plugin registration, and services. The generated [Harness event and service catalogs](2026-06-20-generated-cordis-catalog.md) intentionally summarize inherited Cordis members, so they do not replace a method-level Cordis reference. Keeping a second hand-written copy under the website would drift from the vendored source and make the renderer an additional documentation owner.
|
||||
|
||||
## Decision
|
||||
|
||||
`scripts/cordis-core-api.ts` reads the public declarations and original JSDoc from `vendor/cordis/src` with the TypeScript compiler API. An explicit page manifest generates five files under [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md): Context, Events, Fiber, Registry, and Service. `scripts/gen-cordis-catalog.ts` writes these pages together with the Harness event and service catalogs, and `verify-cordis-catalog` rejects stale output.
|
||||
|
||||
The generator validates that documented classes and methods retain descriptive JSDoc, including parameter and non-void return contracts. It emits declaration-only `ts cordis-catalog` fences with the original JSDoc, then renders the same description, parameters, and return contract as readable Markdown. Source links point to the vendored files, and the five pages cross-link to one another. The Harness catalogs remain the exhaustive inventory of repository-declared events and `ctx.*` services; the core pages document how the inherited Cordis APIs operate.
|
||||
|
||||
`website/docs.ts` publishes the five canonical files under matching `/reference/cordis-api/` and `/en/reference/cordis-api/` routes. Both locales use the English generated source until the generator emits translated pages, so changing language preserves navigation structure and route identity.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Restore the old website files as canonical Markdown.** This would recover the pages quickly, but their signatures and prose could drift from the vendored implementation and the website would regain a second documentation source.
|
||||
|
||||
**Expand the inherited tier of the Harness catalogs in place.** Those catalogs answer which Harness events and services exist. Mixing full framework class references into the same pages would obscure that inventory and reverse their deliberate terse inherited tier.
|
||||
|
||||
**Publish vendored source declarations directly.** Source files are authoritative but do not provide stable topic pages, curated public ordering, or website navigation, and they expose implementation bodies that are not part of the reference contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
The five Cordis API pages follow vendor updates through one deterministic generator and share the repository's documentation freshness gate. The website gains a dedicated Cordis API section without copied site content, while root and English navigation remain structurally identical.
|
||||
|
||||
The page manifest is curated, so a newly public Cordis core type needs an explicit generator entry. Generated prose is English-only, and source JSDoc quality directly limits reference quality; Chinese output requires generator-level translation rather than hand-editing the generated files.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 生成 Cordis 核心 API 参考文档
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-20-generated-cordis-core-api.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
插件作者需要了解 `ctx`、事件派发、Fiber、插件注册和 Service 背后的详细 Cordis API。已有的 [Harness 事件与服务目录](2026-06-20-generated-cordis-catalog.md)有意只简要概括继承自 Cordis 的成员,因此无法替代方法级 Cordis 参考文档。如果在网站下维护另一份手写副本,它会与 vendored 源码产生漂移,也会让渲染器成为额外的文档所有者。
|
||||
|
||||
## 决策
|
||||
|
||||
`scripts/cordis-core-api.ts` 使用 TypeScript Compiler API,从 `vendor/cordis/src` 读取公开声明和原始 JSDoc。一个显式页面清单在 [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md) 下生成五个文件:Context、Events、Fiber、Registry 和 Service。`scripts/gen-cordis-catalog.ts` 将这些页面与 Harness 事件和服务目录一同写入,`verify-cordis-catalog` 会拒绝过期产物。
|
||||
|
||||
生成器会验证所记录的类和方法保留描述性 JSDoc,包括参数和非 void 返回值契约。它生成包含原始 JSDoc 且仅含声明的 `ts cordis-catalog` 代码围栏,再将同一份说明、参数和返回值契约渲染为便于阅读的 Markdown。源码链接指向 vendored 文件,五个页面之间相互交叉链接。Harness 目录仍是仓库声明的事件与 `ctx.*` 服务的完整清单;核心页面负责说明继承自 Cordis 的 API 如何工作。
|
||||
|
||||
`website/docs.ts` 将五个规范源文件发布到结构对应的 `/reference/cordis-api/` 和 `/en/reference/cordis-api/` 路由。在生成器产出翻译页面之前,两个 locale 都使用英文生成源,因此切换语言时导航结构和路由标识保持不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将旧网站文件恢复为规范 Markdown。** 这能快速恢复页面,但其签名和说明可能与 vendored 实现漂移,网站也会重新成为第二个文档来源。
|
||||
|
||||
**直接扩充 Harness 目录中的继承层。** 这些目录回答有哪些 Harness 事件与服务。将完整的框架类参考混入同一页面会模糊这份清单的定位,并推翻继承层保持精简的既有决定。
|
||||
|
||||
**直接发布 vendored 源码声明。** 源文件具有权威性,但不能提供稳定的主题页面、经过筛选的公开顺序或网站导航,还会暴露不属于参考契约的实现体。
|
||||
|
||||
## 影响
|
||||
|
||||
五个 Cordis API 页面通过同一个确定性生成器跟随 vendor 更新,并复用仓库的文档新鲜度检查。网站无需复制内容即可获得独立的 Cordis API 章节,中文入口和英文入口的导航结构保持一致。
|
||||
|
||||
页面清单需要人工维护,因此新增公开 Cordis 核心类型时必须显式添加生成器条目。当前生成说明只有英文,且源码 JSDoc 的质量直接决定参考文档质量;中文产物需要在生成器层实现翻译,不能手工编辑生成文件。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-17-one-send-one-turn.md: 86c056b53700d0e0c02e04a99cf044fb311f5840
|
||||
2026-07-17-one-send-one-turn.zh.md: 3ef9973480481d11d1183760c9fc1f3c247629f4
|
||||
@@ -0,0 +1,45 @@
|
||||
# Agent Note: Remove implicit batching from ordinary sends
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-17-one-send-one-turn.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Suppose a caller submits message A and then message B with two `Agent.send()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work.
|
||||
|
||||
That grouping depends on timing rather than caller intent. Calls from one synchronous stack, neighboring microtasks, event listeners, and model callbacks could be grouped differently even though every caller used the same API.
|
||||
|
||||
This grouping changes behavior, not just the number of model calls. One ordinary turn owns prompt admission, `turn/start`, `turn/end`, and a durability checkpoint. If message B shares message A's turn, B can enter A's model request instead of first seeing A's closed result in the session log. Allowing one message while blocking another also requires a mixed state that no caller requested.
|
||||
|
||||
## Decision
|
||||
|
||||
The rule is simple: each successful `send()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two sends are never silently combined.
|
||||
|
||||
Before enqueueing an item, `send()` checks the agent state and makes a detached, deeply frozen snapshot of the content and resolved source. After enqueueing it, `send()` publishes `agent/queued`.
|
||||
|
||||
If messages A and B are both processed, B's turn starts only after A records `turn/end` and A's durability checkpoint settles. B's request therefore sees whatever closed result A left in the same session log. A checkpoint error is reported, but settlement only releases this ordering barrier; it does not make a failed write durable. Broad `cancel()`, disposal, or a failure before `turn/start` can instead discard an unstarted item without opening an empty turn.
|
||||
|
||||
Prompt admission decides one message at a time. An allowed prompt becomes that turn's `user/message`; a blocked prompt records one durable `prompt/blocked` and closes its one-message turn as `rejected`. Mixed-batch and all-blocked-batch branches do not exist.
|
||||
|
||||
The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in a separate steering FIFO. While a turn remains open, the loop records that input at the next steering checkpoint, which comes before either a model request or the decision whether to continue. Steering makes another step the default, but continuation or terminal policy can still stop before the step starts. Steering left after the turn closes and its durability checkpoint settles becomes later queued input; terminal `agent/turn-stop`, cancellation, or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item.
|
||||
|
||||
`inject()` continues to add model-facing context without submitting an ordinary message; its existing turn-enclosure and flush behavior stays unchanged. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary and steering input and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message. Several one-message turns can share one `running` interval, including turn close and its checkpoint, so `running` does not prove that a turn is open.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep automatic ordinary-send batching to reduce model calls.** This can improve throughput when producers outpace the driver, but it makes turn boundaries depend on scheduling and lets a later message run before the preceding turn closes and reaches its checkpoint. The decision keeps the predictable boundary and accepts the extra calls. Any future batching feature needs an explicit caller-visible contract backed by measurements.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit and property tests submit sends from the same stack, neighboring microtasks, different producers, and reentrant callbacks; every message gets its own FIFO-ordered turn.
|
||||
- A built-stdio test submits two lines and observes two model requests and two turn boundaries.
|
||||
- Delayed and rejected first-turn checkpoints keep the next turn waiting and prove that its request sees the preceding assistant result.
|
||||
- Failure-path tests cover prompt veto, listener failure, broad cancellation, disposal, and failure before `turn/start`; recorded turns stay balanced, messages do not merge, and surviving queued work still drains.
|
||||
- Separate tests cover open-turn, post-turn-close, and idle `steer()`, plus `inject()`, whole-agent status, and `whenIdle()`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion or cancellation handle; broad cancellation can discard the entire unstarted tail, while status and quiescence remain agent-wide observations.
|
||||
|
||||
The trade-off is more model requests and more checkpoints. A busy queue can take longer to drain and can grow under sustained producers. Ordinary-send batching returns only through an explicit, measured contract.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Agent Note: 删除普通 send 的隐式批处理
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-17-one-send-one-turn.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
假设调用方连续两次调用 `Agent.send()`,先提交消息 A,再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待,就把 A、B 放进同一个轮次。调用方明明调用了两次,agent loop(智能体循环)却悄悄把它们变成一个工作单元。
|
||||
|
||||
这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
|
||||
|
||||
这种分组改变的不只是模型调用次数。一个普通轮次包含提示词准入、`turn/start`、`turn/end` 和持久性检查点。如果消息 B 与消息 A 共用轮次,B 可能直接进入 A 的模型请求,而不是先看到 A 在会话日志中已经关闭的结果。若系统允许一条消息、阻止另一条消息,还需要引入调用方没有请求的混合状态。
|
||||
|
||||
## 决策
|
||||
|
||||
规则很简单:一次成功的 `send()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。
|
||||
|
||||
队列项入队之前,`send()` 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,`send()` 发布 `agent/queued`。
|
||||
|
||||
如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
|
||||
|
||||
提示词准入每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词记录一条持久的 `prompt/blocked`,并让自己的单消息轮次以 `rejected` 关闭。实现中不存在混合批次或全阻止批次分支。
|
||||
|
||||
上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入独立的 steering(中途引导)FIFO。只要当前轮次仍然打开,agent loop 就会在下一个 steering 检查点记录该输入;该检查点位于模型请求或继续轮次的决策之前。收到 steering 会把再执行一步作为默认选择,但继续轮次的策略或终止策略仍可在该步骤开始前停止。轮次关闭且其持久性检查点处理结束后,剩余的 steering 会成为后续排队输入;终止性的 `agent/turn-stop`、取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。
|
||||
|
||||
`inject()` 继续添加面向模型的上下文,而不提交普通消息;其现有的轮次封闭与持久化刷新行为保持不变。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,该区间还可能覆盖轮次关闭及其检查点,因此 `running` 不表示轮次一定处于打开状态。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留普通 send 的自动批处理,以减少模型调用。** 当消息进入队列的速度超过驱动器的处理速度时,这种做法可以提高吞吐量,但会让轮次边界取决于调度,并让后一条消息在前一轮关闭且到达检查点之前运行。本决策保留可预测的边界,并接受额外调用。未来若要加入批处理功能,必须提供调用方可见的显式契约,并有测量结果作为依据。
|
||||
|
||||
## 验证
|
||||
|
||||
- 单元测试和性质测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。
|
||||
- stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。
|
||||
- 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。
|
||||
- 失败路径测试覆盖提示词否决、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。
|
||||
- 其他测试分别覆盖轮次打开时、轮次关闭后和空闲时的 `steer()`,以及 `inject()`、面向整个 agent 的状态和 `whenIdle()`。
|
||||
|
||||
## 后果
|
||||
|
||||
普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成或取消句柄;广义取消可以丢弃整个尚未启动的队尾,状态和静止性也仍是面向整个 agent 的观察。
|
||||
|
||||
代价是模型请求和检查点都会增加。繁忙队列可能需要更长时间才能清空;如果生产方持续提交消息,队列也可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。
|
||||
@@ -40,7 +40,7 @@ Replay is positional and therefore permits only one in-flight model stream per s
|
||||
|
||||
### Recording harvests the log; keyless replay needs a providerless config
|
||||
|
||||
Recording runs the scenario with the real `llm-deepseek` adapter and the JSONL persistence backend, then copies the produced `.jsonl` into the scenario dir. Per-event appends are durable, but the harness shuts the subprocess down gracefully (close stdin → `await ctx.dispose()`) before harvesting so the final events are flushed. `llm-replay` itself does no recording — it is replay-only.
|
||||
Recording runs the scenario with the real `llm-deepseek` adapter and the JSONL persistence backend configured with `persistenceCompression: 'none'`, then copies the produced `.jsonl` into the scenario dir. The explicit raw mode keeps committed replay fixtures line-readable while ordinary deployments use the backend's compressed default. Per-event appends are durable, but the harness shuts the subprocess down gracefully (close stdin → `await ctx.dispose()`) before harvesting so the final events are flushed. `llm-replay` itself does no recording — it is replay-only.
|
||||
|
||||
Replay uses a `cordis.snapshot.yml` overlay that replaces the real adapter with `llm-replay` while retaining the live composition. Recording uses the ordinary config and a harness-supplied persistence root. Replay mode skips `.env` loading, so a stray API key cannot trigger a live call. See the [single-source config Agent Note](2026-07-04-single-source-acp-replay-config.md).
|
||||
|
||||
|
||||
82
.agents/skills/dsh-doc-site-sync/SKILL.md
Normal file
82
.agents/skills/dsh-doc-site-sync/SKILL.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: dsh-doc-site-sync
|
||||
description: Use when publishing, updating, moving, or removing DeepSeek Harness documentation website pages; editing website/docs.ts mappings or navigation; diagnosing a page missing from the VitePress site; fixing projected documentation links; or running the docs:dev, docs:check, and doc-sync workflow after website-content changes.
|
||||
---
|
||||
|
||||
# Synchronizing the DeepSeek Harness Documentation Site
|
||||
|
||||
Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree.
|
||||
|
||||
Repository translations follow the sibling pairing contract: English `foo.md`, Chinese `foo.zh.md`, and `foo.i18n.yaml` live together. Never create `zh-CN/` or other locale directories for website content. The site route trees are independent of that source layout: `foo.zh.md` projects to the root route and `foo.md` projects to the matching `/en/` route.
|
||||
|
||||
## Read the owning contracts
|
||||
|
||||
- Read [docs/AGENTS.md](../../../docs/AGENTS.md) and use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) when deciding where content belongs or changing product documentation prose.
|
||||
- Use [dsh-translate-docs](../dsh-translate-docs/SKILL.md) whenever an edited source has a bilingual counterpart.
|
||||
- Read the current `DocsPage` type and entries in [website/docs.ts](../../../website/docs.ts) before changing the manifest; do not rely on a remembered field set.
|
||||
- Read [website/.vitepress/config.ts](../../../website/.vitepress/config.ts) before adding a new section, sidebar collection, locale, or top-level navigation item.
|
||||
|
||||
## Classify the change
|
||||
|
||||
- **Edit an already published page:** change only its canonical Markdown source. Do not touch the manifest unless its route or navigation metadata changes.
|
||||
- **Publish a new page:** create it in its owning `docs/` tier, then add one manifest entry.
|
||||
- **Rename, move, or remove a page:** update the canonical file, manifest entry, and inbound repository links atomically. Remove stale manifest entries; `docs:check` rejects missing sources.
|
||||
- **Publish a generated catalog:** map the generated `docs/` file, but change its generator or source metadata rather than editing the catalog by hand.
|
||||
- **Change site structure:** update the manifest for ordinary pages; update VitePress configuration only when the existing sidebar, section, or locale model cannot express the change.
|
||||
|
||||
Never edit or commit `website/.generated/`, `website/.cache/`, or `website/.dist/`. Never copy a maintained `docs/` page into `website/`.
|
||||
|
||||
## Add or update a manifest entry
|
||||
|
||||
Set every `DocsPage` field deliberately:
|
||||
|
||||
- `source`: repository-relative canonical Markdown path. For a complete bilingual pair, add the English `.md` path through `pairedPages()`; it derives the sibling `.zh.md`, the content locales, and counterpart aliases.
|
||||
- `route`: public VitePress path including the `.md` suffix.
|
||||
- `label`: sidebar label, not necessarily the document H1.
|
||||
- `sidebar`: reuse `zh-guide`, `zh-develop`, or `en-docs` unless the information architecture genuinely needs another collection.
|
||||
- `section`: reuse an existing section when possible. If adding one, also place it in `sectionOrder` in the VitePress config.
|
||||
- `order`: stable order within the section.
|
||||
- `sourceAliases`: optional additional repository paths that should resolve to this page when links are projected. It does not create another public route.
|
||||
|
||||
Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary.
|
||||
|
||||
## Preserve link behavior
|
||||
|
||||
Write normal repository-relative Markdown links in canonical docs. The projector applies these rules:
|
||||
|
||||
- A target present in the manifest becomes a site-relative route.
|
||||
- An existing target outside the manifest becomes a GitHub source link, including supported line suffixes.
|
||||
- External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged.
|
||||
- A missing repository-relative target fails projection instead of silently producing a broken link.
|
||||
|
||||
Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page.
|
||||
|
||||
## Preview and validate
|
||||
|
||||
Run local preview while editing:
|
||||
|
||||
```sh
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
The dev server watches mapped source files and reprojects them. Restart it after changing the manifest if the new source is not picked up automatically.
|
||||
|
||||
Run the focused website gate before treating the mapping as valid:
|
||||
|
||||
```sh
|
||||
pnpm docs:check
|
||||
```
|
||||
|
||||
Before committing a documentation-site change, run:
|
||||
|
||||
```sh
|
||||
pnpm run doc-sync
|
||||
pnpm run lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Use [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md) before pushing. Report the canonical files changed, manifest entries added or removed, public routes affected, and the exact checks run.
|
||||
|
||||
## Keep deployment separate
|
||||
|
||||
Synchronizing content into the VitePress build does not publish it to the internet. Do not add GitHub Pages permissions, deployment workflows, custom domains, or public hosting unless the user explicitly requests deployment and confirms the hosting policy.
|
||||
4
.agents/skills/dsh-doc-site-sync/agents/openai.yaml
Normal file
4
.agents/skills/dsh-doc-site-sync/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "DSH Documentation Site Sync"
|
||||
short_description: "Publish repository docs through the DSH website manifest"
|
||||
default_prompt: "Use $dsh-doc-site-sync to publish or update a DeepSeek Harness documentation page on the website."
|
||||
@@ -1,10 +1,16 @@
|
||||
---
|
||||
name: dsh-translate-docs
|
||||
description: Use when creating or updating the bilingual counterpart of a doc in this repo (English ↔ Chinese pairs) — orients the translator to the pairing contract, the terminology source of truth, the translation rules, and the consistency gate that verifies the result
|
||||
description: Use when creating or updating the bilingual counterpart of a doc in this repo (English ↔ Chinese pairs) — tells the orchestrating agent when to delegate translation to a subagent, and orients the translator to the pairing contract, the terminology source of truth, the translation rules, and the consistency gate that verifies the result
|
||||
---
|
||||
|
||||
# Translating DeepSeek-Harness docs
|
||||
|
||||
## Delegate to a subagent
|
||||
|
||||
When this skill fires and translations need to be written, do not translate yourself: spawn a subagent to do the translation work. If you are that delegated subagent, skip this section; the sections from here on address the agent actually writing the translation.
|
||||
|
||||
## What this skill is
|
||||
|
||||
**This skill is guidance, not a translation memory.** It is the workflow map for keeping `foo.md ↔ foo.zh.md` pairs consistent and natural in both languages. Both languages carry equal authority — a change is authored in either one, and that side is the source for that update. You are the translator: the rules below say what must hold, not how to phrase any particular sentence — phrasing judgment is yours, terminology is not.
|
||||
|
||||
## Sources of truth (read, don't re-summarize)
|
||||
|
||||
@@ -37,7 +37,7 @@ examples/ Runnable cordis.yml leaves over packages/examples bundles (see exam
|
||||
.agents/ Agent workflows and Agent Notes (`notes/`)
|
||||
docs/ architecture, generated catalogs, postmortems, cookbook (see docs/AGENTS.md)
|
||||
scripts/ repo gates and generators
|
||||
website/ VitePress docs site (zh-CN); api/ pages generated from source
|
||||
website/ VitePress projection of selected bilingual docs/ sources
|
||||
```
|
||||
|
||||
Package groups: [packages/README.md](packages/README.md).
|
||||
@@ -89,7 +89,7 @@ pnpm run hygiene
|
||||
out=$(printf 'echo ci smoke\n' | pnpm run demo:echo 2>&1)
|
||||
printf '%s\n' "$out" | grep -q '\[tool call\] echo({"text":"ci smoke"})'
|
||||
printf '%s\n' "$out" | grep -q '\[tool result\] ECHO: CI SMOKE'
|
||||
test -n "$(find .sessions -path '.sessions/cwd-*/main-session-*.jsonl' -type f -print -quit)"
|
||||
test -n "$(find .sessions -path '.sessions/cwd-*/main-session-*.jsonl.zstd' -type f -print -quit)"
|
||||
rm -rf .sessions
|
||||
pnpm exec vitest run --config vitest.e2e.config.ts packages/examples/stdio-demo/tests/built-bin.e2e.ts packages/examples/cli-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/tests/built-bin.e2e.ts packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts
|
||||
```
|
||||
|
||||
@@ -15,9 +15,10 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home;
|
||||
| [Agent Notes](../.agents/notes/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped |
|
||||
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
|
||||
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) |
|
||||
| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
|
||||
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
|
||||
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
|
||||
|
||||
Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
|
||||
|
||||
@@ -64,7 +64,7 @@ sequenceDiagram
|
||||
|
||||
The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.
|
||||
|
||||
`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Recovery compacts between the closed failed step and a fresh retry step, and returns retry only when the surface replacement generation advances; otherwise the original request error remains authoritative.
|
||||
`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and a fresh retry step, and returns retry only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.
|
||||
|
||||
SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages add services (`ctx
|
||||
| `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives and policy events |
|
||||
| `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill provider registry and progressive disclosure |
|
||||
| `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries |
|
||||
| `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-log compaction |
|
||||
| `ctx.compact`, `ctx.toolResultPrune` | [`compact/`](../packages/compact/README.md)/[`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | summary compaction; optional model-free result pruning |
|
||||
| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers |
|
||||
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry + generic `task_*` control tools |
|
||||
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
|
||||
@@ -55,11 +55,11 @@ Waterfall events behave like around-middleware: a listener delegates by calling
|
||||
|
||||
## Default Loop Lifecycle
|
||||
|
||||
The shipped loop drains work from prompt through checkpoint. Every pause is a service call or event available to plugins.
|
||||
The shipped loop drains prompt-to-checkpoint work through plugin-visible services and events.
|
||||
|
||||
A **session** is an append-only event log. A **turn** drains queued input until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points.
|
||||
A **session** is an append-only log. Each ordinary **turn** claims one queued `send()` item; injection claims none. A claimed `send()` successor awaits the preceding claimed ordinary turn's checkpoint but may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)). A turn ends when model and plugins stop it. A **step** is one model request plus tools. Below ([sequence companion](agent-lifecycle.md)), quotes mark durable events; other names are extension points.
|
||||
|
||||
Startup resolves identity. No id mints `<config-id>-session-<uuid>`; `sessionId` resumes or creates; `resumeSessionId` requires history. Active failures emit `agent-loop/config-start-failed(sessionId, error)`, so front doors reject work; teardown stays silent.
|
||||
No id mints `<config-id>-session-<uuid>`; `sessionId` resumes/creates; `resumeSessionId` needs history. Resume restores lineage, seeds, and delegation depth pre-publication. Failures emit `agent-loop/config-start-failed(sessionId, error)`; front doors reject; teardown stays silent.
|
||||
|
||||
### Turn Flow
|
||||
|
||||
@@ -69,13 +69,13 @@ choose declarative identity and fresh/resume path
|
||||
-> enter session + agent -> session/created -> agent/created
|
||||
-> enable driving -> agent/session-start(source) -> start driver
|
||||
forever:
|
||||
wait for queued messages
|
||||
wait for a queued message
|
||||
emit agent/status(running)
|
||||
TURN:
|
||||
'turn/start'
|
||||
each queued message -> agent/prompt-submit
|
||||
claimed message -> agent/prompt-submit
|
||||
allowed prompt -> 'user/message' plus injected context
|
||||
every prompt blocked -> 'turn/end'(rejected)
|
||||
blocked prompt -> 'prompt/blocked' -> 'turn/end'(rejected)
|
||||
STEP loop:
|
||||
drain steering
|
||||
assemble system prompt and tool schemas
|
||||
@@ -111,7 +111,7 @@ Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variab
|
||||
|
||||
Tool-time context—including async `agent.inject()` notices and post-tool `additionalContexts`—settles, then follows recorded results. Steering drains before `agent/post-step`, which observes durable output, results, context, and steering before signal closure. Leftovers become queued input. Terminal `agent/turn-stop` runs after continuation and steering folding, stays authoritative through turn close and flush, and discards later steering but preserves queued prompts.
|
||||
|
||||
`dsh-compact-basic` handles pressure and canonical overflow at checkpoints; retry requires a balanced surface replacement ([decision](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)).
|
||||
Optional pruning precedes summaries; retry requires durable surface progress; cancellation wins ([decision](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)).
|
||||
|
||||
### Failure Boundaries
|
||||
|
||||
@@ -137,7 +137,7 @@ The session log is the source of truth. `deriveMessages()` projects session even
|
||||
|
||||
**Model-visible ⟺ logged**: the log reconstructs every request — messages at `step/start` fronted by the header's session prefix, headers by folding `request/header` — and dev invariants assert this ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
|
||||
|
||||
Durability is a plugin concern. Persistence backends buffer synchronous `session/event` notifications and the loop awaits a turn-end checkpoint before moving on. The `SessionPersistence` seam stores `SessionEvent` directly, with metadata in `SessionHeader`; JSONL and SQLite share one contract suite.
|
||||
Durability is a plugin concern. Backends buffer synchronous `session/event` notifications; the loop awaits a turn-end checkpoint. `SessionPersistence` stores `SessionEvent` directly and metadata in `SessionHeader`; JSONL defaults to checksummed Zstandard, with SQLite under one contract.
|
||||
|
||||
### Model Content
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ flowchart LR
|
||||
pkg_compact_basic["compact-basic"]
|
||||
pkg_token_meter["token-meter"]
|
||||
svc_tokenMeter["ctx.tokenMeter<br/>Replay token measurement"]
|
||||
pkg_compact_tool_result_prune["compact-tool-result-prune"]
|
||||
svc_toolResultPrune["ctx.toolResultPrune<br/>Model-free tool-result pruning"]
|
||||
pkg_session["session"]
|
||||
svc_sessions["ctx.sessions<br/>In-memory session store"]
|
||||
pkg_agent["agent"]
|
||||
@@ -110,6 +112,7 @@ flowchart LR
|
||||
pkg_code_runtime_worker --> svc_codeRuntime
|
||||
pkg_compact --> svc_compact
|
||||
pkg_compact_basic --> svc_compact
|
||||
pkg_compact_tool_result_prune --> svc_toolResultPrune
|
||||
pkg_fs --> svc_fs
|
||||
pkg_fs_local --> svc_fs
|
||||
pkg_fs_sandbox --> svc_fs
|
||||
@@ -193,6 +196,7 @@ flowchart LR
|
||||
svc_tasks --> pkg_tool_subagent
|
||||
svc_tasks --> pkg_tool_tasks
|
||||
svc_tokenMeter --> pkg_compact_basic
|
||||
svc_toolResultPrune --> pkg_compact_basic
|
||||
svc_tools --> pkg_acp
|
||||
svc_tools --> pkg_agent_loop
|
||||
svc_tools --> pkg_tool_ask_user
|
||||
@@ -215,6 +219,7 @@ flowchart LR
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
|
||||
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
|
||||
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`cli-demo`](../packages/examples/cli-demo), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
|
||||
| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`acp`](../packages/ui/acp), [`session-query`](../packages/session-query/session-query) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
|
||||
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | - | - | - | Resolves live and optional persisted logs into one logical corpus for exact reads and relationship traces. |
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface Config {
|
||||
dshHome?: string
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
|
||||
workspaceContext: agentCore.Config['workspaceContext']
|
||||
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
|
||||
@@ -69,9 +71,9 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
|
||||
|
||||
Source: [`packages/examples/acp-demo/src/index.ts:33`](../packages/examples/acp-demo/src/index.ts)
|
||||
Source: [`packages/examples/acp-demo/src/index.ts:36`](../packages/examples/acp-demo/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-agent-loop`
|
||||
|
||||
@@ -224,6 +226,8 @@ export interface Config {
|
||||
dshHome?: string
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** Skill registry, local-provider, and model-facing consumer config. */
|
||||
skills?: agentCore.SkillConfig
|
||||
/** Model-facing bash tool config forwarded through agent-spine-demo. */
|
||||
@@ -235,9 +239,9 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
|
||||
|
||||
Source: [`packages/examples/cli-demo/src/index.ts:22`](../packages/examples/cli-demo/src/index.ts)
|
||||
Source: [`packages/examples/cli-demo/src/index.ts:25`](../packages/examples/cli-demo/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-code-runtime-worker`
|
||||
|
||||
@@ -303,6 +307,22 @@ export interface BasicCompactConfig {
|
||||
|
||||
Source: [`packages/compact/compact-basic/src/types.ts:8`](../packages/compact/compact-basic/src/types.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-compact-tool-result-prune`
|
||||
|
||||
```ts config-catalog
|
||||
/** Character-budget policy for deterministic tool-result pruning. */
|
||||
export interface ToolResultPruneConfig {
|
||||
/** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */
|
||||
thresholdChars?: number
|
||||
/** Maximum leading Unicode code points retained. Defaults to `4096`. */
|
||||
headChars?: number
|
||||
/** Maximum trailing Unicode code points retained. Defaults to `1024`. */
|
||||
tailChars?: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/compact/compact-tool-result-prune/src/types.ts:4`](../packages/compact/compact-tool-result-prune/src/types.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-fs-local`
|
||||
|
||||
```ts config-catalog
|
||||
@@ -722,10 +742,15 @@ export interface Config {
|
||||
* (bash calls, subprocesses). Sessions group under per-cwd subdirectories.
|
||||
*/
|
||||
root: string
|
||||
/** Physical encoding; defaults to checksummed Zstandard frames. */
|
||||
compression?: JsonlCompression
|
||||
}
|
||||
|
||||
/** Physical encoding selected for JSONL session artifacts. */
|
||||
export type JsonlCompression = 'zstd' | 'none'
|
||||
```
|
||||
|
||||
Source: [`packages/session-persistence/session-persistence-jsonl/src/index.ts:24`](../packages/session-persistence/session-persistence-jsonl/src/index.ts)
|
||||
Source: [`packages/session-persistence/session-persistence-jsonl/src/index.ts:36`](../packages/session-persistence/session-persistence-jsonl/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-session-persistence-sqlite`
|
||||
|
||||
@@ -890,6 +915,8 @@ export interface Config {
|
||||
dshHome?: string
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
|
||||
welcome?: string
|
||||
/** Terminal front-door selection and pi-tui presentation settings. */
|
||||
@@ -922,9 +949,9 @@ export interface UiConfig {
|
||||
export type TerminalMode = 'auto' | 'readline' | 'tui'
|
||||
```
|
||||
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`uiTui`](../packages/ui/tui/src/index.ts)
|
||||
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`uiTui`](../packages/ui/tui/src/index.ts)
|
||||
|
||||
Source: [`packages/examples/stdio-demo/src/index.ts:75`](../packages/examples/stdio-demo/src/index.ts)
|
||||
Source: [`packages/examples/stdio-demo/src/index.ts:78`](../packages/examples/stdio-demo/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-subagent-acp`
|
||||
|
||||
@@ -1171,8 +1198,7 @@ export interface Config {
|
||||
/**
|
||||
* Tool filter applied to every child. Filtered tools disappear from its
|
||||
* prompt and reject execution. Requires the provider's `toolFilter`
|
||||
* capability; unknown names fail startup. Children otherwise see this tool,
|
||||
* so deny it or set `maxDepth` to bound recursion.
|
||||
* capability; unknown names fail startup.
|
||||
*/
|
||||
toolFilter?: {
|
||||
/** Global tool names the child keeps; everything else is removed. */
|
||||
@@ -1181,10 +1207,15 @@ export interface Config {
|
||||
deny?: string[]
|
||||
}
|
||||
/**
|
||||
* Maximum child depth. Requires the provider's `depthLimit` capability and a
|
||||
* non-negative safe integer. Omission is unbounded.
|
||||
* Maximum child depth: a non-negative safe integer (default `3`; `0` forbids
|
||||
* delegation entirely), or `'provider-managed'` to send no cap. A numeric cap
|
||||
* requires the provider's `depthLimit` capability (mount fails loud
|
||||
* otherwise). The provider checks the calling agent's current depth at every
|
||||
* start; the tool remains model-visible so runtime policy owns rejection.
|
||||
* `'provider-managed'` is for an out-of-process provider (ACP) whose
|
||||
* recursion budget belongs to the child harness's own deployment.
|
||||
*/
|
||||
maxDepth?: number
|
||||
maxDepth?: number | 'provider-managed'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Context
|
||||
|
||||
The context is the core cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods (`ctx.on`, `ctx.emit`, …) are documented on [Events](./events.md); `ctx.effect` and `ctx.fiber` on [Fiber](./fiber.md); `ctx.plugin` and `ctx.inject` on [Registry](./registry.md).
|
||||
The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md).
|
||||
|
||||
Root and child dependency containers for Cordis plugins.
|
||||
|
||||
A context is a proxy: normal property reads go through the service resolver, while `extend()`, `isolate()`, and `intercept()` create scoped child contexts without mutating their parent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L42)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L42)
|
||||
|
||||
### ctx.extend(meta?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Create a child context with extra metadata on top of the current scope.
|
||||
*
|
||||
@@ -25,17 +27,18 @@ extend(meta = {}): this
|
||||
```
|
||||
|
||||
Create a child context with extra metadata on top of the current scope.
|
||||
|
||||
The child prototypally inherits every property of this context; own properties of `meta` shadow the inherited ones. The parent is not mutated.
|
||||
|
||||
- `meta` — own properties (including symbol keys) to define on the child.
|
||||
|
||||
**Returns** a child context inheriting from this one.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L99)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L99)
|
||||
|
||||
### ctx.isolate(name, label?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Create a child context with an independent service scope for `name`.
|
||||
*
|
||||
@@ -52,6 +55,7 @@ isolate(name: string, label?: symbol)
|
||||
```
|
||||
|
||||
Create a child context with an independent service scope for `name`.
|
||||
|
||||
Below the returned context, reads and writes of the service `name` resolve against the new label instead of the parent's, so a different implementation can be provided without affecting the parent scope. Passing the same `label` to two `isolate()` calls joins their scopes.
|
||||
|
||||
- `name` — the service name to isolate.
|
||||
@@ -59,11 +63,11 @@ Below the returned context, reads and writes of the service `name` resolve again
|
||||
|
||||
**Returns** a child context whose `name` service resolves in the new scope.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L121)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L121)
|
||||
|
||||
### ctx.intercept(name, config)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Add service-specific intercept config for plugins started below this
|
||||
* context.
|
||||
@@ -81,6 +85,7 @@ intercept(name: string, config: any): this
|
||||
```
|
||||
|
||||
Add service-specific intercept config for plugins started below this context.
|
||||
|
||||
Plugins loaded under the returned context see `config` merged into the service's resolved config (ancestor entries first; see `Service[symbols.resolveConfig]`). The parent context is not affected.
|
||||
|
||||
- `name` — the service name whose config to intercept.
|
||||
@@ -88,123 +93,123 @@ Plugins loaded under the returned context see `config` merged into the service's
|
||||
|
||||
**Returns** a child context carrying the additional intercept entry.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L139)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L139)
|
||||
|
||||
### ctx.root
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The root context of the application (every child context shares it). @experimental */
|
||||
root: this
|
||||
```
|
||||
|
||||
The root context of the application (every child context shares it). @experimental
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L22)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L22)
|
||||
|
||||
### ctx.baseUrl
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
|
||||
baseUrl?: string
|
||||
```
|
||||
|
||||
Base URL used to resolve relative plugin/module specifiers, if the runtime sets one.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L24)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L24)
|
||||
|
||||
### ctx.events
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
|
||||
events: EventsService
|
||||
```
|
||||
|
||||
The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L26)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L26)
|
||||
|
||||
### ctx.logger
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The logging service. Call `ctx.logger(name)` for a named logger. */
|
||||
logger: LoggerService
|
||||
```
|
||||
|
||||
The logging service. Call `ctx.logger(name)` for a named logger.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L28)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L28)
|
||||
|
||||
### ctx.reflect
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
|
||||
reflect: ReflectService
|
||||
```
|
||||
|
||||
The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L30)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L30)
|
||||
|
||||
### ctx.registry
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
|
||||
registry: RegistryService
|
||||
```
|
||||
|
||||
The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L32)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L32)
|
||||
|
||||
## Static members
|
||||
|
||||
### Context.effect
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
|
||||
static readonly effect: unique symbol
|
||||
```
|
||||
|
||||
Symbol key under which a disposer exposes its EffectMeta diagnostics tree.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L44)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L44)
|
||||
|
||||
### Context.filter
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
|
||||
static readonly filter: unique symbol
|
||||
```
|
||||
|
||||
Symbol key for a context's listener filter, consulted on every event dispatch.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L46)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L46)
|
||||
|
||||
### Context.isolate
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
|
||||
static readonly isolate: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the isolation map (see the `Context[symbols.isolate]` property).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L48)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L48)
|
||||
|
||||
### Context.intercept
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
|
||||
static readonly intercept: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the intercept map (see the `Context[symbols.intercept]` property).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L50)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L50)
|
||||
|
||||
### Context.is(value)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Returns true for Cordis context proxies and context prototypes.
|
||||
*
|
||||
@@ -218,19 +223,20 @@ static is(value: any): value is Context
|
||||
```
|
||||
|
||||
Returns true for Cordis context proxies and context prototypes.
|
||||
|
||||
Works across realms and across multiple copies of cordis, because the brand is keyed by a global symbol rather than by `instanceof`.
|
||||
|
||||
- `value` — the value to test.
|
||||
|
||||
**Returns** `true` if `value` is a Cordis context, narrowing its type.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L61)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L61)
|
||||
|
||||
## Service store and mixins
|
||||
|
||||
### ctx.get(name, strict?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Read a service from the store without the inject requirement.
|
||||
*
|
||||
@@ -250,11 +256,11 @@ Read a service from the store without the inject requirement.
|
||||
|
||||
**Returns** the service value, or `undefined` when not (yet) provided.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L16)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L16)
|
||||
|
||||
### ctx.set(name, value)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Overwrite a provided service's value.
|
||||
*
|
||||
@@ -269,16 +275,17 @@ set(name: string, value: any): void
|
||||
```
|
||||
|
||||
Overwrite a provided service's value.
|
||||
|
||||
Only the fiber that provided the service may set it; setting an unprovided name throws.
|
||||
|
||||
- `name` — the service name.
|
||||
- `value` — the new service value.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L28)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L28)
|
||||
|
||||
### ctx.provide(name, value)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register a service implementation owned by the current fiber.
|
||||
*
|
||||
@@ -296,6 +303,7 @@ provide(name: string, value?: any): () => void
|
||||
```
|
||||
|
||||
Register a service implementation owned by the current fiber.
|
||||
|
||||
The service becomes visible to dependents in the same isolation scope once the fiber is active; it is unregistered (waking dependents) when the returned disposer runs or the fiber unloads. Throws if the name is already provided in this scope or declared as an accessor.
|
||||
|
||||
- `name` — the service name.
|
||||
@@ -303,11 +311,11 @@ The service becomes visible to dependents in the same isolation scope once the f
|
||||
|
||||
**Returns** a disposer that unregisters the service.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L43)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L43)
|
||||
|
||||
### ctx.accessor(name, options)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Define a computed context property backed by get/set hooks.
|
||||
*
|
||||
@@ -321,16 +329,17 @@ accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
|
||||
```
|
||||
|
||||
Define a computed context property backed by get/set hooks.
|
||||
|
||||
The accessor is removed when the current fiber unloads. Throws if the name is already declared.
|
||||
|
||||
- `name` — the context property name.
|
||||
- `options` — the `get` hook and optional `set` hook.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L55)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L55)
|
||||
|
||||
### ctx.mixin(name, mixins)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Expose selected members of a service directly on `ctx`.
|
||||
*
|
||||
@@ -346,9 +355,10 @@ mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>):
|
||||
```
|
||||
|
||||
Expose selected members of a service directly on `ctx`.
|
||||
|
||||
Each mixed-in key becomes an accessor that forwards to the service (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`. Mixins are removed when the current fiber unloads.
|
||||
|
||||
- `name` — the context property holding the source service.
|
||||
- `mixins` — keys to forward, or a source-key → ctx-key map.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L66)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L66)
|
||||
@@ -1,12 +1,13 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Events
|
||||
|
||||
The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).
|
||||
The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).
|
||||
|
||||
### ctx.parallel(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event, running all listeners concurrently.
|
||||
*
|
||||
@@ -25,11 +26,11 @@ Dispatch an event, running all listeners concurrently.
|
||||
|
||||
**Returns** a promise resolving once every listener has settled.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L43)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L43)
|
||||
|
||||
### ctx.emit(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event synchronously, ignoring listener return values.
|
||||
*
|
||||
@@ -45,11 +46,11 @@ Dispatch an event synchronously, ignoring listener return values.
|
||||
- `name` — the event name.
|
||||
- `args` — arguments passed to every listener.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L52)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L52)
|
||||
|
||||
### ctx.serial(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event, awaiting listeners in order until one bails.
|
||||
*
|
||||
@@ -68,11 +69,11 @@ Dispatch an event, awaiting listeners in order until one bails.
|
||||
|
||||
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L62)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L62)
|
||||
|
||||
### ctx.bail(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event, calling listeners in order until one bails.
|
||||
*
|
||||
@@ -91,11 +92,11 @@ Dispatch an event, calling listeners in order until one bails.
|
||||
|
||||
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L72)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L72)
|
||||
|
||||
### ctx.waterfall(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event whose last argument is a `next` continuation.
|
||||
*
|
||||
@@ -111,6 +112,7 @@ waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K
|
||||
```
|
||||
|
||||
Dispatch an event whose last argument is a `next` continuation.
|
||||
|
||||
Each listener wraps the rest of the chain: calling `next()` invokes the next listener (finally the built-in behavior); not calling it vetoes.
|
||||
|
||||
- `name` — the event name.
|
||||
@@ -118,11 +120,11 @@ Each listener wraps the rest of the chain: calling `next()` invokes the next lis
|
||||
|
||||
**Returns** the outermost listener's return value.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L85)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L85)
|
||||
|
||||
### ctx.on(name, listener, options?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register an event listener owned by the current fiber.
|
||||
*
|
||||
@@ -142,11 +144,11 @@ Register an event listener owned by the current fiber.
|
||||
|
||||
**Returns** a disposer removing the listener; `true` if it was still registered.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L96)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L96)
|
||||
|
||||
### ctx.once(name, listener, options?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Same as `on()`, but the listener disposes itself after its first call.
|
||||
*
|
||||
@@ -166,13 +168,13 @@ Same as `on()`, but the listener disposes itself after its first call.
|
||||
|
||||
**Returns** a disposer removing the listener; `true` if it was still registered.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L105)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L105)
|
||||
|
||||
## EventOptions
|
||||
|
||||
Options accepted by `ctx.on()` and `ctx.once()`.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Options accepted by `ctx.on()` and `ctx.once()`. */
|
||||
interface EventOptions {
|
||||
/** Add the listener before existing listeners for the same event. */
|
||||
@@ -182,14 +184,15 @@ interface EventOptions {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L111)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L111)
|
||||
|
||||
## DispatchMode
|
||||
|
||||
Event dispatch strategy used by the event service.
|
||||
|
||||
`emit` runs synchronous listeners without awaiting them, `parallel` awaits all listeners together, `serial` awaits them in order until one bails, `bail` stops on the first synchronous bail value, and `waterfall` composes listeners around a final `next` callback.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Event dispatch strategy used by the event service.
|
||||
*
|
||||
@@ -201,4 +204,4 @@ Event dispatch strategy used by the event service.
|
||||
type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L31)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L31)
|
||||
@@ -1,12 +1,13 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Fiber
|
||||
|
||||
A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it.
|
||||
A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.
|
||||
|
||||
### ctx.effect(execute, label?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register a cleanup-aware effect on this fiber.
|
||||
*
|
||||
@@ -25,6 +26,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
|
||||
```
|
||||
|
||||
Register a cleanup-aware effect on this fiber.
|
||||
|
||||
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
|
||||
|
||||
- `execute` — the effect body; see `Effect` for accepted shapes.
|
||||
@@ -32,117 +34,118 @@ Register a cleanup-aware effect on this fiber.
|
||||
|
||||
**Returns** a disposer that tears the effect down and settles once done.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L419)
|
||||
|
||||
### ctx.fiber
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The fiber (plugin runtime instance) that owns this context. */
|
||||
fiber: Fiber
|
||||
```
|
||||
|
||||
The fiber (plugin runtime instance) that owns this context.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L11)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L11)
|
||||
|
||||
## The Fiber class
|
||||
|
||||
Runtime instance of one plugin application.
|
||||
|
||||
A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by `ctx.plugin()`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L183)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L183)
|
||||
|
||||
### fiber.uid
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
|
||||
public uid: number | null
|
||||
```
|
||||
|
||||
Unique id within the registry; 0 for the root fiber, `null` once disposed.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L185)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L185)
|
||||
|
||||
### fiber.ctx
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The context this fiber's plugin runs in (extends the parent context). */
|
||||
public readonly ctx: Context
|
||||
```
|
||||
|
||||
The context this fiber's plugin runs in (extends the parent context).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L187)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L187)
|
||||
|
||||
### fiber.config
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The validated plugin config (updated by `update()`). */
|
||||
public config: any
|
||||
```
|
||||
|
||||
The validated plugin config (updated by `update()`).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L189)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L189)
|
||||
|
||||
### fiber.state
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Current lifecycle state; transitions emit `internal/status`. */
|
||||
public state
|
||||
```
|
||||
|
||||
Current lifecycle state; transitions emit `internal/status`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L191)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L191)
|
||||
|
||||
### fiber.dispose
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
|
||||
public readonly dispose: () => Promise<void>
|
||||
```
|
||||
|
||||
Dispose this fiber: unload the plugin, then settle once cleanup finished.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L193)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L193)
|
||||
|
||||
### fiber.store
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
|
||||
public store: Dict<Impl> | undefined
|
||||
```
|
||||
|
||||
Snapshot of required service implementations while loaded; `undefined` otherwise.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L195)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L195)
|
||||
|
||||
### fiber.inertia
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The in-flight load/unload transition, if one is currently running. */
|
||||
public inertia: Promise<void> | undefined
|
||||
```
|
||||
|
||||
The in-flight load/unload transition, if one is currently running.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L197)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L197)
|
||||
|
||||
### fiber.name
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
|
||||
get name()
|
||||
```
|
||||
|
||||
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L340)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L340)
|
||||
|
||||
### fiber.assertActive()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Throw if the fiber has already been disposed.
|
||||
*
|
||||
@@ -156,11 +159,11 @@ Throw if the fiber has already been disposed.
|
||||
|
||||
**Returns** nothing when the fiber is still active.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L355)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L355)
|
||||
|
||||
### fiber.effect(execute, label?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register a cleanup-aware effect on this fiber.
|
||||
*
|
||||
@@ -179,6 +182,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
|
||||
```
|
||||
|
||||
Register a cleanup-aware effect on this fiber.
|
||||
|
||||
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
|
||||
|
||||
- `execute` — the effect body; see `Effect` for accepted shapes.
|
||||
@@ -186,11 +190,11 @@ Register a cleanup-aware effect on this fiber.
|
||||
|
||||
**Returns** a disposer that tears the effect down and settles once done.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L419)
|
||||
|
||||
### fiber.getEffects()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Return metadata for currently registered effects.
|
||||
*
|
||||
@@ -203,11 +207,11 @@ Return metadata for currently registered effects.
|
||||
|
||||
**Returns** one `EffectMeta` tree per labeled live effect.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L572)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L572)
|
||||
|
||||
### fiber.await()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Wait for current lifecycle work and rethrow startup errors.
|
||||
*
|
||||
@@ -221,11 +225,11 @@ Wait for current lifecycle work and rethrow startup errors.
|
||||
|
||||
**Returns** this fiber, once it has settled into a stable state.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L701)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L701)
|
||||
|
||||
### fiber.restart()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispose and immediately reload this plugin with its current config.
|
||||
*
|
||||
@@ -239,11 +243,11 @@ Dispose and immediately reload this plugin with its current config.
|
||||
|
||||
**Returns** a promise resolving once the reload settled.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L715)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L715)
|
||||
|
||||
### fiber.update(config, noSave?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Validate and apply new config, then restart the plugin.
|
||||
*
|
||||
@@ -259,6 +263,7 @@ update(config: any, noSave = false)
|
||||
```
|
||||
|
||||
Validate and apply new config, then restart the plugin.
|
||||
|
||||
Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto or replace the restart.
|
||||
|
||||
- `config` — the new raw config; validated before anything restarts.
|
||||
@@ -266,14 +271,15 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
|
||||
|
||||
**Returns** nothing; the restart runs behind the `internal/update` waterfall.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L733)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L733)
|
||||
|
||||
## Effect
|
||||
|
||||
Effect body result accepted by `ctx.effect()` and plugin startup.
|
||||
|
||||
Either a single disposer, a promise of one, or a (possibly async) iterable yielding several — generator effects register each yielded disposer as it is produced.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Effect body result accepted by `ctx.effect()` and plugin startup.
|
||||
*
|
||||
@@ -286,14 +292,15 @@ type Effect<T = any> =
|
||||
| AsyncEffect<T>
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L82)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L82)
|
||||
|
||||
## Disposable
|
||||
|
||||
Function returned by an effect to release resources during disposal.
|
||||
|
||||
Disposers run in reverse registration order when the owning fiber unloads; they may be async, in which case unloading awaits them.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Function returned by an effect to release resources during disposal.
|
||||
*
|
||||
@@ -303,13 +310,13 @@ Disposers run in reverse registration order when the owning fiber unloads; they
|
||||
type Disposable<T = any> = () => T
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L73)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L73)
|
||||
|
||||
## EffectMeta
|
||||
|
||||
Tree node used to expose nested effect labels for diagnostics.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Tree node used to expose nested effect labels for diagnostics. */
|
||||
interface EffectMeta {
|
||||
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
|
||||
@@ -319,13 +326,13 @@ interface EffectMeta {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L95)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L95)
|
||||
|
||||
## CordisError
|
||||
|
||||
Framework error with a stable machine-readable code.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Framework error with a stable machine-readable code. */
|
||||
class CordisError extends Error {
|
||||
/**
|
||||
@@ -345,13 +352,13 @@ namespace CordisError {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L156)
|
||||
|
||||
## ValidationError
|
||||
|
||||
Error raised when plugin configuration fails standard-schema validation.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Error raised when plugin configuration fails standard-schema validation. */
|
||||
class ValidationError extends TypeError {
|
||||
name = 'ValidationError'
|
||||
@@ -365,4 +372,4 @@ class ValidationError extends TypeError {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L18)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L18)
|
||||
@@ -1,4 +1,5 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Registry
|
||||
|
||||
@@ -6,7 +7,7 @@ Plugin loading and dependency injection.
|
||||
|
||||
### ctx.inject(deps, callback)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Run a callback once the requested services are available.
|
||||
*
|
||||
@@ -21,6 +22,7 @@ inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber
|
||||
```
|
||||
|
||||
Run a callback once the requested services are available.
|
||||
|
||||
Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloaded and re-run whenever a required service changes.
|
||||
|
||||
- `deps` — required services, as an array or a name → config map.
|
||||
@@ -28,11 +30,11 @@ Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloade
|
||||
|
||||
**Returns** the fiber; awaiting it settles once loading finished.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L175)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L175)
|
||||
|
||||
### ctx.plugin(plugin, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Load a plugin in the current context.
|
||||
*
|
||||
@@ -51,13 +53,13 @@ Load a plugin in the current context.
|
||||
|
||||
**Returns** the fiber; awaiting it settles once loading finished (rejecting on config or startup errors).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L184)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L184)
|
||||
|
||||
## Plugin
|
||||
|
||||
Supported plugin entrypoint shapes.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Supported plugin entrypoint shapes. */
|
||||
type Plugin<T = any> =
|
||||
| Plugin.Function<T>
|
||||
@@ -116,14 +118,15 @@ namespace Plugin {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L91)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L91)
|
||||
|
||||
## Inject
|
||||
|
||||
Service dependency declaration accepted by plugins and the `@Inject` decorator.
|
||||
|
||||
Array form requests services without intercept config. Object form maps each service name to optional intercept config for the plugin context.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Service dependency declaration accepted by plugins and the `@Inject`
|
||||
* decorator.
|
||||
@@ -146,4 +149,4 @@ namespace Inject {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L18)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L18)
|
||||
@@ -1,100 +1,102 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Service
|
||||
|
||||
Base class for context services: subclass it and load the subclass as a plugin to register `ctx.<name>`.
|
||||
The base class for context services. A subclass loaded as a plugin registers itself as `ctx.<name>`.
|
||||
|
||||
Base class for services that expose a named API on `ctx`.
|
||||
|
||||
Subclasses call `super(ctx, name)` from their constructor. The service is registered immediately and is automatically removed with the owning fiber.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L11)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L11)
|
||||
|
||||
### service.name
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The service name this instance is registered under. */
|
||||
public name!: string
|
||||
```
|
||||
|
||||
The service name this instance is registered under.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L30)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L30)
|
||||
|
||||
## Static members
|
||||
|
||||
### Service.init
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of an instance method run after construction (class plugins). */
|
||||
static readonly init: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of an instance method run after construction (class plugins).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L13)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L13)
|
||||
|
||||
### Service.check
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
|
||||
static readonly check: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the availability predicate passed to `ctx.provide()`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L15)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L15)
|
||||
|
||||
### Service.config
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the phantom intercept-config type parameter. */
|
||||
static readonly config: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the phantom intercept-config type parameter.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L17)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L17)
|
||||
|
||||
### Service.invoke
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
|
||||
static readonly invoke: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the call body making a service callable (e.g. `ctx.logger()`).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L19)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L19)
|
||||
|
||||
### Service.extend
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the helper deriving an extended service instance. */
|
||||
static readonly extend: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the helper deriving an extended service instance.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L21)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L21)
|
||||
|
||||
### Service.tracker
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the tracker metadata used for context tracing. */
|
||||
static readonly tracker: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the tracker metadata used for context tracing.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L23)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L23)
|
||||
|
||||
### Service.resolveConfig
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the intercept-config resolution helper below. */
|
||||
static readonly resolveConfig: unique symbol
|
||||
```
|
||||
|
||||
Symbol key of the intercept-config resolution helper below.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L25)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L25)
|
||||
@@ -7,7 +7,7 @@ Every cordis event a plugin can listen to: exact signature, dispatch mode, and o
|
||||
|
||||
This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
|
||||
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).
|
||||
|
||||
Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).
|
||||
|
||||
@@ -33,7 +33,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:143`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/disposed` — emit
|
||||
|
||||
@@ -53,7 +53,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:152`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:159`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/error` — emit
|
||||
|
||||
@@ -75,7 +75,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:307`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:314`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/post-step` — serial
|
||||
|
||||
@@ -98,7 +98,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:260`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:267`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/pre-step` — serial
|
||||
|
||||
@@ -121,18 +121,18 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:200`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:207`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/prompt-submit` — waterfall
|
||||
|
||||
Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default.
|
||||
Allow, rewrite, or block one claimed prompt before it becomes a user message. Call `next()` for the unchanged default.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Allow, rewrite, or block one drained prompt before it becomes a user
|
||||
* Allow, rewrite, or block one claimed prompt before it becomes a user
|
||||
* message. Call `next()` for the unchanged default.
|
||||
* @param agent - the agent draining its inbox.
|
||||
* @param content - the drained message's blocks, as queued.
|
||||
* @param agent - the agent whose turn claimed the message.
|
||||
* @param content - the claimed message's blocks, as queued.
|
||||
* @param source - the message's resolved source.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode waterfall
|
||||
@@ -142,7 +142,7 @@ Allow, rewrite, or block one drained prompt before it becomes a user message. Ca
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:210`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:217`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/queued` — emit
|
||||
|
||||
@@ -163,7 +163,7 @@ Detached, frozen content entered the agent's inbox. Source defaults have already
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:171`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:178`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request` — waterfall
|
||||
|
||||
@@ -186,7 +186,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:222`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:229`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request-error` — waterfall
|
||||
|
||||
@@ -211,7 +211,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:274`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:281`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-prefix` — waterfall
|
||||
|
||||
@@ -237,7 +237,7 @@ Compose request-only messages placed before derived history. The frozen result i
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:237`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:244`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-start` — emit
|
||||
|
||||
@@ -259,7 +259,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:184`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:191`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/status` — emit
|
||||
|
||||
@@ -279,7 +279,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:161`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:168`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/step-result` — waterfall
|
||||
|
||||
@@ -301,7 +301,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:248`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:255`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-continuation` — waterfall
|
||||
|
||||
@@ -322,7 +322,7 @@ Override whether the turn continues. The default continues after tool calls or s
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContinuationDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:284`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:291`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-stop` — serial
|
||||
|
||||
@@ -343,7 +343,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContinuationStop](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:294`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:301`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
## `agent-loop/*`
|
||||
|
||||
@@ -585,7 +585,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:112`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:139`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/provider-added` — emit
|
||||
|
||||
@@ -602,7 +602,7 @@ A provider became resolvable in the registry.
|
||||
|
||||
Types: [SubagentProvider](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:86`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:113`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/provider-removed` — emit
|
||||
|
||||
@@ -617,7 +617,7 @@ A provider left the registry. Accepted runs remain holder-owned.
|
||||
'subagent/provider-removed'(name: string): void
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:92`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:119`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/start` — emit
|
||||
|
||||
@@ -639,7 +639,7 @@ A provider established a ready child. For in-process providers, `ctx.agents.get(
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:103`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:130`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `system-prompt/*`
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Every `ctx.<key>` service a plugin can call: the exact public interface with ori
|
||||
|
||||
This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
|
||||
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).
|
||||
|
||||
## `ctx.agentLoop` — `AgentLoop`
|
||||
|
||||
@@ -216,7 +216,7 @@ roots(): Agent[]
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/index.ts:217`](../../packages/core/agent/src/index.ts)
|
||||
Source: [`packages/core/agent/src/index.ts:223`](../../packages/core/agent/src/index.ts)
|
||||
|
||||
## `ctx.approval` — `ApprovalService`
|
||||
|
||||
@@ -718,9 +718,9 @@ Persistence is intentionally not implemented here — persistence plugins subscr
|
||||
* Create a session owned by the calling fiber: disposing that fiber stops
|
||||
* event notification and removes the session from the store. `options.seed`
|
||||
* populates the session with a copy of those events (replay/fork);
|
||||
* `options.meta` attaches creation metadata (validated absolute `cwd`,
|
||||
* `parentSession` lineage) as the immutable {@link SessionHeader} (the store
|
||||
* fills `version`/`id`/`createdAt`).
|
||||
* `options.meta` attaches creation metadata (validated absolute `cwd`, seed
|
||||
* and parent lineage, and delegation depth) as the immutable
|
||||
* {@link SessionHeader} (the store fills `version`/`id`/`createdAt`).
|
||||
*
|
||||
* For an agent whose session must be torn down IN ORDER with its loop (so the
|
||||
* loop's final flush is captured before the store attachment ends), do NOT use this
|
||||
@@ -832,7 +832,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
|
||||
|
||||
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:549`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:553`](../../packages/core/session/src/index.ts)
|
||||
|
||||
## `ctx.skills` — `SkillService`
|
||||
|
||||
@@ -946,7 +946,7 @@ async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
|
||||
|
||||
Types: [SubagentProvider](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:180`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `ctx.systemPrompt` — `SystemPrompt`
|
||||
|
||||
@@ -1120,6 +1120,43 @@ Types: [EpochHeader](../core-data-structures/session.md) · [Message](../core-da
|
||||
|
||||
Source: [`packages/llm/token-meter/src/index.ts:106`](../../packages/llm/token-meter/src/index.ts)
|
||||
|
||||
## `ctx.toolResultPrune` — `ToolResultPruneService`
|
||||
|
||||
Deterministic head/middle/tail pruning for current tool-result surface nodes.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Measure text content in Unicode code points; non-text blocks cost zero.
|
||||
* @param blocks - tool-result content to measure.
|
||||
* @returns total Unicode code points across text blocks.
|
||||
*/
|
||||
measureContent(blocks: readonly ContentBlock[]): number
|
||||
|
||||
/**
|
||||
* Replace an over-budget text middle while retaining rich-block order.
|
||||
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
|
||||
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
|
||||
* @param blocks - original tool-result content.
|
||||
* @returns pruned content, or `null` when the text is within budget.
|
||||
*/
|
||||
pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null
|
||||
|
||||
/**
|
||||
* Prune every over-budget tool result from one stable current-surface snapshot.
|
||||
* Each replacement preserves the complete event data except for `content`,
|
||||
* and points at the shadowed node for durable provenance and replay.
|
||||
* @param session - session whose current surface is rewritten.
|
||||
* @returns landed replacements and aggregate Unicode-code-point savings.
|
||||
* @throws when the session rejects a replacement; replacements committed
|
||||
* earlier in the pass remain durable.
|
||||
*/
|
||||
pruneSession(session: Session): PruneResult
|
||||
```
|
||||
|
||||
Types: [ContentBlock](../core-data-structures/core.md) · [PruneResult](../core-data-structures/compaction.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/compact/compact-tool-result-prune/src/index.ts:39`](../../packages/compact/compact-tool-result-prune/src/index.ts)
|
||||
|
||||
## `ctx.tools` — `ToolRegistry`
|
||||
|
||||
Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.
|
||||
|
||||
@@ -6,7 +6,7 @@ Source: [`packages/compact/compact/src/types.ts`](../../packages/compact/compact
|
||||
|
||||
## The `compact/*` session events
|
||||
|
||||
Compaction extends [`SessionEventMap`](session.md) with three event types via declaration merging. All three are **log-only** — they record the compaction lock and its provenance, and never join the surface. `SurfaceEventType` is deliberately NOT extended (only message-producing events reach the model), so the summary itself rides on a separate `user/message` with `surfaceOp: { op: 'replace', start, end }` — the only surface mutation. See the Agent Note for why reusing `user/message` is honest rather than a workaround.
|
||||
Compaction extends [`SessionEventMap`](session.md) with three event types via declaration merging. All three are **log-only** — they record the compaction lock and its provenance, and never join the surface. `SurfaceEventType` is deliberately NOT extended (only message-producing events reach the model), so the summary itself rides on a separate `user/message` with `surfaceOp: { op: 'replace', start, end }` — the only surface mutation performed by summary compaction. See the Agent Note for why reusing `user/message` is honest rather than a workaround.
|
||||
|
||||
| Event | Payload | Role |
|
||||
|---|---|---|
|
||||
@@ -60,6 +60,36 @@ type CompactionTrigger = 'pressure' | 'context-overflow'
|
||||
|
||||
`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, returning `null` when no safe work exists, and `compactRegion(...)` for an explicit inclusive surface range. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration.
|
||||
|
||||
Pressure compaction runs at serial `agent/post-step`, after successful assistant output, tool results, buffered context, and steering are durable but before `step/end`. Failed-request recovery runs through `agent/request-error` after the failed step closes, and authorizes a fresh numbered-step retry only when the surface replacement generation advances. Region boundaries preserve tool-call/result pairing but do not preserve whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling.
|
||||
Pressure compaction runs at serial `agent/post-step`, after successful assistant output, tool results, buffered context, and steering are durable but before `step/end`. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and authorizes a fresh numbered-step retry only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling.
|
||||
|
||||
The seam exports `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)` for those edge checks. Both validate current surface membership and reject missing seqs and orphan results; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) owns their cache semantics.
|
||||
|
||||
## Tool-result pruning outcomes
|
||||
|
||||
The optional tool-result pruning service reports each durable content replacement and the aggregate Unicode-code-point reduction. Its public result types live in [`compact-tool-result-prune/src/types.ts`](../../packages/compact/compact-tool-result-prune/src/types.ts).
|
||||
|
||||
```ts type-equiv
|
||||
/** Provenance and size accounting for one landed surface replacement. */
|
||||
interface PrunedEntry {
|
||||
/** Full-fidelity tool-result event shadowed by the replacement. */
|
||||
readonly originalSeq: number
|
||||
/** Newly appended pruned tool-result event. */
|
||||
readonly replacementSeq: number
|
||||
/** Tool call shared by the original and replacement. */
|
||||
readonly callId: CallId
|
||||
/** Original text size in Unicode code points. */
|
||||
readonly charsBefore: number
|
||||
/** Replacement text size in Unicode code points. */
|
||||
readonly charsAfter: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Aggregate outcome of one stable-surface pruning pass. */
|
||||
interface PruneResult {
|
||||
/** Replacements in the snapshotted surface order. */
|
||||
readonly pruned: readonly PrunedEntry[]
|
||||
/** Total Unicode code points removed across replacements. */
|
||||
readonly charsRemoved: number
|
||||
}
|
||||
```
|
||||
|
||||
@@ -360,15 +360,20 @@ interface Agent {
|
||||
readonly ctx: Context
|
||||
|
||||
/**
|
||||
* Queue detached, frozen lossless-JSON input; starts a turn when idle.
|
||||
* Queue one detached, frozen lossless-JSON item. If claimed, it is the sole
|
||||
* ordinary message in its FIFO-ordered turn; the next claimed item waits for
|
||||
* that turn's checkpoint.
|
||||
* Invalid input throws synchronously before notification or enqueue.
|
||||
*/
|
||||
send(content: ContentBlock[], options?: SendOptions): void
|
||||
|
||||
/**
|
||||
* Steer a running turn: content is injected between steps of the current
|
||||
* turn. Uses the same owned-value and synchronous-validation boundary as
|
||||
* {@link send}; when idle, behaves exactly like that method.
|
||||
* Submit steering while the agent is `running`. An open turn records it at
|
||||
* the next steering checkpoint before a request or continuation decision;
|
||||
* policy may stop before another step. After turn close and its checkpoint,
|
||||
* any remainder is queued for a later turn; terminal `agent/turn-stop`,
|
||||
* cancellation, or disposal may discard it. Uses the same synchronous
|
||||
* snapshot-and-validation boundary as {@link send}; when idle, delegates to it.
|
||||
*/
|
||||
steer(content: ContentBlock[], options?: SendOptions): void
|
||||
|
||||
@@ -382,10 +387,11 @@ interface Agent {
|
||||
inject(content: ContentBlock[], options?: InjectOptions): void
|
||||
|
||||
/**
|
||||
* Clear queued and steering work, including work waiting to start, and abort
|
||||
* the active step. The supplied reason is preserved across pre-step and active
|
||||
* cancellation windows, and `whenIdle()` resolves after cancellation reaches
|
||||
* quiescence. Idle cancellation is a no-op and does not arm a later cancel.
|
||||
* Clear all queued and steering work, including items waiting to start, and
|
||||
* abort the active step. The supplied reason is preserved across pre-step
|
||||
* and active cancellation windows, and `whenIdle()` resolves after
|
||||
* cancellation reaches quiescence. Idle cancellation is a no-op and does not
|
||||
* arm a later cancel.
|
||||
*/
|
||||
cancel(reason?: string): void
|
||||
|
||||
@@ -395,7 +401,7 @@ interface Agent {
|
||||
}
|
||||
```
|
||||
|
||||
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `AgentOptions` is merge-extensible and currently includes `provider?` and `model?`; dispatch requires both after `agent/request`. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
|
||||
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `running` describes the driver-wide drain interval, which can span turn close, its durability checkpoint, and consecutive queued turns; it does not prove a turn is still open. `AgentOptions` is merge-extensible and currently includes `provider?` and `model?`; dispatch requires both after `agent/request`. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
|
||||
|
||||
The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits.
|
||||
|
||||
@@ -419,13 +425,14 @@ interface HookContext {
|
||||
}
|
||||
```
|
||||
|
||||
`agent/prompt-submit` returns a `PromptDecision` (allow a drained queued message — optionally rewriting its `content` or attaching `additionalContexts` — or block it; a batch whose every prompt is blocked opens a zero-step turn that ends `rejected`):
|
||||
`agent/prompt-submit` returns a `PromptDecision` (allow the turn's claimed queued message — optionally rewriting its `content` or attaching `additionalContexts` — or record `prompt/blocked` and end that zero-step turn as `rejected`):
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Prompt interception result. `allow.content` replaces the prompt and each
|
||||
* `additionalContexts` entry becomes a separate context message. `block` records a
|
||||
* durable `prompt/blocked`; an all-blocked batch ends a zero-step rejected turn.
|
||||
* `additionalContexts` entry becomes a separate context message. `block`
|
||||
* records a durable `prompt/blocked` and ends the claimed prompt's zero-step
|
||||
* turn as rejected.
|
||||
*/
|
||||
type PromptDecision =
|
||||
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] }
|
||||
|
||||
@@ -6,7 +6,7 @@ The seam is a textbook [capability seam](../../.agents/notes/implemented/archite
|
||||
|
||||
## The flush checkpoint
|
||||
|
||||
`session/event` is a *synchronous* notification; persistence plugins buffer it (write-behind) and drain at the awaited `session/flush` checkpoint the loop fires at every turn end. Flush is `ctx.parallel` (awaited): a turn's events are durably committed before the next turn starts, and the turn boundary is the commit boundary. A rejecting flush is reported via `agent/error` and the logger — never as a session event (it would land past the commit boundary), so the backend keeps its buffered events for the next flush.
|
||||
`session/event` is a *synchronous* notification; persistence plugins buffer it (write-behind) until `session/flush`. The loop awaits an ordinary turn's checkpoint before claiming the next queue item; synchronous idle `inject()` schedules its checkpoint without blocking `send()`, and disposal still drains it. A successful flush durably commits the closed turn as one unit; a rejecting flush is reported through `agent/error` and the logger — never as a session event past the closed turn — while the backend keeps its buffered events for the next flush.
|
||||
|
||||
## Crash recovery preserves an interrupted turn
|
||||
|
||||
@@ -60,12 +60,18 @@ interface SessionHeader {
|
||||
* boundary lets resume and replay distinguish parent history from child work.
|
||||
*/
|
||||
readonly seedLength?: number
|
||||
/**
|
||||
* Delegation depth: absent (zero) for a top-level session, parent depth + 1
|
||||
* for a subagent child. Persisted so a recursion budget survives restart and
|
||||
* resume — a runtime-only depth would reset a resumed child to top-level.
|
||||
*/
|
||||
readonly delegationDepth?: number
|
||||
}
|
||||
```
|
||||
|
||||
## `CreateSessionOptions` — seeding and metadata
|
||||
|
||||
Creating a `Session` through the store takes a `seed` (replay/fork an existing event log) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, and — only when reconstructing a persisted session — the original `createdAt` to preserve it.
|
||||
Creating a `Session` through the store takes a `seed` (replay/fork an existing event log) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the `delegationDepth`, and — only when reconstructing a persisted session — the original `createdAt` to preserve it.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -85,6 +91,7 @@ interface CreateSessionOptions {
|
||||
readonly parentSession?: SessionId
|
||||
readonly createdAt?: number
|
||||
readonly seedLength?: number
|
||||
readonly delegationDepth?: number
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -95,7 +102,7 @@ Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resumi
|
||||
|
||||
Both implement the same abstract `SessionPersistence` (locate/create/append/load/list over `SessionEvent`) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)** — an append-only JSONL log per session with crash-safe atomic writes, the interrupted-turn crash recovery above, and a read/replay path.
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)** — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, and a read/replay path.
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)** — `node:sqlite`, one row per `SessionEvent`. The row shape `(session_id, seq, type, time, data, source_event_seqs, surface_op)` maps 1:1 onto the event, including optional surface metadata, so there is no parallel persisted schema to keep in sync.
|
||||
|
||||
Multiple backends sharing one on-disk session coordinate writes through the [shared persistence write-coordinator](../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md).
|
||||
|
||||
@@ -17,27 +17,28 @@ The append-only event types. Merge-extensible: a plugin declares extra event typ
|
||||
*/
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* Opens turn `turn`. `trigger` records what started it — a drained message
|
||||
* batch or an idle-time injection. The turn is the durability/replay
|
||||
* Opens turn `turn`. `trigger` records what started it — one claimed queued
|
||||
* message or an idle-time injection. The turn is the durability/replay
|
||||
* boundary: every event sits between a `turn/start` and its matching
|
||||
* `turn/end` (the turn-enclosure invariant).
|
||||
*/
|
||||
'turn/start': { turn: number; trigger: TurnTrigger }
|
||||
/**
|
||||
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
|
||||
* fires the awaited `session/flush` checkpoint at every turn end, so the turn
|
||||
* boundary is also the durable-commit boundary.
|
||||
* awaits `session/flush` after an ordinary turn ends before claiming the next
|
||||
* queued item. Success commits the turn; rejection is reported live and does
|
||||
* not prevent later work.
|
||||
*/
|
||||
'turn/end': { turn: number; reason: TurnEndReason }
|
||||
/** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
|
||||
'step/start': { turn: number; step: number }
|
||||
/** Closes step `step` of turn `turn`. */
|
||||
'step/end': { turn: number; step: number }
|
||||
/** A user-visible prompt (queued message drained at turn start). */
|
||||
/** A user-visible prompt (the queued message claimed for this turn). */
|
||||
'user/message': { content: ContentBlock[]; source: MessageSource }
|
||||
/**
|
||||
* Durable record of a prompt veto and its reason. It is log-only: the blocked
|
||||
* prompt never enters the model-visible surface, including in a mixed batch.
|
||||
* prompt never enters the model-visible surface, and its turn runs zero steps.
|
||||
*/
|
||||
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
|
||||
/**
|
||||
@@ -480,8 +481,8 @@ interface TurnEndReasonMap {
|
||||
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
||||
'max-tokens': { kind: 'max-tokens' }
|
||||
/**
|
||||
* Policy blocked every prompt before the first step. The zero-step turn still
|
||||
* records a balanced durable boundary and the veto reason.
|
||||
* Policy blocked the turn's claimed prompt before the first step. The
|
||||
* zero-step turn still records a balanced durable boundary and veto reason.
|
||||
*/
|
||||
rejected: { kind: 'rejected'; reason: string }
|
||||
/**
|
||||
@@ -492,7 +493,7 @@ interface TurnEndReasonMap {
|
||||
}
|
||||
```
|
||||
|
||||
`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose whole prompt batch an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible.
|
||||
`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose claimed prompt an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible.
|
||||
|
||||
## The turn-enclosure invariant
|
||||
|
||||
|
||||
@@ -237,5 +237,5 @@ interface SubagentProvider {
|
||||
|
||||
The spawn and fork backends create an ordinary agent through `parent.ctx`, pass cancellation into core creation, and dispose through `AgentHandle`. Provider removal blocks new starts without revoking accepted runs. Each child gets a new flat scope rather than inheriting parent registrations. Depth and fork seeding reuse existing agent and session vocabulary:
|
||||
|
||||
- **Delegation depth** is a merge-extensible `AgentOptions.subagentDepth` field (`0` for a top-level agent, parent + 1 for a child). Only `undefined` means top level; every stored present value must be a non-negative safe integer. The seam owns it — the loop neither sets nor reads it — so a nested spawn validates its parent's stored depth, rejects a derived child depth outside the safe-integer domain, and applies a defined absolute `request.maxDepth` cap to that child.
|
||||
- **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap.
|
||||
- **Fork seeding** uses `CreateAgentOptions.seed` (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `resume` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded).
|
||||
|
||||
@@ -12,7 +12,7 @@ When an interface documents two valid ways to signal something — an adapter ma
|
||||
|
||||
## Async state is not synchronous state
|
||||
|
||||
`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns (the loop batches queued messages). The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly.
|
||||
`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items. The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly.
|
||||
|
||||
## Dispose must reach quiescence, not just request it
|
||||
|
||||
|
||||
@@ -8,21 +8,21 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:353`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:143`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:152`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:307`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:200`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:210`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:171`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:222`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:274`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:237`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:184`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:161`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:248`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:284`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:294`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:150`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:159`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:314`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:267`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:207`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:217`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:178`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:229`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:281`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:244`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:191`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:168`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:255`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:291`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:301`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:31`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:71`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
@@ -32,10 +32,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio), [`token-meter`](../packages/llm/token-meter), [`tui`](../packages/ui/tui), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:112`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:86`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:92`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:103`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:113`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:119`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:130`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
|
||||
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:27`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:33`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:116`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
|
||||
|
||||
@@ -14,4 +14,4 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
|
||||
- **shadowing** — most-specific-wins name resolution: a scoped tool/section/variable replaces its same-named global twin for that scope alone. The per-agent persona and per-agent tool-variant mechanism.
|
||||
- **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool surface for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one.
|
||||
- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/session-start` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
|
||||
- **lineage** — parent/child facts carried as data (`parentSession`, `subagentDepth`); never affects visibility. <a id="lineage"></a>
|
||||
- **lineage** — parent/child facts carried as data (`parentSession`, durable `delegationDepth`, runtime `subagentDepth`); never affects visibility. <a id="lineage"></a>
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
|
||||
**dispose(资源释放)必须等待所有任务完全停稳,不能仅下发终止指令就返回**:如果清理过程只发出终止或中断信号,却不等任务停止就返回,就会留下孤儿进程。清理应采用异步方式,等待所有子任务彻底退出(先发出终止信号,再等待退出);发出信号前应先关闭监听器与通知注册表,使延迟到达的完成事件不再触发通知。测试要证明 dispose 的确等到清理完成:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能只检查进程最终会自行消亡。
|
||||
|
||||
> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns.
|
||||
> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items.
|
||||
|
||||
**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise(`agent/status`、`task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要根据操作次数推断操作与轮次一一对应。
|
||||
**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise(`agent/status`、`task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要把状态当作逐次 `send()` 的结果:多次排队的 `send()` 会作为连续轮次运行,但可能共用一个 `running` 区间;取消或资源释放还可能丢弃尚未启动的队列项。
|
||||
|
||||
## ③ 测试政策清单
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
| waterfall | waterfall | waterfall(瀑布式事件) | | |
|
||||
| wheel | wheel 包 | | | Python 打包格式 |
|
||||
| worktree | worktree | | | git 工作区概念 |
|
||||
| Zstandard | Zstandard | | | RFC 8878 compression format; `zstd` remains a code value. |
|
||||
|
||||
## 双语类(中英文文本各自使用中英文)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ flowchart TD
|
||||
subgraph group_compact["packages/compact"]
|
||||
pkg_compact["compact"]
|
||||
pkg_compact_basic["compact-basic"]
|
||||
pkg_compact_tool_result_prune["compact-tool-result-prune"]
|
||||
end
|
||||
subgraph group_subagent["packages/subagent"]
|
||||
pkg_subagent["subagent"]
|
||||
@@ -180,6 +181,8 @@ flowchart TD
|
||||
pkg_fs --> pkg_sandbox
|
||||
pkg_compact --> pkg_llm
|
||||
pkg_compact --> pkg_session
|
||||
pkg_compact_tool_result_prune --> pkg_llm
|
||||
pkg_compact_tool_result_prune --> pkg_session
|
||||
pkg_web_fetch_local --> pkg_timeout
|
||||
pkg_web_fetch_local --> pkg_web
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
@@ -204,6 +207,7 @@ flowchart TD
|
||||
pkg_skill_local --> pkg_skill
|
||||
pkg_compact_basic --> pkg_agent
|
||||
pkg_compact_basic --> pkg_compact
|
||||
pkg_compact_basic --> pkg_compact_tool_result_prune
|
||||
pkg_compact_basic --> pkg_llm
|
||||
pkg_compact_basic --> pkg_session
|
||||
pkg_compact_basic --> pkg_token_meter
|
||||
@@ -491,6 +495,7 @@ flowchart TD
|
||||
| [`bash`](../packages/bash/bash) | `bash` | [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) |
|
||||
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`web`](../packages/web/web) |
|
||||
@@ -504,7 +509,7 @@ flowchart TD
|
||||
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
|
||||
| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) |
|
||||
| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`home`](../packages/util/home), [`skill`](../packages/skill/skill) |
|
||||
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
|
||||
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) |
|
||||
| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
|
||||
|
||||
@@ -79,7 +79,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
Sources: [`packages/core/session/src/types.ts:255`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:262`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:292`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:324`](../packages/core/session/src/types.ts)
|
||||
Sources: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
|
||||
|
||||
## Events
|
||||
|
||||
@@ -151,7 +151,7 @@ Source: [`packages/ui/user-approval/src/index.ts:68`](../packages/ui/user-approv
|
||||
|
||||
Types: [StreamChunk](core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:219`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `assistant/message` — surface
|
||||
|
||||
@@ -167,7 +167,7 @@ Source: [`packages/core/session/src/types.ts:219`](../packages/core/session/src/
|
||||
|
||||
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:226`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `compact/*`
|
||||
|
||||
@@ -246,7 +246,7 @@ Source: [`packages/compact/compact/src/types.ts:22`](../packages/compact/compact
|
||||
|
||||
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:221`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `hook/*`
|
||||
|
||||
@@ -317,14 +317,14 @@ Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* Durable record of a prompt veto and its reason. It is log-only: the blocked
|
||||
* prompt never enters the model-visible surface, including in a mixed batch.
|
||||
* prompt never enters the model-visible surface, and its turn runs zero steps.
|
||||
*/
|
||||
'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }
|
||||
```
|
||||
|
||||
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:209`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `request/*`
|
||||
|
||||
@@ -338,7 +338,7 @@ Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/
|
||||
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `sandbox/*`
|
||||
|
||||
@@ -369,7 +369,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:34`](../packages/s
|
||||
|
||||
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `step/*`
|
||||
|
||||
@@ -380,7 +380,7 @@ Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/
|
||||
'step/end': { turn: number; step: number }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:194`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:202`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `step/start` — log-only
|
||||
|
||||
@@ -389,7 +389,7 @@ Source: [`packages/core/session/src/types.ts:194`](../packages/core/session/src/
|
||||
'step/start': { turn: number; step: number }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:192`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:200`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `todo/*`
|
||||
|
||||
@@ -402,7 +402,7 @@ Source: [`packages/core/session/src/types.ts:192`](../packages/core/session/src/
|
||||
|
||||
Types: [TodoItem](core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `tool/*`
|
||||
|
||||
@@ -419,7 +419,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/
|
||||
|
||||
Types: [CallId](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `tool/code-dispatch` — log-only
|
||||
|
||||
@@ -463,7 +463,7 @@ Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/c
|
||||
|
||||
Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `turn/*`
|
||||
|
||||
@@ -472,22 +472,23 @@ Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
|
||||
* fires the awaited `session/flush` checkpoint at every turn end, so the turn
|
||||
* boundary is also the durable-commit boundary.
|
||||
* awaits `session/flush` after an ordinary turn ends before claiming the next
|
||||
* queued item. Success commits the turn; rejection is reported live and does
|
||||
* not prevent later work.
|
||||
*/
|
||||
'turn/end': { turn: number; reason: TurnEndReason }
|
||||
```
|
||||
|
||||
Types: [TurnEndReason](core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:190`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:198`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `turn/start` — log-only
|
||||
|
||||
```ts persistence-catalog
|
||||
/**
|
||||
* Opens turn `turn`. `trigger` records what started it — a drained message
|
||||
* batch or an idle-time injection. The turn is the durability/replay
|
||||
* Opens turn `turn`. `trigger` records what started it — one claimed queued
|
||||
* message or an idle-time injection. The turn is the durability/replay
|
||||
* boundary: every event sits between a `turn/start` and its matching
|
||||
* `turn/end` (the turn-enclosure invariant).
|
||||
*/
|
||||
@@ -496,17 +497,17 @@ Source: [`packages/core/session/src/types.ts:190`](../packages/core/session/src/
|
||||
|
||||
Types: [TurnTrigger](core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:184`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:191`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `user/*`
|
||||
|
||||
#### `user/message` — surface
|
||||
|
||||
```ts persistence-catalog
|
||||
/** A user-visible prompt (queued message drained at turn start). */
|
||||
/** A user-visible prompt (the queued message claimed for this turn). */
|
||||
'user/message': { content: ContentBlock[]; source: MessageSource }
|
||||
```
|
||||
|
||||
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:196`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts)
|
||||
|
||||
6
docs/user/develop/basic/config.i18n.yaml
Normal file
6
docs/user/develop/basic/config.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
config.md: 26d2d48ebede74194fbf306aa97d214bdb99b722
|
||||
config.zh.md: 9ed389b16779f25c633d0c8772f8658197ba4322
|
||||
118
docs/user/develop/basic/config.md
Normal file
118
docs/user/develop/basic/config.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Plugin configuration
|
||||
|
||||
English | [中文](config.zh.md)
|
||||
|
||||
Accept configuration supplied through `cordis.yml`.
|
||||
|
||||
## Define the Config type
|
||||
|
||||
Export a `Config` type and a same-named Schemastery schema. Put defaults directly on the schema fields:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export interface Config {
|
||||
greeting: string
|
||||
maxRetries: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
greeting: Schema.string().default('Hello'),
|
||||
maxRetries: Schema.number().default(3),
|
||||
verbose: Schema.boolean().default(false),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
console.log(config.greeting) // User value or schema default.
|
||||
}
|
||||
```
|
||||
|
||||
Configure it in `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
When loading the plugin, Cordis uses the exported schema to validate configuration and fill defaults. Do not export a plain object as `Config`; it does not implement the Standard Schema interface required by Cordis.
|
||||
|
||||
## Schema validation
|
||||
|
||||
Use Schemastery to express stricter validation:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'validated-plugin'
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
timeout: number
|
||||
mode: 'fast' | 'accurate'
|
||||
}
|
||||
|
||||
export const Config = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
timeout: Schema.number().default(30000),
|
||||
mode: Schema.union(['fast', 'accurate']).default('fast'),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
// config is validated and type-safe.
|
||||
}
|
||||
```
|
||||
|
||||
The schema runs while the plugin loads. Invalid configuration fails the load with an actionable error.
|
||||
|
||||
## Design principles
|
||||
|
||||
### Do not hardcode tunable values
|
||||
|
||||
Harness requires **anything that two deployments may want to set differently to be a configuration field**.
|
||||
|
||||
```ts
|
||||
// Wrong: hardcoded timeout.
|
||||
const TIMEOUT = 30000
|
||||
|
||||
// Correct: configurable.
|
||||
export interface Config {
|
||||
timeoutMs: number // Defaults to 30000.
|
||||
}
|
||||
```
|
||||
|
||||
The test is whether `cordis.yml` can change the value without a code edit.
|
||||
|
||||
### Fail loudly on invalid configuration
|
||||
|
||||
If configuration refers to an unregistered LLM provider route or another nonexistent resource, fail early instead of silently skipping it:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface ModelConfig {
|
||||
provider: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: ModelConfig) {
|
||||
if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) {
|
||||
throw new Error(`LLM provider "${config.provider}" is not registered`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Work with HMR
|
||||
|
||||
A configuration edit hot-replaces the plugin: the framework unloads the old instance and loads a new one. Because registrations are effects and clean themselves up, replacement does not retain the old instance's registrations.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Plugins and lifecycle](../framework/) — understand the full plugin lifecycle
|
||||
- [Services and dependencies](../framework/service.md) — provide a service to other plugins
|
||||
@@ -1,24 +1,33 @@
|
||||
# 插件配置
|
||||
|
||||
[English](config.md) | 中文
|
||||
|
||||
让你的插件接受用户在 `cordis.yml` 中传入的配置。
|
||||
|
||||
## 定义 Config 类型
|
||||
|
||||
在插件中导出一个 `Config` 类型,`apply` 的第二个参数就是用户配置:
|
||||
在插件中导出一个 `Config` 类型和同名的 Schemastery schema;默认值直接写在 schema 中:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export interface Config {
|
||||
greeting?: string
|
||||
maxRetries?: number
|
||||
greeting: string
|
||||
maxRetries: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
greeting: Schema.string().default('Hello'),
|
||||
maxRetries: Schema.number().default(3),
|
||||
verbose: Schema.boolean().default(false),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
console.log(config.greeting ?? 'Hello') // 用户配置或默认值
|
||||
console.log(config.greeting) // User value or schema default.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,32 +40,32 @@ export function apply(ctx: Context, config: Config) {
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
只导出类型时,配置原样传入,默认值由代码自己兜底(如上面的 `??`)。想让框架代管默认值和校验,导出一个 schema(见下节)。
|
||||
插件加载时,Cordis 会通过导出的 schema 校验配置,并填充未提供字段的默认值。不要导出普通对象作为 `Config`,因为它不满足 Cordis 要求的 Standard Schema 接口。
|
||||
|
||||
## Schema 校验
|
||||
|
||||
对于需要默认值和严格校验的场景,额外导出一个 Schemastery schema(仓库约定以 `z` 引入)。加载时框架先用它校验并填充默认值,再把结果传给 `apply`:
|
||||
对于需要严格校验的场景,使用 Schemastery 定义 schema:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'validated-plugin'
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
timeout?: number
|
||||
mode?: 'fast' | 'accurate'
|
||||
timeout: number
|
||||
mode: 'fast' | 'accurate'
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z.string().required(),
|
||||
timeout: z.number().default(30000),
|
||||
mode: z.union(['fast', 'accurate'] as const).default('fast'),
|
||||
export const Config = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
timeout: Schema.number().default(30000),
|
||||
mode: Schema.union(['fast', 'accurate']).default('fast'),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
// config 已经过校验,类型安全,默认值已填充
|
||||
// config is validated and type-safe.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -69,13 +78,12 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载
|
||||
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
|
||||
|
||||
```ts
|
||||
// 错误 — 硬编码超时时间
|
||||
// Wrong: hardcoded timeout.
|
||||
const TIMEOUT = 30000
|
||||
|
||||
// 正确 — 可配置
|
||||
// Correct: configurable.
|
||||
export interface Config {
|
||||
/** 默认 30000 */
|
||||
timeoutMs?: number
|
||||
timeoutMs: number // Defaults to 30000.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -83,26 +91,23 @@ export interface Config {
|
||||
|
||||
### 配置错误要响亮
|
||||
|
||||
如果配置引用了不存在的东西(比如一个未注册的 LLM 提供方路由),应该尽早报错,而不是静默跳过:
|
||||
如果配置引用了未注册的 LLM 提供方路由或其他不存在的资源,应该尽早报错,而不是静默跳过:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface Config {
|
||||
export interface ModelConfig {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
export function apply(ctx: Context, config: ModelConfig) {
|
||||
if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) {
|
||||
throw new Error(`LLM provider "${config.provider}" is not registered`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
模型目录只用于发现;适配器可能接受目录之外的模型 ID,因此不能把 `listModels()` 当作请求白名单。
|
||||
|
||||
## 配合 HMR
|
||||
|
||||
配置变更会触发插件热替换:修改 `cordis.yml` 中某个插件的 `config`,框架会卸载旧实例、加载新实例。由于注册都是效果(自动清理),这个过程是安全的。
|
||||
@@ -110,4 +115,4 @@ export function apply(ctx: Context, config: Config) {
|
||||
## 下一步
|
||||
|
||||
- [插件与生命周期](../framework/) — 深入了解插件的完整生命周期
|
||||
- [服务与依赖](../framework/service) — 让你的插件对外提供服务
|
||||
- [服务与依赖](../framework/service.md) — 让你的插件对外提供服务
|
||||
6
docs/user/develop/basic/index.i18n.yaml
Normal file
6
docs/user/develop/basic/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: 5fa46806bc195ad2566fc0a29b45eb1dd7a68179
|
||||
index.zh.md: a6d238c12841c8c25b00376ee032e5db50fc6b4e
|
||||
151
docs/user/develop/basic/index.md
Normal file
151
docs/user/develop/basic/index.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Your first plugin
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This guide creates a minimal Harness plugin and loads it into an agent.
|
||||
|
||||
## What is a plugin?
|
||||
|
||||
In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Register capabilities here.
|
||||
}
|
||||
```
|
||||
|
||||
That is the complete shape.
|
||||
|
||||
## Create the plugin file
|
||||
|
||||
Create `src/my-plugin.ts` in your project:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'hello-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Required dependencies are ready before apply runs.
|
||||
console.log('[hello-plugin] plugin loaded!')
|
||||
}
|
||||
```
|
||||
|
||||
## Register it in cordis.yml
|
||||
|
||||
Add an entry to `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
After startup, the console prints `[hello-plugin] plugin loaded!`.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Anything registered through `ctx`—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually.
|
||||
|
||||
For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => {
|
||||
const timer = setInterval(() => {
|
||||
console.log('heartbeat')
|
||||
}, 5000)
|
||||
|
||||
// The returned function runs when the plugin unloads.
|
||||
return () => clearInterval(timer)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Declare dependencies
|
||||
|
||||
If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`:
|
||||
|
||||
```ts ignore-check
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-tool-plugin'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
The framework waits for every required service before loading the plugin.
|
||||
|
||||
## Three plugin forms
|
||||
|
||||
In addition to a function module, a plugin can use object or class form.
|
||||
|
||||
### Object form
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export default {
|
||||
name: 'my-plugin',
|
||||
inject: ['tools'],
|
||||
apply(ctx: Context) {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Class form
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MyService extends Service {
|
||||
static inject = ['tools']
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myService')
|
||||
// Perform synchronous initialization in the constructor.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md).
|
||||
|
||||
## Complete example
|
||||
|
||||
`examples/echo-agent/src/echo-tool.ts` is a plugin that registers a tool:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'echo-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'echo',
|
||||
description: 'Echo the given text back, uppercased.',
|
||||
parameters: {
|
||||
text: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Build a tool](./tool.md) — learn the tool definition DSL
|
||||
- [Plugin configuration](./config.md) — accept user configuration
|
||||
@@ -1,5 +1,7 @@
|
||||
# 第一个插件
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
本文带你编写一个最小的 Harness 插件并加载到 Agent 中。
|
||||
|
||||
## 插件是什么
|
||||
@@ -12,7 +14,7 @@ import type { Context } from 'cordis'
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 在这里注册能力
|
||||
// Register capabilities here.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,8 +30,8 @@ import type { Context } from 'cordis'
|
||||
export const name = 'hello-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// apply 函数体在插件加载时执行
|
||||
console.log('[hello-plugin] 插件已加载!')
|
||||
// Required dependencies are ready before apply runs.
|
||||
console.log('[hello-plugin] plugin loaded!')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,7 +44,7 @@ export function apply(ctx: Context) {
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
启动后你会在控制台看到 `[hello-plugin] 插件已加载!`。
|
||||
启动后你会在控制台看到 `[hello-plugin] plugin loaded!`。
|
||||
|
||||
## 自动清理
|
||||
|
||||
@@ -59,7 +61,7 @@ export function apply(ctx: Context) {
|
||||
console.log('heartbeat')
|
||||
}, 5000)
|
||||
|
||||
// 返回的函数会在插件卸载时被调用
|
||||
// The returned function runs when the plugin unloads.
|
||||
return () => clearInterval(timer)
|
||||
})
|
||||
}
|
||||
@@ -69,23 +71,15 @@ export function apply(ctx: Context) {
|
||||
|
||||
如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`:
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool-plugin'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 现在可用
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
// ctx.tools is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -99,7 +93,6 @@ export function apply(ctx: Context) {
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export default {
|
||||
name: 'my-plugin',
|
||||
@@ -114,23 +107,18 @@ export default {
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export default class MyService extends Service {
|
||||
static inject = ['tools']
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myService')
|
||||
}
|
||||
|
||||
// 服务的公开方法
|
||||
greet(name: string) {
|
||||
return `Hello, ${name}!`
|
||||
// Perform synchronous initialization in the constructor.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service))。
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service.md))。
|
||||
|
||||
## 完整示例
|
||||
|
||||
@@ -159,5 +147,5 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [开发一个 Tool](tool) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](config) — 让插件接受用户配置
|
||||
- [开发一个 Tool](./tool.md) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](./config.md) — 让插件接受用户配置
|
||||
6
docs/user/develop/basic/tool.i18n.yaml
Normal file
6
docs/user/develop/basic/tool.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
tool.md: 416733bcb584fa5303a8b3ba5e6e904302e7f992
|
||||
tool.zh.md: fce9a7d9b973853c8b4fb9ae2c034e749d8da999
|
||||
208
docs/user/develop/basic/tool.md
Normal file
208
docs/user/develop/basic/tool.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Build a tool
|
||||
|
||||
English | [中文](tool.zh.md)
|
||||
|
||||
A tool is a capability the model can call. This guide builds one with `defineTool`.
|
||||
|
||||
## Minimal example
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'greet',
|
||||
description: 'Greet someone by name.',
|
||||
parameters: {
|
||||
name: { type: 'string', required: true, description: 'The name to greet' },
|
||||
},
|
||||
async execute(args) {
|
||||
// args is inferred as { name: string }.
|
||||
return [{ type: 'text', text: `Hello, ${args.name}!` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Parameter definitions
|
||||
|
||||
`parameters` uses a compact format that the framework converts to the JSON Schema sent to the model.
|
||||
|
||||
### Primitive types
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'number' },
|
||||
recursive: { type: 'boolean' },
|
||||
}
|
||||
// Inferred type: { path: string; limit?: number; recursive?: boolean }
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
}
|
||||
// Inferred type: { mode: string } (enum values are validated at runtime)
|
||||
```
|
||||
|
||||
### Nested objects
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timeout: { type: 'number' },
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
// Inferred type: { options?: { timeout?: number; retries?: number } }
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
}
|
||||
// Inferred type: { tags?: string[] }
|
||||
```
|
||||
|
||||
### Property fields
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|------|------|------|
|
||||
| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | Value type |
|
||||
| `required` | `true` | Marks the property required and affects inference |
|
||||
| `description` | `string` | Description sent to the model |
|
||||
| `enum` | `string[]` | Allowed string values |
|
||||
| `properties` | `SchemaSpec` | Nested properties for an object |
|
||||
| `items` | `SchemaProp` | Element schema for an array |
|
||||
|
||||
## The execute function
|
||||
|
||||
`execute` receives validated, inferred `args` and an `exec` execution context:
|
||||
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'example',
|
||||
description: 'Return an example result.',
|
||||
parameters: {},
|
||||
async execute(args, exec) {
|
||||
// args: inferred from parameters
|
||||
// exec: ToolExecution context
|
||||
|
||||
// Return a ContentBlock array.
|
||||
void args
|
||||
void exec
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Return value
|
||||
|
||||
`execute` returns a `ContentBlock[]` that becomes the tool result visible to the model:
|
||||
|
||||
```ts ignore-check
|
||||
// Text result
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
|
||||
// Multiple blocks
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
```
|
||||
|
||||
### Argument validation
|
||||
|
||||
Before calling `execute`, `defineTool` validates model-generated arguments. Invalid input raises `ToolArgsError`; the framework turns it into an `isError` result so the model can correct its call.
|
||||
|
||||
Do not repeat type validation inside `execute`.
|
||||
|
||||
## Presentation
|
||||
|
||||
A tool can define UI presentation methods for terminal and ACP clients:
|
||||
|
||||
```ts ignore-check
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
// ...
|
||||
presentCall(args) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`presentCall` and `presentResult` are **pure functions**. Streaming UI and session replay may call them more than once.
|
||||
|
||||
## Registration and unloading
|
||||
|
||||
`ctx.tools.register()` returns a disposer, but a registration made through `ctx` is already tracked by the framework. Unloading the plugin removes the tool automatically, so the plugin does not call the disposer itself.
|
||||
|
||||
```ts ignore-check
|
||||
// This is sufficient:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
// No saved disposer or extra cleanup registration is needed.
|
||||
```
|
||||
|
||||
## Complete example
|
||||
|
||||
This tool counts files in a directory:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
export const name = 'file-counter'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'count_files',
|
||||
description: 'Count files in a directory.',
|
||||
parameters: {
|
||||
path: { type: 'string', required: true, description: 'Directory path' },
|
||||
extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' },
|
||||
},
|
||||
async execute(args) {
|
||||
const entries = await readdir(args.path, { withFileTypes: true })
|
||||
let files = entries.filter(e => e.isFile())
|
||||
if (args.extension) {
|
||||
files = files.filter(f => f.name.endsWith(args.extension!))
|
||||
}
|
||||
return [{ type: 'text', text: `Found ${files.length} files.` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Plugin configuration](./config.md) — make the tool configurable
|
||||
- [Capability layering](../practice/) — understand the interface/implementation/consumer pattern
|
||||
@@ -1,5 +1,7 @@
|
||||
# 开发一个 Tool
|
||||
|
||||
[English](tool.md) | 中文
|
||||
|
||||
Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。
|
||||
|
||||
## 最小示例
|
||||
@@ -19,7 +21,7 @@ export function apply(ctx: Context) {
|
||||
name: { type: 'string', required: true, description: 'The name to greet' },
|
||||
},
|
||||
async execute(args) {
|
||||
// args 自动推导为 { name: string }
|
||||
// args is inferred as { name: string }.
|
||||
return [{ type: 'text', text: `Hello, ${args.name}!` }]
|
||||
},
|
||||
}))
|
||||
@@ -33,33 +35,27 @@ export function apply(ctx: Context) {
|
||||
### 基本类型
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'number' },
|
||||
recursive: { type: 'boolean' },
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { path: string; limit?: number; recursive?: boolean }
|
||||
}
|
||||
// Inferred type: { path: string; limit?: number; recursive?: boolean }
|
||||
```
|
||||
|
||||
### 枚举
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { mode: string } (运行时校验 enum 值)
|
||||
}
|
||||
// Inferred type: { mode: string } (enum values are validated at runtime)
|
||||
```
|
||||
|
||||
### 嵌套对象
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -67,22 +63,20 @@ const parameters = {
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { options?: { timeout?: number; retries?: number } }
|
||||
}
|
||||
// Inferred type: { options?: { timeout?: number; retries?: number } }
|
||||
```
|
||||
|
||||
### 数组
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { tags?: string[] }
|
||||
}
|
||||
// Inferred type: { tags?: string[] }
|
||||
```
|
||||
|
||||
### 每个属性的字段
|
||||
@@ -103,15 +97,17 @@ const parameters = {
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
export const tool = defineTool({
|
||||
name: 'example',
|
||||
description: 'Return an example result.',
|
||||
parameters: {},
|
||||
async execute(args, exec) {
|
||||
// args: 根据 parameters 自动推导的类型
|
||||
// exec: ToolExecution 对象,提供执行上下文
|
||||
// args: inferred from parameters
|
||||
// exec: ToolExecution context
|
||||
|
||||
// 返回 ContentBlock 数组
|
||||
// Return a ContentBlock array.
|
||||
void args
|
||||
void exec
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
},
|
||||
})
|
||||
@@ -121,23 +117,15 @@ defineTool({
|
||||
|
||||
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
|
||||
|
||||
```ts
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
```ts ignore-check
|
||||
// Text result
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
|
||||
declare const matchResults: string[]
|
||||
|
||||
// 文本结果
|
||||
function textResult(): ContentBlock[] {
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
}
|
||||
|
||||
// 多个 block
|
||||
function multiBlockResult(): ContentBlock[] {
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
}
|
||||
// Multiple blocks
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
```
|
||||
|
||||
### 参数校验
|
||||
@@ -150,22 +138,14 @@ function multiBlockResult(): ContentBlock[] {
|
||||
|
||||
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result:
|
||||
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
```ts ignore-check
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
description: 'Run a shell command.',
|
||||
parameters: {
|
||||
command: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `ran: ${args.command}` }]
|
||||
},
|
||||
// ...
|
||||
presentCall(args) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command.slice(0, 60),
|
||||
title: args.command,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
@@ -183,25 +163,11 @@ defineTool({
|
||||
|
||||
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
```ts ignore-check
|
||||
// This is sufficient:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
// 这样就够了:
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'noop',
|
||||
description: 'Do nothing.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
|
||||
// 不需要:
|
||||
// const dispose = ctx.tools.register(...)
|
||||
// ctx.effect(() => dispose)
|
||||
// No saved disposer or extra cleanup registration is needed.
|
||||
```
|
||||
|
||||
## 完整实战示例
|
||||
@@ -238,5 +204,5 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件配置](config) — 让你的 tool 可配置
|
||||
- [插件配置](./config.md) — 让你的 tool 可配置
|
||||
- [能力三件套](../practice/) — 了解 seam/impl/consumer 模式
|
||||
6
docs/user/develop/framework/events.i18n.yaml
Normal file
6
docs/user/develop/framework/events.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
events.md: 0c57681a55ea0200fe8f33293176fc94f09a4ce5
|
||||
events.zh.md: 3e14739d4a97ba014d545c9f226000507aaeacef
|
||||
143
docs/user/develop/framework/events.md
Normal file
143
docs/user/develop/framework/events.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Event system
|
||||
|
||||
English | [中文](events.zh.md)
|
||||
|
||||
Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points.
|
||||
|
||||
## Basic use
|
||||
|
||||
### Listen for an event
|
||||
|
||||
```ts ignore-check
|
||||
ctx.on('event-name', (payload) => {
|
||||
// Handle the event.
|
||||
})
|
||||
```
|
||||
|
||||
### Emit an event
|
||||
|
||||
```ts ignore-check
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## Event modes
|
||||
|
||||
Cordis provides several event modes for different interaction contracts.
|
||||
|
||||
### emit — broadcast
|
||||
|
||||
Every listener runs synchronously and return values are ignored:
|
||||
|
||||
```ts ignore-check
|
||||
// Emit
|
||||
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
||||
|
||||
// Listen
|
||||
ctx.on('my-plugin/ready', ({ id }) => {
|
||||
console.log(`${id} is ready`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — short circuit
|
||||
|
||||
Listeners run in order; the first non-`undefined` result becomes the final result:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// Listen: a returned value stops later listeners.
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// Return undefined to continue to the next listener.
|
||||
})
|
||||
```
|
||||
|
||||
### serial — ordered execution
|
||||
|
||||
Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution:
|
||||
|
||||
```ts ignore-check
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — pipeline
|
||||
|
||||
Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call vetoes the pipeline:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
||||
|
||||
// Listen: next() is mandatory.
|
||||
ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.trim()
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
A waterfall listener **must call `next()`**. Omitting it vetoes the pipeline by design, enabling interception and gateway behavior.
|
||||
:::
|
||||
|
||||
## Typed events
|
||||
|
||||
Harness uses TypeScript declaration merging for type-safe events:
|
||||
|
||||
```ts
|
||||
import 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
||||
// are now inferred correctly.
|
||||
```
|
||||
|
||||
## Cordis events and session records
|
||||
|
||||
Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/step-result`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes.
|
||||
|
||||
`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`.
|
||||
|
||||
## Event listeners are effects
|
||||
|
||||
A listener registered with `ctx.on()` is removed automatically when its plugin unloads:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// This listener is removed when the plugin disposes.
|
||||
ctx.on('tools/result', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## Example: logging plugin
|
||||
|
||||
This plugin logs tool calls and results:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const text = result.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Capability layering](../practice/) — understand events within capability interfaces
|
||||
- [LLM adapters](../practice/llm-adapter.md) — implement a complete LLM backend
|
||||
143
docs/user/develop/framework/events.zh.md
Normal file
143
docs/user/develop/framework/events.zh.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 事件系统
|
||||
|
||||
[English](events.md) | 中文
|
||||
|
||||
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 监听事件
|
||||
|
||||
```ts ignore-check
|
||||
ctx.on('event-name', (payload) => {
|
||||
// Handle the event.
|
||||
})
|
||||
```
|
||||
|
||||
### 触发事件
|
||||
|
||||
```ts ignore-check
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## 事件模式
|
||||
|
||||
Cordis 提供多种事件触发模式,适用于不同场景:
|
||||
|
||||
### emit — 广播
|
||||
|
||||
所有监听器同步执行,不关心返回值:
|
||||
|
||||
```ts ignore-check
|
||||
// Emit
|
||||
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
||||
|
||||
// Listen
|
||||
ctx.on('my-plugin/ready', ({ id }) => {
|
||||
console.log(`${id} is ready`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — 短路
|
||||
|
||||
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// Listen: a returned value stops later listeners.
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// Return undefined to continue to the next listener.
|
||||
})
|
||||
```
|
||||
|
||||
### serial — 顺序执行
|
||||
|
||||
监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
|
||||
|
||||
```ts ignore-check
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — 管道
|
||||
|
||||
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
||||
|
||||
// Listen: next() is mandatory.
|
||||
ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.trim()
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。
|
||||
:::
|
||||
|
||||
## Typed Events
|
||||
|
||||
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
|
||||
|
||||
```ts
|
||||
import 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
||||
// are now inferred correctly.
|
||||
```
|
||||
|
||||
## Cordis 事件与会话记录
|
||||
|
||||
Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。
|
||||
|
||||
`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。
|
||||
|
||||
## 事件也是效果
|
||||
|
||||
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// This listener is removed when the plugin disposes.
|
||||
ctx.on('tools/result', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## 实战示例:日志插件
|
||||
|
||||
一个记录所有 tool 调用的简单插件:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const text = result.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
|
||||
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端
|
||||
6
docs/user/develop/framework/index.i18n.yaml
Normal file
6
docs/user/develop/framework/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: 79e925b54509da41535735527e283850384257ec
|
||||
index.zh.md: 62be8c706510704f7b07286f166f14fa81235a0a
|
||||
136
docs/user/develop/framework/index.md
Normal file
136
docs/user/develop/framework/index.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Plugins and lifecycle
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This page describes the Cordis plugin model and lifecycle state machine.
|
||||
|
||||
## Fiber state machine
|
||||
|
||||
Every loaded plugin owns a **Fiber** scope with the following states:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE
|
||||
↘ FAILED
|
||||
ACTIVE → UNLOADING → DISPOSED
|
||||
```
|
||||
|
||||
| State | Meaning |
|
||||
|------|------|
|
||||
| PENDING | Declared, but required dependencies are not ready |
|
||||
| LOADING | Dependencies are ready and `apply` is running |
|
||||
| ACTIVE | The plugin is running |
|
||||
| FAILED | `apply` threw an error |
|
||||
| UNLOADING | The plugin is unloading and disposing resources |
|
||||
| DISPOSED | The plugin is fully unloaded |
|
||||
|
||||
## Dependency-driven loading
|
||||
|
||||
A plugin with `inject` waits for every required service before loading:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools and ctx.llm are ready here.
|
||||
}
|
||||
```
|
||||
|
||||
If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Every registration made through `ctx` is undone when the plugin unloads:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Event listener: removed automatically on unload.
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// Custom resource: the returned disposer runs on unload.
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The framework tracks and disposes all of these operations:
|
||||
- `ctx.on(event, handler)` — event listener
|
||||
- `ctx.tools.register(tool)` — tool registration
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM adapter registration
|
||||
- `ctx.effect(() => cleanup)` — custom resource
|
||||
|
||||
During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single `ctx.effect()` and await its steps serially there.
|
||||
|
||||
## Nested contexts
|
||||
|
||||
`ctx.plugin()` creates a child Fiber that inherits the parent context but has an independent lifecycle:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Register a child plugin.
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// The child has its own Fiber and unloads with its parent.
|
||||
}
|
||||
```
|
||||
|
||||
## Dispose semantics
|
||||
|
||||
To stop a plugin instance early:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare const ctx: Context
|
||||
declare function myPlugin(ctx: Context): void
|
||||
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// Dispose it manually later.
|
||||
await fiber.dispose()
|
||||
```
|
||||
|
||||
`dispose` guarantees:
|
||||
1. All registrations owned by the plugin are removed.
|
||||
2. Child plugins are recursively unloaded.
|
||||
3. The returned promise resolves after all asynchronous cleanup finishes.
|
||||
|
||||
## Hot replacement (HMR)
|
||||
|
||||
With `@cordisjs/plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
|
||||
|
||||
1. Unload the old plugin and clean up its registrations.
|
||||
2. Load the new code.
|
||||
3. Run the new `apply`.
|
||||
|
||||
Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance.
|
||||
|
||||
## Example lifecycle
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
ctx.effect(() => {
|
||||
console.log('effect registered')
|
||||
return () => console.log('effect cleaned up')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Loading prints:
|
||||
```
|
||||
plugin loading
|
||||
effect registered
|
||||
```
|
||||
|
||||
Unloading prints:
|
||||
```
|
||||
effect cleaned up
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Services and dependencies](./service.md) — expose a capability to other plugins
|
||||
- [Event system](./events.md) — communicate between plugins
|
||||
@@ -1,5 +1,7 @@
|
||||
# 插件与生命周期
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
深入了解 Cordis 插件模型和生命周期状态机。
|
||||
|
||||
## Fiber 状态机
|
||||
@@ -25,15 +27,11 @@ ACTIVE → UNLOADING → DISPOSED
|
||||
|
||||
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 到这里时,ctx.tools 和 ctx.llm 一定存在
|
||||
// ctx.tools and ctx.llm are ready here.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -43,23 +41,12 @@ export function apply(ctx: Context) {
|
||||
|
||||
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/some-event'(): void
|
||||
}
|
||||
}
|
||||
|
||||
declare function handler(): void
|
||||
declare function createConnection(): { close(): void }
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// 事件监听——卸载时自动移除
|
||||
ctx.on('my-plugin/some-event', handler)
|
||||
// Event listener: removed automatically on unload.
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// 自定义资源——卸载时调用返回的函数
|
||||
// Custom resource: the returned disposer runs on unload.
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
@@ -73,22 +60,18 @@ export function apply(ctx: Context) {
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册
|
||||
- `ctx.effect(() => cleanup)` — 自定义资源
|
||||
|
||||
插件卸载时,这些注册按倒序逐个撤销。
|
||||
插件卸载时,处置器按注册顺序的反向发起,但多个异步处置器会并发执行,不保证逐个完成。存在顺序依赖的清理步骤必须放进同一个 `ctx.effect()` 返回的处置器中,由该处置器负责串行等待。
|
||||
|
||||
## 嵌套上下文
|
||||
|
||||
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare function childPlugin(ctx: Context): void
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// 注册一个子插件
|
||||
// Register a child plugin.
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// 子插件有自己的 Fiber,父卸载时子也卸载
|
||||
// The child has its own Fiber and unloads with its parent.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,7 +87,7 @@ declare function myPlugin(ctx: Context): void
|
||||
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// 之后可以手动 dispose
|
||||
// Dispose it manually later.
|
||||
await fiber.dispose()
|
||||
```
|
||||
|
||||
@@ -125,11 +108,7 @@ await fiber.dispose()
|
||||
|
||||
## 实战:理解生命周期
|
||||
|
||||
`apply` 函数体就是加载钩子;卸载没有专门的事件——把清理逻辑放进 `ctx.effect()` 的返回函数即可:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
@@ -153,5 +132,5 @@ effect cleaned up
|
||||
|
||||
## 下一步
|
||||
|
||||
- [服务与依赖](service) — 让你的插件对外提供能力
|
||||
- [事件系统](events) — 插件间通信的核心机制
|
||||
- [服务与依赖](./service.md) — 让你的插件对外提供能力
|
||||
- [事件系统](./events.md) — 插件间通信的核心机制
|
||||
6
docs/user/develop/framework/service.i18n.yaml
Normal file
6
docs/user/develop/framework/service.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
service.md: 1bf28cb3c7dfdfbd6d0babfa3b1688ac65eea01e
|
||||
service.zh.md: 17785c056ab9a0a21974e6ed8bbe7f7de05fa00e
|
||||
148
docs/user/develop/framework/service.md
Normal file
148
docs/user/develop/framework/service.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Services and dependencies
|
||||
|
||||
English | [中文](service.zh.md)
|
||||
|
||||
A service is a capability one plugin exposes to other plugins. `inject` declares the services a plugin requires.
|
||||
|
||||
## What is a service?
|
||||
|
||||
In Harness, `tools`, `llm`, and `agents` are services. Each is a named capability mounted on `ctx`:
|
||||
|
||||
```ts ignore-check
|
||||
ctx.tools // ToolRegistry service
|
||||
ctx.llm // LLM service
|
||||
ctx.agents // Agent service
|
||||
```
|
||||
|
||||
Any plugin can provide a service for other plugins to consume.
|
||||
|
||||
## Consume a service
|
||||
|
||||
Declare `inject` to use an existing service:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools exists and is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
When `apply` runs, every service declared by `inject` is ready. If a service is not ready, the plugin waits instead of running.
|
||||
|
||||
## Provide a service
|
||||
|
||||
### Extend Service
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // A service may depend on other services.
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' is the service name.
|
||||
}
|
||||
|
||||
// Public service method.
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After loading this plugin, consumers access the service as `ctx.metrics`:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.metrics.record('tool_call', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Declare its type
|
||||
|
||||
Use TypeScript declaration merging to type `ctx.metrics`:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
metrics: MetricsService
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics')
|
||||
}
|
||||
|
||||
record(event: string, value: number) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency behavior
|
||||
|
||||
### Required and optional dependencies
|
||||
|
||||
```ts ignore-check
|
||||
// Required: the plugin does not load while the service is absent.
|
||||
export const inject = ['tools']
|
||||
|
||||
// Optional: omit inject and query with ctx.get() at the use site.
|
||||
export function apply(ctx: Context) {
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### When a service disappears
|
||||
|
||||
If a required service disappears while the application is running, for example because its provider unloads:
|
||||
|
||||
1. Dependent plugins dispose automatically.
|
||||
2. They load again when the service returns.
|
||||
|
||||
This prevents a plugin from calling a service that no longer exists.
|
||||
|
||||
## Service isolation
|
||||
|
||||
`cordis.yml` can isolate services so separate plugin groups see separate instances of the same service:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 5000
|
||||
- name: './src/plugin-a.ts'
|
||||
|
||||
- id: group-b
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` and `plugin-b` each see the Bash instance in their own group, with no cross-group effect.
|
||||
|
||||
## Built-in Harness services
|
||||
|
||||
The repository generates the service names, public methods, and source locations in the [service catalog](../../../cordis-catalog/services.md). Use that catalog and the service's TypeScript interface while developing a plugin; do not maintain a second static list.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Event system](./events.md) — communicate between plugins without tight coupling
|
||||
- [Capability layering](../practice/) — use services as capability interfaces
|
||||
@@ -1,22 +1,17 @@
|
||||
# 服务与依赖
|
||||
|
||||
[English](service.md) | 中文
|
||||
|
||||
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
|
||||
|
||||
## 什么是服务
|
||||
|
||||
在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
ctx.tools // ToolRegistry 服务
|
||||
ctx.llm // LLM 服务
|
||||
ctx.agents // Agent 注册表服务
|
||||
```ts ignore-check
|
||||
ctx.tools // ToolRegistry service
|
||||
ctx.llm // LLM service
|
||||
ctx.agents // Agent service
|
||||
```
|
||||
|
||||
任何插件都可以提供一个新服务,供其他插件使用。
|
||||
@@ -25,22 +20,12 @@ ctx.agents // Agent 注册表服务
|
||||
|
||||
声明 `inject` 来使用已有服务:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 在这里一定存在且就绪
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
// ctx.tools exists and is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -52,16 +37,15 @@ export function apply(ctx: Context) {
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // 本服务也可以依赖其他服务
|
||||
static inject = ['llm'] // A service may depend on other services.
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' 是服务名
|
||||
super(ctx, 'metrics') // 'metrics' is the service name.
|
||||
}
|
||||
|
||||
// 服务的公开方法
|
||||
// Public service method.
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
@@ -70,9 +54,7 @@ export default class MetricsService extends Service {
|
||||
|
||||
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
@@ -104,18 +86,14 @@ export default class MetricsService extends Service {
|
||||
|
||||
## 依赖的行为
|
||||
|
||||
### 必选依赖 vs 可选读取
|
||||
### 必选依赖 vs 可选依赖
|
||||
|
||||
`inject` 声明的依赖都是必选的:服务不存在时,插件不会加载。如果只想"有则用之",用 `ctx.get()` 读取——服务不存在时返回 `undefined`,插件照常加载:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
// 必选:服务不存在时,插件不会加载
|
||||
```ts ignore-check
|
||||
// Required: the plugin does not load while the service is absent.
|
||||
export const inject = ['tools']
|
||||
|
||||
// Optional: omit inject and query with ctx.get() at the use site.
|
||||
export function apply(ctx: Context) {
|
||||
// 可选读取:不声明 inject,服务不存在时返回 undefined
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
@@ -132,7 +110,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 服务隔离
|
||||
|
||||
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例。用 `@cordisjs/plugin-group` 建组(`group: true` 标记组条目),并在组上声明 `isolate`,把该服务隔离进组内作用域:
|
||||
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
@@ -158,24 +136,13 @@ export function apply(ctx: Context) {
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。`isolate: { bash: true }` 是必需的:不隔离的话,两个组在同一作用域注册同名服务,第二个会直接报重复注册错误。
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。
|
||||
|
||||
## Harness 内置服务一览
|
||||
## Harness 内置服务
|
||||
|
||||
| 服务名 | 提供者 | 用途 |
|
||||
|--------|--------|------|
|
||||
| `tools` | dsh-tools | Tool 注册表 |
|
||||
| `llm` | dsh-llm | LLM 调用 + 适配器注册 |
|
||||
| `agents` | dsh-agent | Agent 注册表 |
|
||||
| `agentLoop` | dsh-agent-loop | Agent 创建与循环执行 |
|
||||
| `sessions` | dsh-session | 会话存储与事件流 |
|
||||
| `systemPrompt` | dsh-system-prompt | 系统提示词组装 |
|
||||
| `bash` | dsh-bash(实现:dsh-bash-local) | Bash 命令执行 |
|
||||
| `fs` | dsh-fs(实现:dsh-fs-local) | 文件系统操作 |
|
||||
| `subagents` | dsh-subagent | 子代理委派 |
|
||||
| `sessionPersistence` | dsh-session-persistence(实现:-jsonl / -sqlite) | 会话持久化 |
|
||||
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [事件系统](events) — 插件间松耦合通信
|
||||
- [事件系统](./events.md) — 插件间松耦合通信
|
||||
- [能力三件套](../practice/) — 服务在 seam 模式中的应用
|
||||
6
docs/user/develop/practice/index.i18n.yaml
Normal file
6
docs/user/develop/practice/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: 0261b49b071167f7c2a33f78bbc1959cc6f1879f
|
||||
index.zh.md: 5819344430fcbde31bf825e9815120983e44e3f6
|
||||
158
docs/user/develop/practice/index.md
Normal file
158
docs/user/develop/practice/index.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Three-layer capability design
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
When a capability is general enough to need replaceable implementations, such as Bash execution, Harness splits it into three packages: an **interface**, an **implementation**, and a **consumer**. Each layer can evolve or be replaced independently.
|
||||
|
||||
## Bash example
|
||||
|
||||
The Bash execution capability consists of:
|
||||
|
||||
- **Interface** (`dsh-bash`) — defines Bash request and result shapes
|
||||
- **Implementation** (`dsh-bash-local`) — executes commands on the local machine
|
||||
- **Consumer** (`dsh-tool-bash`) — exposes the capability as a model-callable tool
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
|
||||
│ (interface) │ │ (implementation) │ │(consumer/tool)│
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
▲ │
|
||||
└────────────────────────────────────────────┘
|
||||
inject: ['bash']
|
||||
```
|
||||
|
||||
## Benefits of the split
|
||||
|
||||
### Replace implementations
|
||||
|
||||
One interface can have multiple implementations selected through `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
# Local execution
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# Or a future remote sandbox implementation
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
```
|
||||
|
||||
The interface and tool remain unchanged while the implementation changes.
|
||||
|
||||
### Evolve independently
|
||||
|
||||
- The interface changes rarely after its contract stabilizes.
|
||||
- Implementations can improve performance and security independently.
|
||||
- Consumers can change how they present the capability to the model.
|
||||
|
||||
### Decouple dependencies
|
||||
|
||||
- The implementation depends on the interface.
|
||||
- The consumer depends on the interface.
|
||||
- The implementation and consumer **do not depend on each other**.
|
||||
|
||||
## Built-in three-layer capabilities
|
||||
|
||||
| Capability | Interface | Implementation | Consumer |
|
||||
|------|-------------|------|---------------|
|
||||
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
|
||||
| Filesystem | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| Subagent | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| Compaction | `dsh-compact` | `dsh-compact-basic` | The implementation consumes agent-loop extension events |
|
||||
|
||||
## Develop a three-layer capability
|
||||
|
||||
### Step 1: define the interface
|
||||
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap/src/index.ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
myCap: MyCapService
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MyCapService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myCap')
|
||||
}
|
||||
|
||||
/** Execute the capability. */
|
||||
abstract execute(request: MyCapRequest): Promise<MyCapResult>
|
||||
}
|
||||
|
||||
export interface MyCapRequest {
|
||||
input: string
|
||||
}
|
||||
|
||||
export interface MyCapResult {
|
||||
output: string
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: write an implementation
|
||||
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap-local/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
|
||||
|
||||
class MyCapLocal extends MyCapService {
|
||||
async execute(request: MyCapRequest): Promise<MyCapResult> {
|
||||
// Concrete implementation.
|
||||
return { output: request.input.toUpperCase() }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'my-cap-local'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(MyCapLocal)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: write a consumer
|
||||
|
||||
```ts ignore-check
|
||||
// packages/my-cap/tool-my-cap/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-my-cap'
|
||||
export const inject = ['tools', 'myCap']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'my_cap',
|
||||
description: 'Execute my capability.',
|
||||
parameters: {
|
||||
input: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
const result = await ctx.myCap.execute({ input: args.input })
|
||||
return [{ type: 'text', text: result.output }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Compose them in cordis.yml
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-my-cap-local'
|
||||
- name: '@deepseek-ai/dsh-tool-my-cap'
|
||||
```
|
||||
|
||||
## Design points
|
||||
|
||||
- **Do not split preemptively** — use three packages only when the capability needs replaceable implementations. A simple tool plugin does not.
|
||||
- **The interface owns Request/Result types** — implementations and consumers depend only on the interface package.
|
||||
- **Explicit > implicit** — resolve defaults in an explicit `resolve(request): Spec` step rather than hiding `?? default` expressions inside `run()`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [LLM adapter](./llm-adapter.md) — implement an LLM backend, a common capability interface extension
|
||||
@@ -1,5 +1,7 @@
|
||||
# 能力的三层拆分
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
当一个能力(插件)足够通用(比如"执行 bash 命令"),Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。
|
||||
|
||||
## 以 Bash 为例
|
||||
@@ -13,7 +15,7 @@
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
|
||||
│ (接口) │ │ (实现) │ │ (消费者/tool)│
|
||||
│ (interface) │ │ (implementation) │ │(consumer/tool)│
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
▲ │
|
||||
└────────────────────────────────────────────┘
|
||||
@@ -27,10 +29,10 @@
|
||||
同一个接口可以有多种实现。用户通过 `cordis.yml` 选择:
|
||||
|
||||
```yaml
|
||||
# 本地执行
|
||||
# Local execution
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# 或:远程沙箱执行(未来)
|
||||
# Or a future remote sandbox implementation
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
@@ -58,13 +60,13 @@
|
||||
| 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | (内置于 agent-loop) |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 |
|
||||
|
||||
## 开发你自己的三件套
|
||||
|
||||
### 第一步:定义接口
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap/src/index.ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
@@ -79,7 +81,7 @@ export abstract class MyCapService extends Service {
|
||||
super(ctx, 'myCap')
|
||||
}
|
||||
|
||||
/** 执行能力的核心方法 */
|
||||
/** Execute the capability. */
|
||||
abstract execute(request: MyCapRequest): Promise<MyCapResult>
|
||||
}
|
||||
|
||||
@@ -101,7 +103,7 @@ import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/
|
||||
|
||||
class MyCapLocal extends MyCapService {
|
||||
async execute(request: MyCapRequest): Promise<MyCapResult> {
|
||||
// 具体实现
|
||||
// Concrete implementation.
|
||||
return { output: request.input.toUpperCase() }
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 第三步:编写消费者 (tool)
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
// packages/my-cap/tool-my-cap/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -140,7 +142,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 在 cordis.yml 中组合
|
||||
|
||||
```yaml ignore-check
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-my-cap-local'
|
||||
- name: '@deepseek-ai/dsh-tool-my-cap'
|
||||
```
|
||||
@@ -153,4 +155,4 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [LLM 适配器](llm-adapter) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
- [LLM 适配器](./llm-adapter.md) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
6
docs/user/develop/practice/llm-adapter.i18n.yaml
Normal file
6
docs/user/develop/practice/llm-adapter.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
llm-adapter.md: f34fc9e1d5b59a323bb562764821ef910025880e
|
||||
llm-adapter.zh.md: 3c781ae8a1a011e2f73d5f6de43f6f75e1fb549f
|
||||
185
docs/user/develop/practice/llm-adapter.md
Normal file
185
docs/user/develop/practice/llm-adapter.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# LLM adapters
|
||||
|
||||
English | [中文](llm-adapter.zh.md)
|
||||
|
||||
This guide connects a new LLM provider to Harness.
|
||||
|
||||
## Overview
|
||||
|
||||
An LLM adapter extends `LlmAdapter` and implements `stream()`, translating Harness's provider-neutral request into a provider API call and translating the response back into Harness chunks.
|
||||
|
||||
## Minimal implementation
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class MyAdapter extends LlmAdapter {
|
||||
private apiKey: string
|
||||
|
||||
constructor(apiKey: string) {
|
||||
super()
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
// 1. Convert options.messages to the provider format.
|
||||
// 2. Call the streaming API.
|
||||
// 3. Convert the response into StreamChunk values.
|
||||
}
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
models: string[]
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
models: Schema.array(Schema.string()).required(),
|
||||
})
|
||||
|
||||
export const name = 'my-llm-adapter'
|
||||
export const inject = ['llm']
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
const adapter = new MyAdapter(config.apiKey)
|
||||
ctx.llm.registerAdapter(config.models, adapter)
|
||||
}
|
||||
```
|
||||
|
||||
## StreamChunk protocol
|
||||
|
||||
`stream()` yields chunks using this protocol:
|
||||
|
||||
```ts
|
||||
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
async function* exampleChunks(): AsyncIterable<StreamChunk> {
|
||||
// 1. Start each content block with block-start.
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
|
||||
// 2. Stream text through text-delta.
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
|
||||
// 3. End each content block with block-end and the complete block.
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
|
||||
// 4. Tool-call block.
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
index: 1,
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
argumentsDelta: '{"command":"ls"}',
|
||||
}
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 1,
|
||||
block: {
|
||||
type: 'tool-call',
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
arguments: '{"command":"ls"}',
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Token usage.
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. Finish reason.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// Alternatively, { kind: 'tool-calls' } requests tool execution.
|
||||
}
|
||||
```
|
||||
|
||||
### Key rules
|
||||
|
||||
- Every `block-start` has a matching `block-end`.
|
||||
- `index` increases from 0 and identifies content-block order.
|
||||
- A `tool-call-delta` carries raw JSON text in `argumentsDelta`, either all at once or over multiple chunks.
|
||||
- `finish` is the final chunk.
|
||||
- Emit `usage` before `finish`.
|
||||
|
||||
## GenerateOptions
|
||||
|
||||
`stream()` receives the exported `GenerateOptions` type. It includes the model, conversation history, system prompt, tool schemas, generation parameters, stop sequences, and abort signal; treat the TypeScript type exported by `@deepseek-ai/dsh-llm` as authoritative. Map supported fields to the provider API. If the provider cannot honor a field, throw `LlmError` with a stable code instead of silently dropping it.
|
||||
|
||||
## Register an adapter
|
||||
|
||||
```ts ignore-check
|
||||
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
```
|
||||
|
||||
The first argument lists the model names handled by the adapter. If `cordis.yml` selects `model: model-name-1`, the service routes that request to this adapter.
|
||||
|
||||
## Use it from cordis.yml
|
||||
|
||||
```yaml
|
||||
- id: my-llm
|
||||
name: './src/my-llm-adapter.ts'
|
||||
config:
|
||||
apiKey: !!js process.env.MY_API_KEY
|
||||
models:
|
||||
- my-model-v1
|
||||
- my-model-v2
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: my-model-v1 # References the model registered above.
|
||||
```
|
||||
|
||||
## Reference implementations
|
||||
|
||||
The repository contains complete implementations:
|
||||
|
||||
- `packages/llm/llm-deepseek/` — DeepSeek API adapter using the OpenAI-compatible format
|
||||
- `packages/llm/llm-pi-ai/` — Pi AI adapter using a different API format
|
||||
- `examples/echo-agent/src/mock-llm.ts` — minimal local teaching adapter
|
||||
|
||||
Start with the mock adapter to study a complete chunk sequence without network behavior.
|
||||
|
||||
## Error handling
|
||||
|
||||
Adapters throw transport and protocol failures as `LlmError` values with stable codes. The agent loop preserves the error and code for diagnostics and policy; it does not convert an ordinary `Error` automatically. Every provider HTTP request must also merge `attributionHeaders()` and forward `options.signal`.
|
||||
|
||||
```ts
|
||||
import {
|
||||
attributionHeaders,
|
||||
LlmAdapter,
|
||||
LlmError,
|
||||
type GenerateOptions,
|
||||
type StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class HttpAdapter extends LlmAdapter {
|
||||
constructor(private readonly endpoint: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...attributionHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ model: options.model, messages: options.messages }),
|
||||
...options.signal ? { signal: options.signal } : {},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
|
||||
}
|
||||
// A real adapter parses the response and emits the complete chunk sequence.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,7 @@
|
||||
# LLM 适配器
|
||||
|
||||
[English](llm-adapter.md) | 中文
|
||||
|
||||
本文介绍如何为 Harness 接入一个新的 LLM 提供方。
|
||||
|
||||
## 概述
|
||||
@@ -10,6 +12,7 @@ LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class MyAdapter extends LlmAdapter {
|
||||
@@ -21,9 +24,9 @@ class MyAdapter extends LlmAdapter {
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
// 1. 将 options.messages 转换为你的 API 格式
|
||||
// 2. 调用 API(流式)
|
||||
// 3. 将 API 响应转换为 StreamChunk 序列
|
||||
// 1. Convert options.messages to the provider format.
|
||||
// 2. Call the streaming API.
|
||||
// 3. Convert the response into StreamChunk values.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +35,11 @@ export interface Config {
|
||||
models: string[]
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
models: Schema.array(Schema.string()).required(),
|
||||
})
|
||||
|
||||
export const name = 'my-llm-adapter'
|
||||
export const inject = ['llm']
|
||||
|
||||
@@ -48,22 +56,22 @@ export function apply(ctx: Context, config: Config) {
|
||||
```ts
|
||||
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
async function* demo(): AsyncIterable<StreamChunk> {
|
||||
// 1. 每个内容块以 block-start 开始
|
||||
async function* exampleChunks(): AsyncIterable<StreamChunk> {
|
||||
// 1. Start each content block with block-start.
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
|
||||
// 2. 文本块使用 text-delta
|
||||
// 2. Stream text through text-delta.
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
|
||||
// 3. 每个内容块以 block-end 结束(携带完整 block)
|
||||
// 3. End each content block with block-end and the complete block.
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
|
||||
// 4. Tool call 块
|
||||
// 4. Tool-call block.
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
@@ -83,12 +91,12 @@ async function* demo(): AsyncIterable<StreamChunk> {
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Token 用量
|
||||
// 5. Token usage.
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. 结束原因
|
||||
// 6. Finish reason.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
|
||||
// Alternatively, { kind: 'tool-calls' } requests tool execution.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,33 +110,11 @@ async function* demo(): AsyncIterable<StreamChunk> {
|
||||
|
||||
## GenerateOptions
|
||||
|
||||
`stream()` 接收的请求包含:
|
||||
|
||||
```ts
|
||||
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const options: GenerateOptions
|
||||
|
||||
options.model // 模型名
|
||||
options.messages // 对话历史 (Message[])
|
||||
options.tools // 可用的 tool schema 列表 (ToolSchema[])
|
||||
options.system // 系统提示词
|
||||
options.maxTokens // 最大输出 token
|
||||
options.temperature // 温度
|
||||
options.signal // 取消信号(必须响应)
|
||||
```
|
||||
|
||||
你的适配器需要将这些映射到具体 API 的参数。
|
||||
`stream()` 接收仓库导出的 `GenerateOptions`。它包含模型名、对话历史、系统提示词、tool schema、生成参数、停止序列和中止信号;完整字段以 `@deepseek-ai/dsh-llm` 导出的 TypeScript 类型为准。适配器必须将支持的字段映射到具体 API;无法支持的字段应抛出带稳定 code 的 `LlmError`,不能静默丢弃。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const ctx: Context
|
||||
declare const adapter: LlmAdapter
|
||||
|
||||
```ts ignore-check
|
||||
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
```
|
||||
|
||||
@@ -148,7 +134,7 @@ ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: my-model-v1 # 引用上面注册的模型名
|
||||
model: my-model-v1 # References the model registered above.
|
||||
```
|
||||
|
||||
## 实战参考
|
||||
@@ -163,20 +149,37 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地
|
||||
|
||||
## 错误处理
|
||||
|
||||
适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。
|
||||
适配器应将传输和协议故障作为带稳定 code 的 `LlmError` 抛出;agent loop 会保留该错误及其 code,供诊断和策略使用。不要依赖普通 `Error` 被自动转换。每个提供方 HTTP 请求还必须合并 `attributionHeaders()`,并传递 `options.signal`。
|
||||
|
||||
```ts
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
attributionHeaders,
|
||||
LlmAdapter,
|
||||
LlmError,
|
||||
type GenerateOptions,
|
||||
type StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class HttpAdapter extends LlmAdapter {
|
||||
private endpoint = 'https://api.example.com/v1/chat'
|
||||
constructor(private readonly endpoint: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, { method: 'POST' })
|
||||
const response = await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...attributionHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ model: options.model, messages: options.messages }),
|
||||
...options.signal ? { signal: options.signal } : {},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
|
||||
}
|
||||
// ... 正常流式处理
|
||||
// A real adapter parses the response and emits the complete chunk sequence.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
```
|
||||
6
docs/user/guide/config.i18n.yaml
Normal file
6
docs/user/guide/config.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
config.md: a3f56018fd43cc803c1710f97c29a77340a0b257
|
||||
config.zh.md: af661b9d7ef72e4085551202169e975bd0c3ec99
|
||||
59
docs/user/guide/config.md
Normal file
59
docs/user/guide/config.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Configuration
|
||||
|
||||
English | [中文](config.zh.md)
|
||||
|
||||
Harness uses `cordis.yml` to describe which plugins an agent loads and the configuration passed to each one. The file composes capabilities; the generated configuration catalog records the fields and defaults each package actually supports, avoiding a second hand-maintained reference.
|
||||
|
||||
## Start from a real configuration
|
||||
|
||||
The repository examples are runnable configurations and the most reliable starting points for a new project:
|
||||
|
||||
- [echo-agent](../../../examples/echo-agent/cordis.yml) uses a local mock model and needs no API key.
|
||||
- [repl-agent](../../../examples/repl-agent/cordis.yml) combines the DeepSeek model, Bash, filesystem, compaction, subagents, and workflows.
|
||||
- [acp-agent](../../../examples/acp-agent/cordis.yml) connects to editor clients over ACP.
|
||||
|
||||
A minimal configuration is a list of plugin entries:
|
||||
|
||||
```yaml
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## Plugin entries
|
||||
|
||||
`name` identifies an npm package or a local module relative to `cordis.yml`; `id` gives the plugin instance a stable identity; and `config` supplies plugin-specific configuration. Set `disabled: true` to skip an entry temporarily.
|
||||
|
||||
```yaml
|
||||
- id: local-tool
|
||||
name: './src/my-tool.ts'
|
||||
disabled: false
|
||||
config:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
Plugins load in file order. Place plugins that depend on services after the applications or capability plugins that provide them. Missing models, tools, and plugins fail as early as possible instead of being silently ignored.
|
||||
|
||||
## JavaScript values and environment variables
|
||||
|
||||
The Cordis loader evaluates runtime expressions tagged with `!!js`. Keep API keys and other secrets in the gitignored `.env` file at the repository root, never in committed configuration.
|
||||
|
||||
```yaml
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
The tag is `!!js`, not `!js`.
|
||||
|
||||
## Exact configuration reference
|
||||
|
||||
The generated [plugin configuration catalog](../../config-catalog.md) lists every current field, type, and default. For composition concepts, continue to the [architecture](../../architecture.md) and [capability interfaces](../../capability-seams.md). To create a configuration, copy the closest entry from the [examples overview](../../../examples/README.md) and adapt it.
|
||||
59
docs/user/guide/config.zh.md
Normal file
59
docs/user/guide/config.zh.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 配置文件
|
||||
|
||||
[English](config.md) | 中文
|
||||
|
||||
Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
|
||||
|
||||
## 从真实配置开始
|
||||
|
||||
仓库中的示例就是可以运行的配置,也是新项目最可靠的起点:
|
||||
|
||||
- [echo-agent](../../../examples/echo-agent/cordis.yml) 使用本地 mock 模型,不需要 API key。
|
||||
- [repl-agent](../../../examples/repl-agent/cordis.yml) 组合 DeepSeek 模型、Bash、文件系统、压缩、子代理和工作流。
|
||||
- [acp-agent](../../../examples/acp-agent/cordis.yml) 通过 ACP 接入编辑器客户端。
|
||||
|
||||
最小配置由一组插件条目组成:
|
||||
|
||||
```yaml
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## 插件条目
|
||||
|
||||
`name` 指定 npm 包或相对于 `cordis.yml` 的本地模块,`id` 为插件实例提供稳定标识,`config` 传入插件自己的配置。需要临时跳过某个条目时可设置 `disabled: true`。
|
||||
|
||||
```yaml
|
||||
- id: local-tool
|
||||
name: './src/my-tool.ts'
|
||||
disabled: false
|
||||
config:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
|
||||
|
||||
## JavaScript 值和环境变量
|
||||
|
||||
Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。
|
||||
|
||||
```yaml
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
标签是 `!!js`,不是 `!js`。
|
||||
|
||||
## 精确配置参考
|
||||
|
||||
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../architecture.md)和[能力接口](../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../examples/README.md)中最接近的例子。
|
||||
6
docs/user/guide/index.i18n.yaml
Normal file
6
docs/user/guide/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: a20b1041e13b01b6b1d01a5baa8975d3e68c6aa0
|
||||
index.zh.md: 56ec50352218e2e28ad2dd7a6ef387376de75606
|
||||
49
docs/user/guide/index.md
Normal file
49
docs/user/guide/index.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Introduction
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
DeepSeek Harness is a **plugin-based agent development framework** built on the [Cordis](https://github.com/cordiverse/cordis) microkernel. Its central idea is simple: **everything is a plugin**.
|
||||
|
||||
## What it is
|
||||
|
||||
Harness implements every capability an AI agent needs—including LLM calls, tool execution, session management, and subtask delegation—as a composable plugin. A `cordis.yml` file declares which plugins to load and how to configure them, assembling a complete agent.
|
||||
|
||||
```yaml
|
||||
# Select the LLM backend
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
|
||||
# Select the application template
|
||||
- name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## Who it is for
|
||||
|
||||
### Application users
|
||||
|
||||
To run an existing agent application, such as a coding assistant or conversational agent:
|
||||
|
||||
1. Copy an example template.
|
||||
2. Add an API key.
|
||||
3. Run it.
|
||||
|
||||
No code is required. See the [quick start](./quickstart.md).
|
||||
|
||||
### Plugin developers
|
||||
|
||||
To add a custom tool, a new LLM adapter, or another execution backend, write a plugin. Harness provides explicit extension interfaces and a type-safe development experience. See [development](../develop/basic/).
|
||||
|
||||
## Core features
|
||||
|
||||
- **Configuration only** — `cordis.yml` selects the capability set; changing a model or adding a tool is a configuration edit.
|
||||
- **Hot replacement (HMR)** — edit plugin code during development without restarting the process.
|
||||
|
||||
## Technology
|
||||
|
||||
- **Runtime**: Node.js ^22.19 or >= 24
|
||||
- **Language**: TypeScript (ESM)
|
||||
- **Framework**: Cordis
|
||||
- **Package manager**: pnpm workspaces (the repository pins pnpm 11)
|
||||
@@ -1,5 +1,7 @@
|
||||
# 介绍
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。
|
||||
|
||||
## 它是什么
|
||||
@@ -7,12 +9,12 @@ DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](
|
||||
Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。
|
||||
|
||||
```yaml
|
||||
# 选择 LLM 后端
|
||||
# Select the LLM backend
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
|
||||
# 选择应用模板
|
||||
# Select the application template
|
||||
- name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
@@ -28,7 +30,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
2. 填写 API key
|
||||
3. 运行
|
||||
|
||||
不需要写任何代码。详见 [快速开始](quickstart)。
|
||||
不需要写任何代码。详见 [快速开始](./quickstart.md)。
|
||||
|
||||
### 插件开发者
|
||||
|
||||
@@ -41,7 +43,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **运行时**: Node.js >= 24
|
||||
- **运行时**: Node.js ^22.19 或 >= 24
|
||||
- **语言**: TypeScript (ESM)
|
||||
- **框架**: Cordis
|
||||
- **包管理**: pnpm workspaces
|
||||
- **包管理**: pnpm workspaces(仓库固定使用 pnpm 11)
|
||||
6
docs/user/guide/quickstart.i18n.yaml
Normal file
6
docs/user/guide/quickstart.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
quickstart.md: acae2ac095e057971043c2bcece7a52d3ebc1c2c
|
||||
quickstart.zh.md: 54643fe54e62dbbd3696362cb43ff8569577c53b
|
||||
99
docs/user/guide/quickstart.md
Normal file
99
docs/user/guide/quickstart.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Quick start
|
||||
|
||||
English | [中文](quickstart.zh.md)
|
||||
|
||||
This guide gets an agent running in five minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) ^22.19 or >= 24
|
||||
- [pnpm](https://pnpm.io/) 11 (use Corepack to select the repository-pinned version)
|
||||
|
||||
```sh
|
||||
# Check versions
|
||||
node -v # v22.19.x, or v24.x and newer
|
||||
corepack enable
|
||||
pnpm -v # 11.x
|
||||
```
|
||||
|
||||
## Step 1: run echo-agent
|
||||
|
||||
echo-agent needs no API key and runs after dependencies are installed.
|
||||
|
||||
```sh
|
||||
# Clone the repository
|
||||
git clone https://github.com/deepseek-harness/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start echo-agent
|
||||
pnpm run demo:echo
|
||||
```
|
||||
|
||||
The process prints:
|
||||
|
||||
```
|
||||
echo-agent ready. Type a message ("echo <text>" triggers the tool).
|
||||
>
|
||||
```
|
||||
|
||||
Enter:
|
||||
|
||||
```
|
||||
> echo hello world
|
||||
```
|
||||
|
||||
The model issues a tool call, and the echo tool returns the text in uppercase:
|
||||
|
||||
```
|
||||
[tool call] echo({"text":"hello world"})
|
||||
[tool result] ECHO: HELLO WORLD
|
||||
```
|
||||
|
||||
Your local environment is ready.
|
||||
|
||||
## Step 2: use a real model
|
||||
|
||||
Next, connect a real DeepSeek model and run the complete command-line agent.
|
||||
|
||||
### Get an API key
|
||||
|
||||
Get an API key from [DeepSeek Platform](https://platform.deepseek.com/).
|
||||
|
||||
### Configure the environment
|
||||
|
||||
Create a gitignored `.env` file in the repository root:
|
||||
|
||||
```sh
|
||||
DEEPSEEK_API_KEY=sk-your-key-here
|
||||
```
|
||||
|
||||
### Start repl-agent
|
||||
|
||||
```sh
|
||||
pnpm run demo:repl
|
||||
```
|
||||
|
||||
```
|
||||
agent REPL ready. Give it a coding task.
|
||||
>
|
||||
```
|
||||
|
||||
This is a complete coding assistant that can read and write files, run commands, and delegate subtasks.
|
||||
|
||||
Try a task:
|
||||
|
||||
```
|
||||
> Create hello.js in the current directory, print "Hello from Harness!", and run it
|
||||
```
|
||||
|
||||
## What happened
|
||||
|
||||
echo-agent and repl-agent use the same application framework (`@deepseek-ai/dsh-stdio-demo`). Their `cordis.yml` files select different plugins and configuration. Custom agents use the same composition model.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Configuration](./config.md) — understand the `cordis.yml` format
|
||||
- [Develop a plugin](../develop/basic/) — build your own tool or backend
|
||||
@@ -1,16 +1,19 @@
|
||||
# 快速开始
|
||||
|
||||
[English](quickstart.md) | 中文
|
||||
|
||||
本指南带你在 5 分钟内跑起一个 Agent。
|
||||
|
||||
## 环境准备
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 24
|
||||
- [pnpm](https://pnpm.io/) >= 9
|
||||
- [Node.js](https://nodejs.org/) ^22.19 或 >= 24
|
||||
- [pnpm](https://pnpm.io/) 11(建议通过 Corepack 使用仓库固定的版本)
|
||||
|
||||
```sh
|
||||
# 确认版本
|
||||
node -v # v24.x 或更高
|
||||
pnpm -v # 9.x 或更高
|
||||
# Check versions
|
||||
node -v # v22.19.x, or v24.x and newer
|
||||
corepack enable
|
||||
pnpm -v # 11.x
|
||||
```
|
||||
|
||||
## 第一步:运行 echo-agent
|
||||
@@ -18,16 +21,14 @@ pnpm -v # 9.x 或更高
|
||||
echo-agent 不需要 API key,装好依赖就能跑。
|
||||
|
||||
```sh
|
||||
# 克隆仓库
|
||||
# Clone the repository
|
||||
git clone https://github.com/deepseek-harness/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
# 如果看到 ERR_PNPM_IGNORED_BUILDS,可以忽略——安装已经成功了。
|
||||
# 想消除这个提示可以跑一次: pnpm approve-builds
|
||||
|
||||
# 启动 echo-agent
|
||||
# Start echo-agent
|
||||
pnpm run demo:echo
|
||||
```
|
||||
|
||||
@@ -85,7 +86,7 @@ agent REPL ready. Give it a coding task.
|
||||
试着给它一个任务:
|
||||
|
||||
```
|
||||
> 在当前目录创建一个 hello.js,内容是打印 "Hello from Harness!",然后运行它
|
||||
> Create hello.js in the current directory, print "Hello from Harness!", and run it
|
||||
```
|
||||
|
||||
## 回头看
|
||||
@@ -94,5 +95,5 @@ echo-agent 和 repl-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio
|
||||
|
||||
## 下一步
|
||||
|
||||
- [配置文件](config) — 了解 `cordis.yml` 的完整语法
|
||||
- [配置文件](./config.md) — 了解 `cordis.yml` 的完整语法
|
||||
- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端
|
||||
6
docs/user/index.i18n.yaml
Normal file
6
docs/user/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: e9a1f03785c7472c47550ec59ea0165d28d3d9a6
|
||||
index.zh.md: 907f1452c9ff50d619989c18dcf2727addb2573d
|
||||
25
docs/user/index.md
Normal file
25
docs/user/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
layout: home
|
||||
hero:
|
||||
name: DeepSeek Harness
|
||||
text: Plugin-based agent development framework
|
||||
tagline: Built on the Cordis microkernel; everything is a plugin
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Quick start
|
||||
link: /en/guide/quickstart
|
||||
- theme: alt
|
||||
text: Develop plugins
|
||||
link: /en/develop/basic/
|
||||
features:
|
||||
- title: Plugin architecture
|
||||
details: Built on the Cordis plugin system. Every capability is registered by a plugin, takes effect when loaded, and is reverted when unloaded.
|
||||
- title: Configuration as composition
|
||||
details: One cordis.yml determines the agent's complete capability set. Change a model or add a tool by editing configuration.
|
||||
- title: Ready to use
|
||||
details: Includes LLM calls, file access, Bash execution, subagent delegation, and the rest of the core toolchain. Copy a template to get started.
|
||||
---
|
||||
|
||||
# DeepSeek Harness
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
@@ -7,15 +7,19 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /zh-CN/guide/quickstart
|
||||
link: /guide/quickstart
|
||||
- theme: alt
|
||||
text: 开发插件
|
||||
link: /zh-CN/develop/basic/
|
||||
link: /develop/basic/
|
||||
features:
|
||||
- title: 插件化架构
|
||||
details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。
|
||||
details: 基于 Cordis 插件系统,所有能力通过插件注册,加载即生效、卸载即还原。
|
||||
- title: 配置即组合
|
||||
details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。
|
||||
- title: 开箱即用
|
||||
details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。
|
||||
---
|
||||
|
||||
# DeepSeek Harness
|
||||
|
||||
[English](index.md) | 中文
|
||||
@@ -12,6 +12,7 @@ export default tseslint.config(
|
||||
'**/.sessions/**',
|
||||
'.claude/**', // harness-local state (worktrees, skills) — other checkouts, not this one's sources
|
||||
'**/.doc-typecheck-*/**',
|
||||
'website/.generated/**',
|
||||
'vendor/**', // vendored source keeps upstream style and idioms
|
||||
'native/**', // imported landlock-run subtree: self-contained workspace with its own gates (native/README.md)
|
||||
'**/*.js',
|
||||
@@ -22,7 +23,7 @@ export default tseslint.config(
|
||||
|
||||
// --- our packages: full strictness -------------------------------------
|
||||
{
|
||||
files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'],
|
||||
files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
|
||||
extends: [
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
],
|
||||
@@ -109,7 +110,7 @@ export default tseslint.config(
|
||||
|
||||
// --- file-local duplication (all owned TypeScript) ---------------------
|
||||
{
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'],
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
|
||||
plugins: { sonarjs },
|
||||
rules: {
|
||||
// Cross-file clones are covered separately by jscpd.
|
||||
@@ -126,7 +127,7 @@ export default tseslint.config(
|
||||
|
||||
// --- formatting (everything we own) -------------------------------------
|
||||
{
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'eslint.config.mjs'],
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts', 'eslint.config.mjs'],
|
||||
plugins: { '@stylistic': stylistic },
|
||||
rules: {
|
||||
'@stylistic/indent': ['error', 2],
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -41,12 +41,14 @@
|
||||
# The ACP server app: the agent-spine-demo spine + JSONL persistence + the ACP bridge.
|
||||
# Persistence root: $DSH_SNAPSHOT_SESSIONS_ROOT when the snapshot harness sets it
|
||||
# (so it can harvest / isolate the log), else ./.sessions for the demo.
|
||||
# Snapshot modes use raw JSONL fixtures; ordinary runs keep the compressed default.
|
||||
- id: acp-agent
|
||||
name: '@deepseek-ai/dsh-acp-demo'
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
# Keep the persona to identity and behavior; tool plugins own tool guidance.
|
||||
@@ -94,12 +96,14 @@
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
maxDepth: 1
|
||||
|
||||
- id: tool-subagent-fork
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: fork
|
||||
toolName: subagent_fork
|
||||
maxDepth: 1
|
||||
|
||||
|
||||
# The worker-thread workflow engine fans a model-written JavaScript script's
|
||||
|
||||
36
examples/acp-agent/depth-two.cordis.snapshot.yml
Normal file
36
examples/acp-agent/depth-two.cordis.snapshot.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
# Keyless counterpart to depth-two.cordis.yml: apply the depth patch and replace
|
||||
# the live adapter with per-session replay.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
path: ./cordis.yml
|
||||
patches:
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- id: sandbox
|
||||
name: '@deepseek-ai/dsh-sandbox-local'
|
||||
config:
|
||||
runnerCommand:
|
||||
- bash
|
||||
- -c
|
||||
- while [ "$1" != "--" ]; do shift; done; shift; exec "$@"
|
||||
- passthrough-runner
|
||||
runnerFailureSignatures:
|
||||
- 'passthrough-runner: profile rejected'
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
maxDepth: 2
|
||||
- insert:
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
providers:
|
||||
- id: deepseek
|
||||
name: DeepSeek
|
||||
models:
|
||||
- id: deepseek-v4-flash
|
||||
- id: deepseek-v4-pro
|
||||
13
examples/acp-agent/depth-two.cordis.yml
Normal file
13
examples/acp-agent/depth-two.cordis.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
# Depth-limit snapshot overlay: keep the default composition and allow two
|
||||
# generations of spawn children before runtime enforcement rejects another.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
path: ./cordis.yml
|
||||
patches:
|
||||
- id: tool-subagent
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
maxDepth: 2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user