From ff8acb0095fae4f64c3a6636ffdb8be3eeffc5d9 Mon Sep 17 00:00:00 2001 From: shivammittal274 Date: Tue, 10 Mar 2026 18:03:56 +0530 Subject: [PATCH 1/3] feat: add sub-agent spawning via delegate_task tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `delegate_task` tool that spawns ephemeral sub-agents with isolated context windows. The sub-agent is an exact replica of the parent — same model, instructions, tools, and compaction — following the AI SDK agent-as-tool pattern (ai-sdk.dev/docs/agents/subagents). - New `sub-agent.ts` with `createDelegateTaskTool` factory - Sub-agent reuses parent's model, full prompt (soul, memory, skills, user prefs, workspace, external integrations), and all tools - 15-step limit (SUB_AGENT_MAX_TURNS), own compaction, no recursion - Skipped in chat mode - New prompt section guiding delegation behavior --- apps/server/src/agent/ai-sdk-agent.ts | 14 ++++- apps/server/src/agent/prompt.ts | 30 +++++++++ apps/server/src/agent/sub-agent.ts | 81 +++++++++++++++++++++++++ packages/shared/src/constants/limits.ts | 1 + 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/agent/sub-agent.ts diff --git a/apps/server/src/agent/ai-sdk-agent.ts b/apps/server/src/agent/ai-sdk-agent.ts index 6eb052d4..f4d1a303 100644 --- a/apps/server/src/agent/ai-sdk-agent.ts +++ b/apps/server/src/agent/ai-sdk-agent.ts @@ -4,6 +4,7 @@ import type { BrowserContext } from '@browseros/shared/schemas/browser-context' import { stepCountIs, ToolLoopAgent, + type ToolSet, type UIMessage, wrapLanguageModel, } from 'ai' @@ -23,6 +24,7 @@ import { createContextOverflowMiddleware } from './context-overflow-middleware' import { buildMcpServerSpecs, createMcpClients } from './mcp-builder' import { buildSystemPrompt } from './prompt' import { createLanguageModel } from './provider-factory' +import { createDelegateTaskTool } from './sub-agent' import { buildBrowserToolSet } from './tool-adapter' import type { ResolvedAgentConfig } from './types' @@ -88,7 +90,7 @@ export class AiSdkAgent { const memoryTools = config.resolvedConfig.chatMode ? {} : buildMemoryToolSet() - const tools = { + const tools: ToolSet = { ...browserTools, ...externalMcpTools, ...filesystemTools, @@ -122,6 +124,16 @@ export class AiSdkAgent { skillsCatalog, }) + // Add sub-agent delegation tool — exact replica of parent, fresh context + if (!config.resolvedConfig.chatMode) { + tools.delegate_task = createDelegateTaskTool({ + model, + instructions, + parentTools: tools, + contextWindow, + }) + } + // Configure compaction for context window management const prepareStep = createCompactionPrepareStep({ contextWindow, diff --git a/apps/server/src/agent/prompt.ts b/apps/server/src/agent/prompt.ts index 01fb13a0..807a5797 100644 --- a/apps/server/src/agent/prompt.ts +++ b/apps/server/src/agent/prompt.ts @@ -412,6 +412,35 @@ All filesystem tools operate relative to this directory. ` } +// ----------------------------------------------------------------------------- +// section: sub-agents +// ----------------------------------------------------------------------------- + +function getSubAgents( + _exclude: Set, + options?: BuildSystemPromptOptions, +): string { + if (options?.chatMode) return '' + + return ` +## Task Delegation + +You can delegate focused subtasks to independent sub-agents using \`delegate_task\`. Each sub-agent runs in its own context window with full tool access. + +**When to delegate:** +- Research requiring reading many pages (the sub-agent explores, you get a summary) +- Data extraction or scraping tasks across multiple sources +- Deep filesystem exploration or code analysis +- Any subtask that would consume significant context in your main conversation + +**How to delegate well:** +- Write clear, self-contained task descriptions +- Include all necessary context: URLs, file paths, search terms, expected output format +- The sub-agent has NO access to your conversation history — include everything it needs +- Do not delegate simple single-step actions (just do them directly) +` +} + const promptSections: Record = { intro: getIntro, 'security-boundary': getSecurityBoundary, @@ -431,6 +460,7 @@ const promptSections: Record = { memory: getMemory, skills: (_exclude: Set, options?: BuildSystemPromptOptions) => options?.skillsCatalog || '', + 'sub-agents': getSubAgents, 'security-reminder': getSecurityReminder, } diff --git a/apps/server/src/agent/sub-agent.ts b/apps/server/src/agent/sub-agent.ts new file mode 100644 index 00000000..fdb36600 --- /dev/null +++ b/apps/server/src/agent/sub-agent.ts @@ -0,0 +1,81 @@ +import { AGENT_LIMITS } from '@browseros/shared/constants/limits' +import type { LanguageModel } from 'ai' +import { stepCountIs, ToolLoopAgent, type ToolSet, tool } from 'ai' +import { z } from 'zod' +import { logger } from '../lib/logger' +import { createCompactionPrepareStep } from './compaction' + +export interface DelegateTaskDeps { + model: LanguageModel + instructions: string + parentTools: ToolSet + contextWindow: number +} + +const SUB_AGENT_SUFFIX = + '\n\nIMPORTANT: When you have finished, write a clear summary of your findings ' + + 'as your final response. This summary will be returned to the main agent, ' + + 'so include all relevant information.' + +/** + * Creates the `delegate_task` tool following the AI SDK subagent pattern. + * The sub-agent is an exact replica of the parent agent — same model, same + * instructions, same tools, same compaction — just with a fresh context + * window and a lower step limit. + * + * @see https://ai-sdk.dev/docs/agents/subagents#basic-subagent-without-streaming + */ +export function createDelegateTaskTool(deps: DelegateTaskDeps) { + // Filter out delegate_task to prevent recursive spawning + const { delegate_task: _, ...subAgentTools } = deps.parentTools + + // Reuse parent's full instructions + summarization suffix + const instructions = deps.instructions + SUB_AGENT_SUFFIX + + // Sub-agent gets its own compaction for context safety + const prepareStep = createCompactionPrepareStep({ + contextWindow: deps.contextWindow, + }) + + // Create the sub-agent once — reused across invocations + const subAgent = new ToolLoopAgent({ + model: deps.model, + instructions, + tools: subAgentTools, + stopWhen: [stepCountIs(AGENT_LIMITS.SUB_AGENT_MAX_TURNS)], + prepareStep, + }) + + return tool({ + description: + 'Delegate a focused subtask to an independent sub-agent with its own context window. ' + + 'Use for research across many pages, data extraction, deep filesystem exploration, ' + + 'or any task that would consume significant context. ' + + 'The sub-agent has full tool access and returns a text summary when done.', + inputSchema: z.object({ + task: z + .string() + .describe( + 'Clear, self-contained description of the subtask. ' + + 'Include all necessary context — URLs, file paths, search terms, expected output format.', + ), + }), + execute: async ({ task }, { abortSignal }) => { + logger.info('Spawning sub-agent', { + taskPreview: task.slice(0, 120), + }) + + const result = await subAgent.generate({ + prompt: task, + abortSignal, + }) + + logger.info('Sub-agent completed', { + steps: result.steps.length, + finishReason: result.finishReason, + }) + + return result.text + }, + }) +} diff --git a/packages/shared/src/constants/limits.ts b/packages/shared/src/constants/limits.ts index f60308d5..da31b220 100644 --- a/packages/shared/src/constants/limits.ts +++ b/packages/shared/src/constants/limits.ts @@ -14,6 +14,7 @@ export const RATE_LIMITS = { export const AGENT_LIMITS = { MAX_TURNS: 100, + SUB_AGENT_MAX_TURNS: 15, DEFAULT_CONTEXT_WINDOW: 200_000, // Compression settings for context compaction heuristics From 2d4514fa6b7b69f4f3aabc09622f81cc79e4d4be Mon Sep 17 00:00:00 2001 From: shivammittal274 <56757235+shivammittal274@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:58:14 +0530 Subject: [PATCH 2/3] Update apps/server/src/agent/sub-agent.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/server/src/agent/sub-agent.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/server/src/agent/sub-agent.ts b/apps/server/src/agent/sub-agent.ts index fdb36600..ce9fbf3f 100644 --- a/apps/server/src/agent/sub-agent.ts +++ b/apps/server/src/agent/sub-agent.ts @@ -65,17 +65,24 @@ export function createDelegateTaskTool(deps: DelegateTaskDeps) { taskPreview: task.slice(0, 120), }) - const result = await subAgent.generate({ - prompt: task, - abortSignal, - }) + try { + const result = await subAgent.generate({ + prompt: task, + abortSignal, + }) - logger.info('Sub-agent completed', { - steps: result.steps.length, - finishReason: result.finishReason, - }) + logger.info('Sub-agent completed', { + steps: result.steps.length, + finishReason: result.finishReason, + }) - return result.text + return result.text || 'Sub-agent completed but produced no text output.' + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + logger.error('Sub-agent failed', { error: message }) + return `Sub-agent failed: ${message}` + } + }, }, }) } From fe84a8af05ebc9bde5706068f0166786643e99b2 Mon Sep 17 00:00:00 2001 From: shivammittal274 Date: Tue, 10 Mar 2026 22:05:35 +0530 Subject: [PATCH 3/3] fix: remove stray closing brace in sub-agent.ts --- apps/server/src/agent/sub-agent.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/agent/sub-agent.ts b/apps/server/src/agent/sub-agent.ts index ce9fbf3f..0079b162 100644 --- a/apps/server/src/agent/sub-agent.ts +++ b/apps/server/src/agent/sub-agent.ts @@ -83,6 +83,5 @@ export function createDelegateTaskTool(deps: DelegateTaskDeps) { return `Sub-agent failed: ${message}` } }, - }, }) }