feat(subagent): catalog one-shot child sessions

This commit is contained in:
Dudu-0223
2026-07-27 16:58:37 +08:00
committed by Tianyi Cui
parent de6e572e51
commit 774ee34b9a
85 changed files with 1034 additions and 539 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-inprocess/README.md
README.md: 0495b7cae003a8c280689c4bfdd991e0f6950569
README.zh.md: 2e512ffd281c6334db925c97b110934bbcc19eef
README.md: 800ba24b65cedbbff31008a94b45957d5a65657e
README.zh.md: f6e0bd6fcb2340d195469ad57e2e96941c684fdd

View File

@@ -11,7 +11,7 @@ This package is the shared run driver for the two in-process providers' one-shot
The driver follows this sequence:
1. Validate the parent depth and optional absolute `maxDepth`, then derive child depth as parent depth plus one and persist it in the child session header.
2. Mint a fresh child session id and call `parent.ctx.agents.create` directly, passing the optional fork seed and required request signal into the factory's creation transaction. During the unpublished setup window, install the requested persona, tool restriction, and structured-output runtime.
2. Mint a fresh child session id and call `parent.ctx.agents.create` directly, passing the optional fork seed and required request signal into the factory's creation transaction. During the unpublished setup window, install the requested persona, tool restriction, structured-output runtime, and a one-shot `agent/step` contribution that appends the resolved `subagent/descriptor` event after the initial `turn/start` and before the first request.
3. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.followup(prompt)` followed by `child.whenIdle()`.
4. Read the child's own last assistant message and latest message-triggered turn reason, excluding the fork seed prefix so a seeded parent message is never mistaken for child output.

View File

@@ -11,7 +11,7 @@
驱动器按以下顺序运行:
1. 校验父 agent 深度和可选的绝对 `maxDepth`,然后把子 agent 深度推导为父 agent 深度加一,并将其持久化到子 agent 会话 header。
2. 生成全新的子 agent 会话 id,并直接调用 `parent.ctx.agents.create`,把可选的 fork 初始内容和必需的请求信号传入工厂的创建事务。在未发布的设置窗口中,安装请求的 persona、工具限制和结构化输出运行时。
2. 生成全新的子 agent 会话 id,并直接调用 `parent.ctx.agents.create`,把可选的 fork 初始内容和必需的请求信号传入工厂的创建事务。在未发布的设置窗口中,安装请求的 persona、工具限制、结构化输出运行时,以及一次性的 `agent/step` contribution;该 contribution 会在初始 `turn/start` 之后、首次请求之前追加已解析的 `subagent/descriptor` 事件。
3. 发布子 agent,保留返回的 `AgentHandle`,并通过先调用 `child.followup(prompt)`、再调用 `child.whenIdle()` 来驱动一项任务。
4. 读取子 agent 自身最后一条 assistant 消息,以及由消息触发的最新轮次原因;排除 fork 初始内容前缀,确保作为初始内容的父 agent 消息绝不会被误认为子 agent 输出。

View File

@@ -24,9 +24,10 @@ import {
resolveChildDepth,
} from '@deepseek-ai/dsh-subagent'
import type {
ResolvedSubagentStartRequest,
SubagentDescriptorData,
SubagentResult,
SubagentRun,
SubagentStartRequest,
SubagentStopReason,
} from '@deepseek-ai/dsh-subagent'
// Type-only: make `ctx.get('sandboxPolicy')` / `ctx.get('approval')` resolve
@@ -72,16 +73,27 @@ function prePublicationAbort(): Error {
return new Error('subagent request was aborted before child publication')
}
/** Append one one-shot descriptor inside the child's initial turn before its first request. */
function attachDescriptorAppend(childCtx: Context, descriptor: SubagentDescriptorData): void {
let appended = false
childCtx.on('agent/step', (agent) => {
if (appended) return
appended = true
agent.session.append('subagent/descriptor', descriptor)
})
}
/**
* Establish and drive one in-process one-shot child. Fulfillment means the agent
* is already published in the registry; rejection means the agent factory's
* creation transaction and any partially-created child have reached quiescence.
* Every start appends its resolved descriptor inside the child's initial turn.
* @param request - the trusted typed start request, including its required signal.
* @param options - the optional fork seed.
* @returns a ready holder-owned run.
*/
export async function startInProcessRun(
request: SubagentStartRequest,
request: ResolvedSubagentStartRequest,
options: InProcessRunOptions,
): Promise<SubagentRun> {
assertSubagentMaxDepth(request.maxDepth)
@@ -116,6 +128,7 @@ export async function startInProcessRun(
if (request.outputSchema !== undefined) {
structured = attachStructuredRuntime(childCtx, request.outputSchema)
}
attachDescriptorAppend(childCtx, request.descriptor)
}
const handle = await parent.ctx.agents.create({

View File

@@ -14,6 +14,7 @@ import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-p
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import ApprovalService, { setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'
@@ -52,9 +53,15 @@ async function setupWalled(script: Script): Promise<{ ctx: Context; parent: Agen
function spawnRequest(parent: Agent) {
return {
label: 'child task',
prompt: [{ type: 'text' as const, text: 'child task' }],
parent,
signal: new AbortController().signal,
descriptor: snapshotSubagentDescriptor({
mode: 'one-shot',
provider: 'spawn',
label: 'child task',
}),
}
}

View File

@@ -8,7 +8,10 @@ import InvariantService from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentService, {
type ResolvedSubagentStartRequest,
type SubagentStartRequest,
} from '@deepseek-ai/dsh-subagent'
import type { Config as ToolConfig, ObjectJsonSchema } from '@deepseek-ai/dsh-tools'
import { defineContentToolFixture, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -69,7 +72,7 @@ async function setup(script: Script, options: SetupOptions = {}) {
name: 'spawn',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: false, persona: false },
inheritsParentContext: false,
start: (request: SubagentStartRequest) => startInProcessRun(request, {}),
start: (request: ResolvedSubagentStartRequest) => startInProcessRun(request, {}),
})
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -78,6 +81,7 @@ async function setup(script: Script, options: SetupOptions = {}) {
function structuredRequest(parent: SubagentStartRequest['parent'], extra?: Partial<SubagentStartRequest>): SubagentStartRequest {
return {
label: 'produce the answer',
prompt: [{ type: 'text', text: 'produce the answer' }],
parent,
signal: new AbortController().signal,

View File

@@ -9,7 +9,7 @@ import InvariantService from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentService, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { maxTokensResponse, MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'
@@ -35,7 +35,17 @@ async function setup(script: Script, parentOptions: Partial<AgentOptions> = {})
}
function request(parent: Agent, signal = new AbortController().signal) {
return { prompt: [{ type: 'text' as const, text: 'child task' }], parent, signal }
return {
label: 'child task',
prompt: [{ type: 'text' as const, text: 'child task' }],
parent,
signal,
descriptor: snapshotSubagentDescriptor({
mode: 'one-shot',
provider: 'test',
label: 'child task',
}),
}
}
function text(blocks: readonly { type: string; text?: string }[]): string {