refactor(subagent): resolve preset references only when starting children

This commit is contained in:
Dudu-0223 committed 2026-10-25 13:22:35 +08:00
1 parent 0a618f13d5
commit 90e398328d
24 files changed
+282 -450

No files matched your search

+1 -2
View File
@@ -9,8 +9,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/prepare` | `serial` | [`packages/preset/agent-preset-registry/src/types.ts:67`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`serial`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:75`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` |
+1 -2
View File
@@ -11,8 +11,7 @@
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/prepare` | `serial` | [`packages/preset/agent-preset-registry/src/types.ts:67`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`serial`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:75`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` |
+1 -22
View File
@@ -618,8 +618,7 @@ async resolve(id?: string): Promise<AgentPreset>
*/
@Remote('read') readDocument(agentPreset: string): Promise<AgentPresetDocument>
/** Prepare and bind an unpublished Agent to the current preset revision.
* Preparation failure preserves any existing binding.
/** Bind an unpublished Agent to the current preset revision.
* @param ctx Agent context from its setup callback.
* @param id Requested preset, or the default.
* @returns Bound preset identity.
@@ -1168,26 +1167,6 @@ Source: [`packages/core/agent-loop/src/index.ts`](../../packages/core/agent-loop
### `agent-preset/*` events
<a id="agent-presetprepare--serial"></a>
#### `agent-preset/prepare` — serial
Prepare shared registrations before binding a preset revision. Listeners are routed to that revision and awaited; rejection preserves the existing binding.
```ts cordis-catalog
/**
* Prepare shared registrations before binding a preset revision.
* Listeners are routed to that revision and awaited; rejection preserves the existing binding.
* @mode serial
* @param preset - the preset whose retained revision is being prepared.
*/
'agent-preset/prepare'(this: Scoped<AgentPreset>, preset: AgentPreset): undefined | Promise<undefined>
```
Types: [Scoped](scope.md)
Source: [`packages/preset/agent-preset-registry/src/types.ts`](../../packages/preset/agent-preset-registry/src/types.ts)
<a id="agent-presetselected--emit"></a>
#### `agent-preset/selected` — emit
+1 -22
View File
@@ -630,8 +630,7 @@ async resolve(id?: string): Promise<AgentPreset>
*/
@Remote('read') readDocument(agentPreset: string): Promise<AgentPresetDocument>
/** Prepare and bind an unpublished Agent to the current preset revision.
* Preparation failure preserves any existing binding.
/** Bind an unpublished Agent to the current preset revision.
* @param ctx Agent context from its setup callback.
* @param id Requested preset, or the default.
* @returns Bound preset identity.
@@ -1180,26 +1179,6 @@ Source: [`packages/core/agent-loop/src/index.ts`](../../packages/core/agent-loop
### `agent-preset/*` events
<a id="agent-presetprepare--serial"></a>
#### `agent-preset/prepare` — serial
Prepare shared registrations before binding a preset revision. Listeners are routed to that revision and awaited; rejection preserves the existing binding.
```ts cordis-catalog
/**
* Prepare shared registrations before binding a preset revision.
* Listeners are routed to that revision and awaited; rejection preserves the existing binding.
* @mode serial
* @param preset - the preset whose retained revision is being prepared.
*/
'agent-preset/prepare'(this: Scoped<AgentPreset>, preset: AgentPreset): undefined | Promise<undefined>
```
Types: [Scoped](scope.zh.md)
Source: [`packages/preset/agent-preset-registry/src/types.ts`](../../packages/preset/agent-preset-registry/src/types.ts)
<a id="agent-presetselected--emit"></a>
#### `agent-preset/selected` — emit
@@ -211,7 +211,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
{
signature: 'async mount(ctx: Context, id?: string): Promise<AgentPreset>',
description: 'Prepare and bind an unpublished Agent to the current preset revision. Preparation failure preserves any existing binding.',
description: 'Bind an unpublished Agent to the current preset revision.',
parameters: [{ name: 'ctx', description: 'Agent context from its setup callback.' }, { name: 'id', description: 'Requested preset, or the default.' }],
returns: 'Bound preset identity.',
},
@@ -4232,14 +4232,6 @@ export const EVENT_API: readonly EventApiEntry[] = [
description: 'A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt.',
parameters: [{ name: 'payload', description: '.error - persistence, setup, or publication failure.' }],
},
{
name: 'agent-preset/prepare',
mode: 'serial',
signature: '\'agent-preset/prepare\'(this: Scoped<AgentPreset>, preset: AgentPreset): undefined | Promise<undefined>',
summary: 'Prepare shared registrations before binding a preset revision.',
description: 'Prepare shared registrations before binding a preset revision. Listeners are routed to that revision and awaited; rejection preserves the existing binding.',
parameters: [{ name: 'preset', description: 'the preset whose retained revision is being prepared.' }],
},
{
name: 'agent-preset/selected',
mode: 'emit',
@@ -21,8 +21,8 @@
en: cd28d9b4f46c46a8
zh: 4921d853f98469fe
/deepseek-ai-dsh-agent-preset-registry/understand-the-implementation:
en: 39a26480d032bad5
zh: fb32565ae87328b4
en: 32c8a5c63022e275
zh: 1d53e28b0d7375da
/deepseek-ai-dsh-agent-preset-registry/further-exploration:
en: bbae8d79844bc481
zh: 4302107fd1f460e6
@@ -53,7 +53,7 @@ The registry writes no declarations. The `read` Remote renders one declaration
<details>
<summary>Implementation internals — click to expand</summary>
Each declaration eagerly creates a registry-owned scope and an in-memory Loader tree. Updating or removing a declaration retires its previous revision. Agents, children and temporary historical reads retain references; releasing the final reference disposes the retired tree. Plugin registrations inherit the preset scope, and the Agent scope’s parent link controls visibility. Before binding a revision, the registry awaits its scoped preparation listeners; failure preserves the previous binding. This lets plugins validate cross-preset references and prepare shared registrations after declarations have loaded. Cold scope reads do not run preparation. The Host continues to share the Agent loop.
Each declaration eagerly creates a registry-owned scope and an in-memory Loader tree. Updating or removing a declaration retires its previous revision. Agents, children and temporary historical reads retain references; releasing the final reference disposes the retired tree. Plugin registrations inherit the preset scope, and the Agent scope’s parent link controls visibility. The Host continues to share the Agent loop.
A preset service provider and its consumers must share one `cordis:group` isolation realm. Entry-local isolation does not propagate through the Agent registration scope: ordinary Context lookup outside the group, including `agent.ctx`, still selects the Host realm. `agentPresets.serviceFor()` explicitly inspects the retained composition; it does not change ordinary service lookup.
@@ -53,7 +53,7 @@ Web 内置定义来自 `dsh-web-app` bundle。定义使用普通插件行;注
<details>
<summary>实现细节 — 点击展开</summary>
每个声明在启动时创建注册表拥有的 scope 和内存 Loader 树。更新或移除声明会让旧代际退役;Agent、子 Agent 和临时历史读取各自持有引用,最后一个引用释放后才销毁插件树。插件注册继承 preset scope,Agent scope 的父链接决定可见性。绑定代际前,注册表等待其作用域内的准备监听器完成;失败时保留原绑定。这让插件能够在声明加载后校验跨 preset 引用,并准备共享注册。冷读取作用域不执行准备。Agent loop 仍由宿主共享。
每个声明在启动时创建注册表拥有的 scope 和内存 Loader 树。更新或移除声明会让旧代际退役;Agent、子 Agent 和临时历史读取各自持有引用,最后一个引用释放后才销毁插件树。插件注册继承 preset scope,Agent scope 的父链接决定可见性;Agent loop 仍由宿主共享。
preset 的服务提供方及其消费方必须共享同一个 `cordis:group` 隔离 realm。条目局部隔离不会沿 Agent 注册 scope 传播:组外的普通 Context 查找(包括 `agent.ctx`)仍选择 Host realm。`agentPresets.serviceFor()` 显式检查保留的组合,不改变普通服务查找。
@@ -3,7 +3,7 @@ import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection'
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { bindScopeParent, createScope, scopeOf, scopeParentOf, scopeTarget, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
import { bindScopeParent, createScope, scopeOf, scopeParentOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { dump } from 'js-yaml'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -289,8 +289,7 @@ export class AgentPresetRegistry extends TypertRemoteService {
}, 'agent-preset.binding')
}
/** Prepare and bind an unpublished Agent to the current preset revision.
* Preparation failure preserves any existing binding.
/** Bind an unpublished Agent to the current preset revision.
* @param ctx Agent context from its setup callback.
* @param id Requested preset, or the default.
* @returns Bound preset identity.
@@ -298,10 +297,8 @@ export class AgentPresetRegistry extends TypertRemoteService {
async mount(ctx: Context, id?: string): Promise<AgentPreset> {
const generation = await this.retain(id)
try {
const preset = { id: generation.mount.presetId }
await this.owner.serial(scopeTarget(preset, generation.key), 'agent-preset/prepare', preset)
await this.bind(ctx, generation)
return preset
return { id: generation.mount.presetId }
} finally {
generation.users--
await this.collect(generation)
@@ -1,7 +1,5 @@
/** Client-safe payloads and event declarations owned by the agent-preset domain. */
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { AgentPreset } from './preset.ts'
/**
@@ -58,13 +56,6 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* Prepare shared registrations before binding a preset revision.
* Listeners are routed to that revision and awaited; rejection preserves the existing binding.
* @mode serial
* @param preset - the preset whose retained revision is being prepared.
*/
'agent-preset/prepare'(this: Scoped<AgentPreset>, preset: AgentPreset): undefined | Promise<undefined>
/**
* One session committed a different agent preset to its durable log.
* Consumers invalidate only state derived from that session's composition.
@@ -24,8 +24,8 @@
en: bb715d037cde14bd
zh: a88e98c07115fbd1
/deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-preset:
en: 250322d87f17bc70
zh: fbd332bebac0f8f6
en: 49eccebd9d8c03a6
zh: f2c13e3ec0cdf9c4
/deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm:
en: 5cf892e4acab3f38
zh: c282b79620bc3de5
@@ -33,8 +33,8 @@
en: 6ce5f749256a7e6c
zh: a4f2adb04a70d9cf
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/design-concept:
en: 3567f2b02924d574
zh: 8c8493d10830ea29
en: 373abf7c7aa3e4f1
zh: be0b4d4a7371d360
/deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership:
en: ef692c4d17290920
zh: b1d0d3dab011e2cd
@@ -57,11 +57,11 @@
en: e4c7376d4f29dbd5
zh: 6b7a9eaf69c9ea53
/deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/what-the-model-sees:
en: 9323df0a41d40c0a
zh: 7ea3773f6362c913
en: f68249e78e3802e0
zh: 3509fa972ed561a0
/deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/token-effect:
en: fbb4cc4600ad76fc
zh: 2080fe6094ad6344
en: c04242443b8fbdb0
zh: 8779ef72c4eaee35
/deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/kv-cache-effect:
en: 5697c49cb84fb2a4
zh: b2b8464d3cbabfa5
+4 -4
View File
@@ -68,7 +68,7 @@ Every backend’s completion notice carries its final answer. Local Spawn and Fo
Set `agentPreset` to give every child an independent preset by default. Set `allowedAgentPresets` to let the model choose an id through `agent_preset`; without this allowlist, the model cannot override the configured choice. Only in-process spawn supports these options; fork and external backends reject them.
The configured default and allowlist must name declared presets. A tool inside a preset validates these references before that preset is bound and before an inheriting Agent is published; a Host-mounted tool validates them at load. Missing references reject the operation, preserving the previous binding on a failed switch. Self-references and references to later declarations are supported. This check reads declarations without activating candidate presets; activation failures reject the selected preset when the child is composed. Without either selection field, the tool does not require the preset service. An omitted model choice uses the configured `agentPreset`, or joins the parent's current preset revision when no default is configured. Any explicit id mounts the currently available revision, even when it matches the parent's id.
The configured default must belong to the allowlist when both are set, and duplicate or empty ids fail at load. Preset ids are resolved when a call starts the child; an unknown or unloadable preset returns an errored tool result without starting a child. Creating a parent Agent or switching its preset does not resolve child references. Without either selection field, the tool does not require the preset service. An omitted model choice uses the configured `agentPreset`, or joins the parent's current preset revision when no default is configured. Any explicit id mounts the currently available revision, even when it matches the parent's id.
Compose persona and tools in the [preset plugin list](../../preset/agent-preset-registry/README.md). Tool configuration containing `persona` or `toolFilter` fails at load with a migration error. Local children use the shared [preset restoration rules](../subagent/README.md#restoring-child-composition).
@@ -90,7 +90,7 @@ This section explains how the tool mirrors provider lifecycle and settles runs;
### Design concept
One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Each preset revision owns one shared installation, including tools with preset selection. Agents inherit its definitions; switching presets changes scope ancestry without reinstalling tools. Preset descriptions are captured at installation and refreshed by reloading the owning tool instance. Discovery belongs to the instance configured with `modelDiscovery` and reads the same durable Session policy as the delegation executor. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Multiple distinctly named delegation tools may expose model selection in one scope.
One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Each preset revision owns one shared registration, including tools with preset selection. Agents inherit its definitions; switching presets changes scope ancestry without reinstalling tools. Discovery belongs to the instance configured with `modelDiscovery` and reads the same durable Session policy as the delegation executor. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Multiple distinctly named delegation tools may expose model selection in one scope.
### Activation ownership
@@ -139,11 +139,11 @@ Read these pages when the package-level contract is not enough; they move from t
#### What the model sees
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. An instance with `modelSelection` adds `provider`, `model`, and `reasoning_effort` plus route guidance in every Session. `allowedAgentPresets` adds the `agent_preset` enum and guidance from the presets’ display descriptions. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:<toolName>` guidance section.
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. An instance with `modelSelection` adds `provider`, `model`, and `reasoning_effort` plus route guidance in every Session. `allowedAgentPresets` adds the `agent_preset` enum and guidance for the default or inherited selection. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:<toolName>` guidance section.
#### Token effect
Fixed schema cost per parent request; `modelSelection` adds three optional route fields, while preset selection adds one enum parameter and preset descriptions. Each tool instance adds one schema; visible delegation tools share one short system-prompt section.
Fixed schema cost per parent request; `modelSelection` adds three optional route fields, while preset selection adds one enum parameter and default guidance. Each tool instance adds one schema; visible delegation tools share one short system-prompt section.
#### KV Cache effect
+4 -4
View File
@@ -68,7 +68,7 @@ kind: "package-reference"
设置 `agentPreset`,为每个子级指定默认的独立 preset。设置 `allowedAgentPresets`,允许模型通过 `agent_preset` 选择 id;没有此白名单时,模型不能覆盖配置中的选择。只有进程内 spawn 支持这些选项;fork 与外部后端会拒绝它们。
配置的默认值与白名单必须引用已声明 preset。preset 内的工具在绑定该 preset 前以及发布继承它的 Agent 前校验这些引用;直接挂载在 Host 的工具在加载时校验。引用缺失会拒绝操作,切换失败时保留原绑定。支持自引用和引用后续声明。此检查只读取声明,不激活候选 preset;创建子级时,选中 preset 的激活失败会拒绝组装。两个选择字段都省略时,工具不要求 preset 服务。模型省略选择时使用配置的 `agentPreset`;若也没有默认值,则加入父方当前 preset 代际。任何显式 id 都挂载当前可用代际,即使与父方 id 相同。
同时配置默认值和白名单时,默认值必须属于白名单;重复或空 id 在加载时被拒绝。调用启动子级时解析 preset id;未知或无法加载的 preset 返回出错的工具结果,不会启动子级。创建父 Agent 或切换其 preset 时不解析子级引用。两个选择字段都省略时,工具不要求 preset 服务。模型省略选择时使用配置的 `agentPreset`;若也没有默认值,则加入父方当前 preset 代际。任何显式 id 都挂载当前可用代际,即使与父方 id 相同。
在 [preset 插件列表](../../preset/agent-preset-registry/README.zh.md)中配置 persona 与工具。包含 `persona` 或 `toolFilter` 的工具配置在加载时会报迁移错误。本地子级采用共享的 [preset 恢复规则](../subagent/README.zh.md#restoring-child-composition)。
@@ -90,7 +90,7 @@ kind: "package-reference"
### 设计理念
一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。每个 preset 代际拥有一份共享安装,配置了 preset 选择的工具也相同。Agent 继承其定义;切换 preset 只改变作用域的继承关系,不重新安装工具。preset 描述在安装时读取,重新加载所属工具实例后更新。发现工具属于配置了 `modelDiscovery` 的实例,与委派执行器读取同一份持久 Session 策略。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。同一作用域中可以有多个名称不同的委派工具公开模型选择。
一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。每个 preset 代际拥有一份共享注册,配置了 preset 选择的工具也相同。Agent 继承其定义;切换 preset 只改变作用域的继承关系,不重新安装工具。发现工具属于配置了 `modelDiscovery` 的实例,与委派执行器读取同一份持久 Session 策略。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。同一作用域中可以有多个名称不同的委派工具公开模型选择。
### Activation 所有权
@@ -139,11 +139,11 @@ kind: "package-reference"
#### 模型看到什么
生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。设置了 `modelSelection` 的实例在每个 Session 中都会增加 `provider`、`model`、`reasoning_effort` 及路由指导。`allowedAgentPresets` 增加 `agent_preset` 枚举和来自 preset 显示描述的引导。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:<toolName>` 指导段。
生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。设置了 `modelSelection` 的实例在每个 Session 中都会增加 `provider`、`model`、`reasoning_effort` 及路由指导。`allowedAgentPresets` 增加 `agent_preset` 枚举及默认或继承选择的引导。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:<toolName>` 指导段。
#### Token 影响
每次父级请求承担固定 schema 成本;`modelSelection` 会增加三个可选路由字段,preset 选择则增加一个枚举参数和 preset 描述。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。
每次父级请求承担固定 schema 成本;`modelSelection` 会增加三个可选路由字段,preset 选择则增加一个枚举参数及默认值引导。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。
#### KV Cache 影响
@@ -41,7 +41,6 @@
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-scope": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
@@ -62,7 +61,6 @@
"@deepseek-ai/dsh-agent-preset-registry": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-scope": "workspace:*",
"@deepseek-ai/dsh-session-persistence": "workspace:*",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
+179 -206
View File
@@ -8,7 +8,7 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import type { AgentOptions } from '@deepseek-ai/dsh-agent'
@@ -142,7 +142,7 @@ function providerWording(inheritsConversation: boolean): { description: string;
/**
* Install one delegation-tool composition.
* The composition owns one shared installation; preset references are checked before binding or Agent publication.
* The owning composition shares its registrations across Agents; child startup resolves the selected preset.
* @param ctx - Context that owns the registrations.
* @param config - delegation-tool configuration.
* @returns when the plugin is ready.
@@ -195,223 +195,196 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
if (config.modelDiscovery === true) registerListSubagentModels(ctx, subagentModelSelectionProjectionHandle)
const install = (runtimeCtx: Context): void => {
const presetDescription = presets.description
// Load order and HMR replacement can change provider availability while
// this fiber remains active.
let mounted: { subagentProvider: SubagentProvider; definition: ToolDefinition; disposeTool: () => void } | undefined
const mount = (subagentProvider: SubagentProvider): void => {
assertSubagentProviderConfiguration(subagentProvider)
const wording = providerWording(subagentProvider.inheritsParentContext)
const providerRouteDefaults = subagentProvider.agentRouteDefaults
const selectionDescription = providerRouteDefaults !== undefined
? ' Supply `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.'
: ' Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.'
const definition = defineTool({
name: toolName,
description: wording.description
+ ' This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes.'
+ (subagentProvider.prepareContinuable !== undefined
? ' The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.'
: ' The completion notice includes its final answer. This backend does not accept follow-up messages.')
+ (modelSelectionEnabled ? selectionDescription : '')
+ (modelSelectionEnabled && subagentProvider.inheritsParentContext
? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.'
: ''),
parameters: {
cwd: {
type: 'string',
description: 'Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent.',
},
description: {
type: 'string',
required: true,
description: 'A short (3-5 word) description of the delegated task, for display.',
},
prompt: {
type: 'string',
required: true,
description: wording.promptDescription,
},
...presets.choices === undefined ? {} : {
agent_preset: {
type: 'string' as const,
enum: [...presets.choices],
description: presetDescription,
},
},
...modelSelectionEnabled ? {
provider: {
type: 'string' as const,
description: 'Child LLM provider id.',
},
model: {
type: 'string' as const,
description: 'Model id within that provider.',
},
reasoning_effort: {
type: 'string' as const,
description: 'Model-specific reasoning effort id.',
},
} : {},
// Load order and HMR replacement can change provider availability while
// this fiber remains active.
let mounted: { subagentProvider: SubagentProvider; definition: ToolDefinition; disposeTool: () => void } | undefined
const mount = (subagentProvider: SubagentProvider): void => {
assertSubagentProviderConfiguration(subagentProvider)
const wording = providerWording(subagentProvider.inheritsParentContext)
const providerRouteDefaults = subagentProvider.agentRouteDefaults
const selectionDescription = providerRouteDefaults !== undefined
? ' Supply `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.'
: ' Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.'
const definition = defineTool({
name: toolName,
description: wording.description
+ ' This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes.'
+ (subagentProvider.prepareContinuable !== undefined
? ' The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.'
: ' The completion notice includes its final answer. This backend does not accept follow-up messages.')
+ (modelSelectionEnabled ? selectionDescription : '')
+ (modelSelectionEnabled && subagentProvider.inheritsParentContext
? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.'
: ''),
parameters: {
cwd: {
type: 'string',
description: 'Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent.',
},
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'activation' },
subagentId: { type: 'string', required: true },
},
},
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }],
description: {
type: 'string',
required: true,
description: 'A short (3-5 word) description of the delegated task, for display.',
},
isConcurrencySafe: () => true,
async execute(args, exec) {
const parent = exec.agent
if (!parent) {
// Non-agent callers provide no parent for delegation ownership.
throw new Error('subagent tool requires a calling agent (exec.agent was undefined)')
}
prompt: {
type: 'string',
required: true,
description: wording.promptDescription,
},
...presets.choices === undefined ? {} : {
agent_preset: {
type: 'string' as const,
enum: [...presets.choices],
description: presets.description,
},
},
...modelSelectionEnabled ? {
provider: {
type: 'string' as const,
description: 'Child LLM provider id.',
},
model: {
type: 'string' as const,
description: 'Model id within that provider.',
},
reasoning_effort: {
type: 'string' as const,
description: 'Model-specific reasoning effort id.',
},
} : {},
},
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'activation' },
subagentId: { type: 'string', required: true },
},
},
render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }],
},
isConcurrencySafe: () => true,
async execute(args, exec) {
const parent = exec.agent
if (!parent) {
// Non-agent callers provide no parent for delegation ownership.
throw new Error('subagent tool requires a calling agent (exec.agent was undefined)')
}
const routes = subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, parent.session)
const modelSelectionPolicy = routes === undefined ? undefined : { routes }
const agentPreset = presets.resolve(args as PresetSelectionRequest)
const modelRequest = args as DelegationModelRequest
const parentOptions = parentAgentOptionsForDelegation(parent)
const requiresRoutePreflight = hasDelegationModelRequest(modelRequest)
|| hasConfiguredLlmSelection(config.agentOptions)
const configuredChildAgentOptions = requiresRoutePreflight && providerRouteDefaults !== undefined
? { ...providerRouteDefaults, ...config.agentOptions }
: config.agentOptions
const requestedChildAgentOptions = requestedAgentOptions(
parentOptions,
configuredChildAgentOptions,
modelRequest,
modelSelectionEnabled,
)
assertAllowedModelSelection(
modelSelectionPolicy,
const routes = subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, parent.session)
const modelSelectionPolicy = routes === undefined ? undefined : { routes }
const agentPreset = presets.resolve(args as PresetSelectionRequest)
const modelRequest = args as DelegationModelRequest
const parentOptions = parentAgentOptionsForDelegation(parent)
const requiresRoutePreflight = hasDelegationModelRequest(modelRequest)
|| hasConfiguredLlmSelection(config.agentOptions)
const configuredChildAgentOptions = requiresRoutePreflight && providerRouteDefaults !== undefined
? { ...providerRouteDefaults, ...config.agentOptions }
: config.agentOptions
const requestedChildAgentOptions = requestedAgentOptions(
parentOptions,
configuredChildAgentOptions,
modelRequest,
modelSelectionEnabled,
)
assertAllowedModelSelection(
modelSelectionPolicy,
parentOptions,
requestedChildAgentOptions,
modelRequest,
)
if (requiresRoutePreflight) {
const llm = ctx.get('llm')
if (llm === undefined) {
throw new Error('cannot resolve the selected child LLM route because the `llm` service is unavailable')
}
await preflightChildLlmRoute(
llm,
parentOptions,
requestedChildAgentOptions,
modelRequest,
exec.signal,
providerRouteDefaults === undefined,
)
if (requiresRoutePreflight) {
const llm = runtimeCtx.get('llm')
if (llm === undefined) {
throw new Error('cannot resolve the selected child LLM route because the `llm` service is unavailable')
}
await preflightChildLlmRoute(
llm,
parentOptions,
requestedChildAgentOptions,
exec.signal,
providerRouteDefaults === undefined,
)
if (runtimeCtx.subagents.getProvider(config.provider) !== subagentProvider) {
throw new Error(`subagent provider "${config.provider}" changed while resolving the child LLM route; retry the delegation`)
}
if (ctx.subagents.getProvider(config.provider) !== subagentProvider) {
throw new Error(`subagent provider "${config.provider}" changed while resolving the child LLM route; retry the delegation`)
}
exec.signal.throwIfAborted()
const maxDepth = runtimeCtx.subagents.resolveMaxDepth(config.maxDepth)
const request = {
...args.cwd === undefined ? {} : { cwd: args.cwd },
prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[],
parent,
...agentPreset === undefined ? {} : { agentPreset },
...requestedChildAgentOptions !== undefined ? { agentOptions: requestedChildAgentOptions } : {},
...maxDepth !== undefined ? { maxDepth } : {},
}
const started = await runtimeCtx.subagents.startActivation({
provider: config.provider,
label: args.description,
request,
signal: exec.signal,
delivery: 'parent',
})
return { kind: 'activation' as const, subagentId: started.childId }
},
})
const disposeTool = runtimeCtx.effect(() => {
const unregister = runtimeCtx.tools.register(definition)
delegationTools.add(definition)
return () => {
delegationTools.delete(definition)
unregister()
}
})
// oxlint-disable-next-line typescript/no-misused-promises -- Tool and guidance disposal are synchronous.
mounted = { subagentProvider, definition, disposeTool }
}
exec.signal.throwIfAborted()
const maxDepth = ctx.subagents.resolveMaxDepth(config.maxDepth)
const request = {
...args.cwd === undefined ? {} : { cwd: args.cwd },
prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[],
parent,
...agentPreset === undefined ? {} : { agentPreset },
...requestedChildAgentOptions !== undefined ? { agentOptions: requestedChildAgentOptions } : {},
...maxDepth !== undefined ? { maxDepth } : {},
}
// Register listeners before checking presence so no synchronous change is missed.
runtimeCtx.on('subagent/provider-added', (subagentProvider) => {
if (subagentProvider.name === config.provider && mounted === undefined) mount(subagentProvider)
})
runtimeCtx.on('subagent/provider-removed', (name) => {
if (name !== config.provider || mounted === undefined) return
mounted.disposeTool()
mounted = undefined
})
const present = runtimeCtx.subagents.getProvider(config.provider)
if (present !== undefined) {
mount(present)
} else {
// A backend fiber may activate later; a misspelled provider remains visible in this log.
runtimeCtx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? 'subagent'}" tool will register when it appears`)
}
runtimeCtx.systemPrompt.section({
name: `tool:${toolName}`,
order: runtimeCtx.systemPrompt.getSectionOrder('TOOL_SUBAGENT'),
text: (context) => {
if (mounted === undefined) return ''
const visible = [...delegationTools]
.filter(tool => runtimeCtx.tools.get(tool.name, context.scope) === tool)
.map(tool => tool.name)
.sort()
if (visible[0] !== toolName) return ''
const names = visible.map(name => `\`${name}\``).join(' or ')
return `Start independent delegations with ${names} together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn.`
const started = await ctx.subagents.startActivation({
provider: config.provider,
label: args.description,
request,
signal: exec.signal,
delivery: 'parent',
})
return { kind: 'activation' as const, subagentId: started.childId }
},
})
runtimeCtx.systemPrompt.context({
name: `tool:${toolName}:depth`,
order: runtimeCtx.systemPrompt.getContextOrder('SUBAGENT_DELEGATION'),
text: (context) => {
if (context.agent === undefined || mounted === undefined
|| runtimeCtx.tools.get(toolName, context.scope) !== mounted.definition) return ''
const maxDepth = runtimeCtx.subagents.resolveMaxDepth(config.maxDepth)
if (maxDepth === undefined) return ''
const depth = delegationDepthOf(context.agent)
if (depth < maxDepth) return ''
return `Delegation depth for \`${toolName}\`: current ${depth}, maximum ${maxDepth}, remaining levels 0.`
+ ` Do not call \`${toolName}\` at this depth; carry out the work directly.`
},
const disposeTool = ctx.effect(() => {
const unregister = ctx.tools.register(definition)
delegationTools.add(definition)
return () => {
delegationTools.delete(definition)
unregister()
}
})
// oxlint-disable-next-line typescript/no-misused-promises -- Tool and guidance disposal are synchronous.
mounted = { subagentProvider, definition, disposeTool }
}
const compositionScope = scopeOf(ctx)
if (!presets.configured || compositionScope === undefined) {
if (presets.configured) await presets.validate(ctx)
install(ctx)
return
// Register listeners before checking presence so no synchronous change is missed.
ctx.on('subagent/provider-added', (subagentProvider) => {
if (subagentProvider.name === config.provider && mounted === undefined) mount(subagentProvider)
})
ctx.on('subagent/provider-removed', (name) => {
if (name !== config.provider || mounted === undefined) return
mounted.disposeTool()
mounted = undefined
})
const present = ctx.subagents.getProvider(config.provider)
if (present !== undefined) {
mount(present)
} else {
// A backend fiber may activate later; a misspelled provider remains visible in this log.
ctx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? 'subagent'}" tool will register when it appears`)
}
let installation: Promise<void> | undefined
const prepare = async (): Promise<undefined> => {
await presets.validate(ctx)
if (installation === undefined) {
const fiber = ctx.plugin(install)
installation = fiber.await().then(() => undefined).catch(async (error: unknown) => {
await fiber.dispose()
installation = undefined
throw error
})
}
await installation
}
ctx.on('agent-preset/prepare', prepare)
// Direct Agent setup and inherited children also validate before publication.
ctx.on('agent/created', prepare)
ctx.systemPrompt.section({
name: `tool:${toolName}`,
order: ctx.systemPrompt.getSectionOrder('TOOL_SUBAGENT'),
text: (context) => {
if (mounted === undefined) return ''
const visible = [...delegationTools]
.filter(tool => ctx.tools.get(tool.name, context.scope) === tool)
.map(tool => tool.name)
.sort()
if (visible[0] !== toolName) return ''
const names = visible.map(name => `\`${name}\``).join(' or ')
return `Start independent delegations with ${names} together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn.`
},
})
ctx.systemPrompt.context({
name: `tool:${toolName}:depth`,
order: ctx.systemPrompt.getContextOrder('SUBAGENT_DELEGATION'),
text: (context) => {
if (context.agent === undefined || mounted === undefined
|| ctx.tools.get(toolName, context.scope) !== mounted.definition) return ''
const maxDepth = ctx.subagents.resolveMaxDepth(config.maxDepth)
if (maxDepth === undefined) return ''
const depth = delegationDepthOf(context.agent)
if (depth < maxDepth) return ''
return `Delegation depth for \`${toolName}\`: current ${depth}, maximum ${maxDepth}, remaining levels 0.`
+ ` Do not call \`${toolName}\` at this depth; carry out the work directly.`
},
})
}
@@ -1,6 +1,4 @@
/** Deployment-owned preset choices for one delegation tool. */
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
/** Optional default and model-selectable preset ids for a spawn tool. */
@@ -16,10 +14,8 @@ export interface PresetSelectionRequest {
readonly agent_preset?: unknown
}
/** Validates configured references and resolves model input against the allowlist. */
/** Validates configured choices and resolves model input against the allowlist. */
export class SubagentPresetSelection {
private descriptions: string[] = []
constructor(private readonly config: PresetSelectionConfig) {
const { agentPreset, allowedAgentPresets } = config
if (allowedAgentPresets === undefined) return
@@ -41,34 +37,12 @@ export class SubagentPresetSelection {
return this.config.allowedAgentPresets
}
/** Model-visible selection guidance and configured preset descriptions. */
/** Model-visible selection and default guidance. */
get description(): string {
const fallback = this.config.agentPreset === undefined
? 'Omit to inherit the parent agent\'s current preset.'
: `Omit to use ${this.config.agentPreset}.`
return `Agent preset for this child. ${fallback}`
+ (this.descriptions.length === 0 ? '' : ` Available presets: ${this.descriptions.join('; ')}`)
}
/**
* Read configured preset declarations before publishing the tool; mounting validates their activation.
* @param ctx - Tool composition containing the preset service.
*/
async validate(ctx: Context): Promise<void> {
const presets = ctx.get('agentPresets')
if (presets === undefined) throw new Error('tool-subagent: agent preset selection requires the agent-presets service')
const ids = new Set([
...this.config.agentPreset === undefined ? [] : [this.config.agentPreset],
...this.config.allowedAgentPresets ?? [],
])
const descriptions: string[] = []
for (const id of ids) {
const preset = await presets.readDocument(id)
if (this.choices?.includes(id) && preset.description !== undefined) {
descriptions.push(`${id}: ${preset.description}`)
}
}
this.descriptions = descriptions
}
/**
@@ -1,5 +1,9 @@
/** Deployment configuration and model-facing selection for spawn presets. */
import { mountWorkingDirectoryFixture } from '../../subagent/tests/working-directory-fixture.ts'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
@@ -8,7 +12,8 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import AgentPresets from '@deepseek-ai/dsh-agent-preset-registry'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { defineTool } from '@deepseek-ai/dsh-tools'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as tool from '../src/index.ts'
import { SubagentPresetSelection } from '../src/preset-selection.ts'
@@ -16,10 +21,22 @@ import { mountScriptedProvider, type Config as ProviderConfig } from './scripted
import { callSubagent, text } from './harness.ts'
const contexts: Context[] = []
const roots: string[] = []
afterEach(async () => {
for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose()
try {
for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose()
} finally {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
}
})
async function mountSpawn(ctx: Context, providerName: string): Promise<void> {
const root = mkdtempSync(join(tmpdir(), 'dsh-tool-preset-'))
roots.push(root)
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(SubagentSpawn, { providerName })
}
async function setup(config: Omit<tool.Config, 'provider'>, providerConfig: Partial<ProviderConfig> = {}) {
const ctx = new Context()
contexts.push(ctx)
@@ -57,7 +74,7 @@ describe('spawn preset selection', () => {
})
const schema = ctx.tools.schemas().find(schema => schema.name === 'subagent')
expect(JSON.stringify(schema)).toContain('agent_preset')
expect(JSON.stringify(schema)).toContain('reviewer persona')
expect(schema).toHaveProperty('parameters.properties.agent_preset.enum', ['writer', 'reviewer'])
expect(schema?.parameters.properties).toHaveProperty('agent_preset')
expect(Object.hasOwn(schema!.parameters.properties!, 'model')).toBe(modelSelection)
expect((await callSubagent(ctx, { ...args, agent_preset: 'reviewer' })).isError).not.toBe(true)
@@ -101,7 +118,6 @@ describe('spawn preset selection', () => {
{ allowedAgentPresets: ['writer', 'writer'] },
{ allowedAgentPresets: [''] },
{ agentPreset: 'writer', allowedAgentPresets: ['reviewer'] },
{ agentPreset: 'absent' },
{ agentPreset: '' },
])('rejects invalid deployment choices before registration: %j', async (config) => {
await expect(setup(config)).rejects.toThrow()
@@ -124,25 +140,39 @@ describe('spawn preset selection', () => {
const ctx = new Context()
contexts.push(ctx)
await expect(tool.apply(ctx, { provider: 'spawn', agentPreset: 'writer' })).rejects.toThrow(/requires the agent-presets service/)
const selection = new SubagentPresetSelection({ agentPreset: 'writer' })
await expect(selection.validate(ctx)).rejects.toThrow(/requires the agent-presets service/)
})
it('reads declared metadata for a broken preset and rejects mounting its composition', async () => {
const { ctx, presets } = await setup({})
it.each([
{ config: { agentPreset: 'absent' }, selection: {} },
{ config: { allowedAgentPresets: ['absent'] }, selection: { agent_preset: 'absent' } },
])('reports a missing preset only when delegation uses it: %j', async ({ config, selection }) => {
const { ctx, provider } = await setup(config)
await provider.dispose()
await mountSpawn(ctx, 'custom-spawn')
const parent = await ctx.agents.create({ sessionId: SessionId('missing-child-preset') })
const sessions = ctx.sessions.list()
const result = await callSubagent(ctx, { ...args, ...selection }, { agent: parent.agent })
expect(result.isError).toBe(true)
expect(text(result)).toContain('Unknown agent preset: absent')
expect(ctx.agents.list()).toEqual([parent.agent])
expect(ctx.sessions.list()).toEqual(sessions)
})
it('reports an unloadable child preset through the tool result', async () => {
const { ctx, presets, provider } = await setup({ agentPreset: 'reviewer' })
await provider.dispose()
await mountSpawn(ctx, 'custom-spawn')
await presets.get('reviewer')!()
await ctx.agentPresets.register({
id: 'reviewer', description: 'Review with unavailable tools.',
plugins: [{ name: 'cordis:missing-plugin' }],
id: 'reviewer', plugins: [{ name: 'cordis:missing-plugin' }],
})
const selection = new SubagentPresetSelection({ allowedAgentPresets: ['reviewer'] })
await selection.validate(ctx)
expect(selection.description).toContain('reviewer: Review with unavailable tools.')
await expect(ctx.agents.create({
sessionId: SessionId('broken-preset'),
setup: async (agentCtx) => { await ctx.agentPresets.mount(agentCtx, 'reviewer') },
})).rejects.toMatchObject({ code: 'agent-preset/invalid' })
expect(ctx.agents.list()).toEqual([])
const parent = await ctx.agents.create({ sessionId: SessionId('broken-child-preset') })
const sessions = ctx.sessions.list()
const result = await callSubagent(ctx, args, { agent: parent.agent })
expect(result.isError).toBe(true)
expect(text(result)).toContain('missing-plugin')
expect(ctx.agents.list()).toEqual([parent.agent])
expect(ctx.sessions.list()).toEqual(sessions)
})
it('creates an Agent from a preset whose delegation tool selects itself', async () => {
@@ -160,7 +190,6 @@ describe('spawn preset selection', () => {
})
const schema = ctx.tools.schemas(handle.agent).find(schema => schema.name === 'self_delegate')
expect(schema).toHaveProperty('parameters.properties.agent_preset.enum', ['self'])
expect(JSON.stringify(schema)).toContain('self: Self-selected composition.')
})
it.each([
@@ -185,31 +214,9 @@ describe('spawn preset selection', () => {
})
const schema = ctx.tools.schemas(handle.agent).find(schema => schema.name === 'peer_delegate')
expect(schema).toHaveProperty('parameters.properties.agent_preset.enum', [other])
expect(JSON.stringify(schema)).toContain(`${other}: ${other} composition.`)
}
})
it('rejects Agent publication when a preset delegation tool names an absent preset', async () => {
const { ctx } = await setup({})
await ctx.agentPresets.register({
id: 'missing-reference',
plugins: [{
name: 'cordis:subagentTool',
config: { provider: 'custom-spawn', toolName: 'missing_delegate', agentPreset: 'absent' },
}],
})
const published = vi.fn()
ctx.on('agent/created', published)
await expect(ctx.agents.create({
sessionId: SessionId('unpublished-preset'),
setup: async (agentCtx) => { await ctx.agentPresets.mount(agentCtx, 'missing-reference') },
})).rejects.toThrow('Unknown agent preset: absent')
expect(published).not.toHaveBeenCalled()
expect(ctx.agents.list()).toEqual([])
expect(ctx.sessions.list()).toEqual([])
expect(ctx.tools.schemas().map(schema => schema.name)).not.toContain('missing_delegate')
})
it('shares each preset tool across Agents and repeated round trips', async () => {
const { ctx } = await setup({})
const declarations = new Map<string, () => Promise<void>>()
@@ -253,89 +260,39 @@ describe('spawn preset selection', () => {
expect(ctx.tools.get('shared_delegate', ptc.agent)).toBe(expected.ptc)
})
it('rechecks references before publishing a child inheriting a prepared preset', async () => {
const { ctx, presets } = await setup({})
await ctx.agentPresets.register({ id: 'parent', plugins: [{
name: 'cordis:subagentTool', config: {
provider: 'custom-spawn', toolName: 'child_delegate', agentPreset: 'reviewer',
},
}] })
const parent = await ctx.agents.create({
sessionId: SessionId('reference-parent'),
setup: async (agentCtx) => { await ctx.agentPresets.mount(agentCtx, 'parent') },
})
await presets.get('reviewer')!()
await expect(ctx.agents.create({
sessionId: SessionId('reference-child'),
setup: (agentCtx) => { ctx.agentPresets.composeFrom(agentCtx, parent.agent.ctx) },
})).rejects.toThrow('Unknown agent preset: reviewer')
expect(ctx.agents.get(SessionId('reference-child'))).toBeUndefined()
expect(ctx.tools.get('child_delegate', parent.agent)).toBeDefined()
})
it('keeps the current preset on a missing reference and retries after declaration', async () => {
it('switches to a preset with a missing child reference and retries delegation after declaration', async () => {
const { ctx } = await setup({})
await mountSpawn(ctx, 'spawn')
await ctx.agentPresets.register({
id: 'pending-reference',
plugins: [{ name: 'cordis:subagentTool', config: {
provider: 'custom-spawn', toolName: 'pending_delegate', agentPreset: 'later',
provider: 'spawn', agentPreset: 'later',
} }],
})
const lease = await ctx.agentPresets.acquireScope('pending-reference')
const definition = ctx.tools.get('subagent', lease.key)
expect(definition).toBeDefined()
await lease[Symbol.asyncDispose]()
const handle = await ctx.agents.create({
sessionId: SessionId('retry-preset'),
agentOptions: { provider: 'mock', model: 'mock' },
setup: async (agentCtx) => { await ctx.agentPresets.mount(agentCtx, 'writer') },
})
await expect(ctx.agentPresets.select(handle.agent, 'pending-reference'))
.rejects.toThrow('Unknown agent preset: later')
expect(ctx.agentPresets.composedPreset(handle.agent.ctx)).toBe('writer')
expect(ctx.agentPresets.presetForSession(handle.agent.session)).toBeNull()
expect(ctx.tools.get('pending_delegate', handle.agent)).toBeUndefined()
await ctx.agentPresets.register({ id: 'later', plugins: [] })
await ctx.agentPresets.select(handle.agent, 'pending-reference')
expect(ctx.tools.get('pending_delegate', handle.agent)).toBeDefined()
expect(ctx.tools.get('subagent', handle.agent)).toBe(definition)
expect(ctx.agentPresets.presetForSession(handle.agent.session)).toBe('pending-reference')
})
const failed = await callSubagent(ctx, args, { agent: handle.agent })
expect(failed.isError).toBe(true)
expect(text(failed)).toContain('Unknown agent preset: later')
it('cleans up a failed shared installation before retrying registration', async () => {
const { ctx } = await setup({})
const blockers: Array<() => void> = []
ctx.loader.builtins.blockDelegate = (owner: Context) => {
blockers.push(owner.tools.register(defineTool({
name: 'collision_delegate', description: 'Conflicting configured tool.', parameters: {},
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
execute: () => Promise.resolve('blocked'),
})))
}
await ctx.agentPresets.register({
id: 'collision', plugins: [
{ name: 'cordis:blockDelegate', inject: ['tools'] },
{ name: 'cordis:subagentTool', config: {
provider: 'custom-spawn', toolName: 'collision_delegate', agentPreset: 'reviewer',
} },
],
})
const handle = await ctx.agents.create({
sessionId: SessionId('retry-installation'),
setup: async (agentCtx) => { await ctx.agentPresets.mount(agentCtx, 'writer') },
})
await expect(ctx.agentPresets.select(handle.agent, 'collision')).rejects.toThrow('already registered')
expect(ctx.agentPresets.composedPreset(handle.agent.ctx)).toBe('writer')
expect(blockers).toHaveLength(1)
blockers[0]!()
await ctx.agentPresets.select(handle.agent, 'collision')
const schema = ctx.tools.schemas(handle.agent).find(tool => tool.name === 'collision_delegate')
expect(schema?.parameters.properties).toHaveProperty('prompt')
await ctx.agentPresets.register({ id: 'later', plugins: [] })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('reviewed')]))
// Completion notices must not consume the response scripted for the child.
ctx.on('agent/pre-step', async ({ agent }, next) => agent === handle.agent ? { kind: 'reject' } : next())
const retried = await callSubagent(ctx, args, { agent: handle.agent })
expect(retried.isError).not.toBe(true)
expect(text(retried)).toContain('started subagent')
await ctx.subagents.waitForChildren(handle.agent)
expect(ctx.tools.get('subagent', handle.agent)).toBe(definition)
})
it('can advertise a preset without a description', async () => {
const { ctx, presets } = await setup({})
await presets.get('reviewer')!()
await ctx.agentPresets.register({ id: 'reviewer', plugins: [] })
const selection = new SubagentPresetSelection({ allowedAgentPresets: ['reviewer'] })
await selection.validate(ctx)
expect(selection.description).not.toContain('Available presets:')
})
})
@@ -29,9 +29,6 @@
{
"path": "../../session/session-projection"
},
{
"path": "../../core/scope"
},
{
"path": "../../llm/llm"
},
-3
View File
@@ -15770,9 +15770,6 @@ importers:
'@deepseek-ai/dsh-llm':
specifier: workspace:*
version: link:../../llm/llm
'@deepseek-ai/dsh-scope':
specifier: workspace:*
version: link:../../core/scope
'@deepseek-ai/dsh-session':
specifier: workspace:*
version: link:../../core/session
@@ -10,7 +10,7 @@
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:10}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your parent agent id is \"{{session:1}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{session:1}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `reviewer_delegate`: current 1, maximum 1, remaining levels 0. Do not call `reviewer_delegate` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:reviewer_delegate:depth","text":"Delegation depth for `reviewer_delegate`: current 1, maximum 1, remaining levels 0. Do not call `reviewer_delegate` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nDelegation depth for `reviewer_delegate`: current 1, maximum 1, remaining levels 0. Do not call `reviewer_delegate` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"tool:reviewer_delegate:depth","text":"Delegation depth for `reviewer_delegate`: current 1, maximum 1, remaining levels 0. Do not call `reviewer_delegate` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[10],"source":{"kind":"fallback"}}}
@@ -304,7 +304,7 @@
},
"agent_preset": {
"type": "string",
"description": "Agent preset for this child. Omit to inherit the parent agent's current preset. Available presets: reviewer: File review.",
"description": "Agent preset for this child. Omit to inherit the parent agent's current preset.",
"enum": [
"reviewer"
]
@@ -373,7 +373,7 @@
},
"agent_preset": {
"type": "string",
"description": "Agent preset for this child. Omit to inherit the parent agent's current preset. Available presets: reviewer: File review.",
"description": "Agent preset for this child. Omit to inherit the parent agent's current preset.",
"enum": [
"reviewer"
]
@@ -341,7 +341,7 @@
},
"agent_preset": {
"type": "string",
"description": "Agent preset for this child. Omit to inherit the parent agent's current preset. Available presets: reviewer: File review.",
"description": "Agent preset for this child. Omit to inherit the parent agent's current preset.",
"enum": [
"reviewer"
]
@@ -10,7 +10,7 @@
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:10}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[10],"source":{"kind":"fallback"}}}
@@ -10,7 +10,7 @@
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:10}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."},{"type":"text","text":"Your parent agent id is \"{{session:1}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{session:1}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{message:11}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Reply with exactly the word","messageSeqs":[10],"source":{"kind":"fallback"}}}