feat(subagent): default continuable delegation to background
This commit is contained in:
@@ -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.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user