feat(subagent): activation-based continuable subagents (source)

Replace the Task-backed continuation manager with one durable Session plus at
most one process-local Activation — a residency epoch for a reconstructed child
Agent, not a request, result, cancellation, or Task boundary. The manager owns
activation admission, authority, the live ownership graph, cold resume, and
child-first disposal; the Agent inbox is the only turn FIFO.

- startContinuable() is async and returns { childId, messageId } at inbox
  acceptance; followup() takes a SubagentAuthority and returns AgentMessageId.
- SubagentProvider.resume?(), SubagentProviderResumeRequest, SubagentRun.steer?(),
  SubagentProviderStartRequest and SubagentContinuation are deleted;
  prepareContinuable?() is the continuable-creation capability.
- Cold resume calls ctx.agents.resume() from the manager through a private
  activation-owner scope, never dispatching through a provider.
- Extract shared child composition, descriptor seeding, depth accounting, and
  one-shot run settlement so the manager and one-shot driver keep one home
  per fact.

Tests and docs follow in subsequent commits.
This commit is contained in:
Dudu-0223
2026-07-30 13:26:13 +08:00
committed by Tianyi Cui
parent dabd50e067
commit 26a117f842
15 changed files with 1721 additions and 868 deletions

View File

@@ -36,9 +36,9 @@ export interface Config {
*/
enableRunInBackground?: boolean
/**
* Background execution policy (default `one-shot`). `continuable` requires
* a provider with persisted resume support and returns both child and Task
* ids; follow-up adapters remain independently optional.
* Background execution policy (default `one-shot`). `continuable` requires a
* provider with the `prepareContinuable` capability and returns the durable
* child id; follow-up adapters remain independently optional.
*/
backgroundMode?: 'one-shot' | 'continuable'
/**
@@ -197,7 +197,7 @@ export function apply(ctx: Context, config: Config): void {
const wording = providerWording(provider.inheritsParentContext)
const backgroundEnabled = config.enableRunInBackground !== false
const continuable = (config.backgroundMode ?? 'one-shot') === 'continuable'
if (continuable && provider.resume === undefined) {
if (continuable && provider.prepareContinuable === undefined) {
throw new Error(
`tool-subagent: provider "${provider.name}" does not support \`backgroundMode: continuable\``,
)
@@ -206,9 +206,9 @@ export function apply(ctx: Context, config: Config): void {
name: config.toolName ?? 'subagent',
description: wording.description + (backgroundEnabled
? continuable
? ' Set `run_in_background: true` to start a continuable background subagent: you receive its'
+ ' stable subagent id and current task id; collect the result with `task_output` and stop it with'
+ ' `task_kill`.'
? ' Set `run_in_background: true` to start a background subagent that keeps its conversation:'
+ ' you receive its subagent id and it works on its own. It does not report back to you, so read'
+ ' its transcript by that id, or send it more work with `send_message`.'
: ' Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.'
: ''),
parameters: {
@@ -226,8 +226,8 @@ export function apply(ctx: Context, config: Config): void {
run_in_background: {
type: 'boolean' as const,
description: continuable
? 'Run as a continuable background subagent and return its subagent and task ids; '
+ 'collect with task_output or stop with task_kill.'
? 'Run as a background subagent that keeps its conversation and return its subagent id; '
+ 'send it more work with send_message.'
: 'Run as a background task and return its id; collect with task_output or stop with task_kill.',
},
} : {},
@@ -241,7 +241,14 @@ export function apply(ctx: Context, config: Config): void {
properties: {
kind: { type: 'string', required: true, const: 'background' },
taskId: { type: 'string', required: true },
subagentId: { type: 'string' },
},
},
{
type: 'object',
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'continuable' },
subagentId: { type: 'string', required: true },
},
},
{
@@ -258,10 +265,10 @@ export function apply(ctx: Context, config: Config): void {
render: (_args, value) => [{
type: 'text',
text: value.kind === 'background'
? value.subagentId === undefined
? `started background subagent task ${value.taskId}`
: `started subagent ${value.subagentId} as task ${value.taskId}`
: outputValueText(value.output),
? `started background subagent task ${value.taskId}`
: value.kind === 'continuable'
? `started subagent ${value.subagentId}`
: outputValueText(value.output),
}],
},
async execute(args, exec) {
@@ -288,16 +295,14 @@ export function apply(ctx: Context, config: Config): void {
throw new Error('run_in_background is disabled for this tool instance (enableRunInBackground: false)')
}
if (continuable) {
const started = ctx.subagents.startContinuable({
// Resolves at inbox acceptance: the child owns its own turns from
// there, so this call neither waits for nor collects a result.
const started = await ctx.subagents.startContinuable({
provider: config.provider,
label: args.description,
request,
signal: exec.signal,
})
return {
kind: 'background' as const,
taskId: started.taskId,
subagentId: started.childId,
}
return { kind: 'continuable' as const, subagentId: started.childId }
}
const tasks = ctx.get('tasks')
if (tasks === undefined) {