Merge remote-tracking branch 'origin/master' into docs/readme-human-polish-2

# Conflicts:
#	apps/cli/README.i18n.yaml
#	apps/cli/README.zh.md
#	packages/core/tools/README.i18n.yaml
#	packages/core/tools/README.zh.md
#	python/sdk/README.i18n.yaml
#	python/sdk/README.zh.md
#	scripts/snapshots/translation-prompt-v4/request-response.expected.json
This commit is contained in:
j-xiang
2026-08-13 10:44:52 +08:00
3512 changed files with 51728 additions and 30967 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/tools/README.md
README.md: 44eb25b79436a75f08406102fc1e3734e59b1001
README.zh.md: a395f453bab5ade3530219f4883575b55c1068ff
README.md: 120931c9f4b4f5e1c39c3ddd8da4b8cae42dbe69
README.zh.md: 92ea290e5f1fc2e66630a1ba63744d3b380d340a

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Tool registry and execution pipeline. Tool plugins register their schemas and executors; the agent loop executes each call through `tools/pre-execute` (the extensible allow/deny gate) → monotonic registered guards → `tools/execute` (an around-dispatch wrapper for timeout/retry/metrics plugins) → `tools/post-execute` (inspect/replace the result, attach context) → the definition-owned `finalizeContent` boundary → the observe-only `tools/result` notification. The registry also owns HOW its tools are presented to the model — its `mode` config selects native function calling, [Code Mode](#code-mode), or both, and one agent shadows that default for itself with `presentAs`.
## Service: `ToolRegistry` (ctx key: `tools`)
## Service: `ToolRuntime` (ctx key: `tools`)
### Config
@@ -13,7 +13,7 @@ tools:
mode: native # native (default) | code | both
```
`native` contributes visible tools as function definitions. `code` contributes the reserved `run_code` transport, the generated `tools:sdk` section, and the `tools:code-only` rule stating that only `run_code` may be called directly — which the executor then enforces, resolving a model-direct call naming any other tool to `UNKNOWN_TOOL` before policy runs; `both` contributes both forms and states no such rule, because its native calls do execute. This is the default for agents that declare none of their own — an agent preset selects its own with [`dsh-agent-tool-mode`](../agent-tool-mode/README.md). The reserved transport cannot be registered, shadowed, restricted, or removed, and its name is reserved whatever the configured mode, because any agent may select a code mode. Non-native modes require a `ctx.codeRuntime` whose `language` has a registered SDK renderer — TypeScript ships via [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md); a Python renderer is built in and drives any runtime that reports `language: 'python'` (a first-party `dsh-code-runtime-python` backend is delivered separately). A runtime language with no renderer fails prompt assembly loudly, and a `systemPrompt.toolOrder` entry for a tool the mode does not contribute rejects prompt assembly. A `system-prompt/assemble` listener may replace the registry's contributions; its returned assembly is authoritative, so that listener owns preserving a usable Code Mode protocol.
`native` contributes visible tools as function definitions. `code` contributes the reserved `run_code` transport, the generated `tools:sdk` section, and the `tools:code-only` rule stating that only `run_code` may be called directly — which the executor then enforces, resolving a model-direct call naming any other tool to `UNKNOWN_TOOL` before policy runs; `both` contributes both forms and states no such rule, because its native calls do execute. This is the default for agents that declare none of their own — an agent preset selects its own with [`dsh-agent-tool-presentation`](../agent-tool-presentation/README.md). The reserved transport cannot be registered, shadowed, restricted, or removed, and its name is reserved whatever the configured mode, because any agent may select a code mode. Non-native modes require a `ctx.codeRuntime` whose `language` has a registered SDK renderer — TypeScript ships via [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md); a Python renderer is built in and drives any runtime that reports `language: 'python'` (a first-party `dsh-code-runtime-python` backend is delivered separately). A runtime language with no renderer fails prompt assembly loudly, and a `systemPrompt.toolOrder` entry for a tool the mode does not contribute rejects prompt assembly. A `system-prompt/assemble` listener may replace the registry's contributions; its returned assembly is authoritative, so that listener owns preserving a usable Code Mode protocol.
### Public API
@@ -148,7 +148,7 @@ Prefix-stable while visible definitions and their order are unchanged. Registrat
#### What the model sees
Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language (the TypeScript `declare const tools` block, or the Python `tools` declaration). `both` exposes normal schemas and this Code Mode API. Under `code` the prompt also carries the `tools:code-only` rule, ordered ahead of the per-tool guidance band so the model reads which tools it may call before it reads what each one is for; `both` renders it empty. The instructions and SDK block match the loaded runtime's language; the TypeScript version (via [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)) is shown below, and the Python version (for any runtime reporting `language: 'python'`) has the same operations and types in Python syntax (`await tools.name(args)`, subscript access for exotic names, `print(...)` and top-level `return`).
Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language (the TypeScript `declare const tools` block, or the Python `tools` declaration). `both` exposes normal schemas and this Code Mode API. Under `code` the prompt also carries the `tools:code-only` rule, ordered ahead of the per-tool guidance band so the model reads which tools it may call before it reads what each one is for; `both` renders it empty. The instructions and SDK block match the loaded runtime's language; the TypeScript version (via [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md)) is shown below, and the Python version (for any runtime reporting `language: 'python'`) has the same operations and types in Python syntax (`await tools.name(args)`, subscript access for exotic names, `print(...)` and top-level `return`).
##### Code Mode SDK instructions
@@ -192,7 +192,7 @@ Append-only; newly visible content follows the reusable request prefix and does
- **Concurrency policy is not an event gate** — `executionMode()` reads the resolved tool definition directly; plugins can only declare a classifier on definitions they own.
- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md).
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-tool-call-timeout-policy` wrapper.
- **Code Mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer (TypeScript or Python); scoped restrictions/shadows and `presentAs` choose each agent's visible bindings and their form, but within one agent no tool can be native-only while another is code-only.
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The durable log copy of each sub-call IS bounded: the `tools/code-dispatch-log` waterfall lets the spill policy replace an oversized `tool/code-dispatch` content with a preview + locator ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)).
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).

