refactor(subagent): flatten activation task options

This commit is contained in:
Dudu-0223
2026-10-22 18:05:13 +08:00
parent 5b1a588284
commit 575606a356
60 changed files with 229 additions and 179 deletions
@@ -101,10 +101,8 @@ describe('web e2e: sidebar subagent activity', () => {
provider: 'spawn',
label: 'sidebar activity child',
signal: new AbortController().signal,
request: {
prompt: [{ type: 'text', text: 'Hold this delegated task open.' }],
parent: parentHandle.agent,
},
prompt: [{ type: 'text', text: 'Hold this delegated task open.' }],
parent: parentHandle.agent,
})
childId = started.childId
await waitForRunningChild(scaffold, adapter, childId)
+2 -4
View File
@@ -152,10 +152,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
provider: 'spawn',
label: LABEL,
signal: new AbortController().signal,
request: {
prompt: [{ type: 'text', text: INITIAL_PROMPT }],
parent,
},
prompt: [{ type: 'text', text: INITIAL_PROMPT }],
parent,
})
childId = started.childId
await waitForAgentToSettle(scaffold, childId)
@@ -48,7 +48,7 @@ it.skipIf(MODE === 'record')('replays inherited schedule tools and refusals in a
const activation = await scaffold.ctx.subagents.startActivation({
provider: 'fork', label: 'Check child reminder refusal', delivery: 'caller',
signal: new AbortController().signal,
request: { parent, prompt: [...childPrompt.data.content] },
parent, prompt: [...childPrompt.data.content],
})
try {
await activation.result
+1 -1
View File
@@ -153,7 +153,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
provider: 'spawn',
label: LABEL,
signal: new AbortController().signal,
request: { prompt: [{ type: 'text', text: INITIAL }], parent },
prompt: [{ type: 'text', text: INITIAL }], parent,
})
childId = started.childId
await waitFor(() => existsSync(readyFile), 'the held child turn to open')
+1 -1
View File
@@ -113,7 +113,7 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByUser over the
provider: 'spawn',
label: 'event-sourcing researcher',
signal: new AbortController().signal,
request: { prompt: [{ type: 'text', text: INITIAL }], parent },
prompt: [{ type: 'text', text: INITIAL }], parent,
})
childId = started.childId
// The hang entry writes readyFile after its prefix chunks, immediately
+2 -2
View File
@@ -45,5 +45,5 @@
en: 85b38f78d87fc88f
zh: 00d5eb625e7dfb61
/development-guide/contributor-reference/documenting-types-verbatim-ts-type-equiv:
en: 9d578b6797be921b
zh: 823cbfcfd9a75c17
en: 6b9b948e0b80cf76
zh: 2642f4127ed9b719
+1 -1
View File
@@ -214,4 +214,4 @@ The [subsystems](subsystems/README.md) pages paste source-equivalent declaration
{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.
`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. Upgrade guides in version directories older than the root package version are frozen; their examples are excluded from current-API compilation. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.
+1 -1
View File
@@ -221,4 +221,4 @@ Web 命令后面的参数会传给 `dsh web`,例如 `pnpm run dev:web --no-ope
{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例的计算之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。
`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例的计算之外。版本目录早于根包版本的升级指南已冻结,其中的示例不参与当前 API 的编译检查。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。
+2 -2
View File
@@ -6,8 +6,8 @@
en: 6af28b3c7e92cf9f
zh: 210f2c490dc23d3c
/subagents/capabilities-and-activation-requests:
en: 8c92b131f67997f6
zh: c2dc606a0859420c
en: f09270fcd7bfa26a
zh: 082664920b3c65e2
/subagents/local-children-and-activations:
en: 6f87231d18d82916
zh: 9f064b23058f8766
+4 -7
View File
@@ -8,7 +8,7 @@ The unified activation API manages local conversations and external executions.
Every backend uses startActivation. Capability flags validate request options; prepareContinuable selects local Agent creation, while start selects one external execution. The model-facing tool uses parent delivery. Workflow and code mode consumers can await caller results without injecting another parent message. Historical children with unavailable descriptors remain discoverable as `mode: 'unknown'` without granting continuation capabilities; the [package README](../../packages/subagent/subagent/README.md) defines catalog persistence.
Optional `request.cwd` selects the child's initial directory; relative paths resolve against the parent's current effective directory. Omitting it captures that directory at admission. Local child Sessions retain the parent's original project, and cold resume retains their logged directory state.
Optional `cwd` selects the child's initial directory; relative paths resolve against the parent's current effective directory. Omitting it captures that directory at admission. Local child Sessions retain the parent's original project, and cold resume retains their logged directory state.
```ts type-equiv
/**
@@ -28,15 +28,13 @@ interface SubagentCapabilities {
```ts type-equiv
/** One managed child execution and its result delivery policy. */
interface SubagentActivationSpec {
interface SubagentActivationSpec extends Omit<SubagentStartRequest, 'label' | 'signal'> {
/** Registered backend to use. */
readonly provider: string
/** Short task label retained in cataloged child membership. */
readonly label: string
/** Optional reserved identity for a local child; external backends allocate their own ids. */
readonly childId?: SessionId
/** Task, parent, and backend-supported execution options. */
readonly request: Omit<SubagentStartRequest, 'label' | 'signal'>
/** Cancellation before publication; callers own later cancellation through dispose. */
readonly signal: AbortSignal
/** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */
@@ -63,9 +61,8 @@ interface SubagentActivation {
```ts type-equiv
/**
* Task and optional capabilities supplied to a backend. startActivation carries
* these fields in request while owning its label, cancellation, and delivery
* policy separately.
* Task and optional capabilities supplied to a backend. The activation spec
* shares these task options and defines its own label and cancellation policy.
*/
interface SubagentStartRequest {
/** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */
+4 -7
View File
@@ -8,7 +8,7 @@
所有后端都使用 startActivation。能力标记校验请求选项;prepareContinuable 选择本地 Agent 创建,start 选择一次外部执行。面向模型的工具采用 parent 回传。Workflow 与 code mode 消费者可以等待 caller 结果,不额外向父代理注入消息。历史子会话的 descriptor 不可用时,以 `mode: 'unknown'` 保持可发现性,但不授予继续执行能力;[包 README](../../packages/subagent/subagent/README.zh.md) 定义目录持久化语义。
可选的 `request.cwd` 指定子级的初始目录;相对路径以父级当前有效目录为基准解析。省略时,在准入阶段捕获该目录。本地子 Session 保留父级的原始项目,冷恢复保留其日志中的目录状态。
可选的 `cwd` 指定子级的初始目录;相对路径以父级当前有效目录为基准解析。省略时,在准入阶段捕获该目录。本地子 Session 保留父级的原始项目,冷恢复保留其日志中的目录状态。
```ts type-equiv
/**
@@ -28,15 +28,13 @@ interface SubagentCapabilities {
```ts type-equiv
/** One managed child execution and its result delivery policy. */
interface SubagentActivationSpec {
interface SubagentActivationSpec extends Omit<SubagentStartRequest, 'label' | 'signal'> {
/** Registered backend to use. */
readonly provider: string
/** Short task label retained in cataloged child membership. */
readonly label: string
/** Optional reserved identity for a local child; external backends allocate their own ids. */
readonly childId?: SessionId
/** Task, parent, and backend-supported execution options. */
readonly request: Omit<SubagentStartRequest, 'label' | 'signal'>
/** Cancellation before publication; callers own later cancellation through dispose. */
readonly signal: AbortSignal
/** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */
@@ -63,9 +61,8 @@ interface SubagentActivation {
```ts type-equiv
/**
* Task and optional capabilities supplied to a backend. startActivation carries
* these fields in request while owning its label, cancellation, and delivery
* policy separately.
* Task and optional capabilities supplied to a backend. The activation spec
* shares these task options and defines its own label and cancellation policy.
*/
interface SubagentStartRequest {
/** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/subagent-activation-options/guide.md
/:
en: b087162d9a24f772
zh: a32f167c3529fb1c
/subagent-activation-options:
en: 20a5b01ef11bff72
zh: 919de21bc865ee9e
/subagent-activation-options/change:
en: 7cc2554992e98c53
zh: 434540bcd037d1f5
/subagent-activation-options/migration:
en: 2243d08661eb457c
zh: a928a8e7fb3515ff
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: "SubagentActivationSpec accepts task options directly instead of a nested request object."
---
# Subagent activation options
English | [中文](guide.zh.md)
## Change
Plugin code calling `ctx.subagents.startActivation()` must pass `prompt`, `parent`, and optional execution settings directly on `SubagentActivationSpec`. The nested `request` field is removed. Type names, provider requests, cancellation, and result delivery are unchanged.
## Migration
1. In each `startActivation()` call, move the fields from `request` to the outer object. For an existing task object, replace `request: task` with `...task`; keep the activation's `label` and `signal` after the spread so they take precedence.
2. Replace reads such as `spec.request.prompt` with `spec.prompt`. Keep backend `SubagentStartRequest` implementations unchanged.
3. Typecheck the plugin and run its delegation tests to verify the updated calls.
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: "SubagentActivationSpec 直接接受任务选项,不再使用嵌套的 request 对象。"
---
# 子代理 activation 选项
[English](guide.md) | 中文
## 变更
调用 `ctx.subagents.startActivation()` 的插件代码必须将 `prompt`、`parent` 和可选执行设置直接放在 `SubagentActivationSpec` 上。嵌套的 `request` 字段已移除。类型名称、提供方请求、取消和结果投递保持不变。
## 迁移
1. 在每个 `startActivation()` 调用中,将 `request` 内的字段移到外层对象。已有任务对象时,将 `request: task` 替换为 `...task`;将 activation 的 `label` 和 `signal` 放在展开操作之后,以确保它们优先生效。
2. 将 `spec.request.prompt` 等读取替换为 `spec.prompt`。保持后端的 `SubagentStartRequest` 实现不变。
3. 对插件运行类型检查和委派测试,验证更新后的调用。
+1 -1
View File
@@ -50,7 +50,7 @@ async function installDelegationTool(ctx: BridgeHarness['ctx']): Promise<() => b
if (exec.agent === undefined) throw new Error('delegation fixture requires an Agent')
const child = await ctx.subagents.startActivation({
provider: 'local-test', label: 'later child', delivery: 'caller', signal: exec.signal,
request: { parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }] },
parent: exec.agent, prompt: [{ type: 'text', text: 'later child task' }],
})
const result = await child.result
delegated = result.stopReason === 'completed'
@@ -54,7 +54,7 @@ async function harness() {
const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const run = await ctx.subagents.startActivation({
provider: 'test', label: 'access target', delivery: 'caller',
request: { parent, prompt: [{ type: 'text', text: 'hold' }] },
parent, prompt: [{ type: 'text', text: 'hold' }],
signal: new AbortController().signal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -381,7 +381,7 @@ describe('headless runner', () => {
if (turns === 1) {
await test.ctx.subagents.startActivation({
provider: 'external', label: 'work', delivery: 'parent', signal: new AbortController().signal,
request: { parent, prompt: [] },
parent, prompt: [],
})
await cleaning.promise
appendTurn(session, 1, message, 'waiting for child', true)
@@ -120,7 +120,7 @@ for (const backend of backends) {
const started = await first.ctx.subagents.startActivation({
provider: 'spawn',
label: 'restart worker',
request: { prompt: [{ type: 'text', text: 'finish before restart' }], parent: firstLead },
prompt: [{ type: 'text', text: 'finish before restart' }], parent: firstLead,
signal: SIGNAL,
delivery: 'caller',
})
@@ -77,7 +77,7 @@ async function spawn(ctx: Context, lead: Agent, description: string | undefined,
const started = await ctx.subagents.startActivation({
provider,
label: `${description ?? 'shared'} responsibility`,
request: { prompt: content(`${description ?? 'shared'} initial`), parent: lead },
prompt: content(`${description ?? 'shared'} initial`), parent: lead,
signal: SIGNAL,
delivery: 'parent',
})
@@ -151,7 +151,7 @@ describe('Tree-owned task boards', () => {
const unnamed = await waitRunning(ctx, await spawn(ctx, lead, undefined))
const grandchildId = (await ctx.subagents.startActivation({
provider: 'spawn', label: 'nested', delivery: 'caller', signal: SIGNAL,
request: { prompt: content('nested'), parent: named },
prompt: content('nested'), parent: named,
})).childId
const grandchild = await waitRunning(ctx, grandchildId)
@@ -129,7 +129,7 @@ async function spawn(ctx: Context, lead: Agent, description: string | undefined,
const started = await ctx.subagents.startActivation({
provider,
label: `${description ?? 'shared'} task`,
request: { prompt: [{ type: 'text', text: prompt }], parent: lead },
prompt: [{ type: 'text', text: prompt }], parent: lead,
signal: SIGNAL,
delivery: 'parent',
})
@@ -88,11 +88,9 @@ export function apply(ctx: Context, config: Config): void {
const activation = await ctx.subagents.startActivation({
provider: providerName,
label: args.description,
request: {
parent,
prompt: [{ type: 'text', text: args.prompt }],
...args.cwd === undefined ? {} : { cwd: args.cwd },
},
parent,
prompt: [{ type: 'text', text: args.prompt }],
...args.cwd === undefined ? {} : { cwd: args.cwd },
signal: exec.signal,
delivery: 'parent',
})
@@ -8092,7 +8092,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SubagentActivationSpec',
declaration: 'export interface SubagentActivationSpec {\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly request: Omit<SubagentStartRequest, \'label\' | \'signal\'>;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}',
declaration: 'export interface SubagentActivationSpec extends Omit<SubagentStartRequest, \'label\' | \'signal\'> {\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}',
},
{
name: 'SubagentCapabilities',
@@ -72,7 +72,7 @@ try {
childId: SessionId("built-child"),
label: "built scope carrier",
delivery: "caller",
request: { parent: parent.agent, prompt: [{ type: "text", text: "Use the unavailable test model." }] },
parent: parent.agent, prompt: [{ type: "text", text: "Use the unavailable test model." }],
signal: new AbortController().signal,
});
await run.result;
+1 -1
View File
@@ -51,7 +51,7 @@ describe('SDK descendant shutdown', () => {
gates.set(id, gate)
const receipt = await ctx.subagents.startActivation({
provider: 'local-test', childId: id, label: 'parked child', delivery: 'caller', signal: new AbortController().signal,
request: { parent, prompt: [{ type: 'text', text: 'work' }] },
parent, prompt: [{ type: 'text', text: 'work' }],
})
receipts.push(receipt)
await gate.entered.promise
+2 -2
View File
@@ -66,7 +66,7 @@ describe('SDK session/wait with parked child input', () => {
for (let level = 0; level < depth; level++) {
const started = await ctx.subagents.startActivation({
provider: 'local-test', label: 'interruptible child', delivery: 'caller', signal: new AbortController().signal,
request: { parent: owner, prompt: [{ type: 'text', text: 'work' }] },
parent: owner, prompt: [{ type: 'text', text: 'work' }],
})
closeChildren ??= () => started.dispose()
child = ctx.agents.get(started.childId)!
@@ -118,7 +118,7 @@ describe('SDK session/wait failures', () => {
const warnings = vi.spyOn(ctx.logger, 'warn')
const child = await ctx.subagents.startActivation({
provider: 'local-test', label: 'child failure', delivery: 'caller', signal: new AbortController().signal,
request: { parent, prompt: [{ type: 'text', text: 'delegated task' }] },
parent, prompt: [{ type: 'text', text: 'delegated task' }],
})
await expect(child.result).resolves.toMatchObject({ stopReason: 'error' })
expect(observed).toEqual([failure])
@@ -9,8 +9,8 @@
en: c949befcfe1e376c
zh: 13c78d0fbbd323ae
/deepseek-ai-dsh-subagent-spawn-in-process/summary:
en: 84d0a0acfc4251fe
zh: 8480ddf68e377522
en: 36ecbffd1d9ac412
zh: 609a2d30499785fc
/deepseek-ai-dsh-subagent-spawn-in-process/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
`dsh-subagent-spawn-in-process` is an in-process subagent backend: it runs each delegated task in a fresh child agent that shares this process and its agent factory, LLM, and tool services. The child starts with an empty conversation, so a task prompt must stand alone; it inherits the parent's working directory, session lineage, provider, model, reasoning effort, and output-token limit unless `request.agentOptions` overrides them. A delegation tool or API call reaches it under the `spawn` provider name. Choose it for the cheapest delegation transport; choose the fork backend when the child must build on the parent's completed conversation turns.
`dsh-subagent-spawn-in-process` is an in-process subagent backend: it runs each delegated task in a fresh child agent that shares this process and its agent factory, LLM, and tool services. The child starts with an empty conversation, so a task prompt must stand alone; it inherits the parent's working directory, session lineage, provider, model, reasoning effort, and output-token limit unless `agentOptions` overrides them. A delegation tool or API call reaches it under the `spawn` provider name. Choose it for the cheapest delegation transport; choose the fork backend when the child must build on the parent's completed conversation turns.
## Table of Contents
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
`dsh-subagent-spawn-in-process` 是一个进程内 subagent 后端:它在当前进程中运行每个委派任务,子 agent(智能体)是一个全新子 `Agent`,复用宿主的 agent 工厂及 LLM(大语言模型)/工具服务。子 agent 以空对话开始,因此任务提示词必须自足;除非 `request.agentOptions` 覆盖,否则它继承父 agent 的工作目录、会话谱系、提供方、模型、推理强度与输出 token 上限。委派工具或 API 调用以 `spawn` 提供方名称找到它。需要成本最低的委派传输时选择它;需要子 agent 建立在父级已完成对话轮次之上时,请选择 fork 后端。
`dsh-subagent-spawn-in-process` 是一个进程内 subagent 后端:它在当前进程中运行每个委派任务,子 agent(智能体)是一个全新子 `Agent`,复用宿主的 agent 工厂及 LLM(大语言模型)/工具服务。子 agent 以空对话开始,因此任务提示词必须自足;除非 `agentOptions` 覆盖,否则它继承父 agent 的工作目录、会话谱系、提供方、模型、推理强度与输出 token 上限。委派工具或 API 调用以 `spawn` 提供方名称找到它。需要成本最低的委派传输时选择它;需要子 agent 建立在父级已完成对话轮次之上时,请选择 fork 后端。
## 目录
+2 -2
View File
@@ -27,8 +27,8 @@
en: 4b40bd380bea81ce
zh: 896a584190981168
/deepseek-ai-dsh-subagent/use-this-package/local-and-external-children:
en: 013644290e74493e
zh: 05014c3c3773edeb
en: d418617831da8105
zh: 049116154b1c2080
/deepseek-ai-dsh-subagent/use-this-package/child-presets:
en: a740e5e9bdd661d9
zh: 712a5458fc8b3615
+1 -1
View File
@@ -58,7 +58,7 @@ At capacity, creation or cold resume rejects with `ACTIVATION_LIMIT_REACHED` (br
### Local and external children
`startActivation({ provider, label, request, signal, delivery })` returns `{ childId, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `request.agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query.
`startActivation({ provider, label, prompt, parent, signal, delivery })` returns `{ childId, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query.
### Child presets
+1 -1
View File
@@ -58,7 +58,7 @@ kind: "package-reference"
### 本地与外部子级
`startActivation({ provider, label, request, signal, delivery })` 返回 `{ childId, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息并支持冷恢复;外部子级只暴露一次执行,拒绝继续输入。`request.agentOptions` 覆盖后端支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 让工作流等待结果,不发送父级通知,也不附加初始返回指导。本地父级投递需要持久化,调用方投递可以使用临时本地会话;冷恢复需要持久化与 Session 查询服务。
`startActivation({ provider, label, prompt, parent, signal, delivery })` 返回 `{ childId, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息并支持冷恢复;外部子级只暴露一次执行,拒绝继续输入。`agentOptions` 覆盖后端支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 让工作流等待结果,不发送父级通知,也不附加初始返回指导。本地父级投递需要持久化,调用方投递可以使用临时本地会话;冷恢复需要持久化与 Session 查询服务。
### 子级 preset
+4 -4
View File
@@ -278,13 +278,13 @@ export class SubagentRuntime extends TypertRemoteService {
if (spec.childId !== undefined && provider.prepareContinuable === undefined) {
throw new SubagentError('reserved child ids require a local backend', 'UNSUPPORTED_CAPABILITY')
}
assertSubagentMaxDepth(spec.request.maxDepth)
this.assertCapabilities(provider, { ...spec.request, signal: spec.signal })
assertSubagentMaxDepth(spec.maxDepth)
this.assertCapabilities(provider, spec)
const manager = this.requireManager()
if (this.ctx.get('agents')?.get(spec.request.parent.id) !== spec.request.parent) {
if (this.ctx.get('agents')?.get(spec.parent.id) !== spec.parent) {
throw new SubagentError('subagent creation requires the exact live parent agent', 'UNAUTHORIZED')
}
if (spec.request.outputSchema !== undefined) assertObjectJsonSchema(spec.request.outputSchema)
if (spec.outputSchema !== undefined) assertObjectJsonSchema(spec.outputSchema)
return provider.prepareContinuable === undefined
? manager.startExternal(spec)
: manager.startLocal(spec)
+18 -18
View File
@@ -320,19 +320,18 @@ export class SubagentManager {
* @returns the durable child id and accepted initial prompt message id.
*/
async startLocal(spec: SubagentActivationSpec): Promise<SubagentActivation & { messageId: MessageId }> {
const request = spec.request
const parent = request.parent
const parent = spec.parent
this.assertAdmitting(parent)
const ancestry = this.ancestryForChild(parent)
return this.trackStartup(ancestry.root, spec.signal, async (signal) => {
const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence()
assertSubagentMaxDepth(request.maxDepth)
assertSubagentMaxDepth(spec.maxDepth)
const childId = spec.childId ?? brandString<SessionId>(randomUUID())
this.assertChildIdAvailable(childId)
const childDepth = resolveChildDepth(parent, request.maxDepth)
const childDepth = resolveChildDepth(parent, spec.maxDepth)
// Snapshot before any await: invalid descriptor JSON rejects the call
// before a child exists, and the detached value is what reaches the log.
const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth)
const agentOptions = resolveChildAgentOptions(parent, spec.agentOptions, childDepth)
const agentProvider = agentOptions.provider
const agentModel = agentOptions.model
const agentReasoningEffort = agentOptions.reasoningEffort
@@ -348,7 +347,7 @@ export class SubagentManager {
// parent's future, not to this child.
const delegatedPolicies = captureDelegatedPolicyOverrides(parent)
const cwd = await this.resolveDirectory(request, signal)
const cwd = await this.resolveDirectory(spec, signal)
const prepared = await this.host.prepareContinuable(spec.provider, {
sessionId: childId,
cwd,
@@ -383,16 +382,16 @@ export class SubagentManager {
parent,
cwd,
seed,
meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined, request.agentPreset),
meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined, spec.agentPreset),
inheritedEventCount,
delegatedPolicies,
descriptor,
},
agentOptions,
composition: { agentPreset: request.agentPreset },
composition: { agentPreset: spec.agentPreset },
signal,
delivery: spec.delivery,
outputSchema: spec.request.outputSchema,
outputSchema: spec.outputSchema,
})
established = activation
const child = requireLocalActivation(activation).handle.agent
@@ -400,8 +399,8 @@ export class SubagentManager {
return await this.submitMaterialized(
activation,
spec.delivery !== 'caller' && isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', child))
? withContinuableReturnGuidance(parent.id, request.prompt)
: request.prompt,
? withContinuableReturnGuidance(parent.id, spec.prompt)
: spec.prompt,
{ source: { kind: 'user' }, signal, delivery: 'queue', authorize: () => { this.assertAdmitting(parent) } },
ancestry.root,
() => {
@@ -420,28 +419,29 @@ export class SubagentManager {
* @returns the accepted activation and its result/disposal capabilities.
*/
async startExternal(spec: SubagentActivationSpec): Promise<SubagentActivation> {
const parent = spec.request.parent
const { provider, delivery, childId: _childId, ...request } = spec
const parent = request.parent
this.assertAdmitting(parent)
const pendingId = brandString<SessionId>(randomUUID())
const ancestry = this.ancestryForChild(parent)
return this.trackStartup(ancestry.root, spec.signal, async (signal) => {
let activation: Activation | undefined
try {
const cwd = await this.resolveDirectory(spec.request, signal)
const cwd = await this.resolveDirectory(spec, signal)
activation = await this.materialize({
kind: 'external',
childId: pendingId,
provider: spec.provider,
provider,
...ancestry,
signal,
delivery: spec.delivery,
start: signal => this.host.startExternal(spec.provider, {
...spec.request, cwd, label: spec.label, signal,
delivery,
start: signal => this.host.startExternal(provider, {
...request, cwd, signal,
}),
})
this.assertAdmitting(parent)
signal.throwIfAborted()
if (spec.delivery === 'parent') {
if (delivery === 'parent') {
establishExternalCatalogChild(parent.session, activation.childId, spec.label)
}
this.accept(activation)
+3 -6
View File
@@ -26,15 +26,13 @@ export function SubagentRunId(id: string): SubagentRunId {
}
/** One managed child execution and its result delivery policy. */
export interface SubagentActivationSpec {
export interface SubagentActivationSpec extends Omit<SubagentStartRequest, 'label' | 'signal'> {
/** Registered backend to use. */
readonly provider: string
/** Short task label retained in cataloged child membership. */
readonly label: string
/** Optional reserved identity for a local child; external backends allocate their own ids. */
readonly childId?: SessionId
/** Task, parent, and backend-supported execution options. */
readonly request: Omit<SubagentStartRequest, 'label' | 'signal'>
/** Cancellation before publication; callers own later cancellation through dispose. */
readonly signal: AbortSignal
/** Parent delivery notifies the model; caller delivery only returns the result. Local children always enter the parent catalog. */
@@ -121,9 +119,8 @@ export interface SubagentCapabilities {
}
/**
* Task and optional capabilities supplied to a backend. startActivation carries
* these fields in request while owning its label, cancellation, and delivery
* policy separately.
* Task and optional capabilities supplied to a backend. The activation spec
* shares these task options and defines its own label and cancellation policy.
*/
export interface SubagentStartRequest {
/** Initial child directory; relative paths resolve against the parent's current directory. Omitted inherits that directory at start. */
@@ -63,7 +63,7 @@ function startSpec(parent: Agent, provider = 'spawn') {
return {
provider,
label: 'child task',
request: { prompt: [{ type: 'text' as const, text: 'child task' }], parent },
prompt: [{ type: 'text' as const, text: 'child task' }], parent,
signal: new AbortController().signal,
}
}
@@ -61,7 +61,7 @@ it('continues the parent through default Messages after a reasoning-bearing cont
delivery: 'parent',
provider: 'spawn',
label: 'child task',
request: { parent, prompt: [{ type: 'text', text: 'child task' }] },
parent, prompt: [{ type: 'text', text: 'child task' }],
signal: new AbortController().signal,
})
await settled.promise
@@ -83,11 +83,9 @@ function startSpec(parent: Agent, agentPreset?: string, provider = 'separate') {
provider,
label: 'preset work',
delivery: 'parent' as const,
request: {
parent,
prompt: [{ type: 'text' as const, text: 'work' }],
...(agentPreset === undefined ? {} : { agentPreset }),
},
parent,
prompt: [{ type: 'text' as const, text: 'work' }],
...(agentPreset === undefined ? {} : { agentPreset }),
signal: new AbortController().signal,
}
}
@@ -123,7 +123,7 @@ function startSpec(parent: Agent, provider = 'spawn', signal: AbortSignal = test
return {
provider,
label: 'child task',
request: { prompt: [{ type: 'text' as const, text: 'child task' }], parent },
prompt: [{ type: 'text' as const, text: 'child task' }], parent,
signal,
}
}
@@ -630,11 +630,9 @@ describe('SubagentRuntime.startActivation', () => {
const started = await ctx.subagents.startActivation({
delivery: 'parent',
...startSpec(parent),
request: {
prompt: message('selected reasoning'),
parent,
agentOptions: { reasoningEffort: effort },
},
prompt: message('selected reasoning'),
parent,
agentOptions: { reasoningEffort: effort },
})
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
@@ -766,7 +764,7 @@ describe('SubagentRuntime.startActivation', () => {
await expect(ctx.subagents.startActivation({
delivery: 'parent',
...startSpec(parent),
request: { prompt: message('deep'), parent, maxDepth: 0 },
prompt: message('deep'), parent, maxDepth: 0,
})).rejects.toThrow(/exceeds maxDepth 0/)
expect(ctx.agents.list().map(agent => agent.id)).toEqual([SessionId('parent')])
})
@@ -776,7 +774,7 @@ describe('SubagentRuntime.startActivation', () => {
await expect(ctx.subagents.startActivation({
delivery: 'parent',
...startSpec(parent),
request: { prompt: message('deep'), parent, maxDepth: Number.NaN },
prompt: message('deep'), parent, maxDepth: Number.NaN,
})).rejects.toThrow(/non-negative safe integer/)
expect(ctx.agents.list().map(agent => agent.id)).toEqual([SessionId('parent')])
})
@@ -3840,11 +3838,9 @@ describe('continuable errors', () => {
const started = await ctx.subagents.startActivation({
delivery: 'parent',
...startSpec(parent),
request: {
prompt: message('routed work'),
parent,
agentOptions: { provider: 'mock', model: 'child-model', reasoningEffort: effort },
},
prompt: message('routed work'),
parent,
agentOptions: { provider: 'mock', model: 'child-model', reasoningEffort: effort },
})
await waitNoActivation(ctx, started.childId)
const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId)
@@ -72,7 +72,7 @@ export async function startExternalActivation(
return ctx.subagents.startActivation({
provider,
label: label ?? 'External test task',
request: { ...execution, parent },
...execution, parent,
signal,
delivery: 'caller',
})
@@ -28,7 +28,7 @@ async function setup(start: (request: SubagentStartRequest) => Promise<SubagentR
ctx, parent, controller,
start: (delivery: 'parent' | 'caller' = 'caller') => ctx.subagents.startActivation({
provider: 'external', label: 'External work', signal: controller.signal, delivery,
request: { parent, prompt: [{ type: 'text', text: 'Work independently' }] },
parent, prompt: [{ type: 'text', text: 'Work independently' }],
}),
records: () => ctx.sessionProjections.snapshot(parent.session, ['subagentCatalog']).values.subagentCatalog,
}
@@ -104,7 +104,7 @@ describe('external subagent activations', () => {
try {
await expect(fixture.ctx.subagents.startActivation({
provider: 'external', label: 'Unpublished work', signal: fixture.controller.signal, delivery: 'parent',
request: { parent, prompt: [{ type: 'text', text: 'Must not publish' }] },
parent, prompt: [{ type: 'text', text: 'Must not publish' }],
})).rejects.toMatchObject({ code: change === 'drain' ? 'DRAINING' : 'UNAUTHORIZED' })
await drain
expect(backend.dispose).toHaveBeenCalledTimes(1)
@@ -367,7 +367,7 @@ it('refuses a reserved Session id for an external provider before dispatch', asy
const fixture = await setup(start)
await expect(fixture.ctx.subagents.startActivation({
provider: 'external', childId: SessionId('reserved'), label: 'External', delivery: 'parent',
signal: fixture.controller.signal, request: { parent: fixture.parent, prompt: [] },
signal: fixture.controller.signal, parent: fixture.parent, prompt: [],
})).rejects.toMatchObject({ code: 'UNSUPPORTED_CAPABILITY' })
expect(start).not.toHaveBeenCalled()
})
@@ -27,7 +27,7 @@ async function setup(start: (request: SubagentStartRequest) => Promise<SubagentR
ctx, parent,
start: (owner: Agent = parent) => ctx.subagents.startActivation({
provider: 'external', label: 'External work', signal: new AbortController().signal, delivery,
request: { parent: owner, prompt: [{ type: 'text', text: 'work' }] },
parent: owner, prompt: [{ type: 'text', text: 'work' }],
}),
}
}
@@ -76,7 +76,7 @@ async function startChild(
const started = await ctx.subagents.startActivation({ delivery: 'parent',
provider: 'spawn',
label,
request: { prompt: [{ type: 'text', text: `task: ${label}` }], parent },
prompt: [{ type: 'text', text: `task: ${label}` }], parent,
signal: testSignal,
})
await vi.waitFor(() => {
@@ -36,7 +36,7 @@ export async function startTestActivation(
request: Omit<SubagentStartRequest, 'signal'> & { signal?: AbortSignal },
) {
const { signal = new AbortController().signal, label = provider, ...task } = request
const activation = await ctx.get('subagents')!.startActivation({ provider, label, request: task, signal, delivery: 'caller' })
const activation = await ctx.get('subagents')!.startActivation({ provider, label, ...task, signal, delivery: 'caller' })
return { ...activation, id: activation.childId, localAgent: ctx.get('agents')!.get(activation.childId)! }
}
@@ -26,9 +26,7 @@ it.each(['absent', 'unused'] as const)('returns the closing answer when send_mes
const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const activation = await ctx.subagents.startActivation({
provider: 'spawn', label: 'Compute the answer', delivery: 'parent', signal: new AbortController().signal,
request: {
parent, prompt: [{ type: 'text', text: 'Find the answer.' }],
},
parent, prompt: [{ type: 'text', text: 'Find the answer.' }],
})
try {
await ctx.subagents.waitForTreeWork(parent)
@@ -60,10 +58,8 @@ it('delivers structured output to the parent when the child has send_message', a
const parent = await ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const activation = await ctx.subagents.startActivation({
provider: 'spawn', label: 'Compute the answer', delivery: 'parent', signal: new AbortController().signal,
request: {
parent, prompt: [{ type: 'text', text: 'Find the answer.' }],
outputSchema: { type: 'object', properties: { answer: { type: 'number' } }, required: ['answer'] },
},
parent, prompt: [{ type: 'text', text: 'Find the answer.' }],
outputSchema: { type: 'object', properties: { answer: { type: 'number' } }, required: ['answer'] },
})
try {
await activation.result
@@ -54,7 +54,7 @@ async function setup() {
label: 'child task',
delivery: 'caller',
signal: new AbortController().signal,
request: { parent: owner, prompt: [{ type: 'text', text: 'child task' }] },
parent: owner, prompt: [{ type: 'text', text: 'child task' }],
})
starts.push(started)
return started
@@ -98,7 +98,7 @@ describe('a delegated child under a preset that mounts the Schedule tools', () =
label: 'child task',
delivery: 'caller',
signal: new AbortController().signal,
request: { parent, prompt: [{ type: 'text', text: 'child task' }] },
parent, prompt: [{ type: 'text', text: 'child task' }],
})
const child = ctx.agents.get(run.childId)
@@ -98,7 +98,7 @@ async function start(subagents: SubagentRuntime, provider: string, request: Suba
?? (await ctx.agents.create({ sessionId: request.parent.id })).agent
const { label, signal, ...options } = request
return subagents.startActivation({
provider, label: label ?? provider, request: { ...options, parent }, signal, delivery,
provider, label: label ?? provider, ...options, parent, signal, delivery,
})
}
@@ -208,7 +208,7 @@ describe('SubagentRuntime', () => {
delivery: 'parent',
provider: 'unused',
label: 'unused child',
request: baseRequest(),
...baseRequest(),
signal: new AbortController().signal,
})).rejects.toMatchObject({ code: 'NO_PROVIDER' })
await expect(subagents.sendMessage(
@@ -226,7 +226,7 @@ describe('SubagentRuntime', () => {
await expect(subagents.startActivation({
provider: provider.name,
label: 'Unavailable runtime',
request: baseRequest(),
...baseRequest(),
signal: new AbortController().signal,
delivery: 'caller',
})).rejects.toMatchObject({ code: 'CONTINUATION_UNAVAILABLE' })
@@ -255,7 +255,7 @@ describe('SubagentRuntime', () => {
provider: provider.name,
label: 'Reserved team child',
childId: SessionId('reserved-child'),
request: { parent, prompt: [{ type: 'text', text: 'Work' }] },
parent, prompt: [{ type: 'text', text: 'Work' }],
signal: new AbortController().signal,
delivery: 'caller',
})).rejects.toMatchObject({ code: 'UNSUPPORTED_CAPABILITY' })
@@ -653,7 +653,7 @@ it('rejects a disposed parent before starting provider execution', async () => {
await handle.dispose()
await expect(subagents.startActivation({
provider: provider.name, label: 'work', delivery: 'caller', signal: new AbortController().signal,
request: { parent: handle.agent, prompt: [] },
parent: handle.agent, prompt: [],
})).rejects.toMatchObject({ code: 'UNAUTHORIZED' })
expect(provider.startCount).toBe(0)
})
@@ -84,7 +84,7 @@ async function start(ctx: Context, parent: Agent, marker: string, description =
const started = await ctx.subagents.startActivation({
provider: 'spawn',
label: description,
request: { prompt: [{ type: 'text', text: `TASK:${marker}` }], parent },
prompt: [{ type: 'text', text: `TASK:${marker}` }], parent,
signal,
delivery: 'caller',
})
@@ -388,10 +388,8 @@ describe('in-process structured output', () => {
if (exec.agent === undefined) throw new Error('expected the structured parent')
descendant = await ctx.subagents.startActivation({
provider: 'spawn', label: 'Nested work', signal: testToolSignal, delivery: 'parent',
request: {
parent: exec.agent, prompt: [{ type: 'text', text: 'Work until released' }],
agentOptions: { provider: 'descendant', model: 'mock' },
},
parent: exec.agent, prompt: [{ type: 'text', text: 'Work until released' }],
agentOptions: { provider: 'descendant', model: 'mock' },
})
return [{ type: 'text', text: 'Child accepted' }]
},
@@ -433,7 +431,7 @@ describe('in-process structured output', () => {
if (exec.agent === undefined) throw new Error('expected the structured parent')
await ctx.subagents.startActivation({
provider: 'delayed-answer', label: 'Compute answer', signal: testToolSignal, delivery: 'parent',
request: { parent: exec.agent, prompt: [{ type: 'text', text: 'Compute the answer' }] },
parent: exec.agent, prompt: [{ type: 'text', text: 'Compute the answer' }],
})
return [{ type: 'text', text: 'Answer computation accepted' }]
},
@@ -71,7 +71,7 @@ describe('subagent working directories', () => {
const started = await ctx.subagents.startActivation({
delivery: 'caller',
provider: 'spawn', label: 'directory child',
request: { parent, cwd: form === 'absolute' ? second : 'second', prompt: [{ type: 'text', text: 'child' }] },
parent, cwd: form === 'absolute' ? second : 'second', prompt: [{ type: 'text', text: 'child' }],
signal: new AbortController().signal,
})
await vi.waitFor(() => { expect(ctx.agents.get(started.childId)).toBeUndefined() })
@@ -99,7 +99,7 @@ describe('subagent working directories', () => {
await expect(ctx.subagents.startActivation({
delivery: 'caller',
provider: 'spawn', label: 'unavailable directory service', childId,
request: { parent, cwd: second, prompt: [{ type: 'text', text: 'child' }] },
parent, cwd: second, prompt: [{ type: 'text', text: 'child' }],
signal: new AbortController().signal,
})).rejects.toThrow(/subagents require the working-directory service/)
@@ -113,7 +113,7 @@ describe('list_agents', () => {
const { ctx, parent } = await setup([textResponse('private result')])
const started = await ctx.subagents.startActivation({
delivery: 'caller', provider: 'spawn', label: 'workflow child',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
prompt: [{ type: 'text', text: 'child task' }], parent,
signal: testToolSignal,
})
await started.result
@@ -162,7 +162,7 @@ describe('list_agents', () => {
const started = await ctx.subagents.startActivation({ delivery: 'parent',
provider: 'spawn',
label: 'real child',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
prompt: [{ type: 'text', text: 'child task' }], parent,
signal: testToolSignal,
})
await waitNoActivation(ctx, started.childId)
@@ -236,7 +236,7 @@ describe('list_agents', () => {
const started = await ctx.subagents.startActivation({ delivery: 'parent',
provider: 'spawn',
label: 'summarize the doc',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
prompt: [{ type: 'text', text: 'child task' }], parent,
signal: testToolSignal,
})
await waitNoActivation(ctx, started.childId)
@@ -301,7 +301,7 @@ describe('list_agents', () => {
const started = await ctx.subagents.startActivation({ delivery: 'parent',
provider: 'spawn',
label: 'waiting branch',
request: { prompt: [{ type: 'text', text: 'branch work' }], parent },
prompt: [{ type: 'text', text: 'branch work' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -309,7 +309,7 @@ describe('list_agents', () => {
const grandchild = await ctx.subagents.startActivation({ delivery: 'parent',
provider: 'spawn',
label: 'nested leaf',
request: { prompt: [{ type: 'text', text: 'leaf work' }], parent: child },
prompt: [{ type: 'text', text: 'leaf work' }], parent: child,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(2) })
@@ -388,7 +388,7 @@ describe('list_agents', () => {
const { ctx, parent } = await setupWith(adapter)
const start = async (label: string) => (await ctx.subagents.startActivation({
delivery: 'caller', provider: 'spawn', label,
request: { prompt: [{ type: 'text', text: label }], parent }, signal: testToolSignal,
prompt: [{ type: 'text', text: label }], parent, signal: testToolSignal,
})).childId
const alpha = await start('alpha task')
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -141,7 +141,7 @@ describe('dsh-tool-subagent-control', () => {
delivery: 'parent',
provider: 'fork',
label: 'fork child',
request: { prompt: [{ type: 'text', text: 'fork task' }], parent },
prompt: [{ type: 'text', text: 'fork task' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(2) })
@@ -177,7 +177,7 @@ describe('dsh-tool-subagent-control', () => {
delivery: 'parent',
provider: 'spawn',
label: 'encoded parent',
request: { prompt: [{ type: 'text', text: 'encoded task' }], parent },
prompt: [{ type: 'text', text: 'encoded task' }], parent,
signal: testToolSignal,
})
await waitNoActivation(ctx, started.childId)
@@ -201,7 +201,7 @@ describe('dsh-tool-subagent-control', () => {
delivery: 'parent',
provider: 'spawn',
label: 'child task',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
prompt: [{ type: 'text', text: 'child task' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -240,7 +240,7 @@ describe('dsh-tool-subagent-control', () => {
delivery: 'parent',
provider: 'spawn',
label: 'child task',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
prompt: [{ type: 'text', text: 'child task' }], parent,
signal: testToolSignal,
})
await waitNoActivation(ctx, started.childId)
@@ -274,7 +274,7 @@ describe('dsh-tool-subagent-control', () => {
delivery: 'parent',
provider: 'spawn',
label: 'long work',
request: { prompt: [{ type: 'text', text: 'long work' }], parent },
prompt: [{ type: 'text', text: 'long work' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -316,7 +316,7 @@ describe('dsh-tool-subagent-control', () => {
delivery: 'parent',
provider: 'spawn',
label: 'child task',
request: { prompt: [{ type: 'text', text: 'child task' }], parent },
prompt: [{ type: 'text', text: 'child task' }], parent,
signal: testToolSignal,
})
await waitNoActivation(ctx, started.childId)
@@ -400,7 +400,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
delivery: 'parent',
provider: 'spawn',
label: 'long work',
request: { prompt: [{ type: 'text', text: 'long work' }], parent },
prompt: [{ type: 'text', text: 'long work' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -457,7 +457,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
delivery: 'parent',
provider: 'spawn',
label: 'child',
request: { prompt: [{ type: 'text', text: 'child work' }], parent },
prompt: [{ type: 'text', text: 'child work' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -466,7 +466,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
delivery: 'parent',
provider: 'spawn',
label: 'grandchild',
request: { prompt: [{ type: 'text', text: 'grandchild work' }], parent: child },
prompt: [{ type: 'text', text: 'grandchild work' }], parent: child,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(2) })
@@ -495,7 +495,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
delivery: 'parent',
provider: 'spawn',
label: 'target',
request: { prompt: [{ type: 'text', text: 'a' }], parent },
prompt: [{ type: 'text', text: 'a' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
@@ -503,7 +503,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
delivery: 'parent',
provider: 'spawn',
label: 'sibling',
request: { prompt: [{ type: 'text', text: 'b' }], parent },
prompt: [{ type: 'text', text: 'b' }], parent,
signal: testToolSignal,
})
await vi.waitFor(() => { expect(adapter.requests).toHaveLength(2) })
@@ -535,7 +535,7 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
delivery: 'parent',
provider: 'spawn',
label: 'settled child',
request: { prompt: [{ type: 'text', text: 'child work' }], parent },
prompt: [{ type: 'text', text: 'child work' }], parent,
signal: testToolSignal,
})
await waitNoActivation(ctx, started.childId)
+1 -1
View File
@@ -325,7 +325,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const started = await runtimeCtx.subagents.startActivation({
provider: config.provider,
label: args.description,
request,
...request,
signal: exec.signal,
delivery: 'parent',
})
@@ -30,10 +30,8 @@ export function apply(ctx: Context): void {
try {
const activation = await ctx.subagents.startActivation({
provider: 'spawn', label: 'Structured answer', delivery: 'parent', signal: exec.signal,
request: {
parent, prompt: [{ type: 'text', text: 'Submit answer 41 through structured_output.' }],
outputSchema: { type: 'object', properties: { answer: { type: 'number' } }, required: ['answer'] },
},
parent, prompt: [{ type: 'text', text: 'Submit answer 41 through structured_output.' }],
outputSchema: { type: 'object', properties: { answer: { type: 'number' } }, required: ['answer'] },
})
await activation.result
return [{ type: 'text', text: `Child ${activation.childId} settled.` }]
+8 -10
View File
@@ -205,16 +205,14 @@ export class PtcWorkflowRun implements WorkflowRun {
label: `${this.meta.name} child ${callId}`,
delivery: 'caller',
signal: this.controller.signal,
request: {
cwd: await this.cwd,
prompt: [{ type: 'text', text: request.prompt }],
parent: this.parent,
...request.schema === undefined ? {} : { outputSchema: request.schema },
...request.provider === undefined && request.model === undefined ? {} : {
agentOptions: {
...request.provider === undefined ? {} : { provider: request.provider },
...request.model === undefined ? {} : { model: request.model },
},
cwd: await this.cwd,
prompt: [{ type: 'text', text: request.prompt }],
parent: this.parent,
...request.schema === undefined ? {} : { outputSchema: request.schema },
...request.provider === undefined && request.model === undefined ? {} : {
agentOptions: {
...request.provider === undefined ? {} : { provider: request.provider },
...request.model === undefined ? {} : { model: request.model },
},
},
})
@@ -118,7 +118,7 @@ describe('workflow host callback validation', () => {
expect(childStart).toHaveBeenCalledOnce()
const selected = childStart.mock.calls[0]![0]
expect(selected.provider).toBe('stub')
expect(selected.request.cwd).toBe(initial)
expect(selected.cwd).toBe(initial)
expect(ensureDirectory).toHaveBeenCalledOnce()
} finally {
release.resolve(undefined)
@@ -207,16 +207,13 @@ describe('workflow runtime outcomes', () => {
await disposing.promise
expect(settled).toBe(false)
expect(activation).toHaveBeenCalledOnce()
const { signal, ...delegation } = activation.mock.calls[0]![0]
const { signal, parent: delegatedParent, ...delegation } = activation.mock.calls[0]![0]
expect(signal).toBeInstanceOf(AbortSignal)
const { parent: delegatedParent, ...request } = delegation.request
expect(delegatedParent).toBe(parent)
expect({ ...delegation, request }).toEqual({
expect(delegation).toEqual({
provider: 'stub', label: 'host-test child 1', delivery: 'caller',
request: {
cwd: ctx.workingDirectory.get(parent.session),
prompt: [{ type: 'text', text: 'answer' }], outputSchema: schema, agentOptions: { model: 'selected' },
},
cwd: ctx.workingDirectory.get(parent.session),
prompt: [{ type: 'text', text: 'answer' }], outputSchema: schema, agentOptions: { model: 'selected' },
})
released.resolve(undefined)
await expect(run.result).resolves.toMatchObject({ stopReason: 'completed' })
+17 -1
View File
@@ -1,5 +1,21 @@
import { describe, expect, it } from 'vitest'
import { builtDeclarationPath } from './doc-typecheck-paths.ts'
import { builtDeclarationPath, isFrozenUpgradeGuidePath } from './doc-typecheck-paths.ts'
describe('frozen upgrade-guide examples', () => {
it('excludes earlier release guides in both languages from current API compilation', () => {
expect(isFrozenUpgradeGuidePath('docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md', '0.2.1-rc.2')).toBe(true)
expect(isFrozenUpgradeGuidePath('docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md', '0.2.1-rc.2')).toBe(true)
})
it.each([
'docs/upgrade-guide/v0.2.1-rc.2/subagent-activations/guide.md',
'docs/upgrade-guide/v0.2.1-rc.3/subagent-activations/guide.md',
'docs/upgrade-guide/vinvalid/subagent-activations/guide.md',
'docs/subsystems/subagent.md',
])('keeps %s subject to current API compilation', (file) => {
expect(isFrozenUpgradeGuidePath(file, '0.2.1-rc.2')).toBe(false)
})
})
describe('builtDeclarationPath', () => {
it('maps package source directories and exact entry files to built declarations', () => {
+13
View File
@@ -1,3 +1,16 @@
import { lt, valid } from 'semver'
/**
* Identify frozen upgrade guides whose examples target an earlier release.
* @param file - Repository-relative path normalized to forward slashes.
* @param currentVersion - Current root package version.
* @returns Whether the guide predates the current upgrade-guide directory.
*/
export function isFrozenUpgradeGuidePath(file: string, currentVersion: string): boolean {
const version = /^docs\/upgrade-guide\/v([^/]+)\/[^/]+\/guide(?:\.zh)?\.md$/.exec(file)?.[1]
return version !== undefined && valid(version) !== null && lt(version, currentVersion)
}
/** Map one workspace source alias target to its declaration-build target. */
export function builtDeclarationPath(candidate: string): string {
// Two workspace path forms exist: whole-package entries end in /src, subpath
+6 -3
View File
@@ -1,7 +1,8 @@
/**
* Typecheck Markdown `ts` fences against the workspace API. `ignore-check` fences are reported as
* opt-outs; generated catalog fragments and source-equivalence blocks are skipped here because their
* owning gates verify them. Byte-identical `.zh.md` copies reuse their unsuffixed sibling's check. A
* owning gates verify them. Frozen upgrade guides retain their release-specific examples.
* Byte-identical `.zh.md` copies reuse their unsuffixed sibling's check. A
* build-coordinated mode consumes existing declarations without emit.
*/
@@ -9,7 +10,7 @@ import { execFileSync } from 'node:child_process'
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import ts from 'typescript'
import { builtDeclarationPath } from './doc-typecheck-paths.ts'
import { builtDeclarationPath, isFrozenUpgradeGuidePath } from './doc-typecheck-paths.ts'
import { markdownFences } from './markdown.ts'
import { partitionPairedMarkdownDerivatives } from './paired-markdown-derivatives.ts'
import { isArchivedAgentNotePath } from './repo-files.ts'
@@ -205,9 +206,11 @@ function remapBlockPaths(output: string, blocks: Block[]): string {
const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const files: string[] = []
const { version } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string }
for (const pattern of markdownGlobs) {
for (const match of globSync(pattern, { cwd: root })) {
if (!isArchivedAgentNotePath(match)) files.push(resolve(root, match))
const file = match.replaceAll('\\', '/')
if (!isArchivedAgentNotePath(file) && !isFrozenUpgradeGuidePath(file, version)) files.push(resolve(root, file))
}
}
files.sort()
@@ -79,7 +79,7 @@ describe('advanced Python snapshot workflow ordering', () => {
const parent = await ctx.agentLoop.create(SessionId(config.parentSessionId), { provider: 'mock', model: 'mock' })
const run = await ctx.subagents.startActivation({
provider: 'spawn', label: 'workflow-child', delivery: 'caller',
request: { parent, prompt: [{ type: 'text', text: config.prompt }] }, signal: new AbortController().signal,
parent, prompt: [{ type: 'text', text: config.prompt }], signal: new AbortController().signal,
})
cleanups.push(() => run.dispose())
const child = await entered.promise