fix(tools): settle executor calls after unload and validate configuration

This commit is contained in:
_Kerman committed 2026-10-19 16:32:29 +08:00
1 parent 3b978adcec
commit e9784c073b
63 files changed
+327 -310

No files matched your search

@@ -399,7 +399,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
const patch = join(root, 'failure.yml')
await mkdir(home)
if (blocked) await writeFile(join(home, 'logs'), 'blocked')
await writeFile(patch, '- id: tool-executor-native\n config:\n maxParallelToolCalls: 0\n')
await writeFile(patch, '- id: agent-loop\n config:\n maxParallelToolCalls: 0\n')
const child = execa(launch.command, [
...launch.args, '--profile', 'sdk', '--patch', patch,
], {
+3 -3
View File
@@ -816,7 +816,7 @@ describe('a user preset declared from the shipped cordis rows', () => {
expect(queriedText).toContain(`"name": "${toolName}"`)
}
// The `Config` provider reads the booted profile tree: the executor selector declares a Config,
// The `Config` provider reads the booted profile tree: the Agent loop declares a Config,
// and the bootstrap include row is a carrier. The name filter keeps each page small.
type ConfigRow = { id: string; patchId: string; name: string; status: string }
type ConfigPage = { data: { entries: ConfigRow[]; total: number; nextOffset: number | null } }
@@ -831,9 +831,9 @@ describe('a user preset declared from the shipped cordis rows', () => {
expect(page.isError, resultText(page)).toBe(false)
return (JSON.parse(resultText(page)) as ConfigPage).data
}
const toolsRows = await listConfigs({ name: '@deepseek-ai/dsh-tool-executor-native' }, 'copied-preset-inspect-configs')
const toolsRows = await listConfigs({ name: '@deepseek-ai/dsh-agent-loop' }, 'copied-preset-inspect-configs')
const toolsRow = toolsRows.entries[0]
expect(toolsRow).toMatchObject({ patchId: 'tool-executor-native', status: 'schema' })
expect(toolsRow).toMatchObject({ patchId: 'agent-loop', status: 'schema' })
expect(toolsRows).toMatchObject({ total: 1, nextOffset: null })
const includes = await listConfigs({ name: 'cordis:include' }, 'copied-preset-inspect-includes')
expect(includes.entries.find(entry => entry.patchId === 'include')).toMatchObject({ status: 'tree' })
+2 -2
View File
@@ -1,4 +1,4 @@
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { executeToolFixture, executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
// Boots the shipped Web composition over the built dist this lane already uses
// and asserts its catalog, defaults, Loader lifecycle, and one complete Auto
// producer-to-tool path. Browser scenarios in this lane own visual behavior.
@@ -680,7 +680,7 @@ it('lets a preset producer reach the background-job registry', async () => {
// `tool-bash` is a preset row and `tasks` is a host registry; the producer
// resolves it with `ctx.get`, so a registry hidden behind a preset realm
// fails here — with every task control still listed in the catalog above.
const started = await executeToolFixture(ctx, {
const started = await executeModelToolFixture(ctx, {
signal,
callId: ToolCallId('shipped-bash-background'),
name: 'bash',
+5 -25
View File
@@ -162,7 +162,7 @@ export interface Config {
## `@deepseek-ai/dsh-agent-tool-presentation`
- `inject`: `tools`
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:26`](../packages/core/agent-tool-presentation/src/index.ts)
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:25`](../packages/core/agent-tool-presentation/src/index.ts)
```ts config-catalog
/**
@@ -3822,40 +3822,19 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-bash-persistent -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-native -->
<a id="deepseek-aidsh-tool-executor-native"></a>
## `@deepseek-ai/dsh-tool-executor-native`
- `inject`: `tools`
- `refs`: `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/tool-executor-native/src/index.ts:14`](../packages/core/tool-executor-native/src/index.ts)
```ts config-catalog
/** Native batch concurrency configuration. */
export interface Config {
/** Maximum overlapping concurrency-safe calls in one batch. */
maxParallelToolCalls: Volatile<number>
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-native -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
<a id="deepseek-aidsh-tool-executor-ptc"></a>
## `@deepseek-ai/dsh-tool-executor-ptc`
- `inject`: `tools` · `systemPrompt` · `ptcRuntime`
- `refs`: `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/tool-executor-ptc/src/index.ts:19`](../packages/core/tool-executor-ptc/src/index.ts)
- `inject`: `tools` · `systemPrompt`
- `source`: [`packages/core/tool-executor-ptc/src/config.ts:5`](../packages/core/tool-executor-ptc/src/config.ts)
```ts config-catalog
/** Program execution limits. */
export interface Config {
/** Maximum overlapping concurrency-safe calls in one model batch. */
maxParallelToolCalls: Volatile<number>
/** Maximum overlapping concurrency-safe program subcalls. */
maxParallelSubCalls?: number
maxParallelSubCalls: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
@@ -4769,6 +4748,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
| `@deepseek-ai/dsh-tool-executor-native` | `tools` | [`packages/core/tool-executor-native/src/index.ts`](../packages/core/tool-executor-native/src/index.ts) |
| `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) |
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) |
+5 -25
View File
@@ -164,7 +164,7 @@ export interface Config {
## `@deepseek-ai/dsh-agent-tool-presentation`
- `inject`: `tools`
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:26`](../packages/core/agent-tool-presentation/src/index.ts)
- `source`: [`packages/core/agent-tool-presentation/src/index.ts:25`](../packages/core/agent-tool-presentation/src/index.ts)
```ts config-catalog
/**
@@ -3824,40 +3824,19 @@ export interface Config {
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-bash-persistent -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-native -->
<a id="deepseek-aidsh-tool-executor-native"></a>
## `@deepseek-ai/dsh-tool-executor-native`
- `inject`: `tools`
- `refs`: `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/tool-executor-native/src/index.ts:14`](../packages/core/tool-executor-native/src/index.ts)
```ts config-catalog
/** Native batch concurrency configuration. */
export interface Config {
/** Maximum overlapping concurrency-safe calls in one batch. */
maxParallelToolCalls: Volatile<number>
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-native -->
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
<a id="deepseek-aidsh-tool-executor-ptc"></a>
## `@deepseek-ai/dsh-tool-executor-ptc`
- `inject`: `tools` · `systemPrompt` · `ptcRuntime`
- `refs`: `Volatile` (`@deepseek-ai/cosmokit`)
- `source`: [`packages/core/tool-executor-ptc/src/index.ts:19`](../packages/core/tool-executor-ptc/src/index.ts)
- `inject`: `tools` · `systemPrompt`
- `source`: [`packages/core/tool-executor-ptc/src/config.ts:5`](../packages/core/tool-executor-ptc/src/config.ts)
```ts config-catalog
/** Program execution limits. */
export interface Config {
/** Maximum overlapping concurrency-safe calls in one model batch. */
maxParallelToolCalls: Volatile<number>
/** Maximum overlapping concurrency-safe program subcalls. */
maxParallelSubCalls?: number
maxParallelSubCalls: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-executor-ptc -->
@@ -4771,6 +4750,7 @@ export interface Config {
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
| `@deepseek-ai/dsh-tool-executor-native` | `tools` | [`packages/core/tool-executor-native/src/index.ts`](../packages/core/tool-executor-native/src/index.ts) |
| `@deepseek-ai/dsh-tool-schedule` | `tools` | [`packages/schedule/tool-schedule/src/index.ts`](../packages/schedule/tool-schedule/src/index.ts) |
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` · `agents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
| `@deepseek-ai/dsh-tool-working-directory` | `tools` · `workingDirectory` | [`packages/session/tool-working-directory/src/index.ts`](../packages/session/tool-working-directory/src/index.ts) |
+1 -2
View File
@@ -1194,7 +1194,6 @@ flowchart TD
pkg_spill_policy --> pkg_session
pkg_spill_policy --> pkg_spill
pkg_spill_policy --> pkg_token_meter
pkg_spill_policy --> pkg_tool_executor_ptc
pkg_spill_policy --> pkg_tools
pkg_session_query_sqlite --> pkg_session
pkg_session_query_sqlite --> pkg_session_persistence
@@ -1769,7 +1768,7 @@ flowchart TD
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`fs-local`](../packages/fs/fs-local), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tools`](../packages/core/tools) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
+1 -2
View File
@@ -1196,7 +1196,6 @@ flowchart TD
pkg_spill_policy --> pkg_session
pkg_spill_policy --> pkg_spill
pkg_spill_policy --> pkg_token_meter
pkg_spill_policy --> pkg_tool_executor_ptc
pkg_spill_policy --> pkg_tools
pkg_session_query_sqlite --> pkg_session
pkg_session_query_sqlite --> pkg_session_persistence
@@ -1771,7 +1770,7 @@ flowchart TD
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`fs-local`](../packages/fs/fs-local), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tools`](../packages/core/tools) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
+2 -2
View File
@@ -1830,8 +1830,8 @@
en: 08be4349548406a9
zh: 8e7cf9966198ef78
/session-persistence-event-catalog/resolved-persistence-types/kind-tool-registry:
en: 4fde95c569339141
zh: 6fa54e19c24ab7f3
en: c59d020828d8211f
zh: 154a30cb69e79489
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
en: a58f339332877b15
zh: b7406af7d8197b64
+1 -1
View File
@@ -7542,7 +7542,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49`
SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
Sources: [`packages/core/tools/src/index.ts:17`](../packages/core/tools/src/index.ts)
Sources: [`packages/core/tools/src/index.ts:18`](../packages/core/tools/src/index.ts)
| Property | Presence | Type |
|---|---|---|
+1 -1
View File
@@ -7544,7 +7544,7 @@ SHA-256: `c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49`
SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
来源:[`packages/core/tools/src/index.ts:17`](../packages/core/tools/src/index.ts)
来源:[`packages/core/tools/src/index.ts:18`](../packages/core/tools/src/index.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+1 -1
View File
@@ -24599,7 +24599,7 @@
"digest": "8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543",
"names": [],
"sources": [
"packages/core/tools/src/index.ts:17"
"packages/core/tools/src/index.ts:18"
]
},
{
+2 -1
View File
@@ -293,7 +293,7 @@ interface ToolExecutionInput {
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'ptc'`, only calls WITH a parent may
* model-direct call: with the PTC executor selected, 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.
*/
@@ -635,6 +635,7 @@ schemas(scope?: ScopeKey): ToolSchema[]
/**
* Provide the executor for the calling scope; an unscoped contribution is the deployment default.
* Rendering any executor-owned section requires the viewing scope's selection to remain available.
* @param executor - initialized provider owned by the contributing plugin.
* @returns the effect disposer. Unloading leaves the selection unavailable until it is replaced.
*/
+2 -1
View File
@@ -293,7 +293,7 @@ interface ToolExecutionInput {
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'ptc'`, only calls WITH a parent may
* model-direct call: with the PTC executor selected, 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.
*/
@@ -635,6 +635,7 @@ schemas(scope?: ScopeKey): ToolSchema[]
/**
* Provide the executor for the calling scope; an unscoped contribution is the deployment default.
* Rendering any executor-owned section requires the viewing scope's selection to remain available.
* @param executor - initialized provider owned by the contributing plugin.
* @returns the effect disposer. Unloading leaves the selection unavailable until it is replaced.
*/
@@ -9,8 +9,8 @@
en: 73002a2963b9ea27
zh: a67ea382d7c70c61
/mount-a-tool-executor/change:
en: b47c04b407b204a9
zh: f3f54c9b25eb1951
en: f732d85027bf0236
zh: d6597cde7ba31e47
/mount-a-tool-executor/migration:
en: 411860d06f1013ea
zh: 615e044545d89b55
@@ -9,7 +9,7 @@ English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-tools` keeps tool registration and executor selection, but no longer accepts `mode` or `maxParallelSubCalls`. Native and PTC execution require `@deepseek-ai/dsh-tool-executor-native` or `@deepseek-ai/dsh-tool-executor-ptc`. Shipped profiles mount an executor; custom compositions must add one. The existing `dsh-agent-tool-presentation` selector remains supported but is deprecated. Enabled `tool-executor-native` and `tool-executor-ptc` rows are required at startup; invalid executor configuration now stops startup.
`@deepseek-ai/dsh-tools` keeps tool registration and executor selection, but rejects the removed `mode` and `maxParallelSubCalls` configuration fields at load time. Native and PTC execution require `@deepseek-ai/dsh-tool-executor-native` or `@deepseek-ai/dsh-tool-executor-ptc`. Shipped profiles mount an executor; custom compositions must add one. The existing `dsh-agent-tool-presentation` selector remains supported but is deprecated. Enabled `tool-executor-native` and `tool-executor-ptc` rows are required at startup; invalid executor configuration now stops startup.
## Migration
@@ -9,7 +9,7 @@ description: "工具执行模式从 dsh-tools 配置移至显式执行器插件
## 变更
`@deepseek-ai/dsh-tools` 保留工具注册和执行器选择,但不再接受 `mode` 或 `maxParallelSubCalls`。原生和 PTC 执行分别要求 `@deepseek-ai/dsh-tool-executor-native` 或 `@deepseek-ai/dsh-tool-executor-ptc`。内置 profile 已挂载执行器,自定义组合必须添加一个。现有 `dsh-agent-tool-presentation` 选择器仍受支持,但已弃用。 启用的 `tool-executor-native` 和 `tool-executor-ptc` 条目属于启动必需项;无效的执行器配置会终止启动。
`@deepseek-ai/dsh-tools` 保留工具注册和执行器选择,但加载时会拒绝已移除的 `mode` 和 `maxParallelSubCalls` 配置字段。原生和 PTC 执行分别要求 `@deepseek-ai/dsh-tool-executor-native` 或 `@deepseek-ai/dsh-tool-executor-ptc`。内置 profile 已挂载执行器,自定义组合必须添加一个。现有 `dsh-agent-tool-presentation` 选择器仍受支持,但已弃用。启用的 `tool-executor-native` 和 `tool-executor-ptc` 条目属于启动必需项;无效的执行器配置会终止启动。
## 迁移
@@ -47,7 +47,6 @@
"license": "MIT",
"dependencies": {
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-tool-executor-native": "workspace:*",
"@deepseek-ai/dsh-util-values": "workspace:*",
"@deepseek-ai/schemastery": "workspace:~",
"zod": "4.4.3"
@@ -95,6 +94,7 @@
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-spill-local": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tool-executor-native": "workspace:*",
"@deepseek-ai/dsh-tool-fs": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
@@ -1,8 +1,8 @@
import type { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
/**
* Exercises scheduler ordering and cancellation with deterministic gated tools.
* ACP expected outputs own transcript-facing coverage.
*/
import type { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import * as PtcToolExecutor from '@deepseek-ai/dsh-tool-executor-ptc'
@@ -37,13 +37,13 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number, exec
await ctx.plugin(ToolRuntime)
let toolPipeline!: ToolExecutionPipeline
await ctx.plugin({
inject: ['tools'], Config: NativeToolExecutor.Config,
apply(owner: Context, config: NativeToolExecutor.Config) {
const executor = new NativeToolExecutor.NativeToolExecutor(owner, () => config.maxParallelToolCalls.get())
inject: ['tools'],
apply(owner: Context) {
const executor = new NativeToolExecutor.NativeToolExecutor(owner, () => executorLimit ?? 10)
toolPipeline = executor['pipeline']
owner.tools.setExecutor(executor)
},
}, { ...executorLimit === undefined ? {} : { maxParallelToolCalls: executorLimit } })
})
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, {
agents: [],
@@ -21,8 +21,8 @@
en: 1550e3dcc5a3aa1e
zh: 47b2d1e0e3e3cbf2
/deepseek-ai-dsh-agent-tool-presentation/use-this-package/what-ptc-mode-requires:
en: d5004b2caa31b245
zh: 32619d7e03c7c2ac
en: 3ec33f18b3ac0859
zh: 92bf371bfbbc511d
/deepseek-ai-dsh-agent-tool-presentation/use-this-package/one-presentation-per-agent:
en: 46bf80f051175567
zh: 8f5e1623da5d1520
@@ -36,8 +36,8 @@
en: 89ecdb3d5d032e7f
zh: 64ae2feb4344c679
/deepseek-ai-dsh-agent-tool-presentation/understand-the-implementation/behavior-notes:
en: 7996925328f0e771
zh: 40146455aac119d2
en: 4a44ac4057732829
zh: 813d3ec7627146fd
/deepseek-ai-dsh-agent-tool-presentation/further-exploration:
en: 6a1d0096e4da25e4
zh: 9d5de770addb69d2
@@ -45,7 +45,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
### What PTC mode requires
Selecting `ptc` needs a composed PTC runtime (`ctx.ptcRuntime`) whose language has a registered SDK renderer — the TypeScript runtime ships via [`dsh-ptc-runtime-node`](../../ptc-runtime/ptc-runtime-node/README.md), and both the TypeScript and Python SDK renderers are built into `dsh-tool-executor-ptc`. A preset that selects a PTC mode against a deployment composing no such runtime refuses to mount, naming this row, so the failure lands where the operator can act instead of at the session's first request.
Selecting `ptc` needs a composed PTC runtime (`ctx.ptcRuntime`) whose language has a registered SDK renderer — the TypeScript runtime ships via [`dsh-ptc-runtime-node`](../../ptc-runtime/ptc-runtime-node/README.md), and both the TypeScript and Python SDK renderers are built into `dsh-tool-executor-ptc`. Selection takes effect even when the runtime is missing; prompt assembly or program execution then reports the missing runtime instead of inheriting native execution.
### One presentation per agent
@@ -73,7 +73,7 @@ The tool registry cannot move into a preset: its consumers are all host-plane
### Behavior notes
`native` applies immediately. A PTC mode instead waits for `ctx.ptcRuntime`, a host-plane service: a preset selecting PTC mode against a deployment composing no runtime holds this row pending, and `dsh-agent-preset-registry` refuses the mount naming this id. `setExecutor` is itself the effect, so the declaration unwinds with this row without a second wrapper owning it.
Both modes mount their executor immediately. PTC checks runtime availability when describing or executing a program. The mounted executor owns its scoped registration, which is withdrawn with this selector's plugin lifetime.
</details>
@@ -45,7 +45,7 @@ kind: "package-reference"
### PTC 模式需要什么
选择 `ptc` 需要已组合的 PTC 运行时(`ctx.ptcRuntime`),且其语言有已注册的 SDK 渲染器——TypeScript 运行时经 [`dsh-ptc-runtime-node`](../../ptc-runtime/ptc-runtime-node/README.zh.md) 交付,TypeScript 与 Python 的 SDK 渲染器都内置在 `dsh-tools` 中。针对未组装此类运行时的部署选择 PTC 模式的 preset 会拒绝挂载并点名这一行,使失败落在操作者可以行动的地方,而不是落在会话的第一次请求上。
选择 `ptc` 需要已组合的 PTC 运行时(`ctx.ptcRuntime`),且其语言有已注册的 SDK 渲染器——TypeScript 运行时经 [`dsh-ptc-runtime-node`](../../ptc-runtime/ptc-runtime-node/README.zh.md) 交付,TypeScript 与 Python 的 SDK 渲染器都内置在 `dsh-tool-executor-ptc` 中。即使缺少运行时,选择也会生效;随后提示词组装或程序执行会报告缺少运行时,而不会继承原生执行方式。
### 每个 agent 只声明一次呈现方式
@@ -73,7 +73,7 @@ kind: "package-reference"
### 行为说明
`native` 立即生效。PTC 模式则等待 `ctx.ptcRuntime`——这是一个宿主平面服务:针对未组装运行时的部署选择 PTC mode 的 preset 会让这一行停在 pending,`dsh-agent-preset-registry` 会指名此 id 拒绝挂载。`setExecutor` 本身就是 effect,因此该声明随这一行撤销,无需第二个包装层拥有它。
两种模式都会立即挂载各自的执行器。PTC 在描述或执行程序时检查运行时是否可用。挂载的执行器拥有作用域注册,并随此选择器的插件生命周期一同撤销。
</details>
@@ -13,9 +13,8 @@ import * as PtcToolExecutor from '@deepseek-ai/dsh-tool-executor-ptc'
export const name = 'tool-presentation'
/**
* Required services. `ptcRuntime` is NOT listed: a `native` row must mount in
* a deployment that composes no runtime, and the mode-dependent wait is
* declared inside {@link apply} instead.
* Tool selection does not require a program runtime; the PTC executor
* reports a missing runtime when its description or execution is requested.
*/
export const inject = ['tools']
@@ -1,9 +1,8 @@
/**
* The row an agent preset carries to pick its tool presentation. What it owes
* its caller: the choice reaches THIS agent and no other, it unwinds with the
* agent, and a code mode composed against a deployment with no PTC runtime
* stops at mount — where a preset's activation audit can name it — rather
* than at the first prompt assembly.
* agent, and a missing PTC runtime rejects the selected scope's prompt
* instead of inheriting the native executor.
*/
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
@@ -68,8 +67,7 @@ describe('the tool-presentation row', () => {
})
it('declares the services it uses without holding a PTC runtime hostage', () => {
// A `native` row must mount where no runtime is composed, so the wait is
// conditional inside apply rather than static metadata.
// Native execution does not require a program runtime.
expect(inject).toEqual(['tools'])
})
@@ -95,17 +93,13 @@ describe('the tool-presentation row', () => {
await expect(ctx.systemPrompt.assemble({ scope: agent })).rejects.toThrow('selected tool executor is unavailable')
})
it('waits for a PTC runtime the deployment does not compose', async () => {
it('rejects PTC assembly when the deployment does not compose its runtime', async () => {
const ctx = await host({ runtime: false })
const { agent, row } = await mount(ctx, { mode: 'ptc' })
// Pending, not applied: `dsh-agent-preset-registry` rejects a mount holding a row
// that never reached a usable state, naming this id — so the preset fails
// where the operator can act, instead of at the first request.
expect(row.ctx.get('ptcRuntime')).toBeUndefined()
const assembly = await ctx.systemPrompt.assemble({ scope: agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['echo'])
await expect(ctx.systemPrompt.assemble({ scope: agent })).rejects.toThrow('requires a PTC runtime')
})
it('applies once the runtime arrives', async () => {
@@ -15,11 +15,11 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-tool-executor-native/use-this-package:
en: 176f69450bacc28e
zh: fc3c2d876195a4a4
en: 0e73b1a49308a0f6
zh: 968b3ee68284d849
/deepseek-ai-dsh-tool-executor-native/understand-the-implementation:
en: d005036097c11788
zh: 65fcaec551dc1697
en: 6a06c6b318f58436
zh: 4f572feca095e3a6
/deepseek-ai-dsh-tool-executor-native/further-exploration:
en: e8ae407b0ecdc5d3
zh: 022a8a714d24da4b
+2 -6
View File
@@ -29,13 +29,9 @@ Add this row to a composition with `dsh-tools` mounted. The nearest scoped execu
```yaml
- name: '@deepseek-ai/dsh-tool-executor-native'
config:
maxParallelToolCalls: 10
```
For Agent steps, configure [agent-loop concurrency](../agent-loop/README.md); the loop always forwards its resolved limit to `execute()`. The executor's `maxParallelToolCalls` supplies the limit only when the caller omits it. This fallback is volatile, defaults to `10`, and is sampled for the next tool group. It must be a positive integer; `1` selects serial execution.
[Configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-executor-native)
Configure [agent-loop concurrency](../agent-loop/README.md) for Agent steps; the loop reads the limit once per batch. This plugin has no configuration fields. Direct `NativeToolExecutor` callers may pass a constructor callback, defaulting to `10`, which is sampled for each tool group when the batch omits `maxParallelToolCalls`. Limits must be positive integers; `1` selects serial execution.
-----
@@ -47,7 +43,7 @@ For Agent steps, configure [agent-loop concurrency](../agent-loop/README.md); th
The executor receives the entire raw call batch and owns parsing, scheduling, cancellation, and optional model-step logging. It reuses the shared invocation pipeline for policy, tool bodies, and result notifications. After abort, it waits for started work and returns ordered error results for skipped calls.
- [`src/index.ts`](src/index.ts) — Validated configuration and scoped registration; tool descriptions and batch execution.
- [`src/index.ts`](src/index.ts) — Scoped executor registration; tool descriptions and batch execution.
</details>
@@ -29,13 +29,9 @@ kind: "package-reference"
```yaml
- name: '@deepseek-ai/dsh-tool-executor-native'
config:
maxParallelToolCalls: 10
```
Agent 步骤使用 [agent-loop 并发配置](../agent-loop/README.zh.md),循环始终向 `execute()` 传入解析后的上限。只有调用方未传入上限时,才使用执行器的 `maxParallelToolCalls`。这个后备配置是 volatile 字段,默认值为 `10`,在下一个工具组开始时读取。值必须是正整数;`1` 表示串行执行。
[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-executor-native)
Agent 步骤使用 [agent-loop 并发配置](../agent-loop/README.zh.md),循环在每个批次开始时读取一次上限。本插件没有配置字段。直接构造 `NativeToolExecutor` 的调用方可以传入回调,默认返回 `10`;批次未指定 `maxParallelToolCalls` 时,每个工具组读取该回调。上限必须是正整数;`1` 表示串行执行。
-----
@@ -47,7 +43,7 @@ Agent 步骤使用 [agent-loop 并发配置](../agent-loop/README.zh.md),循
执行器接收完整的原始调用批次,负责解析、调度、取消和可选的模型步骤日志。它复用共享调用流水线来执行策略、工具和结果通知。中止后,它等待已开始的工作完成,并为跳过的调用返回有序错误结果。
- [`src/index.ts`](src/index.ts) — 配置校验和作用域注册;工具描述和批次执行。
- [`src/index.ts`](src/index.ts) — 作用域执行器注册;工具描述和批次执行。
</details>
@@ -28,17 +28,12 @@
],
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cosmokit": "workspace:~",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-scope": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cosmokit": "workspace:~",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-scope": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*"
@@ -3,15 +3,13 @@ import type { Context } from '@deepseek-ai/cordis'
import { projectToolSchema } from '@deepseek-ai/dsh-tools'
import type { ToolDefinition, ToolExecutor, ToolModelInput, ToolBatchInput, ToolBatchResult } from '@deepseek-ai/dsh-tools'
import { ToolExecutionPipeline, executeToolBatch } from '@deepseek-ai/dsh-tools/pipeline'
import type { Volatile } from '@deepseek-ai/cosmokit'
import z from '@deepseek-ai/schemastery'
/** Executes visible capabilities directly and owns their complete batch settlement. */
export class NativeToolExecutor implements ToolExecutor {
private readonly pipeline: ToolExecutionPipeline
constructor(ctx: Context, private readonly maxParallel: () => number = () => 10) {
this.pipeline = new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) })
this.pipeline = new ToolExecutionPipeline(ctx)
}
describe(tools: readonly ToolDefinition[]): ToolModelInput {
@@ -28,22 +26,10 @@ export const name = 'native-tool-executor'
/** Tool registration and scoped dispatch. */
export const inject = ['tools']
/** Native batch concurrency configuration. */
export interface Config {
/** Maximum overlapping concurrency-safe calls in one batch. */
maxParallelToolCalls: Volatile<number>
}
/** Validated live concurrency limit; one makes each batch serial. */
export const Config: z<{ maxParallelToolCalls?: number }, Config> = z.object({
maxParallelToolCalls: z.number().step(1).min(1).default(10).volatile(),
})
/**
* Select native execution in the mounting scope.
* @param ctx - context owning this executor's selection and lifetime.
* @param config - live batch concurrency configuration.
*/
export function apply(ctx: Context, config: Config): void {
ctx.tools.setExecutor(new NativeToolExecutor(ctx, () => config.maxParallelToolCalls.get()))
export function apply(ctx: Context): void {
ctx.tools.setExecutor(new NativeToolExecutor(ctx))
}
@@ -9,17 +9,17 @@
en: e253ed9cec03a396
zh: 922fde282e2b0fd2
/deepseek-ai-dsh-tool-executor-ptc/summary:
en: 60fb16b4d443d265
zh: 6b0df6471c370c6b
en: 66e159c308afb6c1
zh: 29b1a646ae7f918a
/deepseek-ai-dsh-tool-executor-ptc/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-tool-executor-ptc/use-this-package:
en: 5cfc28690358a419
zh: d066d078ca649022
en: 488f394426b3aaa0
zh: a15185f91a1c342c
/deepseek-ai-dsh-tool-executor-ptc/understand-the-implementation:
en: f054dc0faabb468f
zh: 10f13508bfa9ef84
en: 9d4f61c351945d30
zh: 1c575526230b0e02
/deepseek-ai-dsh-tool-executor-ptc/understand-the-implementation/ptc-mode:
en: 11d32e05ba397f86
zh: 4e071ec4e6222d9d
+4 -5
View File
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Use this executor when models should call tools through `run_code` programs. Mount a compatible PTC runtime and tool registry first. Scoped restrictions determine the generated SDK; only `run_code` is directly callable. Configure root-call and program-subcall concurrency independently.
Use this executor when models should call tools through `run_code` programs. Mount a compatible PTC runtime and tool registry first. Scoped restrictions determine the generated SDK; only `run_code` is directly callable. Configure concurrency for parallel-safe program subcalls.
## Table of Contents
@@ -30,13 +30,12 @@ Add this row to a composition with `dsh-tools` mounted. The nearest scoped execu
```yaml
- name: '@deepseek-ai/dsh-tool-executor-ptc'
config:
maxParallelToolCalls: 10
maxParallelSubCalls: 10
```
For Agent steps, configure [agent-loop concurrency](../agent-loop/README.md); the loop always forwards its resolved limit to `execute()`. The executor's `maxParallelToolCalls` supplies the limit only when the caller omits it. This fallback is volatile, defaults to `10`, and is sampled for the next tool group. It must be a positive integer; `1` selects serial execution.
Root `run_code` calls are exclusive and execute serially. `maxParallelSubCalls` limits parallel-safe nested calls within each program and defaults to `10`; it must be a positive integer, with `1` selecting serial execution. Exclusive calls form barriers.
This package also requires `ctx.ptcRuntime` and `ctx.systemPrompt`; Agent-owned programs need `ctx.workingDirectory`. `maxParallelSubCalls` limits overlapping parallel-safe calls within each program and defaults to `10`.
This package also requires `ctx.ptcRuntime` and `ctx.systemPrompt`; Agent-owned programs need `ctx.workingDirectory`. A missing runtime rejects prompt assembly or execution; the selected PTC executor never falls back to native execution.
Import durable event payloads from `@deepseek-ai/dsh-tool-executor-ptc/types`; this entry is safe for detached readers and client compilation. Host policy extensions import `PtcDispatchLog`, `tools/ptc-dispatch-log`, and the `ptc-mode` message-source declaration through a type-only import of the package entry, without mounting an executor.
@@ -50,7 +49,7 @@ Import durable event payloads from `@deepseek-ai/dsh-tool-executor-ptc/types`; t
<details>
<summary>Implementation internals — click to expand</summary>
The executor receives the entire raw call batch and owns parsing, scheduling, cancellation, and optional model-step logging. It reuses the shared invocation pipeline for policy, tool bodies, and result notifications. After abort, it waits for started work and returns ordered error results for skipped calls.
The executor receives the entire raw call batch and owns parsing, scheduling, cancellation, and optional model-step logging. It reuses the shared invocation pipeline for policy, tool bodies, and result notifications. After abort, it waits for started work and returns ordered error results for skipped calls. Unloading the executor aborts active sandboxed programs and waits for their nested calls to settle. Accepted batches still settle through that executor, potentially with cancellation errors.
### PTC mode
+4 -5
View File
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
当模型应通过 `run_code` 程序调用工具时,使用此执行器。先挂载兼容的 PTC 运行时和工具注册表。作用域限制决定生成的 SDK;只有 `run_code` 能被直接调用。根调用和程序子调用的并发限制分别配置。
当模型应通过 `run_code` 程序调用工具时,使用此执行器。先挂载兼容的 PTC 运行时和工具注册表。作用域限制决定生成的 SDK;只有 `run_code` 能被直接调用。可配置并行安全的程序子调用并发上限。
## 目录
@@ -30,13 +30,12 @@ kind: "package-reference"
```yaml
- name: '@deepseek-ai/dsh-tool-executor-ptc'
config:
maxParallelToolCalls: 10
maxParallelSubCalls: 10
```
Agent 步骤使用 [agent-loop 并发配置](../agent-loop/README.zh.md),循环始终向 `execute()` 传入解析后的上限。只有调用方未传入上限时,才使用执行器的 `maxParallelToolCalls`。这个后备配置是 volatile 字段,默认值为 `10`,在下一个工具组开始时读取。值必须是正整数;`1` 表示串行执行。
根 `run_code` 调用是独占调用,按串行执行。`maxParallelSubCalls` 限制每个程序中并行安全的嵌套调用数,默认值为 `10`;值必须是正整数,`1` 表示串行执行。独占调用构成屏障。
本包还要求 `ctx.ptcRuntime` 和 `ctx.systemPrompt`;带 Agent 的程序要求 `ctx.workingDirectory`。`maxParallelSubCalls` 限制每个程序内重叠的并行安全调用,默认值为 `10`。
本包还要求 `ctx.ptcRuntime` 和 `ctx.systemPrompt`;带 Agent 的程序要求 `ctx.workingDirectory`。缺少运行时时,提示词组装或执行会失败;选中的 PTC 执行器不会回退到原生执行。
持久化事件载荷从 `@deepseek-ai/dsh-tool-executor-ptc/types` 导入;该入口可用于独立读取者与 Client 编译。Host 策略扩展通过包主入口的仅类型导入获取 `PtcDispatchLog`、`tools/ptc-dispatch-log` 与 `ptc-mode` 消息来源声明,无需挂载执行器。
@@ -50,7 +49,7 @@ Agent 步骤使用 [agent-loop 并发配置](../agent-loop/README.zh.md),循
<details>
<summary>实现细节——点击展开</summary>
执行器接收完整的原始调用批次,负责解析、调度、取消和可选的模型步骤日志。它复用共享调用流水线来执行策略、工具和结果通知。中止后,它等待已开始的工作完成,并为跳过的调用返回有序错误结果。
执行器接收完整的原始调用批次,负责解析、调度、取消和可选的模型步骤日志。它复用共享调用流水线来执行策略、工具和结果通知。中止后,它等待已开始的工作完成,并为跳过的调用返回有序错误结果。卸载执行器会中止运行中的沙箱程序,并等待其嵌套调用结束。已接纳批次仍由该执行器结算,但可能返回取消错误。
### PTC mode
@@ -33,7 +33,6 @@
],
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cosmokit": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-ptc-runtime": "workspace:*",
@@ -52,7 +51,6 @@
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cosmokit": "workspace:~",
"@deepseek-ai/dsh-agent": "workspace:*",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
@@ -0,0 +1,13 @@
/** Configuration shared by plugin mounting and direct executor construction. @module */
import z from '@deepseek-ai/schemastery'
/** Program execution limits. */
export interface Config {
/** Maximum overlapping concurrency-safe program subcalls. */
maxParallelSubCalls: number
}
/** Validated program subcall concurrency; one makes subcalls serial. */
export const Config: z<Partial<Config>, Config> = z.object({
maxParallelSubCalls: z.natural().min(1).default(10),
})
+9 -30
View File
@@ -1,14 +1,12 @@
/** PTC execution plugin for the mounting Agent, preset, or deployment scope. @module */
import type { Context } from '@deepseek-ai/cordis'
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
import { RUN_CODE_NAME, ToolNotFoundError, projectToolSchema } from '@deepseek-ai/dsh-tools'
import type { ToolDefinition, ToolExecutor, ToolModelInput, ToolPromptSection, ToolBatchInput, ToolBatchResult } from '@deepseek-ai/dsh-tools'
import { ToolExecutionPipeline, executeToolBatch } from '@deepseek-ai/dsh-tools/pipeline'
import { createRunCodeTool, requirePtcRuntime, type PtcSdkLanguage } from './ptc.ts'
import { renderToolsSdk, type ToolSdkSchema } from './ts-types.ts'
import { renderToolsSdkPy } from './py-types.ts'
import type { Volatile } from '@deepseek-ai/cosmokit'
import z from '@deepseek-ai/schemastery'
import { Config } from './config.ts'
const PTC_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`
@@ -24,17 +22,15 @@ export class PtcExecutor implements ToolExecutor {
private readonly maxParallelSubCalls: number
private ptcTransport: ToolDefinition | undefined
constructor(private readonly ctx: Context, config: Pick<Config, 'maxParallelSubCalls'> = {}, private readonly maxParallel: () => number = () => 10) {
constructor(private readonly ctx: Context, config: Partial<Config> = {}, private readonly maxParallel: () => number = () => 10) {
this.maxParallelSubCalls = Config(config).maxParallelSubCalls
const tools = ctx.tools
this.sections = [
{ name: 'tools:ptc-only', order: ctx.systemPrompt.getSectionOrder('PTC_ONLY'), text: PTC_ONLY_INSTRUCTION, interpolate: false },
{ name: 'tools:sdk', order: ctx.systemPrompt.getSectionOrder('TOOLS_SDK'), text: tools => this.renderSdk(tools), interpolate: false },
]
this.maxParallelSubCalls = config.maxParallelSubCalls ?? 10
if (!Number.isInteger(this.maxParallelSubCalls) || this.maxParallelSubCalls < 1) {
throw new Error('maxParallelSubCalls must be a positive integer')
}
this.pipeline = new ToolExecutionPipeline(ctx, {
get: (name, scope) => this.get(name, scope),
get: (name, scope) => name === RUN_CODE_NAME ? this.requirePtcTransport() : tools.get(name, scope),
reject: (name, nested) => !nested && name !== RUN_CODE_NAME
? new ToolNotFoundError(name, `only \`${RUN_CODE_NAME}\` is callable directly — call \`${name}\` from inside a \`${RUN_CODE_NAME}\` program instead`)
: undefined,
@@ -54,10 +50,6 @@ export class PtcExecutor implements ToolExecutor {
})), runtime.programFormat)
}
private get(name: string, scope?: ScopeKey): ToolDefinition | undefined {
return name === RUN_CODE_NAME ? this.requirePtcTransport() : this.ctx.tools.get(name, scope)
}
execute(input: ToolBatchInput): Promise<ToolBatchResult> {
return executeToolBatch(input, this.pipeline, this.maxParallel)
}
@@ -69,6 +61,7 @@ export class PtcExecutor implements ToolExecutor {
}
}
export { Config } from './config.ts'
export { CodeRunFailedError } from './errors.ts'
export type { PtcDispatchLog } from './types.ts'
export type { PtcDispatchStartEventData, PtcDispatchEventData } from './session-types.ts'
@@ -77,22 +70,8 @@ export { jsonSchemaToPy, renderToolsSdkPy } from './py-types.ts'
/** Cordis plugin name. */
export const name = 'ptc-tool-executor'
/** The program provider activates with its runtime. */
export const inject = ['tools', 'systemPrompt', 'ptcRuntime']
/** Program execution limits. */
export interface Config {
/** Maximum overlapping concurrency-safe calls in one model batch. */
maxParallelToolCalls: Volatile<number>
/** Maximum overlapping concurrency-safe program subcalls. */
maxParallelSubCalls?: number
}
/** Validated program execution configuration. */
export const Config: z<{ maxParallelToolCalls?: number; maxParallelSubCalls?: number }, Config> = z.object({
maxParallelToolCalls: z.number().step(1).min(1).default(10).volatile(),
maxParallelSubCalls: z.natural().min(1).default(10),
})
/** Register the selected executor even when its runtime is unavailable. */
export const inject = ['tools', 'systemPrompt']
/**
* Select PTC execution in the mounting scope.
@@ -100,5 +79,5 @@ export const Config: z<{ maxParallelToolCalls?: number; maxParallelSubCalls?: nu
* @param config - program subcall concurrency limits.
*/
export function apply(ctx: Context, config: Config): void {
ctx.tools.setExecutor(new PtcExecutor(ctx, config, () => config.maxParallelToolCalls.get()))
ctx.tools.setExecutor(new PtcExecutor(ctx, config))
}
@@ -15,8 +15,8 @@ export class PtcCalls {
private disposed = false
/**
* Observe policy changes for the owning tool registry.
* @param ctx - Registry lifecycle and Session events.
* Observe policy changes for the owning executor.
* @param ctx - Executor lifecycle and Session events.
*/
constructor(ctx: Context) {
ctx.on('session/event', (session, event) => {
@@ -28,7 +28,7 @@ export class PtcCalls {
ctx.effect(() => async () => {
this.disposed = true
const calls = [...this.calls.values()].flatMap(calls => [...calls])
for (const call of calls) call.controller.abort(new Error('PTC tool registry disposed'))
for (const call of calls) call.controller.abort(new Error('PTC tool executor disposed'))
await Promise.all(calls.map(call => call.done))
}, 'PTC calls')
}
@@ -48,7 +48,7 @@ export class PtcCalls {
await Promise.all(pending.map(call => call.done))
}
signal.throwIfAborted()
if (this.disposed) throw new Error('PTC tool registry disposed')
if (this.disposed) throw new Error('PTC tool executor disposed')
const controller = new AbortController()
const done = Promise.withResolvers<undefined>()
const call = { mode: mode(), controller, done: done.promise }
+21 -18
View File
@@ -19,7 +19,7 @@ import type {} from '@deepseek-ai/dsh-user-approval'
import type {} from '@deepseek-ai/dsh-working-directory'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import { deepFreeze, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
import { defineTool, parameterSchemaSpecToJsonSchema } from '@deepseek-ai/dsh-tools'
import { defineTool, parameterSchemaSpecToJsonSchema, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import type { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import { toolErrorMessage } from '@deepseek-ai/dsh-tools/pipeline'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
@@ -29,8 +29,7 @@ import type { ToolDefinition, ToolExecutionResult, ToolRunContext } from '@deeps
import type {} from './session-types.ts'
/** The model-facing name of the PTC mode tool. */
export { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
export { RUN_CODE_NAME }
/** Language-specific tool and code-parameter descriptions. */
interface RunCodeFlavor {
@@ -264,19 +263,6 @@ function resolveSandboxPolicy(ctx: Context, exec: ToolRunContext): SandboxExecut
return policy.resolve(exec.agent === undefined ? {} : { session: exec.agent.session })
}
/** A failed log-content listener preserves the settled content and cannot fail the dispatch. */
async function shapeDispatchLog(ctx: Context, dispatch: PtcDispatchLog): Promise<ContentBlock[]> {
try {
return await ctx.waterfall(
scopeTarget(ctx.tools, dispatch.agent), 'tools/ptc-dispatch-log', dispatch,
() => Promise.resolve(dispatch.content),
)
} catch (error: unknown) {
ctx.logger.warn(`tools: ptc-dispatch-log listener failed for ${dispatch.name}: ${toolErrorMessage(error)}; logging the original settled content`)
return dispatch.content
}
}
/**
* Build the reserved `run_code` transport outside registered capabilities.
* @param ctx - executor context owning policy listeners and active program cleanup.
@@ -289,6 +275,23 @@ export function createRunCodeTool(
executor: ToolExecutionPipeline,
maxParallel: number,
): ToolDefinition {
// Retain services for log settlement after the executor context becomes inactive.
const tools = ctx.tools
const logger = ctx.logger
/** A failed log-content listener preserves the settled content and cannot fail the dispatch. */
async function shapeDispatchLog(dispatch: PtcDispatchLog): Promise<ContentBlock[]> {
try {
return await ctx.waterfall(
scopeTarget(tools, dispatch.agent), 'tools/ptc-dispatch-log', dispatch,
() => Promise.resolve(dispatch.content),
)
} catch (error: unknown) {
logger.warn(`tools: ptc-dispatch-log listener failed for ${dispatch.name}: ${toolErrorMessage(error)}; logging the original settled content`)
return dispatch.content
}
}
const stateOwners = new WeakMap<object, WeakSet<PtcRuntime>>()
const calls = new PtcCalls(ctx)
const definition = defineTool({
@@ -536,7 +539,7 @@ export function createRunCodeTool(
// The listener may replace the durable copy with a preview and
// locator; the program's value and model-visible result are
// untouched.
const logged = await shapeDispatchLog(ctx, {
const logged = await shapeDispatchLog({
exec, agent, subCallId, name, isError: result.isError,
// The registry deep-froze this projection at result
// finalization; append snapshots the final copy again, so
@@ -645,7 +648,7 @@ export function createRunCodeTool(
// restricted globals vanish) — the same view the SDK section declared,
// so a program can bind exactly what its prompt promised; sub-dispatch
// re-resolves per call through the same view (exec.agent threads down).
for (const schema of ctx.tools.schemas(exec.agent)) {
for (const schema of tools.schemas(exec.agent)) {
Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(deepFreeze(schema)) })
}
@@ -5,12 +5,12 @@
* store, keyed by the loaded {@link @deepseek-ai/dsh-ptc-runtime#PtcRuntime.language | PTC
* runtime's language}.
*
* Under `mode: 'ptc'` the native tool schemas are omitted from the request, so this generated
* SDK is the model's ONLY source for each tool's argument names, required fields, types,
* With the PTC executor selected, native tool schemas are omitted from the request. This generated
* SDK is the model's only source for each tool's argument names, required fields, types,
* descriptions, and canonical output types. Object-shaped arguments and outputs render as one
* named `TypedDict` per tool (and per nested object), not an opaque `dict[str, Any]`, so the
* shape survives into the program under the mode that has nothing else to carry it.
* @module @deepseek-ai/dsh-tools/src/py-types
* @module
*/
import { assertSupportedJsonSchema } from '@deepseek-ai/dsh-tools'
@@ -31,7 +31,7 @@ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u
* Python identifiers are not ASCII: `路径` is as legal a field name as `path`,
* and rejecting it would degrade the whole enclosing object, dropping every
* field's name, requiredness, and type — information whose only source under
* `mode: 'ptc'` is this generated text.
* PTC execution is this generated text.
*
* NFKC stability is a second and separate condition, because CPython
* normalizes identifiers at compile time while JSON keys are compared as
@@ -162,7 +162,7 @@ interface RenderState {
* (`SyntaxError: source code string cannot contain null bytes`), whether it
* sits in a docstring or in a comment, so one such byte anywhere in a schema
* description would make the whole generated SDK unparseable — under
* `mode: 'ptc'`, the model's only declaration of the tools. The rest are
* PTC execution, the model's only declaration of the tools. The rest are
* legal but invisible; escaping them with the same rule keeps the emitted text
* readable and the treatment uniform.
*
@@ -578,7 +578,7 @@ function renderType(schema: unknown, className: string, state: RenderState): str
}
}
// TypedDict syntax cannot express openness, so an open object states it
// in-band: the annotation is advisory either way, and `mode: 'ptc'`
// in-band: the annotation is advisory either way, and PTC execution
// omits the native schemas, making this line the model's only signal
// that extra keys are accepted.
if (node.additionalProperties !== false) {
@@ -777,8 +777,8 @@ export function renderToolsSdkPy(schemas: readonly ToolSdkSchema[]): string {
// of that method's body. Emitted before the `async def` it would instead
// become the `Tools` class docstring (for the first tool) or a dead
// expression (for every later one), leaving every method undocumented —
// and under `mode: 'ptc'` this SDK is the model's only description of
// what a tool does. A docstring is a complete body, so the `...` stub is
// and with PTC selected, this SDK is the model's only description of what
// a tool does. A docstring is a complete body, so the `...` stub is
// only for the description-less case.
const doc = docLines(schema.description, 2)
members.push(doc.length > 0
@@ -67,7 +67,7 @@ it('honors cancellation while waiting for a canceled predecessor', async () => {
expect(next).not.toHaveBeenCalled()
})
it('unloads the registry only after calls stop and refuses late admission', async () => {
it('unloads the executor only after calls stop and refuses late admission', async () => {
const { ctx, session, calls, mode, signal } = await setup()
const cleanup = Promise.withResolvers<undefined>()
const stopped = Promise.withResolvers<undefined>()
@@ -79,11 +79,11 @@ it('unloads the registry only after calls stop and refuses late admission', asyn
try {
await stopped.promise
const next = calls.run(session.id, signal, mode, async () => 'unexpected')
const rejected = expect(next).rejects.toThrow('PTC tool registry disposed')
const rejected = expect(next).rejects.toThrow('PTC tool executor disposed')
cleanup.resolve(undefined)
await first
await rejected
await disposed
await expect(calls.run(session.id, signal, mode, async () => 'unexpected')).rejects.toThrow('PTC tool registry disposed')
await expect(calls.run(session.id, signal, mode, async () => 'unexpected')).rejects.toThrow('PTC tool executor disposed')
} finally { cleanup.resolve(undefined); await first; await disposed }
})
@@ -83,7 +83,7 @@ async function setup(options: SetupOptions = {}) {
runtime = ctx.ptcRuntime as FakeRuntime
}
return { ctx, tools: {
get: (name: string, scope?: ScopeKey) => executor instanceof PtcExecutor ? executor['get'](name, scope) : ctx.tools.get(name, scope),
get: (name: string, scope?: ScopeKey) => executor instanceof PtcExecutor && name === RUN_CODE_NAME ? executor['requirePtcTransport']() : ctx.tools.get(name, scope),
execute: (input: ToolExecutionInput) => executeCall(ctx, input),
}, systemPrompt: ctx.systemPrompt, runtime: runtime! }
}
@@ -490,7 +490,7 @@ describe('mode-aware wire contribution', () => {
it.each(['typescript', 'python'])('lists code before description for a %s runtime', async (language) => {
const { ctx, systemPrompt } = await setup({ mode: 'ptc', runtime: { language } })
try {
const parameters = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)!.parameters
const parameters = new PtcExecutor(ctx)['requirePtcTransport']().parameters
expect(Object.keys(parameters.properties as Record<string, unknown>).slice(0, 2))
.toEqual(['code', 'description'])
expect(parameters).toHaveProperty('required', ['code', 'description'])
@@ -558,17 +558,17 @@ describe('mode-aware wire contribution', () => {
it.each(['ruby', 'constructor'])('rejects transport schema reads for unsupported runtime language %s', async (language) => {
const { ctx } = await setup({ mode: 'ptc', runtime: { language } })
const definition = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)
const definition = new PtcExecutor(ctx)['requirePtcTransport']()
const message = `no SDK renderer registered for runtime language "${language}" (known: "typescript", "python")`
expect(() => definition?.description).toThrow(message)
expect(() => definition?.parameters).toThrow(message)
expect(() => definition.description).toThrow(message)
expect(() => definition.parameters).toThrow(message)
})
it('rejects transport schema reads when no runtime is mounted', async () => {
const { ctx } = await setup({ mode: 'ptc', runtime: false })
const definition = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)
expect(() => definition?.description).toThrow('requires a PTC runtime')
expect(() => definition?.parameters).toThrow('requires a PTC runtime')
const definition = new PtcExecutor(ctx)['requirePtcTransport']()
expect(() => definition.description).toThrow('requires a PTC runtime')
expect(() => definition.parameters).toThrow('requires a PTC runtime')
})
it("rejects the assembly when toolOrder names a native tool that mode 'ptc' no longer contributes", async () => {
@@ -584,7 +584,7 @@ describe('mode-aware wire contribution', () => {
await ctx.plugin(FakeRuntime, {})
const fiber = await ctx.plugin(ToolRuntime)
await ctx.plugin(PtcToolExecutor)
expect(new PtcExecutor(ctx)['get'](RUN_CODE_NAME)).toBeDefined()
expect(new PtcExecutor(ctx)['requirePtcTransport']()).toBeDefined()
await fiber.dispose()
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.tools).toEqual([])
@@ -718,14 +718,8 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
const gated = registerGated(ctx, 'safe_read', true)
runtime.behavior = async (request) => {
const tools = request.bindings[0]!.functions
const all = Promise.all([
tools.safe_read!({ id: 'a' }),
tools.safe_read!({ id: 'b' }),
tools.safe_read!({ id: 'c' }),
])
const all = Promise.all(Array.from({ length: 3 }, (_, i) => tools.safe_read!({ id: String(i) })))
await expect.poll(() => gated.pending()).toBe(2)
// The third call waits for a slot.
expect(gated.pending()).toBe(2)
gated.release()
await expect.poll(() => gated.pending()).toBe(2)
gated.releaseAll()
@@ -733,7 +727,6 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
return { logs: [], value: 'capped' }
}
const result = await runCode(ctx, 'program')
if (result.isError) console.error('CAP-FAIL:', (result.content[0] as { text: string }).text)
expect(result.isError).toBe(false)
expect(gated.peakLive()).toBe(2)
})
@@ -1659,7 +1652,7 @@ describe('the run_code dispatch bridge', () => {
it('presents the model-authored description as the execute-card title over the program input', async () => {
const { ctx } = await setup({ mode: 'ptc' })
const tool = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)!
const tool = new PtcExecutor(ctx)['requirePtcTransport']()
// The description labels the card (the bash description precedent); the
// program itself remains the expanded raw input.
expect(tool.presentCall?.({ code: 'return 1', description: 'Return the constant one' })).toEqual({
@@ -1687,7 +1680,7 @@ describe('the run_code dispatch bridge', () => {
runtime.behavior = () => Promise.resolve(output)
const result = await runCode(ctx, 'return 1')
const tool = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)!
const tool = new PtcExecutor(ctx)['requirePtcTransport']()
expect(result.content).toEqual([{ type: 'text', text }])
// Presenters keep the pending program title and render this durable content
@@ -1706,7 +1699,7 @@ describe('the run_code dispatch bridge', () => {
})
const result = await runCode(ctx, 'return 1')
const tool = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)!
const tool = new PtcExecutor(ctx)['requirePtcTransport']()
expect(result.content).toEqual([{ type: 'text', text: preview }])
expect('presentResult' in tool).toBe(false)
@@ -1720,7 +1713,7 @@ describe('the run_code dispatch bridge', () => {
})
const result = await runCode(ctx, 'return 1')
const tool = new PtcExecutor(ctx)['get'](RUN_CODE_NAME)!
const tool = new PtcExecutor(ctx)['requirePtcTransport']()
expect(result.isError).toBe(true)
expect(result.content).toEqual([{
@@ -2000,21 +1993,21 @@ describe('the run_code dispatch bridge', () => {
expect(derived[0]?.role).toBe('user')
})
it('direct construction rejects a non-positive parallel sub-call cap at load', async () => {
it('direct construction rejects a non-positive parallel subcall limit', async () => {
const ctx = new Context()
provideWorkingDirectoryFixture(ctx)
await ctx.plugin(SystemPrompt, {})
expect(() => new PtcExecutor(ctx, { maxParallelSubCalls: 0 }))
.toThrow('maxParallelSubCalls must be a positive integer')
try {
await ctx.plugin(SystemPrompt)
expect(() => new PtcExecutor(ctx, { maxParallelSubCalls: 0 })).toThrow('maxParallelSubCalls')
} finally { await ctx.fiber.dispose() }
})
it('direct construction in PTC mode defaults the parallel sub-call cap', async () => {
it('direct construction exposes the PTC transport without configuration', async () => {
const ctx = new Context()
provideWorkingDirectoryFixture(ctx)
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRuntime)
const registry = new PtcExecutor(ctx)
expect(registry['get'](RUN_CODE_NAME)).toBeDefined()
expect(registry['requirePtcTransport']()).toBeDefined()
})
it('defaults to native mode under direct construction with no config', async () => {
@@ -2542,3 +2535,47 @@ describe('PTC state ownership', () => {
} finally { format.mockRestore(); instructions.mockRestore(); await ctx.fiber.dispose() }
})
})
it.each([false, true])('settles nested calls after the PTC executor fiber unloads (Agent: %s)', async (owned) => {
const ctx = new Context()
provideWorkingDirectoryFixture(ctx)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(FakeRuntime)
const provider = ctx.plugin(PtcToolExecutor)
await provider
ctx.tools.register(defineTool({
name: 'echo', description: 'Retained capability.', parameters: {},
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
execute: async () => 'settled',
}))
const entered = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
const runtime = ctx.ptcRuntime as FakeRuntime
runtime.behavior = async (request) => {
entered.resolve(undefined)
await release.promise
return { logs: [], value: await request.bindings[0]!.functions.echo!({}) }
}
const agent = { ctx, id: SessionId('unloading-ptc'), session: Session.create(SessionId('unloading-ptc')) } as Agent
const input = {
calls: [{ type: 'tool-call' as const, id: ToolCallId('unloading-program'), name: RUN_CODE_NAME,
arguments: JSON.stringify({ code: 'return await tools.echo({})', description: 'Invoke retained tool after unload' }) }],
signal: new AbortController().signal,
...owned ? { agent } : {},
}
const pending = ctx.tools.execute(input)
try {
await entered.promise
await provider.dispose()
await expect(ctx.tools.execute(input)).rejects.toThrow('selected tool executor is unavailable')
release.resolve(undefined)
expect((await pending).results[0]?.result).toMatchObject({ isError: false, value: { logs: [], result: 'settled' } })
if (owned) expect(agent.session.snapshotEvents().filter(event => event.type === 'tool/ptc-dispatch'))
.toMatchObject([{ data: { name: 'echo', isError: false, content: [{ type: 'text', text: 'settled' }] } }])
} finally {
release.resolve(undefined)
await Promise.allSettled([pending])
await ctx.fiber.dispose()
}
})
+2 -2
View File
@@ -21,8 +21,8 @@
en: 9d2e6caa39c53d62
zh: 096a777f828c7565
/deepseek-ai-dsh-tools/use-this-package/select-an-executor:
en: f3efeb3d996cb9c5
zh: 442cf4ca6d68fea0
en: 1ddfa584f9280cb7
zh: b0899fcb8163569e
/deepseek-ai-dsh-tools/use-this-package/restrict-tools-per-agent:
en: c6f98b2283cc2e47
zh: c2cdce5d13473f15
+1 -1
View File
@@ -72,7 +72,7 @@ Mount one executor in the deployment, preset, or Agent scope. The nearest scope
- name: '@deepseek-ai/dsh-tool-executor-native'
```
Agent-step concurrency uses the [agent-loop configuration](../agent-loop/README.md). See the [native](../tool-executor-native/README.md) or [PTC](../tool-executor-ptc/README.md) package for executor settings. Custom providers implement only `describe(definitions)` and `execute(batch)`; the shared pipeline is optional. The registry selects the current scoped executor separately for prompt descriptions and each `ctx.tools.execute()` batch. An accepted batch finishes through its selected provider even if that provider unloads; new batches reject until a replacement is registered.
Agent-step concurrency uses the [agent-loop configuration](../agent-loop/README.md). See the [native](../tool-executor-native/README.md) or [PTC](../tool-executor-ptc/README.md) package for executor settings. Custom providers implement only `describe(definitions)` and `execute(batch)`; the shared pipeline is optional. The registry selects the current scoped executor separately for prompt descriptions and each `ctx.tools.execute()` batch. An accepted batch settles through its selected provider even if that provider unloads; settlement may include cancellation errors according to the provider’s disposal policy. New batches reject until a replacement is registered.
### Restrict tools per agent
+1 -1
View File
@@ -72,7 +72,7 @@ ctx.tools.register(defineTool({
- name: '@deepseek-ai/dsh-tool-executor-native'
```
Agent 步骤的并发上限使用 [agent-loop 配置](../agent-loop/README.zh.md)。执行器设置参见[原生](../tool-executor-native/README.zh.md)或 [PTC](../tool-executor-ptc/README.zh.md) 包。自定义提供者只需实现 `describe(definitions)` 和 `execute(batch)`,共享流水线为可选项。注册表分别为提示词描述和每次 `ctx.tools.execute()` 批次选择当前作用域的执行器。已接纳的批次由所选提供者完成,即使该提供者已卸载;注册替代提供者之前,新批次会被拒绝。
Agent 步骤的并发上限使用 [agent-loop 配置](../agent-loop/README.zh.md)。执行器设置参见[原生](../tool-executor-native/README.zh.md)或 [PTC](../tool-executor-ptc/README.zh.md) 包。自定义提供者只需实现 `describe(definitions)` 和 `execute(batch)`,共享流水线为可选项。注册表分别为提示词描述和每次 `ctx.tools.execute()` 批次选择当前作用域的执行器。已接纳批次仍由所选提供者结算,即使该提供者已卸载;根据提供者的卸载策略,结果可能包含取消错误。注册替代提供者之前,新批次会被拒绝。
### 按 agent 限制工具
+1 -1
View File
@@ -142,7 +142,7 @@ export interface ToolExecutionInput {
* mode sets this on SDK sub-dispatches so commit-style observers can wait for
* the outer `run_code` outcome without receiving its live mutable execution.
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'ptc'`, only calls WITH a parent may
* model-direct call: with the PTC executor selected, 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.
*/
+6
View File
@@ -1,5 +1,6 @@
/** Tool definitions and scoped dispatch to plugin-owned executors. @module */
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { AnonymousEntries, NamedEntries, ScopedLayers, scopeOf, scopeChainOf } from '@deepseek-ai/dsh-scope'
import type { ScopeKey, ScopeLayer } from '@deepseek-ai/dsh-scope'
import { ToolExecutorUnavailableError } from './executor.ts'
@@ -79,6 +80,10 @@ class GuardLayer {
/** Concrete tool registry and scoped executor dispatcher. */
export class ToolsService extends Service {
static inject = ['systemPrompt']
static Config = z.object({
mode: z.never().description('Removed. Mount a native or PTC tool executor instead.'),
maxParallelSubCalls: z.never().description('Moved to the PTC tool executor configuration.'),
})
private readonly layers = new ScopedLayers(scope => new ToolLayer(scope), () => { this.ctx.emit('tools/change') })
private readonly scoped = new WeakMap<ScopeKey, Registration>()
@@ -231,6 +236,7 @@ export class ToolsService extends Service {
}
/**
* Provide the executor for the calling scope; an unscoped contribution is the deployment default.
* Rendering any executor-owned section requires the viewing scope's selection to remain available.
* @param executor - initialized provider owned by the contributing plugin.
* @returns the effect disposer. Unloading leaves the selection unavailable until it is replaced.
*/
+11 -6
View File
@@ -9,12 +9,13 @@ 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 { validateJsonSchemaValue } from './json-schema.ts'
import type { ToolsService } from './index.ts'
import type { ToolDefinition, ToolExecutionInput, ToolExecutionMode, ToolExecution, ToolRunContext, ToolExecutionResult, ToolExecutionSuccess, ToolErrorInfo, ToolExecutionToken, PreToolDecision, PostToolDecision } from './contracts.ts'
/** Executor-owned lookup and restrictions applied before extensible execution policy. */
export interface ToolExecutionSource {
/** Read the live definition visible to the calling scope. */
get(name: string, scope?: ScopeKey): ToolDefinition | undefined
/** Override registry lookup for executor-owned capabilities; omitted uses the retained registry. */
get?(name: string, scope?: ScopeKey): ToolDefinition | undefined
/** Reject an otherwise visible invocation before policy; omitted permits it. */
reject?(name: string, nested: boolean): Error | undefined
}
@@ -56,15 +57,19 @@ interface FusedToolSignal {
* for complete calls, including a single-call batch.
*/
export class ToolExecutionPipeline {
private readonly tools: Context['tools']
private readonly tools: ToolsService
constructor(private readonly ctx: Context, private readonly source: ToolExecutionSource) {
constructor(private readonly ctx: Context, private readonly source: ToolExecutionSource = {}) {
// Accepted calls retain their event subject while the service is withdrawn during shutdown.
this.tools = ctx.tools
}
private get(name: string, scope?: ScopeKey): ToolDefinition | undefined {
return this.source.get === undefined ? this.tools.get(name, scope) : this.source.get(name, scope)
}
private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined {
const tool = this.source.get(name, scope)
const tool = this.get(name, scope)
return tool !== undefined && this.source.reject?.(name, nested) === undefined ? tool : undefined
}
@@ -111,7 +116,7 @@ export class ToolExecutionPipeline {
const parent = exec.parent
const signal = exec.signal
// Executor refusals precede extensible policy; unknown tools retain dispatch-stage handling.
const visible = this.source.get(name, agent)
const visible = this.get(name, agent)
const rejection = visible === undefined ? undefined : this.source.reject?.(name, parent !== undefined)
const rejected = rejection !== undefined
const concludingExecutions = this.concludingExecutions
@@ -1,5 +1,5 @@
import { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
/** Covers fail-closed per-call classification and model-schema isolation. */
import { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import { describe, expect, expectTypeOf, it } from 'vitest'
@@ -33,7 +33,7 @@ describe('ToolExecutionPipeline.executionMode', () => {
isConcurrencySafe: () => true,
async execute() { return [] },
}))
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('safe', {}))).toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('safe', {}))).toEqual({ kind: 'parallel' })
})
it('defaults to exclusive for a tool with no isConcurrencySafe declaration', async () => {
@@ -44,12 +44,12 @@ describe('ToolExecutionPipeline.executionMode', () => {
parameters: {},
async execute() { return [] },
}))
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('plain', {}))).toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('plain', {}))).toEqual({ kind: 'exclusive' })
})
it('returns exclusive for an unknown tool', async () => {
const ctx = await setup()
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('nonexistent', {}))).toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('nonexistent', {}))).toEqual({ kind: 'exclusive' })
})
it('returns exclusive when the classifier returns false for these args', async () => {
@@ -61,8 +61,8 @@ describe('ToolExecutionPipeline.executionMode', () => {
isConcurrencySafe: args => args.mode === 'read',
async execute() { return [] },
}))
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('rw', { mode: 'read' }))).toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('rw', { mode: 'write' }))).toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('rw', { mode: 'read' }))).toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('rw', { mode: 'write' }))).toEqual({ kind: 'exclusive' })
})
it('classifies invalid defineContentToolFixture arguments as exclusive without throwing', async () => {
@@ -74,7 +74,7 @@ describe('ToolExecutionPipeline.executionMode', () => {
isConcurrencySafe: () => true,
async execute() { return [] },
}))
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('needs-mode', {}))).toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('needs-mode', {}))).toEqual({ kind: 'exclusive' })
})
it('treats a throwing raw classifier as exclusive', async () => {
@@ -88,7 +88,7 @@ describe('ToolExecutionPipeline.executionMode', () => {
async execute() { return null },
}
ctx.tools.register(raw)
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('thrower', {}))).toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('thrower', {}))).toEqual({ kind: 'exclusive' })
})
it('treats a truthy non-boolean raw result as exclusive', async () => {
@@ -102,7 +102,7 @@ describe('ToolExecutionPipeline.executionMode', () => {
async execute() { return null },
} as unknown as ToolDefinition
ctx.tools.register(raw)
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('truthy', {}))).toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('truthy', {}))).toEqual({ kind: 'exclusive' })
})
it('passes parsed arguments directly to a raw definition', async () => {
@@ -116,7 +116,7 @@ describe('ToolExecutionPipeline.executionMode', () => {
isConcurrencySafe(args) { seen = args; return true },
async execute() { return null },
})
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode(exec('raw-safe', { anything: 1 }))).toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx).executionMode(exec('raw-safe', { anything: 1 }))).toEqual({ kind: 'parallel' })
expect(seen).toEqual({ anything: 1 })
})
+42
View File
@@ -52,6 +52,48 @@ const echoTool = defineTool({
})
describe('ToolRuntime', () => {
it.each([{ mode: 'ptc' }, { maxParallelSubCalls: 4 }])('rejects removed execution configuration %j during plugin mounting', async (config) => {
const ctx = new Context()
try {
await ctx.plugin(SystemPrompt)
// @ts-expect-error Removed fields can still arrive from untyped configuration files.
await expect(ctx.plugin(ToolRuntime, config).await()).rejects.toThrow('invalid config')
} finally { await ctx.fiber.dispose() }
})
it('settles accepted calls after the actual native executor fiber unloads', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
const provider = ctx.plugin(NativeToolExecutor)
await provider
ctx.tools.register(echoTool)
const entered = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
ctx.on('tools/pre-execute', async (exec, next) => {
if (exec.callId === 'first') { entered.resolve(undefined); await release.promise }
return next()
})
const input = {
calls: ['first', 'second'].map(id => ({ type: 'tool-call' as const, id: ToolCallId(id), name: 'echo', arguments: JSON.stringify({ text: id }) })),
signal: new AbortController().signal,
}
const pending = ctx.tools.execute(input)
try {
await entered.promise
await provider.dispose()
await expect(ctx.tools.execute(input)).rejects.toThrow('selected tool executor is unavailable')
release.resolve(undefined)
expect((await pending).results.map(({ result }) => result)).toMatchObject([
{ isError: false, value: 'first' }, { isError: false, value: 'second' },
])
} finally {
release.resolve(undefined)
await Promise.allSettled([pending])
await ctx.fiber.dispose()
}
})
it('preserves deferred loading through registry and prompt schema projection', async () => {
const ctx = await setup()
try {
@@ -2,4 +2,6 @@
config:
executionMode: persistent-in-memory
- id: tool-executor-native
name: '@deepseek-ai/dsh-tool-executor-ptc'
disabled: true
- id: tool-executor-ptc
disabled: false
@@ -3489,7 +3489,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'setExecutor(executor: ToolExecutor): () => void',
description: 'Provide the executor for the calling scope; an unscoped contribution is the deployment default.',
description: 'Provide the executor for the calling scope; an unscoped contribution is the deployment default. Rendering any executor-owned section requires the viewing scope\'s selection to remain available.',
parameters: [{ name: 'executor', description: 'initialized provider owned by the contributing plugin.' }],
returns: 'the effect disposer. Unloading leaves the selection unavailable until it is replaced.',
},
@@ -8394,7 +8394,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ToolsService',
declaration: 'export class ToolsService extends Service {\n static inject;\n constructor(ctx: Context);\n register(definition: ToolDefinition): () => void;\n restrict(filter: ToolRestriction): () => void;\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined;\n definitions(scope?: ScopeKey): readonly ToolDefinition[];\n schemas(scope?: ScopeKey): 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): () => void;\n restrict(filter: ToolRestriction): () => void;\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined;\n definitions(scope?: ScopeKey): readonly ToolDefinition[];\n schemas(scope?: ScopeKey): 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}',
},
{
name: 'ToolUpdate',
+3 -3
View File
@@ -1,5 +1,3 @@
import { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import { executeToolFixture, executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
/**
* The `read_image` tool over the REAL local filesystem and attachment store:
* extension routing, extension-less content sniffing (attachment object paths
@@ -7,6 +5,8 @@ import { executeToolFixture, executeModelToolFixture } from '@deepseek-ai/dsh-to
* commit + image-block rendering, attachment admission failures, and the
* regression that `read` keeps its text-only contract.
*/
import { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import { executeToolFixture, executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import * as PtcToolExecutor from '@deepseek-ai/dsh-tool-executor-ptc'
@@ -840,7 +840,7 @@ describe('registration surface', () => {
it('declares read_image parallel-safe and presents a read-family card', async () => {
const ctx = await setup()
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({
expect(new ToolExecutionPipeline(ctx).executionMode({
signal: testToolSignal, callId: ToolCallId('img-parallel'), name: 'read_image', arguments: { file_path: 'a.png' },
})).toEqual({ kind: 'parallel' })
expect(ctx.tools.get('read_image')?.presentCall?.({ file_path: 'shot.png' })).toEqual({
+6 -6
View File
@@ -1,9 +1,9 @@
import { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
/**
* Consumer API tests over a fake provider and the real policy collaborator: schemas,
* validation, formatting, typed errors, intent dispatch, and observation-driven authorization.
*/
import { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline'
import { executeToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import * as PtcToolExecutor from '@deepseek-ai/dsh-tool-executor-ptc'
@@ -198,11 +198,11 @@ describe('registration', () => {
it('declares read parallel-safe while write/edit remain exclusive', async () => {
const { ctx } = await setup()
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({ signal: testToolSignal, callId: ToolCallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } }))
expect(new ToolExecutionPipeline(ctx).executionMode({ signal: testToolSignal, callId: ToolCallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } }))
.toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({ signal: testToolSignal, callId: ToolCallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } }))
expect(new ToolExecutionPipeline(ctx).executionMode({ signal: testToolSignal, callId: ToolCallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } }))
.toEqual({ kind: 'exclusive' })
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({ signal: testToolSignal, callId: ToolCallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } }))
expect(new ToolExecutionPipeline(ctx).executionMode({ signal: testToolSignal, callId: ToolCallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } }))
.toEqual({ kind: 'exclusive' })
})
@@ -1149,7 +1149,7 @@ describe('scope-aware PTC guidance', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(GuidancePtcRuntime)
await ctx.plugin(ToolRuntime)
await ctx.plugin((mode) === 'ptc' ? PtcToolExecutor : NativeToolExecutor)
await ctx.plugin(mode === 'ptc' ? PtcToolExecutor : NativeToolExecutor)
await ctx.plugin(FakeFs)
await ctx.plugin(ToolFs)
const { key, scope } = await guidanceScope(ctx)
@@ -157,7 +157,7 @@ describe('goal tool registration and presentation', () => {
const { ctx, fiber } = await harness({ blockedAfterConsecutiveRounds: 5 })
expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name))
.toEqual(['create_goal', 'get_goal', 'update_goal'])
const pipeline = new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) })
const pipeline = new ToolExecutionPipeline(ctx)
for (const name of ['create_goal', 'get_goal', 'update_goal']) {
expect(pipeline.executionMode({ signal: testToolSignal, callId: ToolCallId(name), name, arguments: {} }))
.toEqual({ kind: 'exclusive' })
+1 -1
View File
@@ -42,7 +42,6 @@
"dependencies": {
"@deepseek-ai/cosmokit": "workspace:~",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-tool-executor-native": "workspace:*",
"@deepseek-ai/dsh-util-values": "workspace:*",
"@deepseek-ai/schemastery": "workspace:~",
"eventsource-parser": "^3.1.0"
@@ -72,6 +71,7 @@
"@deepseek-ai/dsh-agent-loop": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tool-executor-native": "workspace:*",
"@deepseek-ai/dsh-tool-testkit": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-credentials-local": "workspace:*",
@@ -1,5 +1,5 @@
import { executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
/** Executor plugins follow the Loader-owned preset revision lifetime. */
import { executeModelToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -91,7 +91,7 @@ class ProgramFixture extends PtcRuntime {
}
}
it('loads the separate executor packages through cordis.yml presets and logs PTC subcalls', async () => {
it.each([true, false])('loads cordis.yml executor presets with runtime availability %s', async (hasRuntime) => {
const root = await mkdtemp(join(tmpdir(), 'dsh-executor-packages-'))
const ctx = await harness()
onTestFinished(async () => {
@@ -99,7 +99,7 @@ it('loads the separate executor packages through cordis.yml presets and logs PTC
})
await ctx.plugin(FsLocal)
await ctx.plugin(WorkingDirectory, { defaultDirectory: root })
await ctx.plugin(ProgramFixture)
if (hasRuntime) await ctx.plugin(ProgramFixture)
ctx.tools.register(defineTool({
name: 'echo', description: 'Shared capability.', parameters: { value: { type: 'string', required: true } },
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
@@ -127,6 +127,15 @@ it('loads the separate executor packages through cordis.yml presets and logs PTC
const native = await agentOn(ctx, 'native-package', 'standard')
const ptc = await agentOn(ctx, 'ptc-package', 'ptc')
expect((await ctx.systemPrompt.assemble({ scope: native })).tools.map(tool => tool.name)).toEqual(['echo'])
if (!hasRuntime) {
await expect(ctx.systemPrompt.assemble({ scope: ptc })).rejects.toThrow('requires a PTC runtime')
const batch = await ctx.tools.execute({
agent: ptc, calls: [{ type: 'tool-call', id: ToolCallId('missing-runtime'), name: 'echo', arguments: '{}' }],
signal: new AbortController().signal,
})
expect(batch.results[0]?.result).toMatchObject({ isError: true, error: { info: { code: 'UNKNOWN_TOOL' } } })
return
}
expect((await ctx.systemPrompt.assemble({ scope: ptc })).tools.map(tool => tool.name)).toEqual(['run_code'])
const signal = new AbortController().signal
expect(await executeModelToolFixture(ctx, {
@@ -157,7 +157,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
| `@deepseek-ai/dsh-agent-tool-presentation` | yes | Deprecated tool presentation selector; use dsh-tool-executor-native or dsh-tool-executor-ptc directly |
| `@deepseek-ai/dsh-session` | no | Event-sourced session store for the DeepSeek Harness |
| `@deepseek-ai/dsh-system-prompt` | yes | System prompt assembly registry for the DeepSeek Harness |
| `@deepseek-ai/dsh-tool-executor-native` | yes | Native tool execution plugin |
| `@deepseek-ai/dsh-tool-executor-native` | no | Native tool execution plugin |
| `@deepseek-ai/dsh-tool-executor-ptc` | yes | PTC program execution, tool bindings, and generated SDK prompts |
| `@deepseek-ai/dsh-tools` | no | Tool registry and execution pipeline for the DeepSeek Harness |
-1
View File
@@ -38,7 +38,6 @@
"@deepseek-ai/dsh-output-retention": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-spill": "workspace:*",
"@deepseek-ai/dsh-tool-executor-ptc": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-attachment": "workspace:*",
@@ -111,13 +111,13 @@ describe('dsh-tool-subagent', () => {
it('classifies delegations as concurrency-safe', async () => {
const ctx = await setup({ provider: 'mock' })
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({
expect(new ToolExecutionPipeline(ctx).executionMode({
signal: testToolSignal,
callId: ToolCallId('subagent-first'),
name: 'subagent',
arguments: { description: 'do work', prompt: 'Reply OK' },
})).toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({
expect(new ToolExecutionPipeline(ctx).executionMode({
signal: testToolSignal,
callId: ToolCallId('subagent-second'),
name: 'subagent',
@@ -696,7 +696,7 @@ describe('dsh-tool-subagent local activation', () => {
it('classifies local activations concurrency-safe', async () => {
const { ctx } = await continuableSetup()
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({
expect(new ToolExecutionPipeline(ctx).executionMode({
signal: testToolSignal,
callId: ToolCallId('subagent-continuable'),
name: 'subagent',
@@ -43,6 +43,17 @@ describe('dsh-agent-loop-testkit', () => {
}
})
it('mounts the PTC executor with its runtime', async () => {
const ctx = new Context()
try {
await ctx.plugin(FixtureRuntime)
await mountAgentLoopTestDependencies(ctx, { tools: { mode: 'ptc' } })
expect((await ctx.systemPrompt.assemble()).tools.map(tool => tool.name)).toEqual(['run_code'])
} finally {
await ctx.fiber.dispose()
}
})
it('rejects mutations through an unsupported Agent stub Inbox', () => {
const inbox = unsupportedInbox()
@@ -60,7 +60,7 @@ export function defineContentToolFixture<const S extends ParameterSchemaSpec>(
* @internal
*/
export async function executeToolFixture(ctx: Context, input: ToolExecutionInput): Promise<ToolExecutionResult> {
const pipeline = new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) })
const pipeline = new ToolExecutionPipeline(ctx)
const prepared = await pipeline.prepare(input)
const outcome = prepared.kind === 'dispatch' ? await pipeline.dispatch(prepared.exec) : prepared
return pipeline.complete(prepared.exec, outcome)
+2 -2
View File
@@ -457,9 +457,9 @@ describe('tool-web registration', () => {
const names = ctx.tools.schemas().map(s => s.name)
expect(names).toContain('web_search')
expect(names).toContain('web_fetch')
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({ signal: testToolSignal, callId: ToolCallId('search-safe'), name: 'web_search', arguments: { queries: ['q'] } }))
expect(new ToolExecutionPipeline(ctx).executionMode({ signal: testToolSignal, callId: ToolCallId('search-safe'), name: 'web_search', arguments: { queries: ['q'] } }))
.toEqual({ kind: 'parallel' })
expect(new ToolExecutionPipeline(ctx, { get: (name, scope) => ctx.tools.get(name, scope) }).executionMode({ signal: testToolSignal, callId: ToolCallId('fetch-safe'), name: 'web_fetch', arguments: { url: 'https://a.test' } }))
expect(new ToolExecutionPipeline(ctx).executionMode({ signal: testToolSignal, callId: ToolCallId('fetch-safe'), name: 'web_fetch', arguments: { url: 'https://a.test' } }))
.toEqual({ kind: 'parallel' })
await fiber.dispose()
expect(ctx.tools.schemas().map(s => s.name)).not.toContain('web_search')
+6 -16
View File
@@ -7035,9 +7035,6 @@ importers:
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-tool-executor-native':
specifier: workspace:*
version: link:../../core/tool-executor-native
'@deepseek-ai/dsh-util-values':
specifier: workspace:*
version: link:../../util/values
@@ -7102,6 +7099,9 @@ importers:
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:*
version: link:../../core/system-prompt
'@deepseek-ai/dsh-tool-executor-native':
specifier: workspace:*
version: link:../../core/tool-executor-native
'@deepseek-ai/dsh-tool-fs':
specifier: workspace:*
version: link:../../fs/tool-fs
@@ -7411,13 +7411,6 @@ importers:
version: link:../scope
packages/core/tool-executor-native:
dependencies:
'@deepseek-ai/cosmokit':
specifier: link:../../../vendor/cosmokit
version: link:../../../vendor/cosmokit
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
@@ -7434,9 +7427,6 @@ importers:
packages/core/tool-executor-ptc:
dependencies:
'@deepseek-ai/cosmokit':
specifier: link:../../../vendor/cosmokit
version: link:../../../vendor/cosmokit
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
@@ -11468,9 +11458,6 @@ importers:
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-tool-executor-native':
specifier: workspace:*
version: link:../../core/tool-executor-native
'@deepseek-ai/dsh-util-values':
specifier: workspace:*
version: link:../../util/values
@@ -11562,6 +11549,9 @@ importers:
'@deepseek-ai/dsh-timeout':
specifier: workspace:*
version: link:../../util/timeout
'@deepseek-ai/dsh-tool-executor-native':
specifier: workspace:*
version: link:../../core/tool-executor-native
'@deepseek-ai/dsh-tool-testkit':
specifier: workspace:*
version: link:../../test-support/tool-testkit
-1
View File
@@ -728,7 +728,6 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ToolExecutionResult: 'tools.md',
ToolExecutionToken: 'tools.md',
ToolGuard: 'tools.md',
ToolPresentationMode: 'tools.md',
ToolRuntime: 'tools.md',
ToolsService: 'tools.md',
ToolExecutor: 'tools.md',
@@ -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): () => 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 \"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): 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 \"returns\": \"the definition the scope resolves, or undefined when none is visible.\"\n },\n {\n \"signature\": \"definitions(scope?: ScopeKey): readonly ToolDefinition[]\",\n \"description\": \"Read the visible capability definitions for descriptions and program bindings.\",\n \"parameters\": [\n {\n \"name\": \"scope\",\n \"description\": \"viewing Agent or preset; omitted selects the global view.\"\n }\n ],\n \"returns\": \"definitions after scoped shadowing and restrictions.\"\n },\n {\n \"signature\": \"schemas(scope?: ScopeKey): ToolSchema[]\",\n \"description\": \"Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.\",\n \"parameters\": [\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n }\n ],\n \"returns\": \"one deep-cloned schema per visible tool.\"\n },\n {\n \"signature\": \"setExecutor(executor: ToolExecutor): () => void\",\n \"description\": \"Provide the executor for the calling scope; an unscoped contribution is the deployment default.\",\n \"parameters\": [\n {\n \"name\": \"executor\",\n \"description\": \"initialized provider owned by the contributing plugin.\"\n }\n ],\n \"returns\": \"the effect disposer. Unloading leaves the selection unavailable until it is replaced.\"\n },\n {\n \"signature\": \"async execute(input: ToolBatchInput): Promise<ToolBatchResult>\",\n \"description\": \"Delegate a raw call batch to its Agent's provider or the deployment default. Select at dispatch; accepted batches settle through that provider even if it unloads.\",\n \"parameters\": [\n {\n \"name\": \"input\",\n \"description\": \"original calls, optional owning Agent, and optional logging step.\"\n }\n ],\n \"returns\": \"the selected executor's settled batch; unavailable selections reject.\"\n },\n {\n \"sLine 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): () => 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 \"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): 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 \"returns\": \"the definition the scope resolves, or undefined when none is visible.\"\n },\n {\n \"signature\": \"definitions(scope?: ScopeKey): readonly ToolDefinition[]\",\n \"description\": \"Read the visible capability definitions for descriptions and program bindings.\",\n \"parameters\": [\n {\n \"name\": \"scope\",\n \"description\": \"viewing Agent or preset; omitted selects the global view.\"\n }\n ],\n \"returns\": \"definitions after scoped shadowing and restrictions.\"\n },\n {\n \"signature\": \"schemas(scope?: ScopeKey): ToolSchema[]\",\n \"description\": \"Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.\",\n \"parameters\": [\n {\n \"name\": \"scope\",\n \"description\": \"the viewing scope (the agent); omitted = the global view.\"\n }\n ],\n \"returns\": \"one deep-cloned schema per visible tool.\"\n },\n {\n \"signature\": \"setExecutor(executor: ToolExecutor): () => void\",\n \"description\": \"Provide the executor for the calling scope; an unscoped contribution is the deployment default. Rendering any executor-owned section requires the viewing scope's selection to remain available.\",\n \"parameters\": [\n {\n \"name\": \"executor\",\n \"description\": \"initialized provider owned by the contributing plugin.\"\n }\n ],\n \"returns\": \"the effect disposer. Unloading leaves the selection unavailable until it is replaced.\"\n },\n {\n \"signature\": \"async execute(input: ToolBatchInput): Promise<ToolBatchResult>\",\n \"description\": \"Delegate a raw call batch to its Agent's provider or the deployment default. Select at dispatch; accepted batches settle through that provider even if it unloads.\",\n \"parameters\": [\n {\n \"name\": \"input\",\n \"description\": \"original calls, optional owning Agent, and optional logging step.\"\n }\n ],\n \"returns\": \"the selectedLine 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"}