View File

@@ -4,7 +4,7 @@
工具注册表与执行流水线。工具插件注册各自的 schema 和执行器agent loop智能体循环依次让每次调用经过 `tools/pre-execute`(可扩展的允许/拒绝门禁)→ 已注册的单调守卫 → `tools/execute`(供超时/重试/指标插件使用的环绕分发包装层)→ `tools/post-execute`(检查/替换结果、附加上下文)→ 由工具定义持有的 `finalizeContent` 边界 → 仅观测的 `tools/result` 通知。注册表还决定以何种方式向模型呈现工具:`mode` 配置可以选择原生 Function Calling函数调用、[Code Mode](#code-mode),或同时选择两者;单个 agent 可用 `presentAs` 为自己遮蔽该默认值。
## 服务:`ToolRegistry`ctx 键:`tools`
## 服务:`ToolRuntime`ctx 键:`tools`
### 配置
@@ -13,7 +13,7 @@ tools:
mode: native # native (default) | code | both
```
`native` 以函数定义的形式贡献可见工具。`code` 会提供保留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则。执行器随后强制执行该规则:模型直接调用其他任何工具时,会在策略运行前将该调用解析为 `UNKNOWN_TOOL``both` 同时提供两种形式,且不声明该规则,因为其中的原生调用确实可以执行。没有单独声明呈现模式的 agent 默认采用此配置agent preset 可通过 [`dsh-agent-tool-mode`](../agent-tool-mode/README.md) 自行选择呈现模式。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime``language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md) 交付Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。
`native` 以函数定义的形式贡献可见工具。`code` 会提供保留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则。执行器随后强制执行该规则:模型直接调用其他任何工具时,会在策略运行前将该调用解析为 `UNKNOWN_TOOL``both` 同时提供两种形式,且不声明该规则,因为其中的原生调用确实可以执行。没有单独声明呈现模式的 agent 默认采用此配置agent preset 可通过 [`dsh-agent-tool-presentation`](../agent-tool-presentation/README.md) 自行选择呈现模式。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime``language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md) 交付Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。
### 公开 API
@@ -148,7 +148,7 @@ agent loop 将连续的 `parallel` 调用归入有界滚动池,并把每个 `e
#### 模型看到的内容
Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块TypeScript 的 `declare const tools` 块,或 Python 的 `tools` 声明)。`both` 会同时公开普通 schema 与此 Code Mode API。在 `code` 下,提示词还会带上 `tools:code-only` 规则,其顺序排在逐工具指导段之前,让模型先读到「可以调用哪些工具」再读「每个工具做什么」;`both` 下它渲染为空。说明与 SDK 块随所加载运行时的语言切换;下方展示 TypeScript 版本(经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)Python 版本(用于任何报告 `language: 'python'` 的运行时)以 Python 语法提供相同操作和类型(`await tools.name(args)`、特殊名称用下标访问、`print(...)` 与顶层 `return`)。
Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块TypeScript 的 `declare const tools` 块,或 Python 的 `tools` 声明)。`both` 会同时公开普通 schema 与此 Code Mode API。在 `code` 下,提示词还会带上 `tools:code-only` 规则,其顺序排在逐工具指导段之前,让模型先读到「可以调用哪些工具」再读「每个工具做什么」;`both` 下它渲染为空。说明与 SDK 块随所加载运行时的语言切换;下方展示 TypeScript 版本(经 [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md)Python 版本(用于任何报告 `language: 'python'` 的运行时)以 Python 语法提供相同操作和类型(`await tools.name(args)`、特殊名称用下标访问、`print(...)` 与顶层 `return`)。
##### Code Mode SDK 说明
@@ -192,7 +192,7 @@ The available tools:
- **并发策略不是事件门禁**`executionMode()` 直接读取已解析的工具定义;插件只能在自身拥有的定义上声明分类器。
- **`tools/pre-execute` 有意不允许改写 `exec.arguments`**:否则日志记录和呈现的参数会与实际运行内容失去同步;改写设计记录在[拟议的 Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)中。
- **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。
- **定义中的 `timeoutMs` 仅作声明之用**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。
- **定义中的 `timeoutMs` 仅作声明之用**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-tool-call-timeout-policy` 包装层。
- **Code Mode 的 SDK 语言由当前加载的运行时决定,且呈现方式按 agent 而非按工具**`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器TypeScript 或 Python作用域限制遮蔽与 `presentAs` 会选择每个 agent 的可见绑定及其形态,但在同一个 agent 内不能让一个工具仅使用 Native而另一个仅使用 Code。
- **Code Mode 中间值只存在于执行局部,且没有字节上限**:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用的持久日志副本则确实有上限:`tools/code-dispatch-log` waterfall 允许 spill 策略把过大的 `tool/code-dispatch` 内容替换为预览加定位符([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
- **每次运行都会获得全新的 `run_code` 状态**MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tools",
"description": "Tool registry and execution pipeline for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.5",
"publishConfig": {
"access": "restricted"
},

View File

@@ -12,8 +12,8 @@ import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
import { TOOL_RUNTIME_SCHEDULER } from './index.ts'
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRuntime, ToolRunContext } from './index.ts'
import type {} from './types.ts'
/** The model-facing name of the Code Mode tool. */
@@ -289,7 +289,7 @@ export interface RunCodeBridgeOptions {
* @param options - the registry-private capabilities described above.
* @returns the registry-ready definition.
*/
export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridgeOptions): ToolDefinition {
export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeOptions): ToolDefinition {
const { requireRuntime, peekRuntime, maxParallel, shapeDispatchLog } = options
const definition = defineTool({
name: RUN_CODE_NAME,
@@ -476,7 +476,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
signal: runController.signal,
}
type DispatchOutcome = { isError: true; message: string } | { isError: false; value: JsonValue }
const scheduler = registry[TOOL_REGISTRY_SCHEDULER]
const scheduler = registry[TOOL_RUNTIME_SCHEDULER]
const outcome = await new Promise<DispatchOutcome>((resolve, reject) => {
// Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order.
let parked:

View File

@@ -136,7 +136,7 @@ export type {
declare module '@deepseek-ai/cordis' {
interface Context {
tools: ToolRegistry
tools: ToolRuntime
}
interface Events {
@@ -149,7 +149,7 @@ declare module '@deepseek-ai/cordis' {
* @param exec - the pending call (name, parsed arguments, caller agent).
* @mode waterfall
*/
'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
'tools/pre-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
/**
* Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns
* a normalized result; wrappers may change only `exec.signal`, while call
@@ -160,7 +160,7 @@ declare module '@deepseek-ai/cordis' {
* @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal).
* @mode waterfall
*/
'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
'tools/execute'(this: Scoped<ToolRuntime>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
/**
* Accept, replace, enrich, or block a normalized dispatch result. `next()`
* accepts it unchanged; thrown tools still reach this waterfall as errors. Async
@@ -172,7 +172,7 @@ declare module '@deepseek-ai/cordis' {
* @param result - the dispatch outcome a listener may accept, replace, or block.
* @mode waterfall
*/
'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
'tools/post-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
/**
* Allow a listener to replace content in the DURABLE LOG COPY of one
* `run_code` sub-dispatch outcome before the bridge appends its
@@ -186,7 +186,7 @@ declare module '@deepseek-ai/cordis' {
* @param dispatch - the parent execution, sub-call identity, and the settled content to log.
* @mode waterfall
*/
'tools/code-dispatch-log'(this: Scoped<ToolRegistry>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
'tools/code-dispatch-log'(this: Scoped<ToolRuntime>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
/**
* Observe the frozen, lossless-JSON final outcome. Listener failures are contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`.
@@ -194,7 +194,7 @@ declare module '@deepseek-ai/cordis' {
* @param result - a deep-frozen snapshot of the final returned result.
* @mode emit
*/
'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
'tools/result'(this: Scoped<ToolRuntime>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
/**
* A tool was registered or unregistered, or a scoped restriction changed
* (the available tool set changed — possibly for one scope only). An
@@ -247,7 +247,7 @@ export interface ToolDefinition extends ToolSchema {
finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
/**
* Cooperative tool-call timeout budget in milliseconds. Omit for no deadline.
* Enforced by `@deepseek-ai/dsh-timeout-policy` (a `tools/execute` wrapper); it
* Enforced by `@deepseek-ai/dsh-tool-call-timeout-policy` (a `tools/execute` wrapper); it
* is NEVER sent to the model — `schemas()` whitelists only name/description/
* parameters. Declaring it asserts this tool forwards `exec.signal` to a
* cooperative implementation that can reach quiescence when the signal aborts.
@@ -307,7 +307,7 @@ declare const toolExecutionTokenBrand: unique symbol
export type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
/**
* Caller-supplied description of one tool call. {@link ToolRegistry.execute}
* Caller-supplied description of one tool call. {@link ToolRuntime.execute}
* adds the registry-owned token to form a pipeline {@link ToolExecution};
* callers do not choose that token.
*/
@@ -330,7 +330,7 @@ export interface ToolExecutionInput {
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'code'`, only calls WITH a parent may
* execute a native tool name — a model-direct call (no parent) is denied as
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRegistry.execute}.
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRuntime.execute}.
*/
readonly parent?: ToolExecutionToken
/** Required caller-owned cancellation for this invocation. */
@@ -435,7 +435,7 @@ export type ScheduledToolPreparation =
/**
* Scheduler-only dispatch result. A `post-result` still receives post-execute;
* a `final-result` already matches {@link ToolRegistry.execute} failure semantics.
* a `final-result` already matches {@link ToolRuntime.execute} failure semantics.
* @internal
*/
export type ScheduledToolDispatch =
@@ -444,11 +444,11 @@ export type ScheduledToolDispatch =
/**
* Symbol-keyed scheduler view that keeps pre/post policy ordered while
* overlapping dispatch. Ordinary callers use {@link ToolRegistry.execute};
* overlapping dispatch. Ordinary callers use {@link ToolRuntime.execute};
* this is not a plugin extension point.
* @internal
*/
export interface ToolRegistryScheduler {
export interface ToolRuntimeScheduler {
/** Materialize input, run the ordered pre-execute/guard gate, and decide what stage follows. */
prepare(exec: ToolExecutionInput): Promise<ScheduledToolPreparation>
/** Run only the around-dispatch/body stage. */
@@ -463,7 +463,7 @@ export interface ToolRegistryScheduler {
* Scheduler entry point omitted from the generated named service API.
* @internal
*/
export const TOOL_REGISTRY_SCHEDULER: unique symbol = Symbol('@deepseek-ai/dsh-tools.scheduler')
export const TOOL_RUNTIME_SCHEDULER: unique symbol = Symbol('@deepseek-ai/dsh-tools.scheduler')
/** Canonical error code for cancellation after a tool body was invoked. */
export const TOOL_ABORTED = 'ABORTED'
@@ -784,7 +784,7 @@ function resolveMaxParallelSubCalls(value: number | undefined): number {
* Tool registry and execution pipeline. Scoped registrations shadow globals;
* one visibility resolver feeds presentation, lookup, and dispatch.
*/
export class ToolRegistry extends Service {
export class ToolRuntime extends Service {
static inject = ['systemPrompt']
static Config: z<Config> = z.object({
@@ -793,7 +793,7 @@ export class ToolRegistry extends Service {
})
/** Internal staged view consumed by `dsh-agent-loop`'s parallel scheduler. */
readonly [TOOL_REGISTRY_SCHEDULER]: ToolRegistryScheduler = {
readonly [TOOL_RUNTIME_SCHEDULER]: ToolRuntimeScheduler = {
prepare: exec => this.prepareScheduledExecution(exec),
dispatch: exec => this.dispatchScheduledExecution(exec),
finalize: (exec, result) => this.finalizeScheduledExecution(exec, result),
@@ -948,7 +948,7 @@ export class ToolRegistry extends Service {
if (scopeOf(ctx) === undefined) {
throw new Error('tools.presentAs() requires a scoped context (agent.ctx): a context-global presentation is the `mode` config field on the tools row')
}
const dispose = ctx.effect(function* (this: ToolRegistry) {
const dispose = ctx.effect(function* (this: ToolRuntime) {
yield this.layers.effect(
ctx,
(layer) => {
@@ -1019,7 +1019,7 @@ export class ToolRegistry extends Service {
private requireCodeRuntime(mode: ToolPresentationMode): CodeRuntime {
const runtime = this.ctx.get('codeRuntime')
if (!runtime) {
throw new Error(`dsh-tools: mode "${mode}" requires a code runtime — load a ctx.codeRuntime implementation (e.g. @deepseek-ai/dsh-code-runtime-worker) or set tools mode to "native"`)
throw new Error(`dsh-tools: mode "${mode}" requires a code runtime — load a ctx.codeRuntime implementation (e.g. @deepseek-ai/dsh-code-runtime-worker-thread) or set tools mode to "native"`)
}
if (!Object.hasOwn(SDK_RENDERERS, runtime.language)) {
const known = Object.keys(SDK_RENDERERS).map(name => JSON.stringify(name)).join(', ')
@@ -1314,7 +1314,7 @@ export class ToolRegistry extends Service {
*
* Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `code`
* by an agent preset under a native deployment is the composition
* `dsh-agent-tool-mode` exists for, and reading the deployment default would
* `dsh-agent-tool-presentation` exists for, and reading the deployment default would
* leave exactly that agent uncollapsed — announcing one surface while
* executing another, which is the bypass this collapse closes.
* @param name - the tool name as registered.
@@ -1943,4 +1943,4 @@ function toolAbortedBeforeDispatchResult(prior?: ToolExecutionResult): ToolExecu
}
}
export default ToolRegistry
export default ToolRuntime

View File

@@ -61,7 +61,7 @@ export interface GenericCallView {
kind?: ToolCallKind
/**
* The salient input to show in a detail/expanded view (e.g. a background
* task id). Omit to show nothing; a string renders as-is, an object as pretty
* job id). Omit to show nothing; a string renders as-is, an object as pretty
* JSON. NOT the full raw args object unless that is genuinely what a reader wants.
*/
rawInput?: unknown

View File

@@ -6,7 +6,7 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
import type { Config, JsonSchemaNode, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
@@ -50,7 +50,7 @@ interface SetupOptions {
async function setup(options: SetupOptions = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { ...options.toolOrder ? { toolOrder: options.toolOrder } : {} })
await ctx.plugin(ToolRegistry, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
await ctx.plugin(ToolRuntime, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
let runtime: FakeRuntime | undefined
if (options.runtime !== false) {
await ctx.plugin(FakeRuntime, options.runtime ?? {})
@@ -456,7 +456,7 @@ describe('mode-aware wire contribution', () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(FakeRuntime, {})
const fiber = await ctx.plugin(ToolRegistry, { mode: 'code' })
const fiber = await ctx.plugin(ToolRuntime, { mode: 'code' })
expect(ctx.tools.get(RUN_CODE_NAME)).toBeDefined()
await fiber.dispose()
const assembly = await ctx.systemPrompt.assemble()
@@ -1216,7 +1216,7 @@ describe('the run_code dispatch bridge', () => {
it('executing run_code under a missing runtime is a structured isError, not a crash', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRegistry, { mode: 'code' })
await ctx.plugin(ToolRuntime, { mode: 'code' })
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
@@ -1568,21 +1568,21 @@ describe('the run_code dispatch bridge', () => {
it('direct construction rejects a non-positive parallel sub-call cap at load', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
expect(() => new ToolRegistry(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
expect(() => new ToolRuntime(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
.toThrow('maxParallelSubCalls must be a positive integer')
})
it('direct construction in code mode defaults the parallel sub-call cap', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
const registry = new ToolRuntime(ctx, { mode: 'code' })
expect(registry.get(RUN_CODE_NAME)).toBeDefined()
})
it('defaults to native mode under direct construction with no config', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx)
const registry = new ToolRuntime(ctx)
expect(registry.get(RUN_CODE_NAME)).toBeUndefined()
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
@@ -1590,7 +1590,7 @@ describe('the run_code dispatch bridge', () => {
it('denies a model-direct native-tool call under code mode as UNKNOWN_TOOL', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
const registry = new ToolRuntime(ctx, { mode: 'code' })
registerEcho(ctx, 'write')
const result = await registry.execute({
signal: testToolSignal,
@@ -1610,7 +1610,7 @@ describe('the run_code dispatch bridge', () => {
it('routes a pre-aborted collapsed call through ABORTED_BEFORE_DISPATCH', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
const registry = new ToolRuntime(ctx, { mode: 'code' })
registerEcho(ctx, 'write')
const aborted = new AbortController()
aborted.abort()
@@ -1681,7 +1681,7 @@ describe('per-agent presentation', () => {
// `native` here, so a collapse predicate reading it instead of this
// scope's effective mode would announce [run_code] and still execute the
// native call — the bypass, reopened for exactly the preset composition
// `dsh-agent-tool-mode` produces.
// `dsh-agent-tool-presentation` produces.
expect(ctx.tools.executionMode({
signal: testToolSignal,
callId: CallId('preset-coded-schedule'),

View File

@@ -4,7 +4,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, {
import ToolRuntime, {
defineContentToolFixture,
type ToolDefinition,
type ToolExecutionInput,
@@ -16,7 +16,7 @@ const testToolSignal = new AbortController().signal
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
return ctx
}
@@ -24,7 +24,7 @@ function exec(name: string, args: unknown): ToolExecutionInput {
return { signal: testToolSignal, callId: CallId('c1'), name, arguments: args }
}
describe('ToolRegistry.executionMode', () => {
describe('ToolRuntime.executionMode', () => {
it('returns parallel only for an explicit true classifier', async () => {
const ctx = await setup()
ctx.tools.register(defineContentToolFixture({

View File

@@ -5,9 +5,11 @@
import { describe, expect, it } from 'vitest'
import {
assertManifestComplete,
assertToolsHarvested,
collectToolCatalog,
render,
type ToolCatalog,
type ToolPackage,
} from '../../../../scripts/gen-tool-catalog.ts'
/** JSON Schema shape enough to reach the values AST extraction can't. */
@@ -23,7 +25,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {
@@ -46,7 +48,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('attributes each harvested tool with its registering plugin source', async () => {
const catalog = await collectToolCatalog()
const bash = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-bash')
expect(bash?.sources.bash).toBe('packages/bash/tool-bash/src/index.ts')
expect(bash?.sources.bash).toBe('packages/shell/tool-bash/src/index.ts')
const control = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-subagent-control')
expect(control?.sources).toEqual({
interrupt_agent: 'packages/subagent/tool-subagent-control/src/index.ts',
@@ -91,6 +93,29 @@ describe('gen-tool-catalog assertManifestComplete', () => {
})
})
describe('gen-tool-catalog assertToolsHarvested', () => {
const entry: ToolPackage = {
pkg: '@deepseek-ai/dsh-tool-demo',
dir: 'tool-demo',
source: 'packages/demo/tool-demo/src/index.ts',
requires: ['ctx.tools', 'ctx.somethingUnmounted'],
writes: ['tool/result'],
mount: () => Promise.resolve(),
}
it('accepts a boot that registered at least one tool', () => {
expect(() => { assertToolsHarvested(entry, 1) }).not.toThrow()
})
it('throws, naming the package and its requirements, when a boot registers nothing', () => {
// The failure this guards is silent by construction: the package is in the
// manifest, its plugin merely stays PENDING on an unmounted service, and the
// catalog would ship without its tools while every gate stays green.
expect(() => { assertToolsHarvested(entry, 0) }).toThrow(/@deepseek-ai\/dsh-tool-demo booted without registering a single tool/)
expect(() => { assertToolsHarvested(entry, 0) }).toThrow(/ctx.somethingUnmounted/)
})
})
describe('gen-tool-catalog render', () => {
it('emits a package heading, a tool heading, and a json schema fence', () => {
const catalog: ToolCatalog = [

View File

@@ -5,14 +5,14 @@ import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
const testToolSignal = new AbortController().signal
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(ToolsInvariant)
return ctx
}
@@ -219,7 +219,7 @@ describe('tool-pipeline invariants', () => {
content: [{ type: 'text', text: 'ok' }],
})
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).resolves.toBeUndefined()
})
@@ -233,7 +233,7 @@ describe('tool-pipeline invariants', () => {
name: 'echo',
arguments: {},
})
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).rejects.toThrow(/outside any open turn/)
})
})

View File

@@ -4,7 +4,7 @@ import type { Events } from '@deepseek-ai/cordis'
import { bindScopeParent, createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import type { PreToolDecision, ToolDefinition, ToolExecution, ToolExecutionInput, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -17,7 +17,7 @@ const testToolSignal = new AbortController().signal
async function mount(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
return ctx
}

View File

@@ -4,7 +4,7 @@ import { createUserMessage, CallId, HarnessError, type ContentBlock } from '@de
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { Agent } from '@deepseek-ai/dsh-agent'
import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import ToolRegistry, {
import ToolRuntime, {
defineContentToolFixture, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH,
type InferArgs, type JsonValue, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision,
@@ -16,7 +16,7 @@ const testToolSignal = new AbortController().signal
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
return ctx
}
@@ -33,7 +33,7 @@ const echoTool = defineTool({
},
})
describe('ToolRegistry', () => {
describe('ToolRuntime', () => {
it('registers tools, exposes schemas, and feeds the system-prompt assembly', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
@@ -2449,7 +2449,7 @@ describe('schema DSL optional and nested contracts', () => {
})
})
describe('ToolRegistry.get', () => {
describe('ToolRuntime.get', () => {
it('get() returns the registered tool definition', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)

View File

@@ -36,7 +36,7 @@
"path": "../../interaction/user-approval"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}