feat(subagent): default continuable delegation to background

This commit is contained in:
Dudu-0223
2026-08-11 19:28:32 +08:00
parent 43b66c0a74
commit 8344d64363
73 changed files with 491 additions and 286 deletions
+66 -26
View File
@@ -17,9 +17,13 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
import { assertSubagentMaxDepth, settleRun } from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent'
import type { TaskOutcome } from '@deepseek-ai/dsh-tasks'
import type {} from '@deepseek-ai/dsh-system-prompt'
export const name = 'tool-subagent'
export const inject = ['tools', 'subagents']
export const inject = ['tools', 'subagents', 'systemPrompt']
/** Prompt order after bounded delegation policy and before child reporting. */
const SUBAGENT_SECTION_ORDER = 116
/** Config: which registered provider this tool delegates to, plus child defaults. */
export interface Config {
@@ -36,9 +40,10 @@ export interface Config {
*/
enableRunInBackground?: boolean
/**
* 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.
* Background execution policy (default `one-shot`). `one-shot` defaults calls
* to foreground; `continuable` defaults them to background, requires a provider
* with the `prepareContinuable` capability, and returns the durable child id.
* Follow-up adapters remain independently optional.
*/
backgroundMode?: 'one-shot' | 'continuable'
/**
@@ -208,10 +213,10 @@ function providerWording(inheritsConversation: boolean): { description: string;
return {
description:
'Delegate a task to a subagent that inherits this conversation: a child agent seeded with all '
+ 'completed turns so far (it does not see the current in-flight turn), returning only its final '
+ 'result. Use this when the subtask builds on this conversation\'s context — a follow-up analysis, '
+ 'completed turns so far (it does not see the current in-flight turn). Use this when the subtask '
+ 'builds on this conversation\'s context — a follow-up analysis, '
+ 'a review, a continuation — without consuming this conversation\'s context for the work itself. '
+ 'You receive only its final answer, not its intermediate steps.',
+ 'You receive its result, not its intermediate steps.',
promptDescription:
'The task for the subagent. It already sees this conversation\'s completed turns, so build on them '
+ 'freely and state only what is new.',
@@ -220,9 +225,9 @@ function providerWording(inheritsConversation: boolean): { description: string;
return {
description:
'Delegate a self-contained task to a subagent (a separate agent that works in its own context) '
+ 'and return its final result. Use this to offload focused, independent work — research, a scoped '
+ 'to offload focused, independent work — research, a scoped '
+ 'implementation, an analysis — so it does not consume this conversation\'s context. The subagent '
+ 'runs to completion and you receive only its final answer, not its intermediate steps. Give it a '
+ 'returns its result, not its intermediate steps. Give it a '
+ 'complete, standalone prompt: it does not see this conversation.',
promptDescription:
'The complete, self-contained task for the subagent. It does not share this '
@@ -230,6 +235,35 @@ function providerWording(inheritsConversation: boolean): { description: string;
}
}
interface DelegationRunRequest {
readonly run_in_background?: boolean
}
interface DelegationRunSpec {
readonly runInBackground: boolean
}
/** Resolve the model's optional scheduling request into one execution route. */
function resolveDelegationRun(
request: DelegationRunRequest,
options: { readonly backgroundEnabled: boolean; readonly continuable: boolean },
): DelegationRunSpec {
if (!options.backgroundEnabled) {
// The validator permits undeclared keys, so schema omission also needs
// execution-time enforcement.
if (request.run_in_background === true) {
throw new Error('run_in_background is disabled for this tool instance (enableRunInBackground: false)')
}
return { runInBackground: false }
}
return {
// Continuable work is independently scheduled unless the caller explicitly
// needs the result before its next action. One-shot policy keeps its existing
// foreground default because its background result requires Task collection.
runInBackground: request.run_in_background ?? options.continuable,
}
}
export function apply(ctx: Context, config: Config): void {
// Direct apply() bypasses Schemastery's numeric constraints. A direct-apply
// omission stays capless (the schema default only runs through the loader).
@@ -238,6 +272,9 @@ export function apply(ctx: Context, config: Config): void {
if (config.toolFilter !== undefined && config.toolFilter.allow === undefined && config.toolFilter.deny === undefined) {
throw new Error('tool-subagent: `toolFilter` is configured but names neither `allow` nor `deny` — remove the key or fill the filter')
}
const backgroundEnabled = config.enableRunInBackground !== false
const continuable = (config.backgroundMode ?? 'one-shot') === 'continuable'
const toolName = config.toolName ?? 'subagent'
// Mirror provider lifecycle because sibling load order and HMR replacement
// can change provider availability while this fiber remains active.
let disposeTool: (() => void) | undefined
@@ -252,25 +289,21 @@ 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.prepareContinuable === undefined) {
throw new Error(
`tool-subagent: provider "${provider.name}" does not support \`backgroundMode: continuable\``,
)
}
disposeTool = ctx.tools.register(defineTool({
name: config.toolName ?? 'subagent',
name: toolName,
description: wording.description + (backgroundEnabled
// The completion notice is the continuation service's own behavior, not
// a separately installed capability, so this promise holds whenever the
// continuable background path is reachable at all.
? continuable
? ' Set `run_in_background: true` to start a background subagent that keeps its conversation:'
+ ' this call returns only its subagent id, and the subagent works on its own from there. You'
+ ' are told when it finishes, so never poll or wait on it; `send_message` sends it more work.'
: ' Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.'
: ''),
? ' This tool uses its continuable background route by default and immediately returns a durable subagent id. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` to use a foreground run only when you must receive its result before taking your next action.'
: ' This call waits for the result by default. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.'
: ' This call waits for the subagent and returns its result.'),
parameters: {
description: {
type: 'string',
@@ -286,9 +319,8 @@ export function apply(ctx: Context, config: Config): void {
run_in_background: {
type: 'boolean' as const,
description: continuable
? 'Run as a background subagent that keeps its conversation and return only its subagent id. '
+ 'This call does not wait for it; you are told when it finishes. 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.',
? 'Whether to use the continuable background route. Defaults to true. Set false to run in the foreground and wait only when your next action depends on its result.'
: 'Whether to run as a background task and return its id. Defaults to false; collect with task_output or stop with task_kill.',
},
} : {},
},
@@ -352,12 +384,8 @@ export function apply(ctx: Context, config: Config): void {
...maxDepth !== undefined ? { maxDepth } : {},
}
if (args.run_in_background === true) {
// The validator permits undeclared keys, so schema omission also needs
// execution-time enforcement.
if (!backgroundEnabled) {
throw new Error('run_in_background is disabled for this tool instance (enableRunInBackground: false)')
}
const runSpec = resolveDelegationRun(args, { backgroundEnabled, continuable })
if (runSpec.runInBackground) {
if (continuable) {
// Resolves at inbox acceptance: the child owns its own turns from
// there, so this call neither waits for nor collects a result.
@@ -422,4 +450,16 @@ export function apply(ctx: Context, config: Config): void {
// 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`)
}
if (backgroundEnabled && continuable) {
// The section follows provider availability without its own manual
// lifecycle: empty text is omitted from rendered prompts while the tool is
// absent, and the registration itself stays owned by this plugin fiber.
ctx.systemPrompt.section({
name: `tool:${toolName}`,
order: SUBAGENT_SECTION_ORDER,
text: () => ctx.subagents.getProvider(config.provider) === undefined
? ''
: `Use ${toolName}'s continuable background route by default. Start independent delegations together in one assistant message and continue useful work while they run. Set \`run_in_background: false\` only when you cannot take your next action without that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.`,
})
}
}