Merge pull request #7966 from deepseek-harness/refactor/tool-errors-foundation

feat(tools): structured error identity for registry-owned tool failures
This commit is contained in:
Jiawei Song authored and GitHub committed 2026-11-06 13:11:06 +08:00
commit 7f98d178d1
77 files changed
+928 -188

No files matched your search

@@ -15,8 +15,8 @@
en: 0e1399cad4151f87
zh: 960fa0ccbe0f8fea
/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision/cancellation-codes-record-whether-dispatch-occurred:
en: c787fba8731ac147
zh: 21b5968ae6f8cd59
en: 86c5566581c1d3d6
zh: 3cb71fbb97bdb291
/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision/pre-aborted-entry-short-circuits-after-materialization:
en: 53362db6a0c71698
zh: 311de45ca26d3fdc
@@ -32,7 +32,7 @@ An around-dispatch wrapper may replace `exec.signal` for its delegated lifetime
`ABORTED_BEFORE_DISPATCH` carries `{ name: 'AbortError' }` and model text `Error: tool call aborted before dispatch`. It applies whenever cancellation prevents body invocation, including pre-aborted entry, cancellation during pre-policy or approval, an aborted wrapper signal, a wrapper success overtaken by caller cancellation before delegation, and agent-loop siblings skipped after turn cancellation.
`ABORTED` carries model text `Error: tool call aborted` and applies only after the body was invoked, including cancellation while an around wrapper or post-policy listener waits after body completion. A denial, wrapper failure, tool failure, or post-policy failure remains more specific than generic cancellation. A timeout owned by timeout-policy remains `TOOL_TIMEOUT`, and contexts deferred before a successful outcome is replaced remain attached.
`ABORTED` carries model text `Error: tool call aborted` and applies only after the body was invoked, including cancellation while an around wrapper or post-policy listener waits after body completion. A started body that throws the signal's reason or an `Error` named `AbortError` without `HarnessError` identity keeps that message and carries `{ name: 'AbortError', code: 'ABORTED' }`. A denial, wrapper failure, other tool failure, or post-policy failure remains more specific than generic cancellation. A timeout owned by timeout-policy remains `TOOL_TIMEOUT`, and contexts deferred before a successful outcome is replaced remain attached.
### Pre-aborted entry short-circuits after materialization
@@ -32,7 +32,7 @@ Status: implemented
`ABORTED_BEFORE_DISPATCH` 携带 `{ name: 'AbortError' }` 和模型可见文本 `Error: tool call aborted before dispatch`。凡取消阻止工具主体调用时都使用该结果,包括进入时已中止、前置策略或审批期间取消、包装层信号已中止、包装层在委托前返回的成功结果被调用方取消抢先,以及轮次取消后 agent loop(智能体循环)跳过的同批调用。
`ABORTED` 携带模型可见文本 `Error: tool call aborted`,并且只在工具主体已经调用后使用,包括工具主体完成后环绕包装层或后置策略监听器等待期间发生的取消。拒绝、包装层失败、工具失败或后置策略失败比通用取消更具体。timeout-policy 自身拥有的超时仍为 `TOOL_TIMEOUT`,成功结果被取消替换前延后附加的上下文仍会保留。
`ABORTED` 携带模型可见文本 `Error: tool call aborted`,并且只在工具主体已经调用后使用,包括工具主体完成后环绕包装层或后置策略监听器等待期间发生的取消。已开始的工具主体抛出 signal 的 reason,或不具备 `HarnessError` 身份、name 为 `AbortError` 的 `Error` 时,结果保留该 message,并携带 `{ name: 'AbortError', code: 'ABORTED' }`。拒绝、包装层失败、其他工具失败或后置策略失败比通用取消更具体。timeout-policy 自身拥有的超时仍为 `TOOL_TIMEOUT`,成功结果被取消替换前延后附加的上下文仍会保留。
### 进入时已中止会在物化后短路
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-11-05-harness-error-dependency-free-base.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-11-05-harness-error-dependency-free-base.md
/agent-note-keep-the-harnesserror-base-in-a-dependency-free-package:
en: 6ff178be521c3318
zh: 18a9a8cc261fe975
/agent-note-keep-the-harnesserror-base-in-a-dependency-free-package/problem:
en: 29880462facde90c
zh: d07805b2000c85b1
/agent-note-keep-the-harnesserror-base-in-a-dependency-free-package/decision:
en: caec42054c040d37
zh: d726f0da57dc1e68
/agent-note-keep-the-harnesserror-base-in-a-dependency-free-package/alternatives-considered:
en: 8245e473d4e90250
zh: fe7883dfe13070a9
/agent-note-keep-the-harnesserror-base-in-a-dependency-free-package/consequences:
en: 9a577f5c3bb9fa15
zh: 4c18ec885768b9d3
@@ -0,0 +1,31 @@
# Agent Note: Keep the HarnessError base in a dependency-free package
Status: implemented
English | [中文](2026-11-05-harness-error-dependency-free-base.zh.md)
## Problem
The tool registry records a failed result's `error.name` and `error.code` only when the thrown value is `instanceof HarnessError`, so every expected failure must extend the one shared class; copying its `code` field is not enough. The [archived structured-error-taxonomy note](../../archived/architecture/2026-06-11-structured-error-taxonomy.md) placed the base in `dsh-llm` because `dsh-llm` was then a leaf that every package already imported, so the base added no dependency edge.
`dsh-llm` stopped being a leaf. `ImageBlock` references `ImageAttachmentRef` from `dsh-attachment`, and `dsh-llm` depends on `dsh-typert-protocol`. A class in either lower package cannot extend a base defined in `dsh-llm` without a TypeScript project-reference cycle, so `AttachmentError` and `RemoteError` re-implement the `code` field on plain `Error`, and tool failures that carry them persist without structured identity.
## Decision
`HarnessError` lives in `@deepseek-ai/dsh-error` (`packages/util/error`), a package with no dependencies that contains only the base class. `dsh-llm` depends on it and re-exports the same class from its entry, so existing `import { HarnessError } from '@deepseek-ai/dsh-llm'` statements keep working and both import paths name one constructor, which keeps `instanceof` checks consistent. LLM-specific failure codes and helpers such as `isHarnessError` stay in `dsh-llm`.
New code imports `HarnessError` from `dsh-error` when the importing package does not otherwise need `dsh-llm`, so a package that needs only the base takes no dependency on the LLM runtime. Packages below `dsh-llm`, such as `dsh-attachment`, can extend the base without a cycle.
## Alternatives considered
**Keep the base in `dsh-llm`.** Lower packages would keep re-implementing the field shape, and the registry would keep dropping their codes; recognizing those shapes structurally in the registry would make it trust any object with a string `code`, including Node errno values.
**Recognize specific classes in the registry.** Matching `RemoteError` or `AttachmentError` by marker would add a dependency from `dsh-tools` to each package and repeat for every new low-level error class.
**Move the base to an existing utility package.** `dsh-util-values` and `dsh-brand` have unrelated roles; a dedicated package names the role and keeps the base free of their dependencies and change history.
**Drop the `dsh-llm` re-export.** Every existing import would have to change in one step without a behavior benefit.
## Consequences
Any package can extend `HarnessError`, including packages that `dsh-llm` depends on, and the base no longer changes with the LLM runtime. The cost is one more published package and one more dependency edge for `dsh-llm`. Two import paths exist for one class; both are supported, and the error-handling rules name `dsh-error` as the home.
@@ -0,0 +1,31 @@
# Agent Note: 把 HarnessError 基类放在无依赖的包中
Status: implemented
[English](2026-11-05-harness-error-dependency-free-base.md) | 中文
## 问题
工具注册表只有在抛出值 `instanceof HarnessError` 时,才会在失败结果上记录 `error.name` 与 `error.code`,因此每种预期失败都必须继承这一个共享类;只复制它的 `code` 字段不够。[已归档的结构化错误分类决策](../../archived/architecture/2026-06-11-structured-error-taxonomy.md)把基类放在 `dsh-llm` 中,因为当时 `dsh-llm` 是所有包都已导入的叶子包,放在那里不会增加依赖边。
`dsh-llm` 后来不再是叶子包。`ImageBlock` 引用了 `dsh-attachment` 的 `ImageAttachmentRef`,`dsh-llm` 还依赖 `dsh-typert-protocol`。这两个更底层的包中的类无法继承定义在 `dsh-llm` 里的基类,否则 TypeScript 项目引用会成环,因此 `AttachmentError` 与 `RemoteError` 只能在普通 `Error` 上重复实现 `code` 字段,携带它们的工具失败在持久化时没有结构化身份。
## 决策
`HarnessError` 位于 `@deepseek-ai/dsh-error`(`packages/util/error`)。这个包没有任何依赖,只包含该基类。`dsh-llm` 依赖它,并从自身入口重新导出同一个类,因此现有的 `import { HarnessError } from '@deepseek-ai/dsh-llm'` 继续可用,两种导入路径指向同一个构造函数,`instanceof` 判断保持一致。LLM 专属的失败码以及 `isHarnessError` 等辅助函数仍留在 `dsh-llm`。
当导入方本身不需要 `dsh-llm` 时,新代码从 `dsh-error` 导入 `HarnessError`,这样只需要基类的包不必依赖 LLM 运行时。`dsh-attachment` 等位于 `dsh-llm` 之下的包可以继承该基类而不形成依赖环。
## 考虑过的替代方案
**把基类留在 `dsh-llm`。** 底层包会继续重复实现字段结构,注册表也会继续丢弃它们的 code;如果让注册表按结构识别这类对象,它就会信任任何带字符串 `code` 的对象,包括 Node errno 值。
**在注册表中识别特定类。** 按标记匹配 `RemoteError` 或 `AttachmentError` 会让 `dsh-tools` 依赖每个这样的包,而且每出现一个新的底层错误类都要重复一次。
**把基类移到现有的工具包。** `dsh-util-values` 与 `dsh-brand` 的职责与此无关;独立的包能准确命名职责,也让基类不受它们的依赖与变更历史影响。
**去掉 `dsh-llm` 的重新导出。** 所有现有导入都得一次性修改,却不带来任何行为收益。
## 后果
任何包都可以继承 `HarnessError`,包括 `dsh-llm` 所依赖的包,基类也不再随 LLM 运行时一起变更。代价是多发布一个包,`dsh-llm` 多一条依赖边。同一个类有两种导入路径,两者都受支持,错误处理规则把 `dsh-error` 作为它的归属位置。
+5 -5
View File
@@ -11,20 +11,20 @@ Define failures, their owners, and what remains safe after failure.
Expected failures need declared types, stable discriminants, and handling-relevant details. Reuse package error conventions, preserve causes, and separate machine identity from display text. Never route handling by parsing messages. Unexpected defects such as `assertNever` may remain plain `Error`; containment must never turn them into success.
An error class that carries a machine code extends `HarnessError` from `dsh-llm`, directly or through its package's error class, never bare `Error`. Prefer one error class per owning package or domain over a class per failure; a separate class is justified when callers catch it by type or it carries its own structured fields, such as `ToolArgsError.violations`. `name` is the class name, except for cancellation (below), and `code` values are stable `UPPER_SNAKE_CASE` strings, usually prefixed by the domain and declared as a union or constants beside the class. A code names the failure class its consumers act on, not the call site; add a code rather than reuse one whose meaning differs. The [error class and code guide](references/error-codes.md) defines class choice, naming, the standard conditions, declaration, foreign-error conversion, and compatibility.
An error class that carries a machine code extends `HarnessError` from `dsh-error` (also re-exported by `dsh-llm`), directly or through its package's error class, never bare `Error`. Prefer one error class per owning package or domain over a class per failure; a separate class is justified when callers catch it by type or it carries its own structured fields, such as `ToolArgsError.violations`. `name` is the class name, except for cancellation (below), and `code` values are stable `UPPER_SNAKE_CASE` strings, usually prefixed by the domain and declared as a union or constants beside the class. A code names the failure class its consumers act on, not the call site; add a code rather than reuse one whose meaning differs. The [error class and code guide](references/error-codes.md) defines class choice, naming, the standard conditions, declaration, foreign-error conversion, and compatibility.
Convert foreign failures where the dependency is owned. Node errno, SDK, protocol, native, worker, and cross-realm errors become the owning package's error with the original as `cause`; a foreign numeric or vendor code belongs in the message, not in `code`. Preserve structure across layers: never re-wrap a `HarnessError` in a plain `Error`, store only its message for a later rethrow, or drop its `name` and `code` while redacting text. A process or worker boundary transports `name` and `code` and reconstructs the error.
## Tool failures
The tool registry keeps `error.name` and `error.code` on `tool/result` when the producer supplies them: a `HarnessError` thrown by the tool body, a pre-execute `deny` with `info`, the registry's own cancellation result (`AbortError` with `ABORTED` or `ABORTED_BEFORE_DISPATCH`), or a `tools/execute` wrapper result such as the timeout policy's `TOOL_TIMEOUT`. Any other thrown value, a guard denial, an approval rejection, and a post-execute block persist only text that retry policy, UI, and analytics cannot route on. Every expected failure reachable from a tool body therefore throws a `HarnessError`, normally the owning domain's subclass:
The tool registry keeps `error.name` and `error.code` on `tool/result` when the producer supplies them: a `HarnessError` thrown by the tool body, a pre-execute `deny`, guard denial, or post-execute block with `info` (including the registry's approval denials, `ApprovalError` with `APPROVAL_REJECTED`, `APPROVAL_CANCELLED`, or `APPROVAL_UNAVAILABLE`), the registry's own cancellation result (`AbortError` with `ABORTED` or `ABORTED_BEFORE_DISPATCH`, also applied to a signal reason or an `Error` named `AbortError` that the body throws after cancellation), or a `tools/execute` wrapper result such as the timeout policy's `TOOL_TIMEOUT`. Any other thrown value, and a guard denial or block without `info`, persist only text that retry policy, UI, and analytics cannot route on. Every expected failure reachable from a tool body therefore throws a `HarnessError`, normally the owning domain's subclass:
- Hand-checked argument constraints throw `ToolArgsError` (`INVALID_ARGS`) from `dsh-tools`.
- Missing composition or caller context, such as an absent Agent, service, or open turn, throws the owning package's error with its own code.
- Caller cancellation throws a `HarnessError` whose `name` is `AbortError` and whose code is the `dsh-tools` `TOOL_ABORTED` constant (`ABORTED`), matching the registry's cancellation result; never a bare `AbortError` or `signal.reason`.
- Missing composition or caller context, such as an absent Agent, service, or open turn, throws `ToolContextUnavailableError` (`TOOL_CONTEXT_UNAVAILABLE`) from `dsh-tools`.
- Caller cancellation either propagates the signal's reason or an `Error` named `AbortError`, which the registry classifies as `AbortError` with the `dsh-tools` `TOOL_ABORTED` constant (`ABORTED`), or throws a `HarnessError` with that `name` and code. Never replace it with a different plain `Error`; a failure distinct from the cancellation, such as a cleanup failure, keeps its own class and code.
- Dependency failures follow the conversion rule above; filesystem errno maps to `FsError` codes such as `FS_NOT_FOUND`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR`.
A policy that denies a call in `tools/pre-execute` supplies `info` with its own `name` and `code`. The registry's built-in approval rejections and guard denials currently carry only a reason, so they remain unroutable until the registry gives them codes. A failure the model must not treat as success is an `isError` result; a successful domain outcome in a non-ideal state, such as a non-zero exit or no matches, stays a success value, and a success-shaped failure value is allowed only when the output schema documents it. Tests assert `error.info.name` and `error.info.code` for each expected failure path, not only the message.
A policy that denies a call in `tools/pre-execute`, through `tools.guard()`, or by a post-execute block supplies `info` with its own `name` and `code`. A failure the model must not treat as success is an `isError` result; a successful domain outcome in a non-ideal state, such as a non-zero exit or no matches, stays a success value, and a success-shaped failure value is allowed only when the output schema documents it. Tests assert `error.info.name` and `error.info.code` for each expected failure path, not only the message.
Existing `name` and `code` values persist in Session logs and feed analytics. Renaming or reclassifying one is an externally perceptible change that needs an [upgrade guide](../dsh-create-upgrade-guide/SKILL.md).
@@ -17,7 +17,7 @@ Use this guide when adding or reviewing an error class, an error code, or a fail
## Choose the class
1. Reuse the owning domain's existing class when the failure belongs to that domain: `FsError` for filesystem targets, `SearchError` for `glob`/`grep`, `LspError`, `WebError`, `SubagentError`, `TeamError`, `GoalError`, `WorkflowError`, `UserQuestionError`, `SessionQueryError`, the `Sandbox*Error` classes, and `CodeRunFailedError`.
2. Use `ToolArgsError` (`INVALID_ARGS`) for argument constraints the parameter schema cannot express, such as an empty string, a range, or a cross-field rule.
2. Use `ToolArgsError` (`INVALID_ARGS`) for argument constraints the parameter schema cannot express, such as an empty string, a range, or a cross-field rule, and `ToolContextUnavailableError` (`TOOL_CONTEXT_UNAVAILABLE`) for a tool call that lacks a required Agent, service, or open turn.
3. Declare a new class only for a new owning domain. Prefer one class with a code union over one class per failure; a separate subclass is justified when consumers must catch it by type or it carries its own structured fields, such as `ToolArgsError.violations`, and it then extends the domain class when one exists so the domain still matches with `instanceof`.
4. Never declare a coded class that extends bare `Error`. An existing one is migrated by changing its base class and keeping every `name` and `code`.
@@ -26,7 +26,7 @@ Use this guide when adding or reviewing an error class, an error code, or a fail
- `name` is the PascalCase class name ending in `Error`, such as `JobError` or `SandboxEscalationError`.
- Assign `this.name` a string literal in the constructor. `HarnessError` defaults `name` to `new.target.name`, but a bundler may rename a colliding class binding, and a renamed class would change persisted data.
- Do not reuse another domain's class name, and do not encode a code in the name.
- Cancellation is the one exception: a tool throws a `HarnessError` whose `name` is `AbortError` and whose code is the `dsh-tools` `TOOL_ABORTED` constant (`ABORTED`), so tool-thrown and registry-generated cancellations share one `name` and `code`.
- Cancellation is the one exception: its `name` is `AbortError` and its code is the `dsh-tools` `TOOL_ABORTED` constant (`ABORTED`). The registry applies both to a signal reason or an `Error` named `AbortError` that a tool throws after cancellation, whichever operation inside the tool aborted; a tool that throws its own cancellation error uses a `HarnessError` with the same `name` and code.
## Format the code
+1 -1
View File
@@ -823,7 +823,7 @@ it('routes one browser-authored Auto request through the same model and asks the
event.type === 'tool/result'
&& event.data.message.toolCallId === AUTO_CALL_ID
))
expect(result?.data.error).toBeUndefined()
expect(result?.data.error).toEqual({ name: 'ApprovalError', code: 'APPROVAL_REJECTED' })
const durableModelResult = JSON.stringify(result?.data.message)
expect(durableModelResult).toContain('the user rejected tool \\"bash\\"')
expect(durableModelResult).not.toContain('direct user authorized inspection only')
+1
View File
@@ -5157,6 +5157,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
| `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) |
| `@deepseek-ai/dsh-cua-bundle` | — | [`packages/experimental/cua-bundle/src/index.ts`](../packages/experimental/cua-bundle/src/index.ts) |
| `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) |
| `@deepseek-ai/dsh-error` | — | [`packages/util/error/src/index.ts`](../packages/util/error/src/index.ts) |
| `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) |
| `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) |
| `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) |
+1
View File
@@ -5159,6 +5159,7 @@ export interface Config {
| `@deepseek-ai/dsh-cmdline` | — | [`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts) |
| `@deepseek-ai/dsh-cua-bundle` | — | [`packages/experimental/cua-bundle/src/index.ts`](../packages/experimental/cua-bundle/src/index.ts) |
| `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) |
| `@deepseek-ai/dsh-error` | — | [`packages/util/error/src/index.ts`](../packages/util/error/src/index.ts) |
| `@deepseek-ai/dsh-experimental-badge-skill-bundle` | — | [`packages/experimental/badge-skill-bundle/src/index.ts`](../packages/experimental/badge-skill-bundle/src/index.ts) |
| `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) |
| `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) |
+2 -2
View File
@@ -9,8 +9,8 @@
en: 2cc80a2a79e23005
zh: 87e43383055890ce
/tool-authoring-reference/rules-of-the-execute-contract:
en: f12260d67c4c4d4d
zh: 3955d44622432fd9
en: b81b03778cfdecde
zh: 83f0b7863a5217c6
/tool-authoring-reference/long-running-work:
en: 5f6b0c15cb91e21b
zh: 633a2192df186ac3
+1 -1
View File
@@ -45,7 +45,7 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool. S
- **Execution identity is protected.** The registry materializes `arguments` as detached lossless JSON in one recursive pass, freezes that value before policy starts, and assigns an opaque `exec.token`; `callId`, `name`, `arguments`, `agent`, `token`, the required caller-owned `signal`, and an optional enclosing-transport `parent` token stay immutable through dispatch. `parent` is identity-only and exposes no live outer execution. Treat `args` as readonly input. Only an around-dispatch wrapper receives a mutable view, and it may replace and restore the required `exec.signal` to impose a deadline but cannot remove it.
- **Declare and return one canonical JSON value.** `output.schema` uses `ValueSchemaSpec` and may have an object, array, scalar, or null root. `execute` returns only the inferred value; the registry snapshots it as lossless JSON, validates it, freezes it, and passes it to `output.render(args, value)`. Do not return content blocks from the body or make callers parse prose for ids and fields.
- **Throwing or returning an invalid value means `isError`.** The registry catches throws and contains schema, renderer, metadata-projector, and lossless-JSON failures before observers run. Throw a `HarnessError` subclass with a stable `code` for every expected failure; a thrown `HarnessError` is the only tool-body outcome that keeps `error.name` and `error.code` on the result, and the [error-handling rules](../../.agents/skills/dsh-error-handling/SKILL.md#tool-failures) define the classes and codes. Represent a successful domain outcome in the canonical value even when its Native renderer explains a non-ideal state, such as a non-zero process exit.
- **Honor `exec.signal`.** Cancel in-flight work when it fires and throw a `HarnessError` named `AbortError` whose code is the `TOOL_ABORTED` constant (`ABORTED`), matching the registry's own cancellation result.
- **Honor `exec.signal`.** Cancel in-flight work when it fires and let the signal's reason or a dependency's error named `AbortError` propagate; the registry records it as `AbortError` with the `TOOL_ABORTED` constant (`ABORTED`). Do not replace it with a different plain `Error`.
- **Project durable card data with `presentationMeta` (optional).** `output.presentationMeta(args, value)` derives replayable JSON from the same canonical value. The core persists it on `tool/result` and hands it to `presentResult`, so a card that needs result-time facts—such as `write`/`edit` applied hunks—survives replay without persisting the canonical value. The projector is skipped for nested Code dispatches because they have no cards.
- **Use `exec.agent` for async notifications.** `agent.inject({ content, source: { kind: 'my-plugin' } })` appends durable context the NEXT model request sees — it is not a wake-up (an idle agent stays idle). Declare that `kind` through [`MessageSourceMap`](../subsystems/llm-streaming.md#content-blocks-and-messages) in your plugin: session format V4 rejects the retired `{ kind: 'plugin', plugin: '<name>' }` wrapper at message admission. Guard against disposed agents (try/catch).
+1 -1
View File
@@ -45,7 +45,7 @@ export function apply(ctx: Context) {
- **执行身份受保护。** 注册表在一次递归遍历中将 `arguments` 物化为分离的无损 JSON,在策略开始前冻结该值,并分配一个不透明的 `exec.token`;`callId`、`name`、`arguments`、`agent`、`token`、必填且由调用方持有的 `signal`,以及可选的外层传输 `parent` token 在整个分发过程中保持不可变。`parent` 仅用于身份标识,不暴露活跃的外层执行。请将 `args` 视为只读输入。只有 around-dispatch 包装器会收到可变视图;它可以替换并恢复必填的 `exec.signal` 以施加截止时间,但不能移除该信号。
- **声明并返回一个规范 JSON 值。** `output.schema` 使用 `ValueSchemaSpec`,根可以是对象、数组、标量或 null。`execute` 只返回推导出的值;注册表将其快照为无损 JSON,完成校验和冻结后,再传给 `output.render(args, value)`。工具主体不要返回内容块,也不要迫使调用方从自然语言中解析 id 和字段。
- **抛出异常或返回无效值意味着 `isError`。** 注册表会捕获异常,并在观察者运行前收敛 schema、渲染器、元数据投影器和无损 JSON 失败。每种预期失败都应抛出带稳定 `code` 的 `HarnessError` 子类;在工具主体的各种产出中,只有抛出的 `HarnessError` 会让结果保留 `error.name` 和 `error.code`,具体的错误类和 code 由[错误处理规则](../../.agents/skills/dsh-error-handling/SKILL.md#tool-failures)规定。成功的领域结果即使表示不理想的状态,也应写入规范值;其 Native 渲染器可以解释该状态,例如进程以非零状态退出。
- **遵守 `exec.signal`。** 信号触发时取消进行中的工作,并抛出 `name` 为 `AbortError`、code 为 `TOOL_ABORTED` 常量(值为 `ABORTED`)的 `HarnessError`,与注册表自己生成的取消结果一致。
- **遵守 `exec.signal`。** 信号触发时取消进行中的工作,让 signal 的 reason 或依赖抛出的、name 为 `AbortError` 的错误直接传播;注册表会把它记录为 `AbortError` 与 `TOOL_ABORTED` 常量(值为 `ABORTED`)。不要把它替换成另一个普通 `Error`。
- **使用 `presentationMeta` 投影持久化的卡片数据(可选)。** `output.presentationMeta(args, value)` 从同一个规范值派生可回放的 JSON。核心将其持久化在 `tool/result` 上并传给 `presentResult`,因此需要结果期事实的卡片——例如 `write`/`edit` 的已应用 hunk——无需持久化规范值也能在回放中重现。嵌套 Code 分发没有卡片,因此会跳过该投影器。
- **使用 `exec.agent` 发送异步通知。** `agent.inject({ content, source: { kind: 'my-plugin' } })` 追加持久化上下文,下一次模型请求会看到它——这不是唤醒(空闲的 agent(智能体)保持空闲)。在插件里通过 [`MessageSourceMap`](../subsystems/llm-streaming.zh.md#content-blocks-and-messages) 声明该 `kind`:session format V4 在消息准入处拒绝退役的 `{ kind: 'plugin', plugin: '<name>' }` 包装。请防范已 dispose 的 agent(try/catch)。
+40 -35
View File
@@ -12,6 +12,7 @@ flowchart TD
pkg_brand["brand"]
pkg_chunked_list["chunked-list"]
pkg_deque["deque"]
pkg_error["error"]
pkg_home_paths["home-paths"]
pkg_http_proxy["http-proxy"]
pkg_launch_environment["launch-environment"]
@@ -485,12 +486,8 @@ flowchart TD
subgraph group_workspace["packages/workspace"]
pkg_workspace["workspace"]
end
pkg_llm --> pkg_error
pkg_session --> pkg_scope
pkg_system_prompt --> pkg_llm
pkg_system_prompt --> pkg_scope
pkg_skill --> pkg_llm
pkg_skill --> pkg_scope
pkg_web --> pkg_llm
pkg_api_remotes --> pkg_scope
pkg_attachment --> pkg_brand
pkg_app_boot --> pkg_home_paths
@@ -498,8 +495,6 @@ flowchart TD
pkg_browser_use --> pkg_brand
pkg_client_connection --> pkg_scope
pkg_computer_use --> pkg_brand
pkg_authorization --> pkg_credentials
pkg_authorization --> pkg_llm
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_home_paths
@@ -507,17 +502,17 @@ flowchart TD
pkg_experimental_liveshare --> pkg_scope
pkg_anonymous_user_id --> pkg_brand
pkg_anonymous_user_id --> pkg_home_paths
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_llm
pkg_storage_domain --> pkg_storage
pkg_storage_json --> pkg_storage
pkg_storage_sqlite --> pkg_storage
pkg_subprocess --> pkg_http_proxy
pkg_remote_mock --> pkg_typert_protocol
pkg_typert_loader --> pkg_typert_registry
pkg_skill_office --> pkg_skill
pkg_web_fetch_http --> pkg_timeout
pkg_web_fetch_http --> pkg_web
pkg_system_prompt --> pkg_llm
pkg_system_prompt --> pkg_scope
pkg_skill --> pkg_llm
pkg_skill --> pkg_scope
pkg_web --> pkg_llm
pkg_spill --> pkg_brand
pkg_spill --> pkg_llm
pkg_spill --> pkg_session
@@ -526,17 +521,14 @@ flowchart TD
pkg_attachment_local --> pkg_home_paths
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_deliverable_file_references --> pkg_system_prompt
pkg_authorization --> pkg_credentials
pkg_authorization --> pkg_llm
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_forge_github --> pkg_experimental_forge
pkg_experimental_forge_github --> pkg_subprocess
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
pkg_experimental_webworker_runtime --> pkg_host_webserver
pkg_plugin_recommendations --> pkg_llm
pkg_plugin_recommendations --> pkg_skill
pkg_plugin_recommendations --> pkg_typert_protocol
pkg_skill_badge --> pkg_skill
pkg_host_directory_picker_auto --> pkg_client_connection
pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_native
@@ -545,11 +537,10 @@ flowchart TD
pkg_host_directory_picker_auto --> pkg_host_webserver
pkg_host_frontend_static --> pkg_client_connection
pkg_host_frontend_static --> pkg_host_webserver
pkg_persona --> pkg_system_prompt
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_llm
pkg_sandbox --> pkg_llm
pkg_sandbox --> pkg_session
pkg_sandbox_windows_acl --> pkg_skill
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_session_format_catalog --> pkg_session
pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions
pkg_session_log_deepseek --> pkg_session
@@ -567,11 +558,15 @@ flowchart TD
pkg_fs --> pkg_brand
pkg_fs --> pkg_llm
pkg_fs --> pkg_sandbox
pkg_skill_office --> pkg_skill
pkg_web_fetch_http --> pkg_timeout
pkg_web_fetch_http --> pkg_web
pkg_spill_local --> pkg_spill
pkg_session_log_export --> pkg_session
pkg_session_log_export --> pkg_session_persistence
pkg_config_editor --> pkg_app_boot
pkg_config_editor --> pkg_hmr
pkg_deliverable_file_references --> pkg_system_prompt
pkg_experimental_speech_to_text --> pkg_settings
pkg_experimental_translator --> pkg_credentials
pkg_experimental_translator --> pkg_launch_environment
@@ -580,11 +575,18 @@ flowchart TD
pkg_experimental_translator --> pkg_session_persistence
pkg_experimental_translator --> pkg_timeout
pkg_experimental_translator --> pkg_util_values
pkg_plugin_recommendations --> pkg_llm
pkg_plugin_recommendations --> pkg_skill
pkg_plugin_recommendations --> pkg_typert_protocol
pkg_skill_badge --> pkg_skill
pkg_host_product_telemetry_otel --> pkg_otel
pkg_persona --> pkg_system_prompt
pkg_ptc_runtime --> pkg_sandbox
pkg_sandbox_local --> pkg_llm
pkg_sandbox_local --> pkg_sandbox
pkg_sandbox_local --> pkg_session
pkg_sandbox_windows_acl --> pkg_skill
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
pkg_session_projection_cache --> pkg_session
@@ -717,6 +719,7 @@ flowchart TD
pkg_user_questions --> pkg_typert_protocol
pkg_jobs --> pkg_agent
pkg_jobs --> pkg_brand
pkg_jobs --> pkg_error
pkg_jobs --> pkg_session
pkg_jobs --> pkg_workspace
pkg_sandbox_policy --> pkg_agent
@@ -739,6 +742,7 @@ flowchart TD
pkg_working_directory --> pkg_system_prompt
pkg_terminal --> pkg_agent
pkg_terminal --> pkg_brand
pkg_terminal --> pkg_error
pkg_loader_smoke --> pkg_agent
pkg_loader_smoke --> pkg_http_proxy
pkg_loader_smoke --> pkg_llm
@@ -1540,6 +1544,7 @@ flowchart TD
| [`brand`](../packages/util/brand) | `util` | — |
| [`chunked-list`](../packages/util/chunked-list) | `util` | — |
| [`deque`](../packages/util/deque) | `util` | — |
| [`error`](../packages/util/error) | `util` | — |
| [`home-paths`](../packages/util/home-paths) | `util` | — |
| [`http-proxy`](../packages/util/http-proxy) | `util` | — |
| [`launch-environment`](../packages/util/launch-environment) | `util` | — |
@@ -1558,7 +1563,6 @@ flowchart TD
| [`util-values`](../packages/util/values) | `util` | — |
| [`util-workspace-path`](../packages/util/workspace-path) | `util` | — |
| [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | `llm` | — |
| [`llm`](../packages/llm/llm) | `llm` | — |
| [`scope`](../packages/core/scope) | `core` | — |
| [`fs-patch`](../packages/fs/fs-patch) | `fs` | — |
| [`api-gateway`](../packages/api/gateway) | `api` | — |
@@ -1684,44 +1688,38 @@ flowchart TD
| [`typert-generator`](../packages/typert/generator) | `typert` | — |
| [`typert-protocol`](../packages/typert/protocol) | `typert` | — |
| [`typert-registry`](../packages/typert/registry) | `typert` | — |
| [`llm`](../packages/llm/llm) | `llm` | [`error`](../packages/util/error) |
| [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`skill`](../packages/skill/skill) | `skill` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
| [`client-connection`](../packages/client/connection) | `client` | [`scope`](../packages/core/scope) |
| [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) |
| [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`llm`](../packages/llm/llm) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
| [`experimental-liveshare`](../packages/experimental/liveshare) | `experimental` | [`scope`](../packages/core/scope) |
| [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`storage`](../packages/storage/storage) |
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
| [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) |
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`skill`](../packages/skill/skill) | `skill` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`llm`](../packages/llm/llm) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-forge-github`](../packages/experimental/forge-github) | `experimental` | [`experimental-forge`](../packages/experimental/forge), [`subprocess`](../packages/subprocess/subprocess) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`typert-protocol`](../packages/typert/protocol) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
@@ -1731,14 +1729,21 @@ flowchart TD
| [`otel`](../packages/telemetry/otel) | `telemetry` | [`session`](../packages/core/session) |
| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | `experimental` | [`settings`](../packages/settings/settings) |
| [`experimental-translator`](../packages/experimental/translator) | `experimental` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
| [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`typert-protocol`](../packages/typert/protocol) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
| [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
| [`session-stats`](../packages/session/session-stats) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
@@ -1771,12 +1776,12 @@ flowchart TD
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`error`](../packages/util/error), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`error`](../packages/util/error) |
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
+40 -35
View File
@@ -14,6 +14,7 @@ flowchart TD
pkg_brand["brand"]
pkg_chunked_list["chunked-list"]
pkg_deque["deque"]
pkg_error["error"]
pkg_home_paths["home-paths"]
pkg_http_proxy["http-proxy"]
pkg_launch_environment["launch-environment"]
@@ -487,12 +488,8 @@ flowchart TD
subgraph group_workspace["packages/workspace"]
pkg_workspace["workspace"]
end
pkg_llm --> pkg_error
pkg_session --> pkg_scope
pkg_system_prompt --> pkg_llm
pkg_system_prompt --> pkg_scope
pkg_skill --> pkg_llm
pkg_skill --> pkg_scope
pkg_web --> pkg_llm
pkg_api_remotes --> pkg_scope
pkg_attachment --> pkg_brand
pkg_app_boot --> pkg_home_paths
@@ -500,8 +497,6 @@ flowchart TD
pkg_browser_use --> pkg_brand
pkg_client_connection --> pkg_scope
pkg_computer_use --> pkg_brand
pkg_authorization --> pkg_credentials
pkg_authorization --> pkg_llm
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_home_paths
@@ -509,17 +504,17 @@ flowchart TD
pkg_experimental_liveshare --> pkg_scope
pkg_anonymous_user_id --> pkg_brand
pkg_anonymous_user_id --> pkg_home_paths
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_llm
pkg_storage_domain --> pkg_storage
pkg_storage_json --> pkg_storage
pkg_storage_sqlite --> pkg_storage
pkg_subprocess --> pkg_http_proxy
pkg_remote_mock --> pkg_typert_protocol
pkg_typert_loader --> pkg_typert_registry
pkg_skill_office --> pkg_skill
pkg_web_fetch_http --> pkg_timeout
pkg_web_fetch_http --> pkg_web
pkg_system_prompt --> pkg_llm
pkg_system_prompt --> pkg_scope
pkg_skill --> pkg_llm
pkg_skill --> pkg_scope
pkg_web --> pkg_llm
pkg_spill --> pkg_brand
pkg_spill --> pkg_llm
pkg_spill --> pkg_session
@@ -528,17 +523,14 @@ flowchart TD
pkg_attachment_local --> pkg_home_paths
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_deliverable_file_references --> pkg_system_prompt
pkg_authorization --> pkg_credentials
pkg_authorization --> pkg_llm
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_forge_github --> pkg_experimental_forge
pkg_experimental_forge_github --> pkg_subprocess
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
pkg_experimental_webworker_runtime --> pkg_host_webserver
pkg_plugin_recommendations --> pkg_llm
pkg_plugin_recommendations --> pkg_skill
pkg_plugin_recommendations --> pkg_typert_protocol
pkg_skill_badge --> pkg_skill
pkg_host_directory_picker_auto --> pkg_client_connection
pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_native
@@ -547,11 +539,10 @@ flowchart TD
pkg_host_directory_picker_auto --> pkg_host_webserver
pkg_host_frontend_static --> pkg_client_connection
pkg_host_frontend_static --> pkg_host_webserver
pkg_persona --> pkg_system_prompt
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_llm
pkg_sandbox --> pkg_llm
pkg_sandbox --> pkg_session
pkg_sandbox_windows_acl --> pkg_skill
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_session_format_catalog --> pkg_session
pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions
pkg_session_log_deepseek --> pkg_session
@@ -569,11 +560,15 @@ flowchart TD
pkg_fs --> pkg_brand
pkg_fs --> pkg_llm
pkg_fs --> pkg_sandbox
pkg_skill_office --> pkg_skill
pkg_web_fetch_http --> pkg_timeout
pkg_web_fetch_http --> pkg_web
pkg_spill_local --> pkg_spill
pkg_session_log_export --> pkg_session
pkg_session_log_export --> pkg_session_persistence
pkg_config_editor --> pkg_app_boot
pkg_config_editor --> pkg_hmr
pkg_deliverable_file_references --> pkg_system_prompt
pkg_experimental_speech_to_text --> pkg_settings
pkg_experimental_translator --> pkg_credentials
pkg_experimental_translator --> pkg_launch_environment
@@ -582,11 +577,18 @@ flowchart TD
pkg_experimental_translator --> pkg_session_persistence
pkg_experimental_translator --> pkg_timeout
pkg_experimental_translator --> pkg_util_values
pkg_plugin_recommendations --> pkg_llm
pkg_plugin_recommendations --> pkg_skill
pkg_plugin_recommendations --> pkg_typert_protocol
pkg_skill_badge --> pkg_skill
pkg_host_product_telemetry_otel --> pkg_otel
pkg_persona --> pkg_system_prompt
pkg_ptc_runtime --> pkg_sandbox
pkg_sandbox_local --> pkg_llm
pkg_sandbox_local --> pkg_sandbox
pkg_sandbox_local --> pkg_session
pkg_sandbox_windows_acl --> pkg_skill
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
pkg_session_projection_cache --> pkg_session
@@ -719,6 +721,7 @@ flowchart TD
pkg_user_questions --> pkg_typert_protocol
pkg_jobs --> pkg_agent
pkg_jobs --> pkg_brand
pkg_jobs --> pkg_error
pkg_jobs --> pkg_session
pkg_jobs --> pkg_workspace
pkg_sandbox_policy --> pkg_agent
@@ -741,6 +744,7 @@ flowchart TD
pkg_working_directory --> pkg_system_prompt
pkg_terminal --> pkg_agent
pkg_terminal --> pkg_brand
pkg_terminal --> pkg_error
pkg_loader_smoke --> pkg_agent
pkg_loader_smoke --> pkg_http_proxy
pkg_loader_smoke --> pkg_llm
@@ -1542,6 +1546,7 @@ flowchart TD
| [`brand`](../packages/util/brand) | `util` | — |
| [`chunked-list`](../packages/util/chunked-list) | `util` | — |
| [`deque`](../packages/util/deque) | `util` | — |
| [`error`](../packages/util/error) | `util` | — |
| [`home-paths`](../packages/util/home-paths) | `util` | — |
| [`http-proxy`](../packages/util/http-proxy) | `util` | — |
| [`launch-environment`](../packages/util/launch-environment) | `util` | — |
@@ -1560,7 +1565,6 @@ flowchart TD
| [`util-values`](../packages/util/values) | `util` | — |
| [`util-workspace-path`](../packages/util/workspace-path) | `util` | — |
| [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | `llm` | — |
| [`llm`](../packages/llm/llm) | `llm` | — |
| [`scope`](../packages/core/scope) | `core` | — |
| [`fs-patch`](../packages/fs/fs-patch) | `fs` | — |
| [`api-gateway`](../packages/api/gateway) | `api` | — |
@@ -1686,44 +1690,38 @@ flowchart TD
| [`typert-generator`](../packages/typert/generator) | `typert` | — |
| [`typert-protocol`](../packages/typert/protocol) | `typert` | — |
| [`typert-registry`](../packages/typert/registry) | `typert` | — |
| [`llm`](../packages/llm/llm) | `llm` | [`error`](../packages/util/error) |
| [`session`](../packages/core/session) | `core` | [`scope`](../packages/core/scope) |
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`skill`](../packages/skill/skill) | `skill` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
| [`client-connection`](../packages/client/connection) | `client` | [`scope`](../packages/core/scope) |
| [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) |
| [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`llm`](../packages/llm/llm) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
| [`experimental-liveshare`](../packages/experimental/liveshare) | `experimental` | [`scope`](../packages/core/scope) |
| [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`storage`](../packages/storage/storage) |
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`storage`](../packages/storage/storage) |
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`storage`](../packages/storage/storage) |
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
| [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) |
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`skill`](../packages/skill/skill) | `skill` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`llm`](../packages/llm/llm) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-forge-github`](../packages/experimental/forge-github) | `experimental` | [`experimental-forge`](../packages/experimental/forge), [`subprocess`](../packages/subprocess/subprocess) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`typert-protocol`](../packages/typert/protocol) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
@@ -1733,14 +1731,21 @@ flowchart TD
| [`otel`](../packages/telemetry/otel) | `telemetry` | [`session`](../packages/core/session) |
| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) |
| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | `experimental` | [`settings`](../packages/settings/settings) |
| [`experimental-translator`](../packages/experimental/translator) | `experimental` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
| [`plugin-recommendations`](../packages/experimental/plugin-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`typert-protocol`](../packages/typert/protocol) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
| [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
| [`session-stats`](../packages/session/session-stats) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
@@ -1773,12 +1778,12 @@ flowchart TD
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`error`](../packages/util/error), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`working-directory`](../packages/session/working-directory) | `session` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`error`](../packages/util/error) |
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
+2 -2
View File
@@ -1308,8 +1308,8 @@
en: 0080c345c319a4b5
zh: de1a8b3ed621ce21
/session-persistence-event-catalog/resolved-persistence-types/toolrestriction:
en: d70be5d46cb1ef7d
zh: d3d5a05d62e443fe
en: b2488366535b5fdc
zh: b64d50345521eb99
/session-persistence-event-catalog/resolved-persistence-types/toolresultmessage:
en: 2258683c3cfa4ce3
zh: 5663aa76c911761b
+1 -1
View File
@@ -5269,7 +5269,7 @@ Sources: [`packages/llm/llm/src/types.ts:134`](../packages/llm/llm/src/types.ts)
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
Sources: [`packages/core/tools/src/contracts.ts:376`](../packages/core/tools/src/contracts.ts)
Sources: [`packages/core/tools/src/contracts.ts:395`](../packages/core/tools/src/contracts.ts)
| Property | Presence | Type |
|---|---|---|
+1 -1
View File
@@ -5271,7 +5271,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8`
SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f`
来源:[`packages/core/tools/src/contracts.ts:376`](../packages/core/tools/src/contracts.ts)
来源:[`packages/core/tools/src/contracts.ts:395`](../packages/core/tools/src/contracts.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+1 -1
View File
@@ -223,7 +223,7 @@
{"digest":"3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90","names":[],"sources":["packages/llm/llm/src/types.ts:497"]},
{"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1","names":[],"sources":[]},
{"digest":"3c2535ca0a2062f172f15344f8040755002b5ac506cf94fc4ebaa822b15feb8c","names":[],"sources":[]},
{"digest":"3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f","names":["packages/core/tools/src/contracts.ts#ToolRestriction"],"sources":["packages/core/tools/src/contracts.ts:376"]},
{"digest":"3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f","names":["packages/core/tools/src/contracts.ts#ToolRestriction"],"sources":["packages/core/tools/src/contracts.ts:395"]},
{"digest":"3d7d07060a7be77984feb651ea2bf1925d3e0477911d9d3ed86330d1e8ca6182","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackItem"],"sources":["packages/feedback/message-feedback/src/types.ts:20"]},
{"digest":"3e0eff5946255ff05d88a10839c60305135801f97794ea48d409f8503303317e","names":[],"sources":[]},
{"digest":"3e2ece4967cfebe95f450a93f45523dff99d6749f6269f2f60f84dfecfcf3693","names":[],"sources":[]},
+2 -2
View File
@@ -21,8 +21,8 @@
en: 14522295539ea2fb
zh: 572c462f2c6b70ad
/tools/execution-extensible-waterfalls-plus-monotonic-policy:
en: 0baa3a0d6fb8d3be
zh: 3583d70d2507cac8
en: 0900eea6efe5a500
zh: 13fa6eb7dbd8c9dd
/tools/the-enforced-raw-json-schema-subset:
en: 28e354bfa2f9cebb
zh: be1f75abccf8afdf
+20 -9
View File
@@ -455,7 +455,7 @@ interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
`ToolExecutionToken` is an opaque runtime `Symbol` used only for identity comparison. Before policy, `execute()` materializes and freezes arguments, rejects non-JSON input, and assigns the token. Identity fields, the required caller signal, and the optional parent token remain readonly. A `ToolDispatchExecution` wrapper may replace but not remove the signal; the registry re-fuses the caller signal before invoking the body. Final observers receive the frozen execution identity.
A `ToolGuard` is scope-aware final pre-dispatch policy. Its return type deliberately has no allow result: `undefined` preserves the waterfall decision, while a returned reason can only reduce permission, so a later listener cannot undo it.
A `ToolGuard` is scope-aware final pre-dispatch policy. Its return type deliberately has no allow result: `undefined` preserves the waterfall decision, while a returned reason or `ToolGuardDenial` can only reduce permission, so a later listener cannot undo it. A denial's optional `info` becomes the failed result's structured error identity.
```ts type-equiv
/**
@@ -464,9 +464,19 @@ A `ToolGuard` is scope-aware final pre-dispatch policy. Its return type delibera
* returning `undefined` leaves it unchanged. Because guards have no allow
* result, listener ordering cannot turn a denial back into permission.
* @param execution - the identity-protected call after extensible pre-execute policy completed.
* @returns a final denial reason, or `undefined` to leave the call allowed.
* @returns a final denial reason, a denial with structured error identity, or `undefined` to leave the call allowed.
*/
type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
type ToolGuard = (execution: Readonly<ToolExecution>) => string | ToolGuardDenial | undefined
```
```ts type-equiv
/** A guard denial: the model-facing reason and optional structured error identity recorded on the result. */
interface ToolGuardDenial {
/** Model-facing reason rendered as the failed call's content. */
readonly reason: string
/** Structured error identity persisted beside the reason. */
readonly info?: ToolErrorInfo
}
```
```ts type-equiv
@@ -522,7 +532,7 @@ interface ToolErrorInfo {
}
```
The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/ptc-dispatch` stores the sub-call's rendered `content`, `isError`, optional structured `error`, and presentation `meta`. Replay reproduces presentation but cannot reconstruct canonical intermediate values. Optional `ToolErrorInfo.reason` retains raw user-facing detail without adding it to model-facing content. The registry sets `info` from a thrown `HarnessError` (its `name` and `code`), from a pre-execute `deny` that supplies it, and on its own cancellation results (`AbortError` with `ABORTED` or `ABORTED_BEFORE_DISPATCH`); a `tools/execute` wrapper may also return a result with `info`, such as the timeout policy's `TOOL_TIMEOUT`. Any other thrown value, guard denial, approval rejection, or post-execute block produces an `error` without `info`.
The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/ptc-dispatch` stores the sub-call's rendered `content`, `isError`, optional structured `error`, and presentation `meta`. Replay reproduces presentation but cannot reconstruct canonical intermediate values. Optional `ToolErrorInfo.reason` retains raw user-facing detail without adding it to model-facing content. The registry sets `info` from a thrown `HarnessError` (its `name` and `code`), from a pre-execute `deny` that supplies it, and on its own cancellation results (`AbortError` with `ABORTED` or `ABORTED_BEFORE_DISPATCH`); when a started body throws the signal's reason or an `Error` named `AbortError` after cancellation, the result keeps that message and carries `AbortError` with `ABORTED`; a `tools/execute` wrapper may also return a result with `info`, such as the timeout policy's `TOOL_TIMEOUT`. An `ask` that approval does not grant becomes a denial with the name `ApprovalError`, which identifies the registry's denial and is not a thrown class, and `APPROVAL_REJECTED`, `APPROVAL_CANCELLED`, or `APPROVAL_UNAVAILABLE`. A guard denial or post-execute block carries `info` only when it supplies one; any other thrown value produces an `error` without `info`.
On success the registry snapshots and validates the body value, freezes it, and invokes the pure renderer plus the optional presentation metadata projector for native and nested calls. It separately materializes the durable presentation fields immediately before `tools/result`; an invalid value, renderer/projector failure, or non-JSON presentation becomes a JSON-safe `isError`. The final live observer therefore sees the exact execution-local value beside fields safe for the later durable append.
@@ -550,17 +560,18 @@ type PreToolDecision =
```ts type-equiv
/**
* Post-dispatch decision: accept, replace one projection, attach context for the
* next request, or block by turning corrective feedback into an error result.
* next request, or block by turning corrective feedback into an error result
* whose optional `info` is its structured error identity.
*/
type PostToolDecision =
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessage[] }
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessage[] }
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessage[] }
| { kind: 'block'; feedback: ContentBlock[]; info?: ToolErrorInfo; additionalContexts?: UserMessage[] }
```
Call `next()` for the default or return a decision to short-circuit. Pre-policy may deny or ask; only `allowed-once` proceeds, while a non-grant, missing approval channel or service, or agent-less request becomes a denial. A deny may attach structured identity and user-facing detail without changing its model-facing reason. Guards may still impose a final denial. Arguments cannot be rewritten because history, audit, UI, and execution must agree.
Post-policy may replace either content or value, never both. Content replacement preserves the canonical value and existing metadata; value replacement is revalidated and recomputes content/metadata; a block removes the value and becomes an `isError` containing corrective feedback. Content replacement is presentation policy, not confidentiality policy: a listener that must hide the programmatic value blocks or replaces it. `tools/result` receives the frozen execution and result after normalization; observers cannot transform them, and observer failures are contained. Unknown and throwing tools both become structured errors (`ToolNotFoundError` maps to `UNKNOWN_TOOL`), so the call fails without ending the turn.
Post-policy may replace either content or value, never both. Content replacement preserves the canonical value and existing metadata; value replacement is revalidated and recomputes content/metadata; a block removes the value and becomes an `isError` containing corrective feedback. Content replacement is presentation policy, not confidentiality policy: a listener that must hide the programmatic value blocks or replaces it. `tools/result` receives the frozen execution and result after normalization; observers cannot transform them, and observer failures are contained. Unknown and throwing tools both become structured errors (`ToolNotFoundError` maps to `UNKNOWN_TOOL`), so the call fails without ending the turn. A tool body whose composition or call lacks a required Agent, service, or open turn throws `ToolContextUnavailableError` (`TOOL_CONTEXT_UNAVAILABLE`).
## The enforced raw JSON Schema subset
@@ -733,9 +744,9 @@ guard(guard: ToolGuard): () => void
/**
* Check global and inherited guards after pre-execute policy and approval resolve.
* @param exec - invocation whose guards are checked before dispatch.
* @returns the first denial reason, or undefined when no guard denies.
* @returns the first denial with its optional structured identity, or undefined when no guard denies.
*/
checkGuards(exec: ToolExecution): string | undefined
checkGuards(exec: ToolExecution): ToolGuardDenial | undefined
```
Types: [ScopeKey](scope.md)
+20 -9
View File
@@ -455,7 +455,7 @@ interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
`ToolExecutionToken` 是不透明的运行时 `Symbol`,仅用于身份比较。策略执行前,`execute()` 会物化并冻结参数、拒绝非 JSON 输入并分配 token。身份字段、调用方必需的 signal 和可选的 parent token 均保持 readonly。`ToolDispatchExecution` 包装层可以替换 signal 但不能移除;注册表会在调用工具函数体前重新融合调用方的 signal。最终观察者接收冻结的执行身份。
`ToolGuard` 是感知作用域的最终预分派策略。其返回类型有意不包含 allow 结果:`undefined` 保留 waterfall 的决策,而返回的 reason 只能缩减权限,因此后续监听器无法撤销它。
`ToolGuard` 是感知作用域的最终预分派策略。其返回类型有意不包含 allow 结果:`undefined` 保留 waterfall 的决策,而返回的 reason 或 `ToolGuardDenial` 只能缩减权限,因此后续监听器无法撤销它。拒绝中可选的 `info` 会成为失败结果的结构化错误身份。
```ts type-equiv
/**
@@ -464,9 +464,19 @@ interface ToolDispatchExecution extends Omit<ToolExecution, 'signal'> {
* returning `undefined` leaves it unchanged. Because guards have no allow
* result, listener ordering cannot turn a denial back into permission.
* @param execution - the identity-protected call after extensible pre-execute policy completed.
* @returns a final denial reason, or `undefined` to leave the call allowed.
* @returns a final denial reason, a denial with structured error identity, or `undefined` to leave the call allowed.
*/
type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
type ToolGuard = (execution: Readonly<ToolExecution>) => string | ToolGuardDenial | undefined
```
```ts type-equiv
/** A guard denial: the model-facing reason and optional structured error identity recorded on the result. */
interface ToolGuardDenial {
/** Model-facing reason rendered as the failed call's content. */
readonly reason: string
/** Structured error identity persisted beside the reason. */
readonly info?: ToolErrorInfo
}
```
```ts type-equiv
@@ -522,7 +532,7 @@ interface ToolErrorInfo {
}
```
结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta`,`tool/ptc-dispatch` 则存储子调用渲染后的 `content`、`isError`、可选结构化 `error` 和展示 `meta`。回放可以重现展示,却无法重建规范的中间值。可选的 `ToolErrorInfo.reason` 保留面向用户的原始详情,不将其加入模型可见内容。注册表会在以下情况设置 `info`:抛出的是 `HarnessError`(取其 `name` 和 `code`)、pre-execute 的 `deny` 自带 `info`,以及注册表自己生成的取消结果(`AbortError`,code 为 `ABORTED` 或 `ABORTED_BEFORE_DISPATCH`);`tools/execute` 包装器也可能返回带 `info` 的结果,例如超时策略的 `TOOL_TIMEOUT`。其他抛出值、guard 拒绝、审批拒绝和 post-execute block 产生的 `error` 都不含 `info`。
结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta`,`tool/ptc-dispatch` 则存储子调用渲染后的 `content`、`isError`、可选结构化 `error` 和展示 `meta`。回放可以重现展示,却无法重建规范的中间值。可选的 `ToolErrorInfo.reason` 保留面向用户的原始详情,不将其加入模型可见内容。注册表会在以下情况设置 `info`:抛出的是 `HarnessError`(取其 `name` 和 `code`)、pre-execute 的 `deny` 自带 `info`,以及注册表自己生成的取消结果(`AbortError`,code 为 `ABORTED` 或 `ABORTED_BEFORE_DISPATCH`);已开始的工具主体在取消后抛出 signal 的 reason 或 name 为 `AbortError` 的 `Error` 时,结果保留其 message,并带上 `AbortError` 与 `ABORTED`;`tools/execute` 包装器也可能返回带 `info` 的结果,例如超时策略的 `TOOL_TIMEOUT`。审批未授予的 `ask` 会变成 name 为 `ApprovalError` 的拒绝(该 name 标识注册表的拒绝,并不对应可抛出的类),code 为 `APPROVAL_REJECTED`、`APPROVAL_CANCELLED` 或 `APPROVAL_UNAVAILABLE`。guard 拒绝或 post-execute block 只有自带 `info` 时才会携带它;其他抛出值产生的 `error` 不含 `info`。
成功时,注册表会快照并校验函数体返回值,将其冻结,然后调用纯渲染器,并为原生与嵌套调用执行可选的展示元数据投影器。注册表会在 `tools/result` 之前另行物化持久展示字段;无效值、渲染器/投影器失败或非 JSON 展示都会转为 JSON 安全的 `isError`。因此,最终实时观察者能看到精确的执行期值,以及可安全用于后续持久追加的字段。
@@ -550,17 +560,18 @@ type PreToolDecision =
```ts type-equiv
/**
* Post-dispatch decision: accept, replace one projection, attach context for the
* next request, or block by turning corrective feedback into an error result.
* next request, or block by turning corrective feedback into an error result
* whose optional `info` is its structured error identity.
*/
type PostToolDecision =
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessage[] }
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessage[] }
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessage[] }
| { kind: 'block'; feedback: ContentBlock[]; info?: ToolErrorInfo; additionalContexts?: UserMessage[] }
```
调用 `next()` 获取默认决策,或直接返回一个决策以短路。前置策略可以 deny 或 ask;只有 `allowed-once` 才继续执行,而未授权、缺少审批通道或服务、或无 agent 的请求都会变为拒绝。拒绝可以附带结构化身份与用户可见详情,而不改变其模型可见原因。Guard 仍可施加最终拒绝。参数不可被改写,因为历史记录、审计、UI 和执行必须保持一致。
后置策略可以替换内容或值,但不能同时替换两者。替换内容会保留规范值和现有元数据;替换值会重新校验并重新计算内容/元数据;阻止会移除值,并转为包含纠正反馈的 `isError`。内容替换是展示策略,而非保密策略;需要隐藏程序化值的监听器必须阻止或替换该值。`tools/result` 在归一化后接收冻结的执行和结果;观察者无法对其进行变换,观察者的失败也会被隔离。未知工具和抛出异常的工具都会变为结构化错误(`ToolNotFoundError` 映射为 `UNKNOWN_TOOL`),调用失败但不终止当前轮次。
后置策略可以替换内容或值,但不能同时替换两者。替换内容会保留规范值和现有元数据;替换值会重新校验并重新计算内容/元数据;阻止会移除值,并转为包含纠正反馈的 `isError`。内容替换是展示策略,而非保密策略;需要隐藏程序化值的监听器必须阻止或替换该值。`tools/result` 在归一化后接收冻结的执行和结果;观察者无法对其进行变换,观察者的失败也会被隔离。未知工具和抛出异常的工具都会变为结构化错误(`ToolNotFoundError` 映射为 `UNKNOWN_TOOL`),调用失败但不终止当前轮次。工具主体所需的 Agent、服务或进行中的 turn 不存在时,抛出 `ToolContextUnavailableError`(`TOOL_CONTEXT_UNAVAILABLE`)。
## 已强制执行的原始 JSON Schema 子集
@@ -733,9 +744,9 @@ guard(guard: ToolGuard): () => void
/**
* Check global and inherited guards after pre-execute policy and approval resolve.
* @param exec - invocation whose guards are checked before dispatch.
* @returns the first denial reason, or undefined when no guard denies.
* @returns the first denial with its optional structured identity, or undefined when no guard denies.
*/
checkGuards(exec: ToolExecution): string | undefined
checkGuards(exec: ToolExecution): ToolGuardDenial | undefined
```
Types: [ScopeKey](scope.zh.md)
+2 -2
View File
@@ -45,8 +45,8 @@
en: 2dee0a37e0c1fa38
zh: 0963cba32685612e
/deepseek-ai-dsh-tools/understand-the-implementation/execution-and-cancellation:
en: dc44a88997777988
zh: 56454aeace1c8b77
en: c6db8efb31370459
zh: 07aec8a6230d943a
/deepseek-ai-dsh-tools/understand-the-implementation/ptc-mode:
en: 83cf9a5077158879
zh: 80508a70a3674226
+1 -1
View File
@@ -125,7 +125,7 @@ The registry holds typed `ToolDefinition`s in scoped layers and projects them on
### Execution and cancellation
Each typed invocation materializes and freezes parsed arguments, assigns an opaque correlation token, and runs policy and dispatch. A pre-execute denial may attach `ToolErrorInfo` beside its model-facing reason; the native and PTC durable projections preserve the structured name, code, and optional user-facing reason without adding that detail to model content. Cancellation is cooperative and quiescent: every tool body receives the caller-owned `exec.signal` and must observe it; cancellation before body invocation is `ABORTED_BEFORE_DISPATCH`, after invocation it replaces only a successful outcome with `ABORTED`. Denials, wrapper failures, tool failures, post-policy failures, and timeout-owned `TOOL_TIMEOUT` remain more specific. Unknown and throwing tools become structured errors (`UNKNOWN_TOOL`), so a call fails without ending the turn.
Each typed invocation materializes and freezes parsed arguments, assigns an opaque correlation token, and runs policy and dispatch. A pre-execute denial may attach `ToolErrorInfo` beside its model-facing reason; the native and PTC durable projections preserve the structured name, code, and optional user-facing reason without adding that detail to model content. Cancellation is cooperative and quiescent: every tool body receives the caller-owned `exec.signal` and must observe it; cancellation before body invocation is `ABORTED_BEFORE_DISPATCH`, after invocation it replaces only a successful outcome with `ABORTED`, and a thrown signal reason or `Error` named `AbortError` without `HarnessError` identity keeps its message but carries `ABORTED`. Denials, wrapper failures, tool failures, post-policy failures, and timeout-owned `TOOL_TIMEOUT` remain more specific. Unknown and throwing tools become structured errors (`UNKNOWN_TOOL`), so a call fails without ending the turn.
### PTC mode
+1 -1
View File
@@ -125,7 +125,7 @@ Agent 步骤的并发上限使用 [agent-loop 配置](../agent-loop/README.zh.md
### 执行与取消
每次类型化调用都会实体化并冻结解析后的参数、分配不透明关联 token,再运行策略与分发。pre-execute 拒绝可以在模型可见原因旁附带 `ToolErrorInfo`;原生与 PTC 持久投影会保留结构化名称、代码与可选用户可见原因,但不会把该详情加入模型内容。取消采用协作式并等待完全停稳:每个工具主体都收到调用方拥有的 `exec.signal` 且必须观测它;调用主体前的取消为 `ABORTED_BEFORE_DISPATCH`,调用主体后的取消只能把成功结果替换为 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败与超时产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。未知工具与抛出异常的工具都会变成结构化错误(`UNKNOWN_TOOL`),因此调用会失败而不会结束轮次。
每次类型化调用都会实体化并冻结解析后的参数、分配不透明关联 token,再运行策略与分发。pre-execute 拒绝可以在模型可见原因旁附带 `ToolErrorInfo`;原生与 PTC 持久投影会保留结构化名称、代码与可选用户可见原因,但不会把该详情加入模型内容。取消采用协作式并等待完全停稳:每个工具主体都收到调用方拥有的 `exec.signal` 且必须观测它;调用主体前的取消为 `ABORTED_BEFORE_DISPATCH`,调用主体后的取消只能把成功结果替换为 `ABORTED`,抛出的 signal reason,或不具备 `HarnessError` 身份、name 为 `AbortError` 的 `Error`,保留其 message 并携带 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败与超时产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。未知工具与抛出异常的工具都会变成结构化错误(`UNKNOWN_TOOL`),因此调用会失败而不会结束轮次。
### PTC mode
+31 -4
View File
@@ -1,5 +1,6 @@
/** Tool definitions and invocation data shared by registries and executors. @module */
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { TOOL_CONTEXT_UNAVAILABLE } from './error-codes.ts'
import type { ToolCallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
import type { Agent, ModelSelection } from '@deepseek-ai/dsh-agent'
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
@@ -306,6 +307,23 @@ export class ToolNotFoundError extends HarnessError {
}
}
/**
* Thrown by a tool body when its composition or call lacks context the tool
* requires, such as a calling Agent, an injected service, or an open turn.
* The failure is not an argument the model can correct; `message` names the
* missing context.
*/
export class ToolContextUnavailableError extends HarnessError {
/**
* @param message - names the tool and the missing context.
* @param options - standard `cause` chaining.
*/
constructor(message: string, options?: ErrorOptions) {
super(message, TOOL_CONTEXT_UNAVAILABLE, options)
this.name = 'ToolContextUnavailableError'
}
}
/** Thrown when a tool body or post-policy value violates its declared output. */
export class ToolOutputError extends HarnessError {
/** Schema/value violations in validation order. */
@@ -362,12 +380,13 @@ export type PreToolDecision =
/**
* Post-dispatch decision: accept, replace one projection, attach context for the
* next request, or block by turning corrective feedback into an error result.
* next request, or block by turning corrective feedback into an error result
* whose optional `info` is its structured error identity.
*/
export type PostToolDecision =
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessage[] }
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessage[] }
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessage[] }
| { kind: 'block'; feedback: ContentBlock[]; info?: ToolErrorInfo; additionalContexts?: UserMessage[] }
/**
* Per-scope filter over global tools. Restrictions intersect and do not affect
@@ -386,6 +405,14 @@ export interface ToolRestriction {
* returning `undefined` leaves it unchanged. Because guards have no allow
* result, listener ordering cannot turn a denial back into permission.
* @param execution - the identity-protected call after extensible pre-execute policy completed.
* @returns a final denial reason, or `undefined` to leave the call allowed.
* @returns a final denial reason, a denial with structured error identity, or `undefined` to leave the call allowed.
*/
export type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
export type ToolGuard = (execution: Readonly<ToolExecution>) => string | ToolGuardDenial | undefined
/** A guard denial: the model-facing reason and optional structured error identity recorded on the result. */
export interface ToolGuardDenial {
/** Model-facing reason rendered as the failed call's content. */
readonly reason: string
/** Structured error identity persisted beside the reason. */
readonly info?: ToolErrorInfo
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Error identities the registry records on failed tool results. The values are
* persisted contract; the entry does not re-export them, and consumers compare
* the recorded strings.
* @module
*/
/** Error name the registry records when approval does not grant a call. */
export const APPROVAL_ERROR_NAME = 'ApprovalError'
/** Approval code for a call the user rejected. */
export const APPROVAL_REJECTED = 'APPROVAL_REJECTED'
/** Approval code for an approval request cancelled without a caller cancellation. */
export const APPROVAL_CANCELLED = 'APPROVAL_CANCELLED'
/** Approval code for a call that needs approval when no approval service, Agent, or channel can answer it. */
export const APPROVAL_UNAVAILABLE = 'APPROVAL_UNAVAILABLE'
/** One code an approval denial records under {@link APPROVAL_ERROR_NAME}. */
export type ApprovalErrorCode = typeof APPROVAL_REJECTED | typeof APPROVAL_CANCELLED | typeof APPROVAL_UNAVAILABLE
/** Code of `ToolContextUnavailableError`. */
export const TOOL_CONTEXT_UNAVAILABLE = 'TOOL_CONTEXT_UNAVAILABLE'
+5 -5
View File
@@ -11,7 +11,7 @@ import { projectToolSchema } from './schema.ts'
import { assertSupportedJsonSchema } from './json-schema.ts'
import { RUN_CODE_NAME, ToolActivationError } from './contracts.ts'
import type {
ToolDefinition, ToolRestriction, ToolExecution, ToolGuard,
ToolDefinition, ToolRestriction, ToolExecution, ToolGuard, ToolGuardDenial,
ResolvedToolSet, ToolActivationContext, ToolRegistrationOptions,
} from './contracts.ts'
@@ -388,13 +388,13 @@ export class ToolsService extends Service {
/**
* Check global and inherited guards after pre-execute policy and approval resolve.
* @param exec - invocation whose guards are checked before dispatch.
* @returns the first denial reason, or undefined when no guard denies.
* @returns the first denial with its optional structured identity, or undefined when no guard denies.
*/
checkGuards(exec: ToolExecution): string | undefined {
checkGuards(exec: ToolExecution): ToolGuardDenial | undefined {
for (const layer of [this.guards.global, ...this.guards.chainLayers(exec.agent)]) {
for (const guard of layer.guards.values()) {
const reason = guard(exec)
if (reason !== undefined) return reason
const denial = guard(exec)
if (denial !== undefined) return typeof denial === 'string' ? { reason: denial } : denial
}
}
return undefined
+36 -13
View File
@@ -8,6 +8,7 @@ import type { UserMessage } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-user-approval'
import { assertNever, deepFreeze, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
import { TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, ToolNotFoundError, ToolOutputError } from './contracts.ts'
import { APPROVAL_CANCELLED, APPROVAL_ERROR_NAME, APPROVAL_REJECTED, APPROVAL_UNAVAILABLE, type ApprovalErrorCode } from './error-codes.ts'
import { validateJsonSchemaValue } from './json-schema.ts'
import type { ToolsService } from './index.ts'
import type { ResolvedToolSet, ToolDefinition, ToolExecutionInput, ToolExecutionMode, ToolExecution, ToolRunContext, ToolExecutionResult, ToolExecutionSuccess, ToolErrorInfo, ToolExecutionToken, PreToolDecision, PostToolDecision } from './contracts.ts'
@@ -210,9 +211,9 @@ export class ToolExecutionPipeline {
if (decision.kind === 'cancel') {
return { kind: 'post-result', exec, result: toolAbortedBeforeDispatchResult() }
}
const denialReason = decision.kind === 'allow' ? this.tools.checkGuards(exec) : decision.reason
const denialInfo = decision.kind === 'deny' ? decision.info : undefined
if (denialReason !== undefined) {
const denial = decision.kind === 'allow' ? this.tools.checkGuards(exec) : decision
if (denial !== undefined) {
const { reason: denialReason, info: denialInfo } = denial
return {
kind: 'post-result',
exec,
@@ -254,6 +255,10 @@ export class ToolExecutionPipeline {
* Dispatch the registered body with the original caller signal fused back
* into any around-wrapper replacement. Cancellation never abandons the body:
* a started promise reaches quiescence before its outcome becomes `ABORTED`.
* After cancellation, a thrown signal reason or an `Error` named
* `AbortError` without `HarnessError` identity keeps its message and gains
* the `ABORTED` identity, whichever operation inside the body aborted; a
* thrown `HarnessError` and any other failure stay unchanged.
*/
private async dispatchToolBody(exec: MutableToolRunContext): Promise<ToolExecutionResult> {
const state = this.cancellationStates.get(exec)
@@ -278,7 +283,9 @@ export class ToolExecutionPipeline {
? toolAbortedResult(result)
: result
} catch (error: unknown) {
return toolErrorResult(error)
return isAborted(signal) && isUnclassifiedCancellation(error, signal)
? toolErrorResult(error, { name: 'AbortError', code: TOOL_ABORTED })
: toolErrorResult(error)
} finally {
fused.dispose()
exec.signal = wrapperSignal
@@ -427,7 +434,8 @@ export class ToolExecutionPipeline {
* session to audit to and no UI to route to. Otherwise the outcome maps
* one-to-one — `allowed-once` proceeds; the three non-grants deny with
* distinct reasons so the model can tell a human "no" from an absent
* approval channel.
* approval channel. Every denial carries `ApprovalError` with
* `APPROVAL_REJECTED`, `APPROVAL_CANCELLED`, or `APPROVAL_UNAVAILABLE`.
*/
private async serviceAsk(
exec: ToolExecution,
@@ -436,13 +444,13 @@ export class ToolExecutionPipeline {
const approval = this.ctx.get('approval')
if (approval === undefined) {
return {
decision: { kind: 'deny', reason: ask.reason ?? `tool "${exec.name}" requires approval (not yet supported)` },
decision: approvalDenial(ask.reason ?? `tool "${exec.name}" requires approval (not yet supported)`, APPROVAL_UNAVAILABLE),
approvalCancelled: false,
}
}
if (exec.agent === undefined) {
return {
decision: { kind: 'deny', reason: `tool "${exec.name}" requires approval, but the call has no agent to route it through` },
decision: approvalDenial(`tool "${exec.name}" requires approval, but the call has no agent to route it through`, APPROVAL_UNAVAILABLE),
approvalCancelled: false,
}
}
@@ -457,15 +465,15 @@ export class ToolExecutionPipeline {
switch (outcome) {
case 'allowed-once': return { decision: { kind: 'allow' }, approvalCancelled: false }
case 'rejected': return {
decision: { kind: 'deny', reason: `the user rejected tool "${exec.name}"` },
decision: approvalDenial(`the user rejected tool "${exec.name}"`, APPROVAL_REJECTED),
approvalCancelled: false,
}
case 'cancelled': return {
decision: { kind: 'deny', reason: `approval for tool "${exec.name}" was cancelled` },
decision: approvalDenial(`approval for tool "${exec.name}" was cancelled`, APPROVAL_CANCELLED),
approvalCancelled: true,
}
case 'unavailable': return {
decision: { kind: 'deny', reason: `tool "${exec.name}" requires approval, but no approval channel is available` },
decision: approvalDenial(`tool "${exec.name}" requires approval, but no approval channel is available`, APPROVAL_UNAVAILABLE),
approvalCancelled: false,
}
default: return assertNever(outcome, 'ApprovalOutcome')
@@ -494,7 +502,7 @@ export class ToolExecutionPipeline {
return this.markCanonical(exec, {
content: decision.feedback,
isError: true,
error: { message },
error: { message, ...decision.info === undefined ? {} : { info: decision.info } },
...decisionContexts.length > 0 ? { additionalContexts: decisionContexts } : {},
})
}
@@ -687,13 +695,28 @@ function errorInfo(error: unknown): ToolErrorInfo | undefined {
}
}
/** Materialize a non-granting approval outcome as a denial with routable `ApprovalError` identity. */
function approvalDenial(reason: string, code: ApprovalErrorCode): Extract<PreToolDecision, { kind: 'deny' }> {
return { kind: 'deny', reason, info: { name: APPROVAL_ERROR_NAME, code } }
}
/** Whether a value thrown after cancellation is that cancellation and carries no identity of its own. */
function isUnclassifiedCancellation(error: unknown, signal: AbortSignal): boolean {
if (errorInfo(error) !== undefined) return false
try {
return error === signal.reason || (error instanceof Error && error.name === 'AbortError')
} catch {
// A value whose prototype or `name` cannot be read is not a recognizable cancellation.
return false
}
}
/** Mint a same-process correlation token whose identity is its value. */
function createExecutionToken(): ToolExecutionToken {
return Symbol('dsh.tool.execution') as ToolExecutionToken
}
function toolErrorResult(error: unknown): ToolExecutionResult {
const info = errorInfo(error)
function toolErrorResult(error: unknown, info = errorInfo(error)): ToolExecutionResult {
const message = toolErrorMessage(error)
return {
content: [{ type: 'text', text: `Error: ${message}` }],
+14
View File
@@ -335,6 +335,20 @@ describe('scoped execution dispatch', () => {
expect(bodyCalls).toBe(0)
})
it('records the structured identity of a guard denial', async () => {
const ctx = await mount()
ctx.tools.register(tool('t'))
ctx.tools.guard(() => ({ reason: 'outside the shared workspace', info: { name: 'ForkDeniedError', code: 'FORK_PATH_OUTSIDE_WORKSPACE' } }))
const result = await executeToolFixture(ctx, { signal: testToolSignal, callId: ToolCallId('c1'), name: 't', arguments: {} })
expect(result).toMatchObject({
content: [{ type: 'text', text: 'Error: outside the shared workspace' }],
isError: true,
error: { message: 'outside the shared workspace', info: { name: 'ForkDeniedError', code: 'FORK_PATH_OUTSIDE_WORKSPACE' } },
})
})
it('live-iterates a guard registered by an earlier guard', async () => {
const ctx = await mount()
const calls: string[] = []
+155 -1
View File
@@ -11,7 +11,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import ToolRuntime, { defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError, TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, type InferArgs, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision, type JsonSchemaNode, type ToolDefinition, type ToolDispatchExecution, type ToolExecutionResult, type ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { ToolContextUnavailableError, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError, TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, type InferArgs, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision, type JsonSchemaNode, type ToolDefinition, type ToolDispatchExecution, type ToolExecutionResult, type ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
@@ -569,6 +569,23 @@ describe('ToolRuntime', () => {
expect(result.error?.message).toBe(message)
})
it('records the structured identity of a post-execute block', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
ctx.on('tools/post-execute', async (): Promise<PostToolDecision> => ({
kind: 'block',
feedback: [{ type: 'text', text: 'blocked by hook' }],
info: { name: 'HookBlockedError', code: 'HOOK_BLOCKED' },
}))
const result = await executeToolFixture(ctx, { signal: testToolSignal, callId: ToolCallId('block-info'), name: 'echo', arguments: {} })
expect(result).toMatchObject({
content: [{ type: 'text', text: 'blocked by hook' }],
isError: true,
error: { message: 'blocked by hook', info: { name: 'HookBlockedError', code: 'HOOK_BLOCKED' } },
})
})
it('contains a non-JSON post-execute failure projection as a safe final error', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
@@ -776,6 +793,25 @@ describe('ToolRuntime', () => {
})
})
it('records ToolContextUnavailableError thrown by a tool body', async () => {
const ctx = await setup()
ctx.tools.register({
...echoTool,
name: 'needs-agent',
execute() { return Promise.reject(new ToolContextUnavailableError('needs-agent requires a calling agent')) },
})
const result = await executeToolFixture(ctx, {
signal: testToolSignal, callId: ToolCallId('needs-agent'), name: 'needs-agent', arguments: {},
})
expect(result).toMatchObject({
content: [{ type: 'text', text: 'Error: needs-agent requires a calling agent' }],
isError: true,
error: { info: { name: 'ToolContextUnavailableError', code: 'TOOL_CONTEXT_UNAVAILABLE' } },
})
})
it('ToolNotFoundError carries a stable message and code', async () => {
const { HarnessError } = await import('@deepseek-ai/dsh-llm')
const err = new ToolNotFoundError('ghost')
@@ -837,6 +873,7 @@ describe('ToolRuntime', () => {
const result = await executeToolFixture(ctx, { signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } })
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'Error: tool "echo" requires approval (not yet supported)' })
expect(result.error?.info).toEqual({ name: 'ApprovalError', code: 'APPROVAL_UNAVAILABLE' })
})
describe('ask routing through ctx.approval', () => {
@@ -893,6 +930,7 @@ describe('ToolRuntime', () => {
const result = await executeToolFixture(ctx, { signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() })
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'Error: the user rejected tool "echo"' })
expect(result.error?.info).toEqual({ name: 'ApprovalError', code: 'APPROVAL_REJECTED' })
})
it('denies with the cancellation reason on cancelled', async () => {
@@ -903,6 +941,7 @@ describe('ToolRuntime', () => {
const result = await executeToolFixture(ctx, { signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() })
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'Error: approval for tool "echo" was cancelled' })
expect(result.error?.info).toEqual({ name: 'ApprovalError', code: 'APPROVAL_CANCELLED' })
})
it('returns ABORTED_BEFORE_DISPATCH when caller cancellation overtakes approval', async () => {
@@ -947,6 +986,7 @@ describe('ToolRuntime', () => {
const result = await executeToolFixture(ctx, { signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() })
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'Error: tool "echo" requires approval, but no approval channel is available' })
expect(result.error?.info).toEqual({ name: 'ApprovalError', code: 'APPROVAL_UNAVAILABLE' })
})
it('denies an agent-less execution without asking — nothing to route or audit through', async () => {
@@ -962,6 +1002,7 @@ describe('ToolRuntime', () => {
expect(asked).toBe(false)
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'Error: tool "echo" requires approval, but the call has no agent to route it through' })
expect(result.error?.info).toEqual({ name: 'ApprovalError', code: 'APPROVAL_UNAVAILABLE' })
})
it('turns a rogue outcome from a NON-conforming approval stand-in into an isError result', async () => {
@@ -1729,6 +1770,119 @@ describe('ToolRuntime', () => {
})
})
it.each([
['the signal reason', (signal: AbortSignal): void => { signal.throwIfAborted() }, 'caller stopped'],
['a dependency AbortError', (): void => { throw new DOMException('This operation was aborted', 'AbortError') }, 'This operation was aborted'],
])('classifies %s thrown after cancellation as ABORTED', async (_label, fail, message) => {
const ctx = await setup()
const entered = Promise.withResolvers<undefined>()
ctx.tools.register({
...echoTool,
name: 'throws-on-abort',
async execute(_args, exec) {
entered.resolve(undefined)
await new Promise<void>((resolve) => {
exec.signal.addEventListener('abort', () => { resolve() }, { once: true })
})
fail(exec.signal)
return 'not reached'
},
})
const controller = new AbortController()
const pending = executeToolFixture(ctx, {
callId: ToolCallId('throws-on-abort'), name: 'throws-on-abort', arguments: {}, signal: controller.signal,
})
await entered.promise
controller.abort('caller stopped')
await expect(pending).resolves.toMatchObject({
content: [{ type: 'text', text: `Error: ${message}` }],
isError: true,
error: { message, info: { name: 'AbortError', code: TOOL_ABORTED } },
})
})
it('keeps a HarnessError thrown after cancellation', async () => {
const ctx = await setup()
const entered = Promise.withResolvers<undefined>()
ctx.tools.register({
...echoTool,
name: 'domain-abort',
execute(_args, exec) {
entered.resolve(undefined)
return new Promise<string>((_resolve, reject) => {
exec.signal.addEventListener('abort', () => {
reject(new HarnessError('search cancelled', 'SEARCH_ABORTED'))
}, { once: true })
})
},
})
const controller = new AbortController()
const pending = executeToolFixture(ctx, {
callId: ToolCallId('domain-abort'), name: 'domain-abort', arguments: {}, signal: controller.signal,
})
await entered.promise
controller.abort('caller stopped')
await expect(pending).resolves.toMatchObject({
content: [{ type: 'text', text: 'Error: search cancelled' }],
isError: true,
error: { info: { name: 'HarnessError', code: 'SEARCH_ABORTED' } },
})
})
it.each([
['a distinct failure', (): void => { throw new Error('cleanup failed') }, 'Error: cleanup failed'],
['an uninspectable value', (): void => {
throw new Proxy({}, {
getPrototypeOf: () => { throw new Error('prototype trap') },
has: () => { throw new Error('has trap') },
get: () => { throw new Error('get trap') },
})
}, 'Error: <unprintable thrown value>'],
])('keeps %s thrown during cancellation without info', async (_label, fail, text) => {
const ctx = await setup()
const entered = Promise.withResolvers<undefined>()
ctx.tools.register({
...echoTool,
name: 'fails-during-abort',
async execute(_args, exec) {
entered.resolve(undefined)
await new Promise<void>((resolve) => {
exec.signal.addEventListener('abort', () => { resolve() }, { once: true })
})
fail()
return 'not reached'
},
})
const controller = new AbortController()
const pending = executeToolFixture(ctx, {
callId: ToolCallId('fails-during-abort'), name: 'fails-during-abort', arguments: {}, signal: controller.signal,
})
await entered.promise
controller.abort('caller stopped')
const result = await pending
expect(result).toMatchObject({ content: [{ type: 'text', text }], isError: true })
expect(result.error?.info).toBeUndefined()
})
it('keeps a plain failure without info when the call was not cancelled', async () => {
const ctx = await setup()
ctx.tools.register({
...echoTool,
name: 'plain-failure',
execute() { return Promise.reject(new Error('disk full')) },
})
const result = await executeToolFixture(ctx, {
callId: ToolCallId('plain-failure'), name: 'plain-failure', arguments: {}, signal: new AbortController().signal,
})
expect(result).toMatchObject({ isError: true, error: { message: 'disk full' } })
expect(result.error?.info).toBeUndefined()
})
it('materializes a pre-aborted call and publishes one result without entering pipeline phases', async () => {
const ctx = await setup()
const phases = { pre: 0, around: 0, body: 0, post: 0, result: 0 }
@@ -4214,10 +4214,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
returns: 'the effect disposer that removes this guard.',
},
{
signature: 'checkGuards(exec: ToolExecution): string | undefined',
signature: 'checkGuards(exec: ToolExecution): ToolGuardDenial | undefined',
description: 'Check global and inherited guards after pre-execute policy and approval resolve.',
parameters: [{ name: 'exec', description: 'invocation whose guards are checked before dispatch.' }],
returns: 'the first denial reason, or undefined when no guard denies.',
returns: 'the first denial with its optional structured identity, or undefined when no guard denies.',
},
],
},
@@ -7816,7 +7816,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'PostToolDecision',
declaration: 'export type PostToolDecision = {\n kind: \'accept\';\n content?: ContentBlock[];\n value?: never;\n additionalContexts?: UserMessage[];\n} | {\n kind: \'accept\';\n value: JsonValue;\n content?: never;\n additionalContexts?: UserMessage[];\n} | {\n kind: \'block\';\n feedback: ContentBlock[];\n additionalContexts?: UserMessage[];\n};',
declaration: 'export type PostToolDecision = {\n kind: \'accept\';\n content?: ContentBlock[];\n value?: never;\n additionalContexts?: UserMessage[];\n} | {\n kind: \'accept\';\n value: JsonValue;\n content?: never;\n additionalContexts?: UserMessage[];\n} | {\n kind: \'block\';\n feedback: ContentBlock[];\n info?: ToolErrorInfo;\n additionalContexts?: UserMessage[];\n};',
},
{
name: 'PreparedAdapterCall',
@@ -9860,7 +9860,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ToolGuard',
declaration: 'export type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined;',
declaration: 'export type ToolGuard = (execution: Readonly<ToolExecution>) => string | ToolGuardDenial | undefined;',
},
{
name: 'ToolGuardDenial',
declaration: 'export interface ToolGuardDenial {\n readonly reason: string;\n readonly info?: ToolErrorInfo;\n}',
},
{
name: 'ToolHistory',
@@ -9928,7 +9932,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ToolsService',
declaration: 'export class ToolsService extends Service {\n static inject;\n static Config;\n constructor(ctx: Context);\n register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void;\n restrict(filter: ToolRestriction): () => void;\n get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined;\n executable(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined;\n async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>;\n definitions(scope?: ScopeKey, toolSet?: ResolvedToolSet): readonly ToolDefinition[];\n schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[];\n setExecutor(executor: ToolExecutor): () => void;\n async execute(input: ToolBatchInput): Promise<ToolBatchResult>;\n guard(guard: ToolGuard): () => void;\n checkGuards(exec: ToolExecution): string | undefined;\n}',
declaration: 'export class ToolsService extends Service {\n static inject;\n static Config;\n constructor(ctx: Context);\n register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void;\n restrict(filter: ToolRestriction): () => void;\n get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined;\n executable(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined;\n async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>;\n definitions(scope?: ScopeKey, toolSet?: ResolvedToolSet): readonly ToolDefinition[];\n schemas(scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolSchema[];\n setExecutor(executor: ToolExecutor): () => void;\n async execute(input: ToolBatchInput): Promise<ToolBatchResult>;\n guard(guard: ToolGuard): () => void;\n checkGuards(exec: ToolExecution): ToolGuardDenial | undefined;\n}',
},
{
name: 'ToolUpdate',
@@ -36,8 +36,8 @@
en: b76a432aad14aa90
zh: b0cd916f7897fb42
/deepseek-ai-dsh-repeat-tool-reminder/understand-the-implementation/reminder-delivery:
en: a3edc5c0f98b0d1e
zh: 173f6166ce3af779
en: e11bb31ae82954cd
zh: 14f9c32ca93e7096
/deepseek-ai-dsh-repeat-tool-reminder/understand-the-implementation/source-map:
en: ba2eb423e165510d
zh: db5155503a8504cb
@@ -88,7 +88,7 @@ Each agent's chain is keyed by `(tool name, canonical arguments)` — two calls
### Reminder delivery
Reminders ride the post-execute decision's `additionalContexts` (source `{kind: 'repeat-tool-reminder', form: 'notice', summary: '<tool> × <count>'}`), never a `content` replacement: the `tool/result` event stays the tool's own output for audit. The loop buffers the context and appends it as an injected `user/message` after the step's tool results, which the session renders as a plain synthetic user message — model-visible, source-attributed, and reconstructable from the session log with no new session event. The guard always delegates via `next()` and prepends its reminder to the downstream decision's context array, so both decision variants (a blocked call included) still get the nudge while every entry retains its own source and metadata.
Reminders ride the post-execute decision's `additionalContexts` (source `{kind: 'repeat-tool-reminder', form: 'notice', summary: '<tool> × <count>'}`), never a `content` replacement: the `tool/result` event stays the tool's own output for audit. The loop buffers the context and appends it as an injected `user/message` after the step's tool results, which the session renders as a plain synthetic user message — model-visible, source-attributed, and reconstructable from the session log with no new session event. The guard always delegates via `next()` and prepends its reminder to the downstream decision's context array, so both decision variants (a blocked call included) still get the nudge while every entry retains its own source and metadata and a downstream block keeps its feedback and structured error identity.
### Source map
@@ -88,7 +88,7 @@ guard 建立在四项承诺之上:
### 提醒传递
提醒随 post-execute 决策的 `additionalContexts`(来源为 `{kind: 'repeat-tool-reminder', form: 'notice', summary: '<tool> × <count>'}`)传递,绝不替换 `content`:用于审计的 `tool/result` 事件仍保留工具自己的输出。循环会缓冲这段上下文,并在该步骤的工具结果之后作为注入的 `user/message` 追加,会话将其渲染为普通的合成用户消息——模型可见、带有来源归属,且无需新会话事件即可从会话日志重建。guard 始终通过 `next()` 委派,并把提醒放在下游决策的上下文数组之前,因此两种决策变体(包括被阻止的调用)都会收到提醒,同时每个条目保留自己的来源与元数据。
提醒随 post-execute 决策的 `additionalContexts`(来源为 `{kind: 'repeat-tool-reminder', form: 'notice', summary: '<tool> × <count>'}`)传递,绝不替换 `content`:用于审计的 `tool/result` 事件仍保留工具自己的输出。循环会缓冲这段上下文,并在该步骤的工具结果之后作为注入的 `user/message` 追加,会话将其渲染为普通的合成用户消息——模型可见、带有来源归属,且无需新会话事件即可从会话日志重建。guard 始终通过 `next()` 委派,并把提醒放在下游决策的上下文数组之前,因此两种决策变体(包括被阻止的调用)都会收到提醒,同时每个条目保留自己的来源与元数据,下游阻止也保留其反馈与结构化错误标识。
### 源码地图
@@ -221,13 +221,7 @@ export function apply(ctx: Context, config: Config): void {
const reminder = observe(exec)
const downstream = await next()
if (!reminder) return downstream
if (downstream.kind === 'block') {
return { kind: 'block', feedback: downstream.feedback, additionalContexts: prependContext(reminder, downstream.additionalContexts) }
}
return {
...downstream,
additionalContexts: prependContext(reminder, downstream.additionalContexts),
}
return { ...downstream, additionalContexts: prependContext(reminder, downstream.additionalContexts) }
})
// A user interjection changes the context; repetition across it is not a
@@ -321,6 +321,7 @@ describe('fold onto the downstream decision', () => {
ctx.on('tools/post-execute', async () => ({
kind: 'block' as const,
feedback: [{ type: 'text' as const, text: 'nope' }],
info: { name: 'DownstreamPolicyError', code: 'DOWNSTREAM_BLOCKED' },
additionalContexts: [createUserMessage({
content: [{ type: 'text' as const, text: 'downstream-ctx' }], source: { kind: 'test' as const },
})],
@@ -347,6 +348,8 @@ describe('fold onto the downstream decision', () => {
const results = agent.session.snapshotEvents().filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
expect(results.every(r => r.data.message.isError)).toBe(true)
expect(results[1]!.data.message.content).toEqual([{ type: 'text', text: 'nope' }])
// The block's structured error identity survives the reminder fold.
expect(results.map(r => r.data.error)).toEqual([0, 1].map(() => ({ name: 'DownstreamPolicyError', code: 'DOWNSTREAM_BLOCKED' })))
})
it('preserves a downstream canonical value replacement while folding', async () => {
+4 -4
View File
@@ -30,8 +30,8 @@
en: cce846c7e3b0c33e
zh: 17f353d970b4216d
/deepseek-ai-dsh-jobs/use-this-package/what-can-go-wrong:
en: fcfee3f72ea703ac
zh: 80299901bd44783b
en: 11a642faa1856789
zh: ffdc16cd7214cff7
/deepseek-ai-dsh-jobs/understand-the-implementation:
en: 7e13d669ecc96d59
zh: b5fe7ae65812d926
@@ -39,8 +39,8 @@
en: 2a42e1e06dfc469e
zh: 5c2c77eca52846f0
/deepseek-ai-dsh-jobs/understand-the-implementation/source-map:
en: c9a58b4340999ae4
zh: ef68a0b71a06d183
en: 474dd94bf936e0e1
zh: bec0e9768a2d5d50
/deepseek-ai-dsh-jobs/understand-the-implementation/service-operations:
en: b2e4ef34ccf1e749
zh: 128b57b57ed09316
+2 -1
View File
@@ -54,7 +54,7 @@ Loading these two plugins on a harness base that already provides the agent, too
### What can go wrong
Any preflight rejection leaves no job id or registered work. Jobs managed by the shipped in-process registry die with the harness process; durable execution across restarts needs a different backend implementing this contract.
Any preflight rejection leaves no job id or registered work. The seam exports `JobError`, a `HarnessError` whose code is one of `JOB_ERROR_CODES` (`JOB_NOT_FOUND`, `JOB_FOREIGN_SESSION`, `JOB_LIMIT_REACHED`, and others), for implementations and job tools to report expected job failures with a routable `error.name` and `error.code`. Jobs managed by the shipped in-process registry die with the harness process; durable execution across restarts needs a different backend implementing this contract.
-----
@@ -82,6 +82,7 @@ This section explains the design decisions behind the contract and points at the
| [`src/types.ts`](src/types.ts) | Shared vocabulary: `JobSpec`, `JobHandle`, `JobHooks`, `JobOutcome`, `JobEvent`, and the read results |
| [`src/view.ts`](src/view.ts) | Client-safe leaf: `JobView`, `JobChunk`, `JobStatus`, and the merge-extensible `JobKindMap` |
| [`src/brand.ts`](src/brand.ts) | the `JobId` branded identifier, importable without the agent dependency |
| [`src/error.ts`](src/error.ts) | `JobError` and its closed `JOB_ERROR_CODES` list |
| [`src/archive-admission.ts`](src/archive-admission.ts) | The `job` family of the Workspace registry's archive admission, installed by the seam's constructor for every implementation |
### Service operations
+2 -1
View File
@@ -54,7 +54,7 @@ kind: "package-reference"
### 可能出什么问题
任何预检拒绝都不会留下 job id 或已注册的工作。由随附的进程内注册表管理的任务会随 harness 进程终止而消失;跨重启的持久执行需要一个实现本约定的不同后端。
任何预检拒绝都不会留下 job id 或已注册的工作。接缝导出 `JobError`:它是 `HarnessError`,code 取自 `JOB_ERROR_CODES`(如 `JOB_NOT_FOUND`、`JOB_FOREIGN_SESSION`、`JOB_LIMIT_REACHED`),供实现和任务工具报告预期的任务失败,使结果带有可路由的 `error.name` 和 `error.code`。由随附的进程内注册表管理的任务会随 harness 进程终止而消失;跨重启的持久执行需要一个实现本约定的不同后端。
-----
@@ -82,6 +82,7 @@ kind: "package-reference"
| [`src/types.ts`](src/types.ts) | 共享词汇:`JobSpec`、`JobHandle`、`JobHooks`、`JobOutcome`、`JobEvent` 与读取结果 |
| [`src/view.ts`](src/view.ts) | 客户端安全叶子:`JobView`、`JobChunk`、`JobStatus` 与可合并扩展的 `JobKindMap` |
| [`src/brand.ts`](src/brand.ts) | 带类型标记的 `JobId` 标识符,无需 agent 依赖即可导入 |
| [`src/error.ts`](src/error.ts) | `JobError` 及其封闭的 `JOB_ERROR_CODES` 列表 |
| [`src/archive-admission.ts`](src/archive-admission.ts) | Workspace 注册表归档准入中的 `job` 族,由接缝构造函数为每个实现安装 |
### 服务操作
+2
View File
@@ -41,6 +41,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
@@ -53,6 +54,7 @@
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
+41
View File
@@ -0,0 +1,41 @@
/** Machine-routable background-job failures. @module */
import { HarnessError } from '@deepseek-ai/dsh-error'
/**
* Every {@link JobError} code:
* - `JOB_BACKGROUND_DISABLED` — the calling tool's configuration turns background execution off.
* - `JOB_CONTROLLER_UNAVAILABLE` — the jobs service is composed but no attached job controller serves the
* job's owner. A call whose composition lacks the jobs service itself throws `ToolContextUnavailableError`.
* - `JOB_NOT_FOUND` — the job id names no listed job.
* - `JOB_FOREIGN_SESSION` — the job belongs to another Session.
* - `JOB_OWNER_NOT_LIVE` — the owning Agent is no longer live.
* - `JOB_LIMIT_REACHED` — the registry's configured job limit is reached.
*/
export const JOB_ERROR_CODES = [
'JOB_BACKGROUND_DISABLED',
'JOB_CONTROLLER_UNAVAILABLE',
'JOB_NOT_FOUND',
'JOB_FOREIGN_SESSION',
'JOB_OWNER_NOT_LIVE',
'JOB_LIMIT_REACHED',
] as const
/** One code from {@link JOB_ERROR_CODES}. */
export type JobErrorCode = typeof JOB_ERROR_CODES[number]
/** Background-job failure carrying a stable {@link JobErrorCode}. */
export class JobError extends HarnessError {
override readonly code: JobErrorCode
/**
* @param message - names the job operation and the safe next step.
* @param code - the failure class consumers route on.
* @param options - standard `cause` chaining.
*/
constructor(message: string, code: JobErrorCode, options?: ErrorOptions) {
super(message, code, options)
this.name = 'JobError'
this.code = code
}
}
+1
View File
@@ -15,6 +15,7 @@ import { installJobArchiveAdmission } from './archive-admission.ts'
import type { JobEvents, JobId, JobOutputRead, JobRead, JobSpec, JobView } from './types.ts'
export { JobId } from './types.ts'
export { JOB_ERROR_CODES, JobError, type JobErrorCode } from './error.ts'
export type {
JobAppendOptions,
JobChannel,
+13
View File
@@ -0,0 +1,13 @@
import { HarnessError } from '@deepseek-ai/dsh-error'
import { JOB_ERROR_CODES, JobError } from '@deepseek-ai/dsh-jobs'
import { describe, expect, it } from 'vitest'
describe('JobError', () => {
it.each(JOB_ERROR_CODES)('is a HarnessError named JobError with code %s', (code) => {
const cause = new Error('registry detail')
const error = new JobError('job operation failed', code, { cause })
expect(error).toBeInstanceOf(HarnessError)
expect(error).toMatchObject({ name: 'JobError', code, message: 'job operation failed', cause })
})
})
+3
View File
@@ -20,6 +20,9 @@
{
"path": "../../core/agent"
},
{
"path": "../../util/error"
},
{
"path": "../../core/session"
},
+2
View File
@@ -62,6 +62,7 @@
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
},
"dependencies": {
@@ -75,6 +76,7 @@
},
"devDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:*",
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/dsh-package-manifest": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
}
+5 -17
View File
@@ -1,25 +1,13 @@
/**
* Harness error base with a stable machine-routable code and chained cause.
* Package errors extend it so tool results and replay can retain failure class.
* LLM failure codes and helpers over the shared Harness error base. The base
* class lives in `@deepseek-ai/dsh-error` and is re-exported here, so both
* import paths name the same class.
* @module @deepseek-ai/dsh-llm/error
*/
/**
* Base class for all harness errors. Carries a `code` (stable, programmatic —
* e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
* human-readable `message`, and supports `cause` chaining via the standard
* `ErrorOptions`. `name` defaults to the subclass constructor name.
*/
export class HarnessError extends Error {
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
readonly code: string
import { HarnessError } from '@deepseek-ai/dsh-error'
constructor(message: string, code: string, options?: ErrorOptions) {
super(message, options)
this.code = code
this.name = new.target.name
}
}
export { HarnessError }
/**
* Every failure code the built-in providers and the LLM runtime raise. An entry
+3
View File
@@ -37,6 +37,9 @@
},
{
"path": "../../typert/protocol"
},
{
"path": "../../util/error"
}
]
}
+2
View File
@@ -34,12 +34,14 @@
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
}
+8 -4
View File
@@ -6,6 +6,7 @@
import { Context, Service } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { HarnessError } from '@deepseek-ai/dsh-error'
import { TerminalBackendCleanupError } from './types.ts'
import type {
TerminalBackend,
@@ -62,11 +63,14 @@ export type TerminalErrorCode =
| 'SEND_ACTIVE'
| 'SERVICE_DISPOSING'
/** Error carrying a stable {@link TerminalErrorCode}. */
export class TerminalError extends Error {
constructor(message: string, readonly code: TerminalErrorCode) {
super(message)
/** Harness error carrying a stable {@link TerminalErrorCode}. */
export class TerminalError extends HarnessError {
override readonly code: TerminalErrorCode
constructor(message: string, code: TerminalErrorCode) {
super(message, code)
this.name = 'TerminalError'
this.code = code
}
}
@@ -5,6 +5,7 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import TerminalSessionService, { TerminalBackendCleanupError, TerminalError, TerminalSessionId } from '@deepseek-ai/dsh-terminal'
import { HarnessError } from '@deepseek-ai/dsh-error'
import type {
TerminalBackend,
TerminalBackendSession,
@@ -193,7 +194,15 @@ describe('TerminalSessionService ownership and lifecycle', () => {
await expect(ctx.terminals.spawn(owner, { type: 'stub', name: 'main' })).rejects.toMatchObject({ code: 'DUPLICATE_NAME' })
const operation = ctx.terminals.startSend(owner, created.sessionId, { text: 'echo hi', submit: true })
expect(() => ctx.terminals.startSend(owner, created.sessionId, { text: 'pwd', submit: true })).toThrow(TerminalError)
let concurrent: unknown
try {
ctx.terminals.startSend(owner, created.sessionId, { text: 'pwd', submit: true })
} catch (error: unknown) {
concurrent = error
}
expect(concurrent).toBeInstanceOf(TerminalError)
expect(concurrent).toBeInstanceOf(HarnessError)
expect(concurrent).toMatchObject({ name: 'TerminalError', code: 'SEND_ACTIVE' })
expect(operation.readOutput()).toEqual({ delta: 'delta', truncated: false })
expect(operation.cancel()).toBe(true)
await operation.done
+3
View File
@@ -17,6 +17,9 @@
{
"path": "../../core/agent"
},
{
"path": "../../util/error"
},
{
"path": "../../util/brand"
}
+2 -2
View File
@@ -15,8 +15,8 @@
en: 1eb35b015a6c423f
zh: a8a3dceee2c9e943
/util-shared-utilities/packages:
en: 5d7eb32e5a9e81dc
zh: 5e419d4b44ffbd10
en: e345b399695a4e93
zh: 179fee3e2c4d90cc
/util-shared-utilities/related-documentation:
en: 432ae81110914571
zh: c5bf4efc007311f3
+1
View File
@@ -27,6 +27,7 @@ Each package provides one primitive or one data set; open a package page for how
| Package | Role |
|---|---|
| [`brand/`](brand/README.md) | Nominal string types and their stateless constructor |
| [`error/`](error/README.md) | The dependency-free `HarnessError` base class with a stable machine-routable code |
| [`package-manifest/`](package-manifest/README.md) | Shared TypeScript declarations for package manifests |
| [`plugin-detail/`](plugin-detail/README.md) | Selects plugin detail documents and reads them from local packages and npm file CDNs |
| [`plugin-market-index/`](plugin-market-index/README.md) | Validates plugin market index format 1 and builds pinned install arguments |
+1
View File
@@ -27,6 +27,7 @@ kind: "package-group"
| 包 | 职责 |
|---|---|
| [`brand/`](brand/README.zh.md) | 提供名义字符串类型及其无状态构造函数 |
| [`error/`](error/README.zh.md) | 无依赖、带稳定可路由 code 的 `HarnessError` 基类 |
| [`package-manifest/`](package-manifest/README.zh.md) | 包 manifest(元数据清单)的共享 TypeScript 声明 |
| [`plugin-detail/`](plugin-detail/README.zh.md) | 选择插件详细介绍文档,并从本地包和 npm 文件 CDN 读取它们 |
| [`plugin-market-index/`](plugin-market-index/README.zh.md) | 校验插件市场索引格式 1,并生成锁定版本的安装参数 |
+34
View File
@@ -0,0 +1,34 @@
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/error/README.md
/:
en: 765d60b0cc7f8cf1
zh: 66b731f7143ccc5c
/deepseek-ai-dsh-error:
en: 78fc89983568c13f
zh: 12bb37d45b49562b
/deepseek-ai-dsh-error/summary:
en: eefb4278f326243a
zh: 681e25fc7d65865e
/deepseek-ai-dsh-error/table-of-contents:
en: c992e0cd2e7042d6
zh: 2e8cd826664632d1
/deepseek-ai-dsh-error/use-this-package:
en: 1b8c9b68ec9b48fc
zh: 4b0b54d3330b288b
/deepseek-ai-dsh-error/understand-the-implementation:
en: 42b15ac6321706aa
zh: 4ed0861911abaf5d
/deepseek-ai-dsh-error/understand-the-implementation/source-map:
en: a2e80abfe169f97f
zh: eb37e22c32f66add
/deepseek-ai-dsh-error/understand-the-implementation/why-it-stays-dependency-free:
en: 79564181c4c59db6
zh: 845856fa121b3ff7
/deepseek-ai-dsh-error/further-exploration:
en: 530bc3a0690a4012
zh: 192682c5a8571dfe
/deepseek-ai-dsh-error/dev-note:
en: 5de1d6304e00c58f
zh: 0087361e6f535488
+76
View File
@@ -0,0 +1,76 @@
---
description: "Dependency-free HarnessError base class whose stable code lets tool results, retries, and replay route failures."
kind: "package-library"
---
# @deepseek-ai/dsh-error
English | [中文](README.zh.md)
## Summary
`dsh-error` holds `HarnessError`, the base class for every expected Harness failure. A `HarnessError` carries a stable machine-routable `code` beside its human-readable `message` and optional `cause`, and its `name` is the constructing class name. The tool registry records `name` and `code` of a thrown `HarnessError` on `tool/result`, so retry policy, UI, replay, and analytics route on the code instead of parsing text. The package has no dependencies, so packages below `dsh-llm` in the dependency graph can extend the base too; `dsh-llm` re-exports the same class.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
Extend `HarnessError` with one class per owning package or domain and give each failure a stable `UPPER_SNAKE_CASE` code:
```ts
import { HarnessError } from '@deepseek-ai/dsh-error'
export class ArchiveError extends HarnessError {}
throw new ArchiveError('archive entry is missing', 'ARCHIVE_ENTRY_NOT_FOUND')
```
Assign `this.name` a string literal in a subclass constructor when a bundler could rename the class, because `name` is persisted. Import the class from `@deepseek-ai/dsh-error` or from its `@deepseek-ai/dsh-llm` re-export; both name the same class, so `instanceof` checks agree. The [error class and code guide](../../../.agents/skills/dsh-error-handling/references/error-codes.md) defines class choice, naming, and code conventions.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
### Source map
| File | Role |
|---|---|
| [`src/index.ts`](src/index.ts) | The `HarnessError` base class |
### Why it stays dependency-free
`dsh-llm` depends on lower packages such as `dsh-attachment` and `dsh-typert-protocol`, so a base class defined there could not be extended below it without a dependency cycle. Keeping the class in a package without dependencies lets every package extend it; the [decision note](../../../.agents/notes/implemented/architecture/2026-11-05-harness-error-dependency-free-base.md) records the relocation.
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
- [Tools subsystem](../../../docs/subsystems/tools.md) — how the registry records `ToolErrorInfo` from a thrown `HarnessError`.
- [Error-handling rules](../../../.agents/skills/dsh-error-handling/SKILL.md) — when code must throw a `HarnessError`.
-----
<a id="dev-note"></a>
## Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
+76
View File
@@ -0,0 +1,76 @@
---
description: "无依赖的 HarnessError 基类,其稳定 code 让工具结果、重试与回放能够按失败类别路由。"
kind: "package-library"
---
# @deepseek-ai/dsh-error
[English](README.md) | 中文
## 概述
`dsh-error` 存放 `HarnessError`,即所有预期 Harness 失败的基类。`HarnessError` 在人类可读的 `message` 与可选的 `cause` 之外,携带稳定、可供机器路由的 `code`,其 `name` 为构造它的类名。工具注册表会把抛出的 `HarnessError` 的 `name` 与 `code` 记录到 `tool/result` 上,使重试策略、UI、回放与数据分析按 code 路由,而不必解析文本。本包没有任何依赖,因此依赖图中位于 `dsh-llm` 之下的包也能继承该基类;`dsh-llm` 会重新导出同一个类。
## 目录
- [使用本包](#use-this-package)
- [理解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
每个所属包或领域继承出一个 `HarnessError` 子类,并为每种失败指定稳定的 `UPPER_SNAKE_CASE` code:
```ts
import { HarnessError } from '@deepseek-ai/dsh-error'
export class ArchiveError extends HarnessError {}
throw new ArchiveError('archive entry is missing', 'ARCHIVE_ENTRY_NOT_FOUND')
```
打包工具可能重命名类时,在子类构造函数中把 `this.name` 赋为字符串字面量,因为 `name` 会被持久化。可以从 `@deepseek-ai/dsh-error` 导入该类,也可以使用 `@deepseek-ai/dsh-llm` 的重新导出;两者是同一个类,因此 `instanceof` 判断结果一致。[错误类与 code 指南](../../../.agents/skills/dsh-error-handling/references/error-codes.md)规定了类的选择、命名与 code 约定。
-----
<a id="understand-the-implementation"></a>
## 理解实现
<details>
<summary>实现细节——点击展开</summary>
### 源码地图
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | `HarnessError` 基类 |
### 为什么保持无依赖
`dsh-llm` 依赖 `dsh-attachment`、`dsh-typert-protocol` 等更底层的包,定义在其中的基类无法被这些包继承,否则会形成依赖环。把该类放在没有依赖的包中,所有包都可以继承它;[决策记录](../../../.agents/notes/implemented/architecture/2026-11-05-harness-error-dependency-free-base.zh.md)记录了这次迁移。
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
- [工具子系统](../../../docs/subsystems/tools.zh.md) — 注册表如何根据抛出的 `HarnessError` 记录 `ToolErrorInfo`。
- [错误处理规则](../../../.agents/skills/dsh-error-handling/SKILL.md) — 何时必须抛出 `HarnessError`。
-----
<a id="dev-note"></a>
## 开发备注
<details>
<summary>维护者的工作背景——点击展开</summary>
无。
</details>
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@deepseek-ai/dsh-error",
"description": "Dependency-free HarnessError base class with a stable machine-routable code for the DeepSeek Harness",
"version": "0.2.2-rc.1",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/util/error"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"lib/**/*.js.map",
"src"
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~"
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Harness error base with a stable machine-routable code and chained cause.
* Package errors extend it so tool results and replay can retain failure class.
* @module @deepseek-ai/dsh-error
*/
/**
* Base class for all harness errors. Carries a `code` (stable, programmatic —
* e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
* human-readable `message`, and supports `cause` chaining via the standard
* `ErrorOptions`. `name` defaults to the subclass constructor name.
*/
export class HarnessError extends Error {
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
readonly code: string
constructor(message: string, code: string, options?: ErrorOptions) {
super(message, options)
this.code = code
this.name = new.target.name
}
}
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { HarnessError } from '../src/index.ts'
class SampleError extends HarnessError {}
describe('HarnessError', () => {
it('carries a code, a cause, and the constructing class name', () => {
const cause = new Error('underlying failure')
const error = new SampleError('operation failed', 'SAMPLE_FAILED', { cause })
expect(error).toBeInstanceOf(Error)
expect(error).toBeInstanceOf(HarnessError)
expect(error).toMatchObject({ name: 'SampleError', code: 'SAMPLE_FAILED', message: 'operation failed', cause })
})
it('names a direct instance HarnessError', () => {
expect(new HarnessError('failed', 'FAILED').name).toBe('HarnessError')
})
})
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
]
}
@@ -1,4 +1,4 @@
/** Generated by scripts/gen-workflow-guest.ts. Do not edit directly. */
/** Self-contained ESM guest transported into the mounted PTC runtime. */
export const WORKFLOW_GUEST_SOURCE = "import * as vm from \"node:vm\";\n//#region packages/workflow/workflow-ptc/src/realm.ts\n/**\n* Materializes script VM values as plain JSON and renders thrown values.\n* Getters and proxy traps may execute inside the confined Node process;\n* process isolation and cancellation belong to PTC, not the VM.\n* @module @deepseek-ai/dsh-workflow-ptc/realm\n*/\n/** Thrown by {@link materializeFromRealm}; the caller wraps it into the right `WorkflowError` code. */\nvar MaterializeError = class extends Error {\n\tpath;\n\treason;\n\tconstructor(path, reason) {\n\t\tsuper(`${path}: ${reason}`);\n\t\tthis.path = path;\n\t\tthis.reason = reason;\n\t\tthis.name = \"MaterializeError\";\n\t}\n};\n/**\n* Render a thrown value to failure text without ever throwing: prefer the\n* `stack` (host or realm — a realm error's `stack` is a plain string read),\n* fall back to `message`, then `String()`. Reading those properties MAY run\n* script code; if that code itself throws, a fixed label is returned instead.\n* @param error - any value thrown in the host or guest realm.\n* @returns human-readable text for the failure report; prefers the stack.\n*/\nfunction renderThrown(error) {\n\ttry {\n\t\tconst stack = error?.stack;\n\t\tif (typeof stack === \"string\" && stack.length > 0) return stack;\n\t\tconst message = error?.message;\n\t\tif (typeof message === \"string\" && message.length > 0) return message;\n\t\treturn String(error);\n\t} catch {\n\t\treturn \"[unrenderable thrown value]\";\n\t}\n}\n/**\n* Whether an object's prototype chain represents a plain data object: `null`, or a prototype\n* whose own prototype is `null` (the realm's `Object.prototype` — which we\n* cannot compare by identity across realms). A `Date`/`Map`/class instance\n* has a longer chain and is rejected.\n*/\nfunction hasPlainPrototype(value) {\n\tconst proto = Object.getPrototypeOf(value);\n\tif (proto === null) return true;\n\treturn Object.getPrototypeOf(proto) === null;\n}\n/**\n* Copy `value` (typically from the vm realm) into plain host JSON data. Root `undefined` is\n* returned unchanged; nested `undefined` and values JSON cannot represent losslessly fail\n* with the offending path. Property accessors run normally, and a throwing read is wrapped\n* with its rendered failure.\n*\n* @param value - the realm value to materialize.\n* @param root - the path label for the root value (error messages).\n* @returns the host-realm copy (plain objects/arrays/scalars only).\n* @throws {@link MaterializeError} for unsupported values, cycles, sparse arrays, exotic\n* prototypes, or property reads that throw.\n*/\nfunction materializeFromRealm(value, root = \"value\") {\n\tif (value === void 0) return void 0;\n\ttry {\n\t\treturn materialize(value, root, /* @__PURE__ */ new Set());\n\t} catch (error) {\n\t\tif (error instanceof MaterializeError) throw error;\n\t\tthrow new MaterializeError(root, `reading the value threw: ${renderThrown(error)}`);\n\t}\n}\nfunction materialize(value, path, seen) {\n\tswitch (typeof value) {\n\t\tcase \"boolean\":\n\t\tcase \"string\": return value;\n\t\tcase \"number\":\n\t\t\tif (!Number.isFinite(value)) throw new MaterializeError(path, \"non-finite numbers are not JSON data\");\n\t\t\treturn value;\n\t\tcase \"bigint\": throw new MaterializeError(path, \"bigints are not JSON data\");\n\t\tcase \"function\": throw new MaterializeError(path, \"functions are not plain JSON data\");\n\t\tcase \"symbol\": throw new MaterializeError(path, \"symbols are not plain JSON data\");\n\t\tcase \"undefined\": throw new MaterializeError(path, \"undefined is not JSON data\");\n\t\tcase \"object\": break;\n\t}\n\tif (value === null) return null;\n\tconst objectValue = value;\n\tif (seen.has(objectValue)) throw new MaterializeError(path, \"circular references are not JSON data\");\n\tseen.add(objectValue);\n\ttry {\n\t\tif (Array.isArray(objectValue)) return materializeArray(objectValue, path, seen);\n\t\treturn materializeObject(objectValue, path, seen);\n\t} finally {\n\t\tseen.delete(objectValue);\n\t}\n}\nfunction materializeArray(value, path, seen) {\n\tconst out = [];\n\tfor (let index = 0; index < value.length; index++) {\n\t\tif (!(index in value)) throw new MaterializeError(`${path}[${index}]`, \"sparse arrays are not JSON data\");\n\t\tout.push(materialize(value[index], `${path}[${index}]`, seen));\n\t}\n\tfor (const key of Object.keys(value)) {\n\t\tconst index = Number(key);\n\t\tif (!Number.isInteger(index) || index < 0 || index >= value.length) throw new MaterializeError(`${path}.${key}`, \"arrays with non-index properties are not JSON data\");\n\t}\n\tif (Object.getOwnPropertySymbols(value).length > 0) throw new MaterializeError(path, \"symbol-keyed properties are not plain JSON data\");\n\treturn out;\n}\nfunction materializeObject(value, path, seen) {\n\tif (!hasPlainPrototype(value)) throw new MaterializeError(path, \"only plain objects and arrays are JSON data (exotic prototype)\");Line truncated
export const WORKFLOW_GUEST_SOURCE = "import * as vm from \"node:vm\";\n//#region packages/workflow/workflow-ptc/src/realm.ts\n/**\n* Materializes script VM values as plain JSON and renders thrown values.\n* Getters and proxy traps may execute inside the confined Node process;\n* process isolation and cancellation belong to PTC, not the VM.\n* @module @deepseek-ai/dsh-workflow-ptc/realm\n*/\n/** Thrown by {@link materializeFromRealm}; the caller wraps it into the right `WorkflowError` code. */\nvar MaterializeError = class extends Error {\n\tpath;\n\treason;\n\tconstructor(path, reason) {\n\t\tsuper(`${path}: ${reason}`);\n\t\tthis.path = path;\n\t\tthis.reason = reason;\n\t\tthis.name = \"MaterializeError\";\n\t}\n};\n/**\n* Render a thrown value to failure text without ever throwing: prefer the\n* `stack` (host or realm — a realm error's `stack` is a plain string read),\n* fall back to `message`, then `String()`. Reading those properties MAY run\n* script code; if that code itself throws, a fixed label is returned instead.\n* @param error - any value thrown in the host or guest realm.\n* @returns human-readable text for the failure report; prefers the stack.\n*/\nfunction renderThrown(error) {\n\ttry {\n\t\tconst stack = error?.stack;\n\t\tif (typeof stack === \"string\" && stack.length > 0) return stack;\n\t\tconst message = error?.message;\n\t\tif (typeof message === \"string\" && message.length > 0) return message;\n\t\treturn String(error);\n\t} catch {\n\t\treturn \"[unrenderable thrown value]\";\n\t}\n}\n/**\n* Whether an object's prototype chain represents a plain data object: `null`, or a prototype\n* whose own prototype is `null` (the realm's `Object.prototype` — which we\n* cannot compare by identity across realms). A `Date`/`Map`/class instance\n* has a longer chain and is rejected.\n*/\nfunction hasPlainPrototype(value) {\n\tconst proto = Object.getPrototypeOf(value);\n\tif (proto === null) return true;\n\treturn Object.getPrototypeOf(proto) === null;\n}\n/**\n* Copy `value` (typically from the vm realm) into plain host JSON data. Root `undefined` is\n* returned unchanged; nested `undefined` and values JSON cannot represent losslessly fail\n* with the offending path. Property accessors run normally, and a throwing read is wrapped\n* with its rendered failure.\n*\n* @param value - the realm value to materialize.\n* @param root - the path label for the root value (error messages).\n* @returns the host-realm copy (plain objects/arrays/scalars only).\n* @throws {@link MaterializeError} for unsupported values, cycles, sparse arrays, exotic\n* prototypes, or property reads that throw.\n*/\nfunction materializeFromRealm(value, root = \"value\") {\n\tif (value === void 0) return void 0;\n\ttry {\n\t\treturn materialize(value, root, /* @__PURE__ */ new Set());\n\t} catch (error) {\n\t\tif (error instanceof MaterializeError) throw error;\n\t\tthrow new MaterializeError(root, `reading the value threw: ${renderThrown(error)}`);\n\t}\n}\nfunction materialize(value, path, seen) {\n\tswitch (typeof value) {\n\t\tcase \"boolean\":\n\t\tcase \"string\": return value;\n\t\tcase \"number\":\n\t\t\tif (!Number.isFinite(value)) throw new MaterializeError(path, \"non-finite numbers are not JSON data\");\n\t\t\treturn value;\n\t\tcase \"bigint\": throw new MaterializeError(path, \"bigints are not JSON data\");\n\t\tcase \"function\": throw new MaterializeError(path, \"functions are not plain JSON data\");\n\t\tcase \"symbol\": throw new MaterializeError(path, \"symbols are not plain JSON data\");\n\t\tcase \"undefined\": throw new MaterializeError(path, \"undefined is not JSON data\");\n\t\tcase \"object\": break;\n\t}\n\tif (value === null) return null;\n\tconst objectValue = value;\n\tif (seen.has(objectValue)) throw new MaterializeError(path, \"circular references are not JSON data\");\n\tseen.add(objectValue);\n\ttry {\n\t\tif (Array.isArray(objectValue)) return materializeArray(objectValue, path, seen);\n\t\treturn materializeObject(objectValue, path, seen);\n\t} finally {\n\t\tseen.delete(objectValue);\n\t}\n}\nfunction materializeArray(value, path, seen) {\n\tconst out = [];\n\tfor (let index = 0; index < value.length; index++) {\n\t\tif (!(index in value)) throw new MaterializeError(`${path}[${index}]`, \"sparse arrays are not JSON data\");\n\t\tout.push(materialize(value[index], `${path}[${index}]`, seen));\n\t}\n\tfor (const key of Object.keys(value)) {\n\t\tconst index = Number(key);\n\t\tif (!Number.isInteger(index) || index < 0 || index >= value.length) throw new MaterializeError(`${path}.${key}`, \"arrays with non-index properties are not JSON data\");\n\t}\n\tif (Object.getOwnPropertySymbols(value).length > 0) throw new MaterializeError(path, \"symbol-keyed properties are not plain JSON data\");\n\treturn out;\n}\nfunction materializeObject(value, path, seen) {\n\tif (!hasPlainPrototype(value)) throw new MaterializeError(path, \"only plain objects and arrays are JSON data (exotic prototype)\");Line truncated
+18
View File
@@ -13091,6 +13091,9 @@ importers:
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-error':
specifier: workspace:*
version: link:../../util/error
'@deepseek-ai/dsh-session':
specifier: workspace:*
version: link:../../core/session
@@ -13211,6 +13214,9 @@ importers:
'@deepseek-ai/dsh-attachment':
specifier: workspace:*
version: link:../../attachment/attachment
'@deepseek-ai/dsh-error':
specifier: workspace:*
version: link:../../util/error
'@deepseek-ai/dsh-package-manifest':
specifier: workspace:*
version: link:../../util/package-manifest
@@ -17187,6 +17193,9 @@ importers:
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-error':
specifier: workspace:*
version: link:../../util/error
'@deepseek-ai/dsh-session':
specifier: workspace:*
version: link:../../core/session
@@ -17744,6 +17753,12 @@ importers:
specifier: workspace:~
version: link:../../../vendor/cordis
packages/util/error:
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
version: link:../../../vendor/cordis
packages/util/file-drag:
dependencies:
mime:
@@ -18386,6 +18401,9 @@ importers:
'@deepseek-ai/dsh-deepseek-llm-api-extensions':
specifier: workspace:*
version: link:../../packages/llm/deepseek-llm-api-extensions
'@deepseek-ai/dsh-error':
specifier: workspace:*
version: link:../../packages/util/error
'@deepseek-ai/dsh-file-reference':
specifier: workspace:*
version: link:../../packages/context/file-reference
+1
View File
@@ -46,6 +46,7 @@
"@deepseek-ai/dsh-credentials": "workspace:*",
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*",
"@deepseek-ai/dsh-error": "workspace:*",
"@deepseek-ai/dsh-file-reference": "workspace:*",
"@deepseek-ai/dsh-fs": "workspace:*",
"@deepseek-ai/dsh-fs-local": "workspace:*",
+1
View File
@@ -94,6 +94,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
'packages/util/atomic-write': 'Zero-dependency filesystem write utility.',
'packages/util/brand': 'Stateless nominal-string and canonical-key constructors.',
'packages/util/crypto': 'Zero-dependency identifier minting utility.',
'packages/util/error': 'Zero-dependency HarnessError base class.',
'packages/util/deque': 'Zero-dependency circular deque utility.',
'packages/util/chunked-list': 'Persistent collection operations and checkpoint validation without a plugin surface.',
'packages/util/code-language': 'Zero-dependency file-extension to syntax-highlighting language table.',
+1
View File
@@ -843,6 +843,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ToolExecutionResult: 'tools.md',
ToolExecutionToken: 'tools.md',
ToolGuard: 'tools.md',
ToolGuardDenial: 'tools.md',
ToolRuntime: 'tools.md',
ToolsService: 'tools.md',
ToolExecutor: 'tools.md',
+2
View File
@@ -80,6 +80,8 @@ const PEER_REQUIRED_HOST_EXPORTS = {
// CUA checks UserQuestionError and preserves AttachmentError from image admission.
'@deepseek-ai/dsh-attachment': ['admitEncodedImages'],
'@deepseek-ai/dsh-user-questions': ['UserQuestionError'],
// The tool registry and every subclass rely on `instanceof` against one HarnessError class.
'@deepseek-ai/dsh-error': ['HarnessError'],
// `instanceof` checks need the translator's own class instance.
'@deepseek-ai/dsh-experimental-translator': ['TranslationError'],
// Stateless; a safe classification would turn the forge panel's forge and worktree peers into dependencies.
+5
View File
@@ -851,6 +851,11 @@
"symbol": "ToolGuard",
"source": "packages/core/tools/src/contracts.ts"
},
{
"doc": "docs/subsystems/tools.md",
"symbol": "ToolGuardDenial",
"source": "packages/core/tools/src/contracts.ts"
},
{
"doc": "docs/subsystems/tools.md",
"symbol": "ToolErrorInfo",
@@ -18,6 +18,7 @@ const CANONICAL = '## Known Limitations and Deferred Work'
/** Packages audited as having no limitations section, keyed by repo-relative directory. */
const NO_LIMITATIONS: Readonly<Record<string, string>> = {
'packages/util/brand': 'Stateless nominal-string and canonical-key helpers have no deferred work.',
'packages/util/error': 'A single base class with no behavior beyond its constructor has no deferred work.',
}
/** A heading that reads as a limitations section — canonical or drifted. */
@@ -35,6 +35,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
'packages/experimental/liveshare-protocol': 'The package only defines wire frames and identity values that its consumers exchange; it registers nothing model-facing.',
'packages/util/brand': 'The package only constructs plain string values and registers nothing model-facing.',
'packages/util/error': 'The package only defines the HarnessError base class; packages that throw it own any model-facing text.',
'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
'packages/util/workspace-path': 'The package only formats Workspace paths for browser UI; it never constructs model input.',
@@ -14,7 +14,7 @@
{"type":"session/title","data":{"title":"Inspect the exact tools service","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778145981,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778145981,"index":0,"dt":[],"id":"inspect-tools-api","name":"cordis_inspect_query","args":["{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"]},{"type":"chunk","time":1788778145981,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"tools\"}}"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"inspect-tools-api"},"toolCallId":"inspect-tools-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"tools\",\n \"description\": \"Concrete tool registry and scoped executor dispatcher.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"tools\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"tools\"\n ],\n \"expression\": \"ctx.tools\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void\",\n \"description\": \"Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.\",\n \"parameters\": [\n {\n \"name\": \"definition\",\n \"description\": \"tool schema, execution, and optional finalization/presentation callbacks.\"\n },\n {\n \"name\": \"options\",\n \"description\": \"optional step-time activation condition.\"\n }\n ],\n \"returns\": \"the exact disposer that unregisters the tool.\"\n },\n {\n \"signature\": \"restrict(filter: ToolRestriction): () => void\",\n \"description\": \"Restrict global tools for the calling agent scope. Empty filters, unknown names, scope-local names, and reserved transport names fail. Restrictions intersect; scoped registrations remain visible.\",\n \"parameters\": [\n {\n \"name\": \"filter\",\n \"description\": \"global-tool mask: `allow` (keep only) and/or `deny` (remove).\"\n }\n ],\n \"returns\": \"the exact disposer that lifts this restriction.\"\n },\n {\n \"signature\": \"get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined\",\n \"description\": \"Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.\",\n \"parameters\": [\n {\n \"name\": \"name\",\n \"description\": \"the tool name as registered.\"\n },\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n },\n {\n \"name\": \"toolSet\",\n \"description\": \"step admission; omit only to inspect registered definitions.\"\n }\n ],\n \"returns\": \"the definition the scope resolves, or undefined when none is visible.\"\n },\n {\n \"signature\": \"executable(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined\",\n \"description\": \"Resolve the definition a call may execute. With a step's admitted set, the set decides; without one, conditional registrations are unavailable.\",\n \"parameters\": [\n {\n \"name\": \"name\",\n \"description\": \"the tool name as registered.\"\n },\n {\n \"name\": \"scope\",\n \"description\": \"the calling agent; omitted = the global view.\"\n },\n {\n \"name\": \"toolSet\",\n \"description\": \"step admission, or omitted for a call outside a model step.\"\n }\n ],\n \"returns\": \"the executable definition, or undefined when the call must fail as `UNKNOWN_TOOL`.\"\n },\n {\n \"signature\": \"async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>\",\n \"description\": \"Resolve conditions once for this step without changing shared registrations. Cancellation or a rejected condition publishes no partial selection. Removed registrations cannot be restored by a late condition result.\",\n \"parameters\": [\n {\n \"name\": \"context\",\n \"description\": \"preparing agent, captured model selection, and cancellation.\"\n }\n ],\n \"returns\": \"the complete selection; the caller disposes it after its tool batch settles.\"\n },\n {\n \"signature\": \"definitions(scope?: ScopeKey, toolSet?: ResolvedToolSet): readonly ToolDefinition[]\",\n \"description\": \"Read the visible capability definitions for descriptions and program bindings.Line truncated
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"inspect-tools-api"},"toolCallId":"inspect-tools-api","content":[{"type":"text","text":"{\n \"platform\": \"host\",\n \"provider\": \"Service\",\n \"method\": \"listService\",\n \"data\": {\n \"mode\": \"service\",\n \"service\": {\n \"key\": \"tools\",\n \"description\": \"Concrete tool registry and scoped executor dispatcher.\",\n \"access\": {\n \"optional\": {\n \"expression\": \"ctx.get(\\\"tools\\\")\",\n \"requiresUndefinedCheck\": true\n },\n \"hardDependency\": {\n \"inject\": [\n \"tools\"\n ],\n \"expression\": \"ctx.tools\"\n }\n },\n \"methods\": [\n {\n \"signature\": \"register(definition: ToolDefinition, options: ToolRegistrationOptions = {}): () => void\",\n \"description\": \"Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.\",\n \"parameters\": [\n {\n \"name\": \"definition\",\n \"description\": \"tool schema, execution, and optional finalization/presentation callbacks.\"\n },\n {\n \"name\": \"options\",\n \"description\": \"optional step-time activation condition.\"\n }\n ],\n \"returns\": \"the exact disposer that unregisters the tool.\"\n },\n {\n \"signature\": \"restrict(filter: ToolRestriction): () => void\",\n \"description\": \"Restrict global tools for the calling agent scope. Empty filters, unknown names, scope-local names, and reserved transport names fail. Restrictions intersect; scoped registrations remain visible.\",\n \"parameters\": [\n {\n \"name\": \"filter\",\n \"description\": \"global-tool mask: `allow` (keep only) and/or `deny` (remove).\"\n }\n ],\n \"returns\": \"the exact disposer that lifts this restriction.\"\n },\n {\n \"signature\": \"get(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined\",\n \"description\": \"Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.\",\n \"parameters\": [\n {\n \"name\": \"name\",\n \"description\": \"the tool name as registered.\"\n },\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n },\n {\n \"name\": \"toolSet\",\n \"description\": \"step admission; omit only to inspect registered definitions.\"\n }\n ],\n \"returns\": \"the definition the scope resolves, or undefined when none is visible.\"\n },\n {\n \"signature\": \"executable(name: string, scope?: ScopeKey, toolSet?: ResolvedToolSet): ToolDefinition | undefined\",\n \"description\": \"Resolve the definition a call may execute. With a step's admitted set, the set decides; without one, conditional registrations are unavailable.\",\n \"parameters\": [\n {\n \"name\": \"name\",\n \"description\": \"the tool name as registered.\"\n },\n {\n \"name\": \"scope\",\n \"description\": \"the calling agent; omitted = the global view.\"\n },\n {\n \"name\": \"toolSet\",\n \"description\": \"step admission, or omitted for a call outside a model step.\"\n }\n ],\n \"returns\": \"the executable definition, or undefined when the call must fail as `UNKNOWN_TOOL`.\"\n },\n {\n \"signature\": \"async resolve(context: ToolActivationContext): Promise<ResolvedToolSet>\",\n \"description\": \"Resolve conditions once for this step without changing shared registrations. Cancellation or a rejected condition publishes no partial selection. Removed registrations cannot be restored by a late condition result.\",\n \"parameters\": [\n {\n \"name\": \"context\",\n \"description\": \"preparing agent, captured model selection, and cancellation.\"\n }\n ],\n \"returns\": \"the complete selection; the caller disposes it after its tool batch settles.\"\n },\n {\n \"signature\": \"definitions(scope?: ScopeKey, toolSet?: ResolvedToolSet): readonly ToolDefinition[]\",\n \"description\": \"Read the visible capability definitions for descriptions and program bindings.Line truncated
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146017,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778146017,"index":0,"dt":[],"id":"inspect-subprocess-api","name":"cordis_inspect_query","args":["{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"]},{"type":"chunk","time":1788778146017,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-subprocess-api","name":"cordis_inspect_query","arguments":"{\"platform\":\"host\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"subprocess\"}}"}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
@@ -18,7 +18,7 @@
{"type":"hook/result","data":{"turn":1,"point":"PreToolUse","handlerId":"claude-code:PreToolUse:1","decision":"ask","exitCode":0,"durationMs":4.532041999999819}}
{"type":"approval/asked","data":{"id":"{{approval:1}}","toolName":"bash","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","reason":"bash requires manual approval in this session"}}
{"type":"approval/decided","data":{"id":"{{approval:1}}","outcome":"rejected"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311"},"toolCallId":"call_00_6k0oGSliVHxGSgqBmMEO4311","content":[{"type":"text","text":"Error: the user rejected tool \"bash\""}],"isError":true,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311"},"toolCallId":"call_00_6k0oGSliVHxGSgqBmMEO4311","content":[{"type":"text","text":"Error: the user rejected tool \"bash\""}],"isError":true,"id":"{{message:5}}"},"error":{"name":"ApprovalError","code":"APPROVAL_REJECTED"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The bash tool returned an error saying it requires manual approval in this session. I'll report this verbatim."},{"type":"text","text":"The tool result I got back verbatim is:\n\n```\nError: bash requires manual approval in this session\n```"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":166,"outputTokens":45,"cacheReadTokens":2816,"reasoningTokens":22},"stream":[{"type":"chunk","time":1788699375166,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699375166,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," bash"," tool"," returned"," an"," error"," saying"," it"," requires"," manual"," approval"," in"," this"," session","."," I","'ll"," report"," this"," verb","atim","."]},{"type":"chunk","time":1788699375166,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699375166,"index":1,"dt":[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," tool"," result"," I"," got"," back"," verb","atim"," is",":\n\n","```\n","Error",":"," bash"," requires"," manual"," approval"," in"," this"," session","\n","```"]},{"type":"chunk","time":1788699375167,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The bash tool returned an error saying it requires manual approval in this session. I'll report this verbatim."}}},{"type":"chunk","time":1788699375167,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The tool result I got back verbatim is:\n\n```\nError: bash requires manual approval in this session\n```"}}},{"type":"chunk","time":1788699375167,"chunk":{"type":"usage","usage":{"inputTokens":166,"outputTokens":45,"cacheReadTokens":2816,"reasoningTokens":22}}},{"type":"chunk","time":1788699375167,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
@@ -24,7 +24,7 @@
{"type":"step/start","data":{"turn":1,"step":3}}
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699410720,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699410720,"index":0,"dt":[],"id":"pty-signal","name":"terminal_signal","args":["{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"]},{"type":"chunk","time":1788699410720,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}},{"type":"chunk","time":1788699410720,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699410720,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"pty-signal","name":"terminal_signal","arguments":"{\"sessionId\":\"pty-missing\",\"signal\":\"SIGINT\"}"}}
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"pty-signal"},"toolCallId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true,"id":"{{message:9}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"pty-signal"},"toolCallId":"pty-signal","content":[{"type":"text","text":"Error: unknown PTY session pty-missing"}],"isError":true,"id":"{{message:9}}"},"error":{"name":"TerminalError","code":"NO_SESSION"}},"sourceEventSeqs":[24],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":3}}
{"type":"step/start","data":{"turn":1,"step":4}}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788699410738,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699410738,"index":0,"dt":[],"id":"pty-kill","name":"terminal_close","args":["{\"sessionId\":\"pty-1\"}"]},{"type":"chunk","time":1788699410738,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-kill","name":"terminal_close","arguments":"{\"sessionId\":\"pty-1\"}"}}},{"type":"chunk","time":1788699410738,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788699410738,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
+1
View File
@@ -453,6 +453,7 @@
"@deepseek-ai/dsh-deepseek-llm-api-extensions": ["./packages/llm/deepseek-llm-api-extensions/src"],
"@deepseek-ai/dsh-deliverable-file-references": ["./packages/deliverables/deliverable-file-references/src"],
"@deepseek-ai/dsh-deque": ["./packages/util/deque/src"],
"@deepseek-ai/dsh-error": ["./packages/util/error/src"],
"@deepseek-ai/dsh-file-reference": ["./packages/context/file-reference/src"],
"@deepseek-ai/dsh-file-reference-local": ["./packages/context/file-reference-local/src"],
"@deepseek-ai/dsh-fs": ["./packages/fs/fs/src"],
+1
View File
@@ -343,6 +343,7 @@
{ "path": "./packages/boot/hmr" },
{ "path": "./vendor/logger-console" },
{ "path": "./packages/util/brand" },
{ "path": "./packages/util/error" },
{ "path": "./packages/util/package-manifest" },
{ "path": "./packages/util/plugin-detail" },
{ "path": "./packages/util/plugin-market-index" },