refactor(subagent): narrow continuation interface

This commit is contained in:
Tianyi Cui
2026-07-28 00:10:24 +08:00
committed by imccyu
parent 644bf00b86
commit f14121a4c2
55 changed files with 669 additions and 441 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: 525760ccc413bb46ca5ea3a37e610a3ff58b8068
README.zh.md: 4d02e2bb89f38e449dfd8bf31a39b79891f6a69e
README.md: 8d266e93021285e27e7819386a4de9c33492a796
README.zh.md: 79450a32a7ecc3cf2a442524a2680614b3f28ed0

View File

@@ -14,7 +14,7 @@ The driver follows this sequence:
2. Call `parent.ctx.agents.create` directly, passing the required request signal into the factory's creation transaction. A continuable request publishes exactly `request.continuation.sessionId` instead of an internally minted id.
3. During that transaction's unpublished setup window, install the requested persona, tool restriction, structured-output runtime, and — for a continuable request — the prepended one-shot `agent/prompt-submit` contribution. It appends the `subagent/descriptor` event before downstream prompt admission can block or throw; allowed admission opens the initial turn afterward, while the final required checkpoint persists the descriptor even when no turn opens.
4. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.followup(prompt)` followed by `child.whenIdle()`.
5. For a continuable start or resume, call `child.ctx.sessions.flushRequired(child.session)` again before returning the result. This final confirmation requires an installed durability listener and retries events retained after a failed turn checkpoint; if no listener participates or any listener fails, `result` rejects with `SubagentError.code === 'DURABILITY_FAILED'`, retains the checkpoint failure as `cause`, and names the resumability risk in its message. Activation cancellation during this await owns the unpublished result even when the completed turn was already recorded or the checkpoint subsequently fails. Foreground runs keep the loop's best-effort checkpoint behavior.
5. For a continuable start or resume, call `child.ctx.sessions.flush(child.session)` again before returning the result and require its participation result to be `true`. This final confirmation retries events retained after a failed turn checkpoint; if no listener participates or any listener fails, `result` rejects with `SubagentError.code === 'DURABILITY_FAILED'`, retains the checkpoint failure as `cause`, and names the resumability risk in its message. Activation cancellation during this await owns the unpublished result even when the completed turn was already recorded or the checkpoint subsequently fails. Foreground runs keep the loop's best-effort checkpoint behavior.
6. Read the child's own last assistant message and latest message-triggered turn reason, excluding any fork seed and later plugin-owned between-turn records.
The child gets the parent's working-directory/session lineage and inherits the parent provider, model, and output-token cap unless `request.agentOptions` overrides them. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset.

View File

@@ -14,7 +14,7 @@
2. 直接调用 `parent.ctx.agents.create`,把必需的请求信号传入工厂的创建事务。可继续请求会精确发布 `request.continuation.sessionId`,而不是内部生成的 ID。
3. 在该事务未发布的设置窗口中,安装请求的 persona、工具限制和结构化输出运行时;对于可继续请求,还会前置安装一次性的 `agent/prompt-submit` 贡献。它会在下游 prompt admission 能够阻止请求或抛出异常之前追加 `subagent/descriptor` 事件;admission 获准后才会开启初始轮次,即使没有轮次开启,最终的必需检查点仍会持久化该描述符。
4. 发布子 agent,保留返回的 `AgentHandle`,并通过先调用 `child.followup(prompt)`、再调用 `child.whenIdle()` 来驱动一项任务。
5. 对于可继续启动或恢复,在返回结果前再次调用 `child.ctx.sessions.flushRequired(child.session)`。这次最终确认要求有已安装的持久性监听器参与,并会重试轮次检查点失败后保留的事件;如果没有监听器参与或任一监听器失败,`result` 会以 `SubagentError.code === 'DURABILITY_FAILED'` 拒绝,将检查点失败保留为 `cause`,并在消息中说明恢复风险。即使已记录完成的轮次,或随后检查点失败,等待期间发生的激活取消仍决定尚未发布的结果。前台运行保留循环的尽力检查点行为。
5. 对于可继续启动或恢复,在返回结果前再次调用 `child.ctx.sessions.flush(child.session)`,并要求其参与结果为 `true`。这次最终确认会重试轮次检查点失败后保留的事件;如果没有监听器参与或任一监听器失败,`result` 会以 `SubagentError.code === 'DURABILITY_FAILED'` 拒绝,将检查点失败保留为 `cause`,并在消息中说明恢复风险。即使已记录完成的轮次,或随后检查点失败,等待期间发生的激活取消仍决定尚未发布的结果。前台运行保留循环的尽力检查点行为。
6. 读取子 agent 自身最后一条 assistant 消息,以及由消息触发的最新轮次原因;排除任何 fork 初始内容和后续由插件拥有的轮次间记录。
子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型和输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。

View File

@@ -15,10 +15,10 @@ import { createUserMessage, errorChain, type ContentBlock, type MessageSource }
import { assertSubagentMaxDepth, delegationDepthOf, SubagentError } from '@deepseek-ai/dsh-subagent'
import type {
SubagentDescriptorData,
SubagentProviderResumeRequest,
SubagentProviderStartRequest,
SubagentResult,
SubagentResumeRequest,
SubagentRun,
SubagentStartRequest,
SubagentStopReason,
} from '@deepseek-ai/dsh-subagent'
// Type-only: make `ctx.get('sandboxPolicy')` / `ctx.get('approval')` resolve
@@ -108,7 +108,7 @@ function attachDescriptorAppend(childCtx: Context, descriptor: SubagentDescripto
* @returns a ready holder-owned run.
*/
export async function startInProcessRun(
request: SubagentStartRequest,
request: SubagentProviderStartRequest,
options: InProcessRunOptions,
): Promise<SubagentRun> {
assertSubagentMaxDepth(request.maxDepth)
@@ -197,10 +197,10 @@ export async function startInProcessRun(
* (loaded through the parent's persistence-backed registry `resume`), so a
* fork child never re-forks current parent history; the persisted header
* remains authoritative for lineage and the delegation-depth floor.
* @param request - the fully resolved resume request from the low-level service.
* @param request - the fully resolved resume request from the continuation manager.
* @returns a fresh ready holder-owned run for this activation.
*/
export async function resumeInProcessRun(request: SubagentResumeRequest): Promise<SubagentRun> {
export async function resumeInProcessRun(request: SubagentProviderResumeRequest): Promise<SubagentRun> {
if (request.signal.aborted) throw prePublicationAbort()
const descriptor = request.descriptor
const agentOptions: AgentOptions = {
@@ -269,7 +269,10 @@ function driveTurn(
await child.whenIdle()
if (durability === 'required') {
try {
await child.ctx.sessions.flushRequired(child.session)
const participated = await child.ctx.sessions.flush(child.session)
if (!participated) {
throw new Error(`session "${child.id}" required durability checkpoint has no registered listener`)
}
} catch (error: unknown) {
if (!signal.aborted) {
throw new SubagentError(

View File

@@ -73,6 +73,21 @@ describe('startInProcessRun', () => {
expect(ctx.agents.get(run.id)).toBeUndefined()
})
it('uses explicit child model selectors when the parent has none and preserves its cwd', async () => {
const { ctx } = await setup([textResponse('driver answer')])
const parent = ctx.agentLoop.create(SessionId('bare-parent'), {}, { cwd: '/workspace' })
const run = await startInProcessRun({
...request(parent),
agentOptions: { provider: 'mock', model: 'mock' },
}, {})
const child = ctx.agents.get(run.id)!
expect(child.options).toMatchObject({ provider: 'mock', model: 'mock' })
expect(child.session.header.cwd).toBe('/workspace')
await expect(run.result).resolves.toMatchObject({ stopReason: 'completed' })
await run.dispose()
})
it('rejects a continuable child when no durability listener is registered', async () => {
const { parent } = await setup([textResponse('driver answer')])
@@ -346,9 +361,9 @@ describe('startInProcessRun', () => {
acceptsNextStep: false,
ctx: {
sessions: {
flushRequired: () => {
flush: () => {
flushes++
return Promise.resolve()
return Promise.resolve(true)
},
},
} as unknown as Context,