From e6c04e91760bed451360f5186df1330bca2a5636 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:50:53 +0530 Subject: [PATCH 01/15] remove unnecessary lint --- extensions/cli/src/subagent/executor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/cli/src/subagent/executor.ts b/extensions/cli/src/subagent/executor.ts index 1580e7f7f9..bb0982f003 100644 --- a/extensions/cli/src/subagent/executor.ts +++ b/extensions/cli/src/subagent/executor.ts @@ -54,7 +54,6 @@ async function buildAgentSystemMessage( /** * Execute a subagent in a child session */ -// eslint-disable-next-line complexity export async function executeSubAgent( options: SubAgentExecutionOptions, ): Promise { From ce5cf78419e9dda17cf559c8db7b9b2cae102434 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:57:27 +0530 Subject: [PATCH 02/15] add tool result in chat history for subagent when chathistoryservice is disabled in subagent, manually update the last tool state with output and status --- extensions/cli/src/stream/handleToolCalls.ts | 32 ++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/extensions/cli/src/stream/handleToolCalls.ts b/extensions/cli/src/stream/handleToolCalls.ts index bc5d99176e..f4f477da8b 100644 --- a/extensions/cli/src/stream/handleToolCalls.ts +++ b/extensions/cli/src/stream/handleToolCalls.ts @@ -147,13 +147,41 @@ export async function handleToolCalls( // Execute the valid preprocessed tool calls // Note: executeStreamedToolCalls adds tool results to toolCallStates via - // services.chatHistory.addToolResult() internally - const { hasRejection } = await executeStreamedToolCalls( + // services.chatHistory.addToolResult() internally when service is available + const { hasRejection, chatHistoryEntries } = await executeStreamedToolCalls( preprocessedCalls, callbacks, isHeadless, ); + // When ChatHistoryService is disabled in subagent execution, + // manually update the local chatHistory array with tool results + if (!useService && chatHistoryEntries.length > 0) { + const lastAssistantIndex = chatHistory.findLastIndex( + (item) => item.message.role === "assistant" && item.toolCallStates, + ); + if ( + lastAssistantIndex >= 0 && + chatHistory[lastAssistantIndex].toolCallStates + ) { + for (const entry of chatHistoryEntries) { + const toolState = chatHistory[lastAssistantIndex].toolCallStates!.find( + (ts) => ts.toolCallId === entry.tool_call_id, + ); + if (toolState) { + toolState.status = entry.status; + toolState.output = [ + { + content: String(entry.content) || "", + name: "Tool Result", + description: "Tool execution result", + }, + ]; + } + } + } + } + if (isHeadless && hasRejection) { logger.debug( "Tool call rejected in headless mode - returning current content", From 4125fb22e339fee401dd99f8a395790de0fd0a97 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:58:57 +0530 Subject: [PATCH 03/15] prevent nested subagent execution --- extensions/cli/src/subagent/executor.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/extensions/cli/src/subagent/executor.ts b/extensions/cli/src/subagent/executor.ts index bb0982f003..2b6ebd794f 100644 --- a/extensions/cli/src/subagent/executor.ts +++ b/extensions/cli/src/subagent/executor.ts @@ -8,6 +8,8 @@ import { streamChatResponse } from "../stream/streamChatResponse.js"; import { escapeEvents } from "../util/cli.js"; import { logger } from "../util/logger.js"; +let isInsideSubagent = false; + /** * Options for executing a subagent */ @@ -57,6 +59,14 @@ async function buildAgentSystemMessage( export async function executeSubAgent( options: SubAgentExecutionOptions, ): Promise { + if (isInsideSubagent) { + return { + success: false, + response: "", + error: "Nested subagent invocation is not allowed", + }; + } + const { agent: subAgent, prompt, abortController, onOutputUpdate } = options; const mainAgentPermissionsState = @@ -64,6 +74,8 @@ export async function executeSubAgent( SERVICE_NAMES.TOOL_PERMISSIONS, ); + isInsideSubagent = true; + try { logger.debug("Starting subagent execution", { agent: subAgent.model?.name, @@ -167,6 +179,8 @@ export async function executeSubAgent( ? lastMessage.message.content : ""; + logger.debug("debug1 subagent chathistory", { chatHistory }); + logger.debug("Subagent execution completed", { agent: model?.name, responseLength: response.length, @@ -208,5 +222,7 @@ export async function executeSubAgent( response: "", error: error.message, }; + } finally { + isInsideSubagent = false; } } From bbcf04e687dfc0279ab41d3ab93e638355f59a41 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:19:07 +0530 Subject: [PATCH 04/15] add builtin subagents explorer and code reviewer --- .../cli/src/subagent/builtInSubagents.ts | 90 +++++++++++++++++++ extensions/cli/src/subagent/executor.ts | 2 - extensions/cli/src/subagent/get-agents.ts | 44 ++++++--- 3 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 extensions/cli/src/subagent/builtInSubagents.ts diff --git a/extensions/cli/src/subagent/builtInSubagents.ts b/extensions/cli/src/subagent/builtInSubagents.ts new file mode 100644 index 0000000000..0c60d5f5ad --- /dev/null +++ b/extensions/cli/src/subagent/builtInSubagents.ts @@ -0,0 +1,90 @@ +import type { ModelConfig } from "@continuedev/config-yaml"; + +import { logger } from "src/util/logger.js"; + +export interface BuiltInSubagent { + name: string; + systemPrompt: string; + model: string; +} + +export const NAVIGATOR_SUBAGENT: BuiltInSubagent = { + name: "navigator", + model: "claude-haiku-4-5", + systemPrompt: `You are a Codebase Navigator subagent specialized in exploring, searching, and mapping large codebases. + +When to use: + + Use this subagent whenever you need to explore or find or understand a codebase or a folder. + +When navigating a codebase, you will: + +1. **Locate Relevant Code**: Use file and code search tools to find the most relevant files, modules, functions, and types. Prefer a small, high-signal set of locations over exhaustive listings. + +2. **Trace Behavior and Dependencies**: Follow call chains, imports, and data flow to understand how the relevant pieces interact, including upstream/downstream dependencies and important side effects. + +3. **Map the Codebase for Others**: Build a concise mental map: which components are core, which are helpers, where entry points live, and how configuration or environment affects behavior. + +Your output should be concise and actionable, starting with a brief summary of what you found and listing the key files/paths, functions, symbols, and important relationships or flows between them in plain language. If you cannot find something, describe what you searched for, where you looked, and suggest next places or strategies to investigate.`, +}; + +export const CODE_REVIEWER_SUBAGENT: BuiltInSubagent = { + name: "code-reviewer", + model: "claude-sonnet-4-6", + systemPrompt: `You are a Senior Code Reviewer with expertise in software architecture, design patterns, and best practices. Your role is to review completed project steps against original plans and ensure code quality standards are met. + +When to use: + + Use this subagent whenever you are requested to review code, or after a feature or refactor is implemented and you want a structured review against the original plan and code quality standards. + +When reviewing completed work, you will: + +1. **Plan Alignment Analysis**: Compare implementation against original plans, identify justified vs problematic deviations, and verify all planned functionality is complete + +2. **Code Quality Assessment**: Review adherence to patterns, error handling, type safety, naming conventions, test coverage, and potential security or performance issues + +3. **Architecture and Design Review**: Ensure proper architectural patterns, separation of concerns, loose coupling, system integration, and scalability considerations + +4. **Documentation and Standards**: Verify appropriate comments, function documentation, file headers, and adherence to project-specific coding standards + +5. **Issue Identification and Recommendations**: Categorize issues as Critical/Important/Suggestions with specific examples, actionable recommendations, and code examples when helpful + +Your output should be structured, actionable, and focused on helping maintain high code quality while ensuring project goals are met. Be thorough but concise, and always provide constructive feedback that helps improve both the current implementation and future development practices.`, +}; + +export const BUILT_IN_SUBAGENTS: BuiltInSubagent[] = [ + NAVIGATOR_SUBAGENT, + CODE_REVIEWER_SUBAGENT, +]; + +export function createBuiltInSubagentModel( + subagent: BuiltInSubagent, + baseModel: ModelConfig, +): ModelConfig { + return { + ...baseModel, + name: subagent.name, + model: subagent.model, + roles: ["subagent"], + chatOptions: { + ...baseModel.chatOptions, + baseSystemMessage: subagent.systemPrompt, + }, + }; +} + +export function isLocalAnthropicModel(model: ModelConfig | null): boolean { + if (!model) { + return false; + } + + const isAnthropic = model.provider === "anthropic"; + const hasDirectApiKey = + typeof model.apiKey === "string" && model.apiKey.length > 0; + + logger.debug("subagent_enabled_for_anthropic", { + enabled: isAnthropic && hasDirectApiKey, + }); + + return isAnthropic && hasDirectApiKey; +} diff --git a/extensions/cli/src/subagent/executor.ts b/extensions/cli/src/subagent/executor.ts index 2b6ebd794f..3790250bb7 100644 --- a/extensions/cli/src/subagent/executor.ts +++ b/extensions/cli/src/subagent/executor.ts @@ -179,8 +179,6 @@ export async function executeSubAgent( ? lastMessage.message.content : ""; - logger.debug("debug1 subagent chathistory", { chatHistory }); - logger.debug("Subagent execution completed", { agent: model?.name, responseLength: response.length, diff --git a/extensions/cli/src/subagent/get-agents.ts b/extensions/cli/src/subagent/get-agents.ts index 9abdc2d3ad..872f551036 100644 --- a/extensions/cli/src/subagent/get-agents.ts +++ b/extensions/cli/src/subagent/get-agents.ts @@ -1,24 +1,48 @@ +import { createLlmApi } from "../config.js"; import { ModelService } from "../services/ModelService.js"; import type { ModelServiceState } from "../services/types.js"; -/** - * Get an agent by name - */ +import { + BUILT_IN_SUBAGENTS, + createBuiltInSubagentModel, + isLocalAnthropicModel, +} from "./builtInSubagents.js"; + +function getAllSubagentModels(modelState: ModelServiceState) { + const configSubagents = ModelService.getSubagentModels(modelState); + + if (!isLocalAnthropicModel(modelState.model)) { + return configSubagents; + } + + const builtInSubagents = BUILT_IN_SUBAGENTS.map((subagent) => { + const subagentModel = createBuiltInSubagentModel( + subagent, + modelState.model!, + ); + return { + llmApi: createLlmApi(subagentModel, modelState.authConfig), + model: subagentModel, + assistant: modelState.assistant, + authConfig: modelState.authConfig, + }; + }); + + return [...configSubagents, ...builtInSubagents]; +} + export function getSubagent(modelState: ModelServiceState, name: string) { return ( - ModelService.getSubagentModels(modelState).find( + getAllSubagentModels(modelState).find( (model) => model.model.name === name, ) ?? null ); } -/** - * Generate dynamic tool description listing available agents - */ export function generateSubagentToolDescription( modelState: ModelServiceState, ): string { - const agentList = ModelService.getSubagentModels(modelState) + const agentList = getAllSubagentModels(modelState) .map( (subagentModel) => ` - ${subagentModel.model.name}: ${subagentModel.model.chatOptions?.baseSystemMessage}`, @@ -34,7 +58,5 @@ ${agentList} } export function getAgentNames(modelState: ModelServiceState): string[] { - return ModelService.getSubagentModels(modelState).map( - (model) => model.model.name, - ); + return getAllSubagentModels(modelState).map((model) => model.model.name); } From d5676dbe7d889136abd838fb20760a45e89d31d0 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:22:56 +0530 Subject: [PATCH 05/15] refine subagent tool description --- extensions/cli/src/subagent/get-agents.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/cli/src/subagent/get-agents.ts b/extensions/cli/src/subagent/get-agents.ts index 872f551036..563ac9820e 100644 --- a/extensions/cli/src/subagent/get-agents.ts +++ b/extensions/cli/src/subagent/get-agents.ts @@ -49,8 +49,11 @@ export function generateSubagentToolDescription( ) .join("\n"); - // todo: refine this prompt later - return `Launch a specialized subagent to handle a specific task. + return `Launch an autonomous specialized subagent to handle a specific task. + +You have the independence to make decisions within you scope. You should focus on the specific task given by the main agent. + +Remember: You are part of a larger system. Your specialized focus helps the main agent handle multiple concerns efficiently. Here are the available subagents: ${agentList} From e19a8b1ffe84ef7ad3986848d282fcf46cdb570b Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:26:25 +0530 Subject: [PATCH 06/15] put subagent tool out of beta --- extensions/cli/src/index.ts | 4 ---- extensions/cli/src/services/index.ts | 8 +------- extensions/cli/src/tools/index.tsx | 9 ++------- extensions/cli/src/tools/toolsConfig.ts | 9 --------- 4 files changed, 3 insertions(+), 27 deletions(-) diff --git a/extensions/cli/src/index.ts b/extensions/cli/src/index.ts index a3a718e3ef..f251a427ee 100644 --- a/extensions/cli/src/index.ts +++ b/extensions/cli/src/index.ts @@ -200,10 +200,6 @@ addCommonOptions(program) ) .option("--resume", "Resume from last session") .option("--fork ", "Fork from an existing session ID") - .option( - "--beta-subagent-tool", - "Enable beta Subagent tool for invoking subagents", - ) .action(async (prompt, options) => { // Telemetry: record command invocation await posthogService.capture("cliCommand", { command: "cn" }); diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index c1807e55c0..9be4c98e49 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -1,9 +1,6 @@ import { loadAuthConfig } from "../auth/workos.js"; import { initializeWithOnboarding } from "../onboarding.js"; -import { - setBetaSubagentToolEnabled, - setBetaUploadArtifactToolEnabled, -} from "../tools/toolsConfig.js"; +import { setBetaUploadArtifactToolEnabled } from "../tools/toolsConfig.js"; import { logger } from "../util/logger.js"; import { AgentFileService } from "./AgentFileService.js"; @@ -66,9 +63,6 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { if (commandOptions.betaUploadArtifactTool) { setBetaUploadArtifactToolEnabled(true); } - if (commandOptions.betaSubagentTool) { - setBetaSubagentToolEnabled(true); - } // Handle onboarding for TUI mode (headless: false) unless explicitly skipped if (!initOptions.headless && !initOptions.skipOnboarding) { const authConfig = loadAuthConfig(); diff --git a/extensions/cli/src/tools/index.tsx b/extensions/cli/src/tools/index.tsx index eb10d870d6..127038d93c 100644 --- a/extensions/cli/src/tools/index.tsx +++ b/extensions/cli/src/tools/index.tsx @@ -31,10 +31,7 @@ import { runTerminalCommandTool } from "./runTerminalCommand.js"; import { checkIfRipgrepIsInstalled, searchCodeTool } from "./searchCode.js"; import { skillsTool } from "./skills.js"; import { subagentTool } from "./subagent.js"; -import { - isBetaSubagentToolEnabled, - isBetaUploadArtifactToolEnabled, -} from "./toolsConfig.js"; +import { isBetaUploadArtifactToolEnabled } from "./toolsConfig.js"; import { type Tool, type ToolCall, @@ -127,9 +124,7 @@ export async function getAllAvailableTools( tools.push(exitTool); } - if (isBetaSubagentToolEnabled()) { - tools.push(await subagentTool()); - } + tools.push(await subagentTool()); tools.push(await skillsTool()); diff --git a/extensions/cli/src/tools/toolsConfig.ts b/extensions/cli/src/tools/toolsConfig.ts index e9a2e02330..b1703a545b 100644 --- a/extensions/cli/src/tools/toolsConfig.ts +++ b/extensions/cli/src/tools/toolsConfig.ts @@ -4,7 +4,6 @@ */ let betaUploadArtifactToolEnabled = false; -let betaSubagentToolEnabled = false; export function setBetaUploadArtifactToolEnabled(enabled: boolean): void { betaUploadArtifactToolEnabled = enabled; @@ -13,11 +12,3 @@ export function setBetaUploadArtifactToolEnabled(enabled: boolean): void { export function isBetaUploadArtifactToolEnabled(): boolean { return betaUploadArtifactToolEnabled; } - -export function setBetaSubagentToolEnabled(enabled: boolean): void { - betaSubagentToolEnabled = enabled; -} - -export function isBetaSubagentToolEnabled(): boolean { - return betaSubagentToolEnabled; -} From c1166405417d894a3699ce96016d1a3a3341d607 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:17:31 +0530 Subject: [PATCH 07/15] rename to getAgents --- extensions/cli/src/subagent/{get-agents.ts => getAgents.ts} | 0 extensions/cli/src/tools/subagent.test.ts | 2 +- extensions/cli/src/tools/subagent.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename extensions/cli/src/subagent/{get-agents.ts => getAgents.ts} (100%) diff --git a/extensions/cli/src/subagent/get-agents.ts b/extensions/cli/src/subagent/getAgents.ts similarity index 100% rename from extensions/cli/src/subagent/get-agents.ts rename to extensions/cli/src/subagent/getAgents.ts diff --git a/extensions/cli/src/tools/subagent.test.ts b/extensions/cli/src/tools/subagent.test.ts index 3af2ff4bfe..9aae206637 100644 --- a/extensions/cli/src/tools/subagent.test.ts +++ b/extensions/cli/src/tools/subagent.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { services } from "../services/index.js"; import { serviceContainer } from "../services/ServiceContainer.js"; import { executeSubAgent } from "../subagent/executor.js"; -import { getAgentNames, getSubagent } from "../subagent/get-agents.js"; +import { getAgentNames, getSubagent } from "../subagent/getAgents.js"; import { subagentTool } from "./subagent.js"; diff --git a/extensions/cli/src/tools/subagent.ts b/extensions/cli/src/tools/subagent.ts index 43d2d29eb4..86645b5062 100644 --- a/extensions/cli/src/tools/subagent.ts +++ b/extensions/cli/src/tools/subagent.ts @@ -6,7 +6,7 @@ import { generateSubagentToolDescription, getSubagent, getAgentNames as getSubagentNames, -} from "../subagent/get-agents.js"; +} from "../subagent/getAgents.js"; import { SUBAGENT_TOOL_META } from "../subagent/index.js"; import { logger } from "../util/logger.js"; From af13df6f0f0d6ea7c8c7ca771564aa85520f81a1 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:45:56 +0530 Subject: [PATCH 08/15] change code review to generalist subagent --- .../cli/src/subagent/builtInSubagents.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/extensions/cli/src/subagent/builtInSubagents.ts b/extensions/cli/src/subagent/builtInSubagents.ts index 0c60d5f5ad..2880c7e422 100644 --- a/extensions/cli/src/subagent/builtInSubagents.ts +++ b/extensions/cli/src/subagent/builtInSubagents.ts @@ -28,33 +28,29 @@ When navigating a codebase, you will: Your output should be concise and actionable, starting with a brief summary of what you found and listing the key files/paths, functions, symbols, and important relationships or flows between them in plain language. If you cannot find something, describe what you searched for, where you looked, and suggest next places or strategies to investigate.`, }; -export const CODE_REVIEWER_SUBAGENT: BuiltInSubagent = { - name: "code-reviewer", +export const GENERALIST_SUBAGENT: BuiltInSubagent = { + name: "general-tasker", model: "claude-sonnet-4-6", - systemPrompt: `You are a Senior Code Reviewer with expertise in software architecture, design patterns, and best practices. Your role is to review completed project steps against original plans and ensure code quality standards are met. + systemPrompt: `You are a Generalist subagent capable of handling any development task delegated to you. When to use: - Use this subagent whenever you are requested to review code, or after a feature or refactor is implemented and you want a structured review against the original plan and code quality standards. + Use this subagent for any task that doesn't require a specialized subagent, including but not limited to: implementing features, fixing bugs, refactoring, code review, documentation, research, debugging, and analysis. -When reviewing completed work, you will: +When handling a task, you will: -1. **Plan Alignment Analysis**: Compare implementation against original plans, identify justified vs problematic deviations, and verify all planned functionality is complete +1. **Interpret the Request**: Understand what is being asked, whether it's exploration, implementation, review, analysis, or something else entirely. Adapt your approach based on the nature of the task. -2. **Code Quality Assessment**: Review adherence to patterns, error handling, type safety, naming conventions, test coverage, and potential security or performance issues +2. **Gather Context**: Use available tools to explore the codebase, read relevant files, and understand the surrounding architecture before taking action or forming conclusions. -3. **Architecture and Design Review**: Ensure proper architectural patterns, separation of concerns, loose coupling, system integration, and scalability considerations +3. **Communicate Results**: Provide clear, actionable output tailored to the task. Summarize what you did or discovered, highlight key insights or changes, and note any open questions or recommended next steps. -4. **Documentation and Standards**: Verify appropriate comments, function documentation, file headers, and adherence to project-specific coding standards - -5. **Issue Identification and Recommendations**: Categorize issues as Critical/Important/Suggestions with specific examples, actionable recommendations, and code examples when helpful - -Your output should be structured, actionable, and focused on helping maintain high code quality while ensuring project goals are met. Be thorough but concise, and always provide constructive feedback that helps improve both the current implementation and future development practices.`, +You are flexible and resourceful. If a task is ambiguous, make reasonable assumptions and state them. If you encounter blockers, describe what you attempted and suggest alternatives.`, }; export const BUILT_IN_SUBAGENTS: BuiltInSubagent[] = [ NAVIGATOR_SUBAGENT, - CODE_REVIEWER_SUBAGENT, + GENERALIST_SUBAGENT, ]; export function createBuiltInSubagentModel( From 9a6f30813df54952265e8ae2cac2ff5db61d02b7 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:45:43 +0530 Subject: [PATCH 09/15] refactor subagent executor to subagent service --- .../cli/src/services/SubAgentService.ts | 268 ++++++++++++++++++ extensions/cli/src/services/index.ts | 8 + extensions/cli/src/services/types.ts | 6 + extensions/cli/src/subagent/executor.ts | 226 --------------- extensions/cli/src/tools/subagent.test.ts | 14 +- extensions/cli/src/tools/subagent.ts | 4 +- 6 files changed, 293 insertions(+), 233 deletions(-) create mode 100644 extensions/cli/src/services/SubAgentService.ts delete mode 100644 extensions/cli/src/subagent/executor.ts diff --git a/extensions/cli/src/services/SubAgentService.ts b/extensions/cli/src/services/SubAgentService.ts new file mode 100644 index 0000000000..a4e2ce203c --- /dev/null +++ b/extensions/cli/src/services/SubAgentService.ts @@ -0,0 +1,268 @@ +import type { ChatHistoryItem } from "core"; + +import { streamChatResponse } from "../stream/streamChatResponse.js"; +import { escapeEvents } from "../util/cli.js"; +import { logger } from "../util/logger.js"; + +import { BaseService } from "./BaseService.js"; +import { serviceContainer } from "./ServiceContainer.js"; +import type { ToolPermissionServiceState } from "./ToolPermissionService.js"; +import { type ModelServiceState, SERVICE_NAMES } from "./types.js"; + +/** Types */ + +export interface SubAgentExecutionOptions { + agent: ModelServiceState; + prompt: string; + parentSessionId: string; + abortController: AbortController; + onOutputUpdate?: (output: string) => void; +} + +export interface SubAgentResult { + success: boolean; + response: string; + error?: string; +} + +export interface PendingExecution { + agentName: string; + startTime: number; +} + +export interface SubAgentServiceState { + isInsideSubagent: boolean; + currentExecution: PendingExecution | null; +} + +/** Service */ + +export class SubAgentService extends BaseService { + constructor() { + super("SubAgentService", { + isInsideSubagent: false, + currentExecution: null, + }); + } + + async doInitialize(): Promise { + return { + isInsideSubagent: false, + currentExecution: null, + }; + } + + private async buildAgentSystemMessage( + agent: ModelServiceState, + services: any, + ): Promise { + const baseMessage = services.systemMessage + ? await services.systemMessage.getSystemMessage( + services.toolPermissions.getState().currentMode, + ) + : ""; + + const agentPrompt = agent.model?.chatOptions?.baseSystemMessage || ""; + + if (agentPrompt) { + return `${baseMessage}\n\n${agentPrompt}`; + } + + return baseMessage; + } + + async executeSubAgent( + options: SubAgentExecutionOptions, + ): Promise { + if (this.currentState.isInsideSubagent) { + return { + success: false, + response: "", + error: "Nested subagent invocation is not allowed", + }; + } + + const { + agent: subAgent, + prompt, + abortController, + onOutputUpdate, + } = options; + + const mainAgentPermissionsState = + await serviceContainer.get( + SERVICE_NAMES.TOOL_PERMISSIONS, + ); + + this.setState({ + isInsideSubagent: true, + currentExecution: { + agentName: subAgent.model?.name || "unknown", + startTime: Date.now(), + }, + }); + + this.emit("subagentStarted", { + agentName: subAgent.model?.name, + prompt, + }); + + try { + logger.debug("Starting subagent execution", { + agent: subAgent.model?.name, + }); + + const { model, llmApi } = subAgent; + if (!model || !llmApi) { + throw new Error("Model or LLM API not available"); + } + + // allow all tools for now + // todo: eventually we want to show the same prompt in a dialog whether asking whether that tool call is allowed or not + serviceContainer.set( + SERVICE_NAMES.TOOL_PERMISSIONS, + { + ...mainAgentPermissionsState, + permissions: { + policies: [{ tool: "*", permission: "allow" }], + }, + }, + ); + + const { services } = await import("./index.js"); + + // Build agent system message + const systemMessage = await this.buildAgentSystemMessage( + subAgent, + services, + ); + + // Store original system message function + const originalGetSystemMessage = services.systemMessage?.getSystemMessage; + + // Store original ChatHistoryService ready state + const chatHistorySvc = services.chatHistory; + const originalIsReady = + chatHistorySvc && typeof chatHistorySvc.isReady === "function" + ? chatHistorySvc.isReady + : undefined; + + // Override system message for this execution + if (services.systemMessage) { + services.systemMessage.getSystemMessage = async () => systemMessage; + } + + // Temporarily disable ChatHistoryService to prevent it from interfering with child session + if (chatHistorySvc && originalIsReady) { + chatHistorySvc.isReady = () => false; + } + + const chatHistory = [ + { + message: { role: "user", content: prompt }, + contextItems: [], + }, + ] as ChatHistoryItem[]; + + const escapeHandler = () => { + abortController.abort(); + chatHistory.push({ + message: { + role: "user", + content: "Subagent execution was cancelled by the user.", + }, + contextItems: [], + }); + }; + + escapeEvents.on("user-escape", escapeHandler); + + try { + let accumulatedOutput = ""; + + // Execute the chat stream with child session + await streamChatResponse( + chatHistory, + model, + llmApi, + abortController, + { + onContent: (content: string) => { + accumulatedOutput += content; + onOutputUpdate?.(accumulatedOutput); + }, + onToolResult: (result: string) => { + // todo: skip tool outputs - show tool names and params + accumulatedOutput += `\n\n${result}`; + onOutputUpdate?.(accumulatedOutput); + }, + }, + false, // Not compacting + ); + + // The last message (mostly) contains the important output to be submitted back to the main agent + const lastMessage = chatHistory.at(-1); + const response = + typeof lastMessage?.message?.content === "string" + ? lastMessage.message.content + : ""; + + logger.debug("Subagent execution completed", { + agent: model?.name, + responseLength: response.length, + }); + + this.emit("subagentCompleted", { + agentName: model?.name, + success: true, + }); + + return { + success: true, + response, + }; + } finally { + escapeEvents.removeListener("user-escape", escapeHandler); + + // Restore original system message function + if (services.systemMessage && originalGetSystemMessage) { + services.systemMessage.getSystemMessage = originalGetSystemMessage; + } + + // Restore original ChatHistoryService ready state + if (chatHistorySvc && originalIsReady) { + chatHistorySvc.isReady = originalIsReady; + } + + // Restore original main agent tool permissions + serviceContainer.set( + SERVICE_NAMES.TOOL_PERMISSIONS, + mainAgentPermissionsState, + ); + } + } catch (error: any) { + logger.error("Subagent execution failed", { + agent: subAgent.model?.name, + error: error.message, + }); + + this.emit("subagentFailed", { + agentName: subAgent.model?.name, + error: error.message, + }); + + return { + success: false, + response: "", + error: error.message, + }; + } finally { + this.setState({ + isInsideSubagent: false, + currentExecution: null, + }); + } + } +} + +export const subAgentService = new SubAgentService(); diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index dafc7075bd..05e7eec3b1 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -18,6 +18,7 @@ import { quizService } from "./QuizService.js"; import { ResourceMonitoringService } from "./ResourceMonitoringService.js"; import { serviceContainer } from "./ServiceContainer.js"; import { StorageSyncService } from "./StorageSyncService.js"; +import { subAgentService } from "./SubAgentService.js"; import { SystemMessageService } from "./SystemMessageService.js"; import { InitializeToolServiceOverrides, @@ -326,6 +327,12 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) { [], // No dependencies ); + serviceContainer.register( + SERVICE_NAMES.SUBAGENT, + () => subAgentService.initialize(), + [], // No dependencies + ); + // Eagerly initialize all services to ensure they're ready when needed // This avoids race conditions and "service not ready" errors await serviceContainer.initializeAll(); @@ -391,6 +398,7 @@ export const services = { gitAiIntegration: gitAiIntegrationService, backgroundJobs: backgroundJobService, quiz: quizService, + subAgent: subAgentService, } as const; export type ServicesType = typeof services; diff --git a/extensions/cli/src/services/types.ts b/extensions/cli/src/services/types.ts index 3b3776883b..87cff6b30c 100644 --- a/extensions/cli/src/services/types.ts +++ b/extensions/cli/src/services/types.ts @@ -130,6 +130,11 @@ export interface ArtifactUploadServiceState { lastError: string | null; } +export type { + PendingExecution, + SubAgentServiceState, +} from "./SubAgentService.js"; + export type { BackgroundJob, BackgroundJobStatus, @@ -159,6 +164,7 @@ export const SERVICE_NAMES = { GIT_AI_INTEGRATION: "gitAiIntegration", BACKGROUND_JOBS: "backgroundJobs", QUIZ: "quiz", + SUBAGENT: "subagent", } as const; /** diff --git a/extensions/cli/src/subagent/executor.ts b/extensions/cli/src/subagent/executor.ts deleted file mode 100644 index 3790250bb7..0000000000 --- a/extensions/cli/src/subagent/executor.ts +++ /dev/null @@ -1,226 +0,0 @@ -import type { ChatHistoryItem } from "core"; - -import { services } from "../services/index.js"; -import { serviceContainer } from "../services/ServiceContainer.js"; -import type { ToolPermissionServiceState } from "../services/ToolPermissionService.js"; -import { ModelServiceState, SERVICE_NAMES } from "../services/types.js"; -import { streamChatResponse } from "../stream/streamChatResponse.js"; -import { escapeEvents } from "../util/cli.js"; -import { logger } from "../util/logger.js"; - -let isInsideSubagent = false; - -/** - * Options for executing a subagent - */ -export interface SubAgentExecutionOptions { - agent: ModelServiceState; - prompt: string; - parentSessionId: string; - abortController: AbortController; - onOutputUpdate?: (output: string) => void; -} - -/** - * Result from executing a subagent - */ -export interface SubAgentResult { - success: boolean; - response: string; - error?: string; -} - -/** - * Build system message for the agent - */ -async function buildAgentSystemMessage( - agent: ModelServiceState, - services: any, -): Promise { - const baseMessage = services.systemMessage - ? await services.systemMessage.getSystemMessage( - services.toolPermissions.getState().currentMode, - ) - : ""; - - const agentPrompt = agent.model?.chatOptions?.baseSystemMessage || ""; - - // Combine base system message with agent-specific prompt - if (agentPrompt) { - return `${baseMessage}\n\n${agentPrompt}`; - } - - return baseMessage; -} - -/** - * Execute a subagent in a child session - */ -export async function executeSubAgent( - options: SubAgentExecutionOptions, -): Promise { - if (isInsideSubagent) { - return { - success: false, - response: "", - error: "Nested subagent invocation is not allowed", - }; - } - - const { agent: subAgent, prompt, abortController, onOutputUpdate } = options; - - const mainAgentPermissionsState = - await serviceContainer.get( - SERVICE_NAMES.TOOL_PERMISSIONS, - ); - - isInsideSubagent = true; - - try { - logger.debug("Starting subagent execution", { - agent: subAgent.model?.name, - }); - - const { model, llmApi } = subAgent; - if (!model || !llmApi) { - throw new Error("Model or LLM API not available"); - } - - // allow all tools for now - // todo: eventually we want to show the same prompt in a dialog whether asking whether that tool call is allowed or not - - serviceContainer.set( - SERVICE_NAMES.TOOL_PERMISSIONS, - { - ...mainAgentPermissionsState, - permissions: { - policies: [{ tool: "*", permission: "allow" }], - }, - }, - ); - - // Build agent system message - const systemMessage = await buildAgentSystemMessage(subAgent, services); - - // Store original system message function - const originalGetSystemMessage = services.systemMessage?.getSystemMessage; - - // Store original ChatHistoryService ready state - const chatHistorySvc = services.chatHistory; - const originalIsReady = - chatHistorySvc && typeof chatHistorySvc.isReady === "function" - ? chatHistorySvc.isReady - : undefined; - - // Override system message for this execution - if (services.systemMessage) { - services.systemMessage.getSystemMessage = async () => systemMessage; - } - - // Temporarily disable ChatHistoryService to prevent it from interfering with child session - if (chatHistorySvc && originalIsReady) { - chatHistorySvc.isReady = () => false; - } - - const chatHistory = [ - { - message: { - role: "user", - content: prompt, - }, - contextItems: [], - }, - ] as ChatHistoryItem[]; - - const escapeHandler = () => { - abortController.abort(); - chatHistory.push({ - message: { - role: "user", - content: "Subagent execution was cancelled by the user.", - }, - contextItems: [], - }); - }; - - escapeEvents.on("user-escape", escapeHandler); - - try { - let accumulatedOutput = ""; - - // Execute the chat stream with child session - await streamChatResponse( - chatHistory, - model, - llmApi, - abortController, - { - onContent: (content: string) => { - accumulatedOutput += content; - if (onOutputUpdate) { - onOutputUpdate(accumulatedOutput); - } - }, - onToolResult: (result: string) => { - // todo: skip tool outputs - show tool names and params - accumulatedOutput += `\n\n${result}`; - if (onOutputUpdate) { - onOutputUpdate(accumulatedOutput); - } - }, - }, - false, // Not compacting - ); - - // The last message (mostly) contains the important output to be submitted back to the main agent - const lastMessage = chatHistory.at(-1); - const response = - typeof lastMessage?.message?.content === "string" - ? lastMessage.message.content - : ""; - - logger.debug("Subagent execution completed", { - agent: model?.name, - responseLength: response.length, - }); - - return { - success: true, - response, - }; - } finally { - if (escapeHandler) { - escapeEvents.removeListener("user-escape", escapeHandler); - } - - // Restore original system message function - if (services.systemMessage && originalGetSystemMessage) { - services.systemMessage.getSystemMessage = originalGetSystemMessage; - } - - // Restore original ChatHistoryService ready state - if (chatHistorySvc && originalIsReady) { - chatHistorySvc.isReady = originalIsReady; - } - - // Restore original main agent tool permissions - serviceContainer.set( - SERVICE_NAMES.TOOL_PERMISSIONS, - mainAgentPermissionsState, - ); - } - } catch (error: any) { - logger.error("Subagent execution failed", { - agent: subAgent.model?.name, - error: error.message, - }); - - return { - success: false, - response: "", - error: error.message, - }; - } finally { - isInsideSubagent = false; - } -} diff --git a/extensions/cli/src/tools/subagent.test.ts b/extensions/cli/src/tools/subagent.test.ts index 9aae206637..cb8f74271b 100644 --- a/extensions/cli/src/tools/subagent.test.ts +++ b/extensions/cli/src/tools/subagent.test.ts @@ -2,13 +2,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { services } from "../services/index.js"; import { serviceContainer } from "../services/ServiceContainer.js"; -import { executeSubAgent } from "../subagent/executor.js"; +import { subAgentService } from "../services/SubAgentService.js"; import { getAgentNames, getSubagent } from "../subagent/getAgents.js"; import { subagentTool } from "./subagent.js"; vi.mock("../subagent/get-agents.js"); -vi.mock("../subagent/executor.js"); +vi.mock("../services/SubAgentService.js", () => ({ + subAgentService: { + executeSubAgent: vi.fn(), + }, +})); vi.mock("../services/ServiceContainer.js", () => ({ serviceContainer: { get: vi.fn(), @@ -85,7 +89,7 @@ describe("subagentTool", () => { vi.mocked(getSubagent).mockReturnValue({ model: { name: "test-model" }, } as any); - vi.mocked(executeSubAgent).mockResolvedValue({ + vi.mocked(subAgentService.executeSubAgent).mockResolvedValue({ success: true, response: "subagent-output", } as any); @@ -100,8 +104,8 @@ describe("subagentTool", () => { { toolCallId: "tool-call-id", parallelToolCallCount: 1 }, ); - expect(vi.mocked(executeSubAgent)).toHaveBeenCalledTimes(1); - const [options] = vi.mocked(executeSubAgent).mock.calls[0]; + expect(vi.mocked(subAgentService.executeSubAgent)).toHaveBeenCalledTimes(1); + const [options] = vi.mocked(subAgentService.executeSubAgent).mock.calls[0]; expect(options.prompt).toBe("Subagent prompt"); expect(options.parentSessionId).toBe("parent-session-id"); diff --git a/extensions/cli/src/tools/subagent.ts b/extensions/cli/src/tools/subagent.ts index 86645b5062..21dceb6ecc 100644 --- a/extensions/cli/src/tools/subagent.ts +++ b/extensions/cli/src/tools/subagent.ts @@ -1,7 +1,7 @@ import { services } from "../services/index.js"; import { serviceContainer } from "../services/ServiceContainer.js"; +import { subAgentService } from "../services/SubAgentService.js"; import { ModelServiceState, SERVICE_NAMES } from "../services/types.js"; -import { executeSubAgent } from "../subagent/executor.js"; import { generateSubagentToolDescription, getSubagent, @@ -78,7 +78,7 @@ export const subagentTool = async (): Promise => { } // Execute subagent with output streaming - const result = await executeSubAgent({ + const result = await subAgentService.executeSubAgent({ agent, prompt, parentSessionId, From 60ade991fc24415ace28ea13cef8e3ea86e6f4aa Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:14:31 +0530 Subject: [PATCH 10/15] separate out subagent output from tool results --- .../cli/src/services/SubAgentService.ts | 22 ++--- extensions/cli/src/tools/subagent.test.ts | 9 -- extensions/cli/src/tools/subagent.ts | 14 --- extensions/cli/src/ui/TUIChat.tsx | 4 + extensions/cli/src/ui/ToolResultSummary.tsx | 51 ----------- .../cli/src/ui/components/SubAgentOutput.tsx | 91 +++++++++++++++++++ 6 files changed, 105 insertions(+), 86 deletions(-) create mode 100644 extensions/cli/src/ui/components/SubAgentOutput.tsx diff --git a/extensions/cli/src/services/SubAgentService.ts b/extensions/cli/src/services/SubAgentService.ts index a4e2ce203c..4aeeeea5d5 100644 --- a/extensions/cli/src/services/SubAgentService.ts +++ b/extensions/cli/src/services/SubAgentService.ts @@ -16,7 +16,6 @@ export interface SubAgentExecutionOptions { prompt: string; parentSessionId: string; abortController: AbortController; - onOutputUpdate?: (output: string) => void; } export interface SubAgentResult { @@ -82,12 +81,7 @@ export class SubAgentService extends BaseService { }; } - const { - agent: subAgent, - prompt, - abortController, - onOutputUpdate, - } = options; + const { agent: subAgent, prompt, abortController } = options; const mainAgentPermissionsState = await serviceContainer.get( @@ -180,7 +174,6 @@ export class SubAgentService extends BaseService { try { let accumulatedOutput = ""; - // Execute the chat stream with child session await streamChatResponse( chatHistory, model, @@ -189,15 +182,20 @@ export class SubAgentService extends BaseService { { onContent: (content: string) => { accumulatedOutput += content; - onOutputUpdate?.(accumulatedOutput); + this.emit("subagentContent", { + agentName: model?.name, + content: accumulatedOutput, + }); }, onToolResult: (result: string) => { - // todo: skip tool outputs - show tool names and params accumulatedOutput += `\n\n${result}`; - onOutputUpdate?.(accumulatedOutput); + this.emit("subagentContent", { + agentName: model?.name, + content: accumulatedOutput, + }); }, }, - false, // Not compacting + false, ); // The last message (mostly) contains the important output to be submitted back to the main agent diff --git a/extensions/cli/src/tools/subagent.test.ts b/extensions/cli/src/tools/subagent.test.ts index cb8f74271b..9c05983b28 100644 --- a/extensions/cli/src/tools/subagent.test.ts +++ b/extensions/cli/src/tools/subagent.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { services } from "../services/index.js"; import { serviceContainer } from "../services/ServiceContainer.js"; import { subAgentService } from "../services/SubAgentService.js"; import { getAgentNames, getSubagent } from "../subagent/getAgents.js"; @@ -109,14 +108,6 @@ describe("subagentTool", () => { expect(options.prompt).toBe("Subagent prompt"); expect(options.parentSessionId).toBe("parent-session-id"); - expect(typeof options.onOutputUpdate).toBe("function"); - - options.onOutputUpdate?.("partial-output"); - expect(vi.mocked(services.chatHistory.addToolResult)).toHaveBeenCalledWith( - "tool-call-id", - "partial-output", - "calling", - ); expect(result).toBe( "subagent-output\n\nstatus: completed\n", diff --git a/extensions/cli/src/tools/subagent.ts b/extensions/cli/src/tools/subagent.ts index 21dceb6ecc..3ac3f53426 100644 --- a/extensions/cli/src/tools/subagent.ts +++ b/extensions/cli/src/tools/subagent.ts @@ -77,25 +77,11 @@ export const subagentTool = async (): Promise => { throw new Error("No active session found"); } - // Execute subagent with output streaming const result = await subAgentService.executeSubAgent({ agent, prompt, parentSessionId, abortController: new AbortController(), - onOutputUpdate: context?.toolCallId - ? (output: string) => { - try { - chatHistoryService.addToolResult( - context.toolCallId, - output, - "calling", - ); - } catch { - // Ignore errors during streaming updates - } - } - : undefined, }); logger.debug("subagent result", { result }); diff --git a/extensions/cli/src/ui/TUIChat.tsx b/extensions/cli/src/ui/TUIChat.tsx index 62174ceada..7dec77ccd9 100644 --- a/extensions/cli/src/ui/TUIChat.tsx +++ b/extensions/cli/src/ui/TUIChat.tsx @@ -28,6 +28,7 @@ import { BottomStatusBar } from "./components/BottomStatusBar.js"; import { ResourceDebugBar } from "./components/ResourceDebugBar.js"; import { ScreenContent } from "./components/ScreenContent.js"; import { StaticChatContent } from "./components/StaticChatContent.js"; +import { SubAgentOutput } from "./components/SubAgentOutput.js"; import { useNavigation } from "./context/NavigationContext.js"; import { useChat } from "./hooks/useChat.js"; import { useContextPercentage } from "./hooks/useContextPercentage.js"; @@ -416,6 +417,9 @@ const TUIChat: React.FC = ({ loadingColor="grey" /> + {/* Subagent Output */} + + {/* Temporary status message */} {statusMessage && ( diff --git a/extensions/cli/src/ui/ToolResultSummary.tsx b/extensions/cli/src/ui/ToolResultSummary.tsx index 97e1794279..9e2de08243 100644 --- a/extensions/cli/src/ui/ToolResultSummary.tsx +++ b/extensions/cli/src/ui/ToolResultSummary.tsx @@ -115,57 +115,6 @@ const ToolResultSummary: React.FC = ({ } } - // show streaming output for subagent tool output - if (toolName === "Subagent") { - const metadataIndex = content.indexOf(""); - const actualOutput = - metadataIndex >= 0 ? content.slice(0, metadataIndex).trim() : content; - - if (!actualOutput) { - return ( - - - Subagent executing... - - ); - } - - const outputLines = actualOutput.split("\n"); - const MAX_TASK_OUTPUT_LINES = 20; - - if (outputLines.length <= MAX_TASK_OUTPUT_LINES) { - return ( - - - - Subagent output: - - - {actualOutput.trimEnd()} - - - ); - } else { - const lastLines = outputLines.slice(-MAX_TASK_OUTPUT_LINES).join("\n"); - return ( - - - - Subagent output: - - - - ... +{outputLines.length - MAX_TASK_OUTPUT_LINES} lines - - - - {lastLines.trimEnd()} - - - ); - } - } - // Handle all other cases with text summary const getSummary = () => { // Check if this is a user cancellation first diff --git a/extensions/cli/src/ui/components/SubAgentOutput.tsx b/extensions/cli/src/ui/components/SubAgentOutput.tsx new file mode 100644 index 0000000000..c29074e52c --- /dev/null +++ b/extensions/cli/src/ui/components/SubAgentOutput.tsx @@ -0,0 +1,91 @@ +import { Box, Text } from "ink"; +import React, { useEffect, useState } from "react"; + +import { subAgentService } from "../../services/SubAgentService.js"; +import { LoadingAnimation } from "../LoadingAnimation.js"; +import { MarkdownRenderer } from "../MarkdownRenderer.js"; + +interface SubAgentState { + agentName: string | undefined; + content: string; + isRunning: boolean; + prompt?: string; +} + +export const SubAgentOutput: React.FC = () => { + const [state, setState] = useState({ + agentName: undefined, + content: "", + isRunning: false, + }); + + useEffect(() => { + const onStarted = (data: { agentName: string; prompt: string }) => { + setState({ + agentName: data.agentName, + content: "", + isRunning: true, + prompt: data.prompt, + }); + }; + + const onContent = (data: { agentName: string; content: string }) => { + setState((prev) => ({ + ...prev, + agentName: data.agentName, + content: data.content, + })); + }; + + const onCompleted = () => { + setState((prev) => ({ ...prev, isRunning: false })); + }; + + const onFailed = () => { + setState((prev) => ({ ...prev, isRunning: false })); + }; + + subAgentService.on("subagentStarted", onStarted); + subAgentService.on("subagentContent", onContent); + subAgentService.on("subagentCompleted", onCompleted); + subAgentService.on("subagentFailed", onFailed); + + return () => { + subAgentService.off("subagentStarted", onStarted); + subAgentService.off("subagentContent", onContent); + subAgentService.off("subagentCompleted", onCompleted); + subAgentService.off("subagentFailed", onFailed); + }; + }, []); + + if (!state.isRunning) { + return null; + } + + const MAX_OUTPUT_LINES = 15; + const lines = state.content.split("\n"); + const displayContent = + lines.length > MAX_OUTPUT_LINES + ? lines.slice(-MAX_OUTPUT_LINES).join("\n") + : state.content; + const hiddenLines = + lines.length > MAX_OUTPUT_LINES ? lines.length - MAX_OUTPUT_LINES : 0; + + return ( + + + + + {" "} + Subagent: {state.agentName || "unknown"} + + + {state.content && ( + + {hiddenLines > 0 && ... +{hiddenLines} lines} + + + )} + + ); +}; From 064f45b6c2beed9f6f2480e405852cea8053673d Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:27:02 +0530 Subject: [PATCH 11/15] emit a single line instead of accumulated output --- .../cli/src/services/SubAgentService.ts | 10 +-- .../cli/src/ui/components/SubAgentOutput.tsx | 79 +++++++++---------- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/extensions/cli/src/services/SubAgentService.ts b/extensions/cli/src/services/SubAgentService.ts index 4aeeeea5d5..dd05dec676 100644 --- a/extensions/cli/src/services/SubAgentService.ts +++ b/extensions/cli/src/services/SubAgentService.ts @@ -172,8 +172,6 @@ export class SubAgentService extends BaseService { escapeEvents.on("user-escape", escapeHandler); try { - let accumulatedOutput = ""; - await streamChatResponse( chatHistory, model, @@ -181,17 +179,17 @@ export class SubAgentService extends BaseService { abortController, { onContent: (content: string) => { - accumulatedOutput += content; this.emit("subagentContent", { agentName: model?.name, - content: accumulatedOutput, + content, + type: "content", }); }, onToolResult: (result: string) => { - accumulatedOutput += `\n\n${result}`; this.emit("subagentContent", { agentName: model?.name, - content: accumulatedOutput, + content: result, + type: "toolResult", }); }, }, diff --git a/extensions/cli/src/ui/components/SubAgentOutput.tsx b/extensions/cli/src/ui/components/SubAgentOutput.tsx index c29074e52c..76f7db666c 100644 --- a/extensions/cli/src/ui/components/SubAgentOutput.tsx +++ b/extensions/cli/src/ui/components/SubAgentOutput.tsx @@ -5,45 +5,43 @@ import { subAgentService } from "../../services/SubAgentService.js"; import { LoadingAnimation } from "../LoadingAnimation.js"; import { MarkdownRenderer } from "../MarkdownRenderer.js"; -interface SubAgentState { - agentName: string | undefined; - content: string; - isRunning: boolean; - prompt?: string; -} - export const SubAgentOutput: React.FC = () => { - const [state, setState] = useState({ - agentName: undefined, - content: "", - isRunning: false, - }); + const [agentName, setAgentName] = useState(undefined); + const [contentLines, setContentLines] = useState([]); + const [isRunning, setIsRunning] = useState(false); useEffect(() => { - const onStarted = (data: { agentName: string; prompt: string }) => { - setState({ - agentName: data.agentName, - content: "", - isRunning: true, - prompt: data.prompt, - }); - }; - - const onContent = (data: { agentName: string; content: string }) => { - setState((prev) => ({ - ...prev, - agentName: data.agentName, - content: data.content, - })); + const onStarted = (data: { agentName: string }) => { + setAgentName(data.agentName); + setContentLines([]); + setIsRunning(true); }; - const onCompleted = () => { - setState((prev) => ({ ...prev, isRunning: false })); + const onContent = (data: { + agentName: string; + content: string; + type: "content" | "toolResult"; + }) => { + setAgentName(data.agentName); + setContentLines((prev) => { + const newLines = data.content.split("\n"); + if (data.type === "toolResult") { + return [...prev, "", ...newLines]; + } + if (prev.length === 0) { + return newLines; + } + const lastLine = prev[prev.length - 1]; + return [ + ...prev.slice(0, -1), + lastLine + newLines[0], + ...newLines.slice(1), + ]; + }); }; - const onFailed = () => { - setState((prev) => ({ ...prev, isRunning: false })); - }; + const onCompleted = () => setIsRunning(false); + const onFailed = () => setIsRunning(false); subAgentService.on("subagentStarted", onStarted); subAgentService.on("subagentContent", onContent); @@ -58,18 +56,19 @@ export const SubAgentOutput: React.FC = () => { }; }, []); - if (!state.isRunning) { + if (!isRunning) { return null; } const MAX_OUTPUT_LINES = 15; - const lines = state.content.split("\n"); const displayContent = - lines.length > MAX_OUTPUT_LINES - ? lines.slice(-MAX_OUTPUT_LINES).join("\n") - : state.content; + contentLines.length > MAX_OUTPUT_LINES + ? contentLines.slice(-MAX_OUTPUT_LINES).join("\n") + : contentLines.join("\n"); const hiddenLines = - lines.length > MAX_OUTPUT_LINES ? lines.length - MAX_OUTPUT_LINES : 0; + contentLines.length > MAX_OUTPUT_LINES + ? contentLines.length - MAX_OUTPUT_LINES + : 0; return ( @@ -77,10 +76,10 @@ export const SubAgentOutput: React.FC = () => { {" "} - Subagent: {state.agentName || "unknown"} + Subagent: {agentName || "unknown"} - {state.content && ( + {contentLines.length > 0 && ( {hiddenLines > 0 && ... +{hiddenLines} lines} From c6961b4a75e20a84f5170364fd506891eba7c0a3 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:16:38 +0530 Subject: [PATCH 12/15] separate out execution contexts of subagents --- .../cli/src/services/ExecutionContext.ts | 12 ++ .../cli/src/services/SubAgentService.ts | 161 +++++++++--------- extensions/cli/src/stream/handleToolCalls.ts | 7 +- .../src/stream/streamChatResponse.helpers.ts | 7 +- .../cli/src/stream/streamChatResponse.ts | 12 +- 5 files changed, 108 insertions(+), 91 deletions(-) create mode 100644 extensions/cli/src/services/ExecutionContext.ts diff --git a/extensions/cli/src/services/ExecutionContext.ts b/extensions/cli/src/services/ExecutionContext.ts new file mode 100644 index 0000000000..8860a1fe2f --- /dev/null +++ b/extensions/cli/src/services/ExecutionContext.ts @@ -0,0 +1,12 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +import type { ToolPermissionServiceState } from "./ToolPermissionService.js"; + +interface ExecutionContext { + executionId: string; + systemMessage: string; + permissions: ToolPermissionServiceState; +} + +/* Scopes system message and permissions per subagent execution, enabling parallel execution without global mutations*/ +export const executionContext = new AsyncLocalStorage(); diff --git a/extensions/cli/src/services/SubAgentService.ts b/extensions/cli/src/services/SubAgentService.ts index dd05dec676..750e669299 100644 --- a/extensions/cli/src/services/SubAgentService.ts +++ b/extensions/cli/src/services/SubAgentService.ts @@ -5,6 +5,7 @@ import { escapeEvents } from "../util/cli.js"; import { logger } from "../util/logger.js"; import { BaseService } from "./BaseService.js"; +import { executionContext } from "./ExecutionContext.js"; import { serviceContainer } from "./ServiceContainer.js"; import type { ToolPermissionServiceState } from "./ToolPermissionService.js"; import { type ModelServiceState, SERVICE_NAMES } from "./types.js"; @@ -25,13 +26,13 @@ export interface SubAgentResult { } export interface PendingExecution { + executionId: string; agentName: string; startTime: number; } export interface SubAgentServiceState { - isInsideSubagent: boolean; - currentExecution: PendingExecution | null; + activeExecutions: Map; } /** Service */ @@ -39,18 +40,24 @@ export interface SubAgentServiceState { export class SubAgentService extends BaseService { constructor() { super("SubAgentService", { - isInsideSubagent: false, - currentExecution: null, + activeExecutions: new Map(), }); } async doInitialize(): Promise { return { - isInsideSubagent: false, - currentExecution: null, + activeExecutions: new Map(), }; } + private generateExecutionId(): string { + return `subagent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + } + + isInsideSubagent(): boolean { + return executionContext.getStore() !== undefined; + } + private async buildAgentSystemMessage( agent: ModelServiceState, services: any, @@ -73,7 +80,7 @@ export class SubAgentService extends BaseService { async executeSubAgent( options: SubAgentExecutionOptions, ): Promise { - if (this.currentState.isInsideSubagent) { + if (this.isInsideSubagent()) { return { success: false, response: "", @@ -82,19 +89,23 @@ export class SubAgentService extends BaseService { } const { agent: subAgent, prompt, abortController } = options; + const executionId = this.generateExecutionId(); + const agentName = subAgent.model?.name || "unknown"; const mainAgentPermissionsState = await serviceContainer.get( SERVICE_NAMES.TOOL_PERMISSIONS, ); - this.setState({ - isInsideSubagent: true, - currentExecution: { - agentName: subAgent.model?.name || "unknown", - startTime: Date.now(), - }, - }); + const execution: PendingExecution = { + executionId, + agentName, + startTime: Date.now(), + }; + + const activeExecutions = new Map(this.currentState.activeExecutions); + activeExecutions.set(executionId, execution); + this.setState({ activeExecutions }); this.emit("subagentStarted", { agentName: subAgent.model?.name, @@ -103,6 +114,7 @@ export class SubAgentService extends BaseService { try { logger.debug("Starting subagent execution", { + executionId, agent: subAgent.model?.name, }); @@ -111,18 +123,6 @@ export class SubAgentService extends BaseService { throw new Error("Model or LLM API not available"); } - // allow all tools for now - // todo: eventually we want to show the same prompt in a dialog whether asking whether that tool call is allowed or not - serviceContainer.set( - SERVICE_NAMES.TOOL_PERMISSIONS, - { - ...mainAgentPermissionsState, - permissions: { - policies: [{ tool: "*", permission: "allow" }], - }, - }, - ); - const { services } = await import("./index.js"); // Build agent system message @@ -131,22 +131,21 @@ export class SubAgentService extends BaseService { services, ); - // Store original system message function - const originalGetSystemMessage = services.systemMessage?.getSystemMessage; + // allow all tools for now + // todo: eventually we want to show the same prompt in a dialog whether asking whether that tool call is allowed or not + const subAgentPermissions: ToolPermissionServiceState = { + ...mainAgentPermissionsState, + permissions: { + policies: [{ tool: "*", permission: "allow" }], + }, + }; - // Store original ChatHistoryService ready state const chatHistorySvc = services.chatHistory; const originalIsReady = chatHistorySvc && typeof chatHistorySvc.isReady === "function" ? chatHistorySvc.isReady : undefined; - // Override system message for this execution - if (services.systemMessage) { - services.systemMessage.getSystemMessage = async () => systemMessage; - } - - // Temporarily disable ChatHistoryService to prevent it from interfering with child session if (chatHistorySvc && originalIsReady) { chatHistorySvc.isReady = () => false; } @@ -172,40 +171,51 @@ export class SubAgentService extends BaseService { escapeEvents.on("user-escape", escapeHandler); try { - await streamChatResponse( - chatHistory, - model, - llmApi, - abortController, + const result = await executionContext.run( { - onContent: (content: string) => { - this.emit("subagentContent", { - agentName: model?.name, - content, - type: "content", - }); - }, - onToolResult: (result: string) => { - this.emit("subagentContent", { - agentName: model?.name, - content: result, - type: "toolResult", - }); - }, + executionId, + systemMessage, + permissions: subAgentPermissions, + }, + async () => { + await streamChatResponse( + chatHistory, + model, + llmApi, + abortController, + { + onContent: (content: string) => { + this.emit("subagentContent", { + agentName: model?.name, + content, + type: "content", + }); + }, + onToolResult: (result: string) => { + this.emit("subagentContent", { + agentName: model?.name, + content: result, + type: "toolResult", + }); + }, + }, + false, + ); + + const lastMessage = chatHistory.at(-1); + const response = + typeof lastMessage?.message?.content === "string" + ? lastMessage.message.content + : ""; + + return { success: true as const, response }; }, - false, ); - // The last message (mostly) contains the important output to be submitted back to the main agent - const lastMessage = chatHistory.at(-1); - const response = - typeof lastMessage?.message?.content === "string" - ? lastMessage.message.content - : ""; - logger.debug("Subagent execution completed", { + executionId, agent: model?.name, - responseLength: response.length, + responseLength: result.response.length, }); this.emit("subagentCompleted", { @@ -213,31 +223,17 @@ export class SubAgentService extends BaseService { success: true, }); - return { - success: true, - response, - }; + return result; } finally { escapeEvents.removeListener("user-escape", escapeHandler); - // Restore original system message function - if (services.systemMessage && originalGetSystemMessage) { - services.systemMessage.getSystemMessage = originalGetSystemMessage; - } - - // Restore original ChatHistoryService ready state if (chatHistorySvc && originalIsReady) { chatHistorySvc.isReady = originalIsReady; } - - // Restore original main agent tool permissions - serviceContainer.set( - SERVICE_NAMES.TOOL_PERMISSIONS, - mainAgentPermissionsState, - ); } } catch (error: any) { logger.error("Subagent execution failed", { + executionId, agent: subAgent.model?.name, error: error.message, }); @@ -253,10 +249,9 @@ export class SubAgentService extends BaseService { error: error.message, }; } finally { - this.setState({ - isInsideSubagent: false, - currentExecution: null, - }); + const updatedExecutions = new Map(this.currentState.activeExecutions); + updatedExecutions.delete(executionId); + this.setState({ activeExecutions: updatedExecutions }); } } } diff --git a/extensions/cli/src/stream/handleToolCalls.ts b/extensions/cli/src/stream/handleToolCalls.ts index f4f477da8b..4a0b79443b 100644 --- a/extensions/cli/src/stream/handleToolCalls.ts +++ b/extensions/cli/src/stream/handleToolCalls.ts @@ -4,6 +4,7 @@ import { createHistoryItem } from "core/util/messageConversion.js"; import { checkToolPermission } from "src/permissions/permissionChecker.js"; +import { executionContext } from "../services/ExecutionContext.js"; import { SERVICE_NAMES, serviceContainer, @@ -199,10 +200,12 @@ export async function handleToolCalls( export async function getRequestTools(isHeadless: boolean) { const availableTools = await getAllAvailableTools(isHeadless); + const ctx = executionContext.getStore(); const permissionsState = - await serviceContainer.get( + ctx?.permissions ?? + (await serviceContainer.get( SERVICE_NAMES.TOOL_PERMISSIONS, - ); + )); const allowedTools: Tool[] = []; for (const tool of availableTools) { diff --git a/extensions/cli/src/stream/streamChatResponse.helpers.ts b/extensions/cli/src/stream/streamChatResponse.helpers.ts index 6f8f3fa18d..5998727810 100644 --- a/extensions/cli/src/stream/streamChatResponse.helpers.ts +++ b/extensions/cli/src/stream/streamChatResponse.helpers.ts @@ -11,6 +11,7 @@ import { ToolPermissionServiceState } from "src/services/ToolPermissionService.j import { checkToolPermission } from "../permissions/permissionChecker.js"; import { toolPermissionManager } from "../permissions/permissionManager.js"; import { ToolCallRequest, ToolPermissions } from "../permissions/types.js"; +import { executionContext } from "../services/ExecutionContext.js"; import { SERVICE_NAMES, serviceContainer, @@ -562,10 +563,12 @@ export async function executeStreamedToolCalls( callbacks?.onToolStart?.(call.name, call.arguments); // Check tool permissions using helper + const ctx = executionContext.getStore(); const permissionState = - await serviceContainer.get( + ctx?.permissions ?? + (await serviceContainer.get( SERVICE_NAMES.TOOL_PERMISSIONS, - ); + )); const permissionResult = await checkToolPermissionApproval( permissionState.permissions, call, diff --git a/extensions/cli/src/stream/streamChatResponse.ts b/extensions/cli/src/stream/streamChatResponse.ts index d0b677b741..d028eda6e1 100644 --- a/extensions/cli/src/stream/streamChatResponse.ts +++ b/extensions/cli/src/stream/streamChatResponse.ts @@ -9,6 +9,7 @@ import type { } from "openai/resources.mjs"; import { pruneLastMessage } from "../compaction.js"; +import { executionContext } from "../services/ExecutionContext.js"; import { services } from "../services/index.js"; import { posthogService } from "../telemetry/posthogService.js"; import { telemetryService } from "../telemetry/telemetryService.js"; @@ -455,10 +456,13 @@ export async function streamChatResponse( chatHistory = refreshChatHistoryFromService(chatHistory, isCompacting); logger.debug("Starting conversation iteration"); - // Get system message once per iteration (can change based on tool permissions mode) - const systemMessage = await services.systemMessage.getSystemMessage( - services.toolPermissions.getState().currentMode, - ); + // Get system message from context if running in subagent, else use global service + const ctx = executionContext.getStore(); + const systemMessage = + ctx?.systemMessage ?? + (await services.systemMessage.getSystemMessage( + services.toolPermissions.getState().currentMode, + )); // Recompute tools on each iteration to handle mode changes during streaming const rawTools = await getRequestTools(isHeadless); From 59057b51eeaf1736ad265572a518bc23e0810feb Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:22:50 +0530 Subject: [PATCH 13/15] refactor subagent execution into subagent service file --- .../cli/src/services/ExecutionContext.ts | 12 ---- .../cli/src/services/SubAgentService.ts | 69 +++++++++++-------- extensions/cli/src/stream/handleToolCalls.ts | 4 +- .../src/stream/streamChatResponse.helpers.ts | 4 +- .../cli/src/stream/streamChatResponse.ts | 4 +- 5 files changed, 47 insertions(+), 46 deletions(-) delete mode 100644 extensions/cli/src/services/ExecutionContext.ts diff --git a/extensions/cli/src/services/ExecutionContext.ts b/extensions/cli/src/services/ExecutionContext.ts deleted file mode 100644 index 8860a1fe2f..0000000000 --- a/extensions/cli/src/services/ExecutionContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -import type { ToolPermissionServiceState } from "./ToolPermissionService.js"; - -interface ExecutionContext { - executionId: string; - systemMessage: string; - permissions: ToolPermissionServiceState; -} - -/* Scopes system message and permissions per subagent execution, enabling parallel execution without global mutations*/ -export const executionContext = new AsyncLocalStorage(); diff --git a/extensions/cli/src/services/SubAgentService.ts b/extensions/cli/src/services/SubAgentService.ts index 750e669299..ccdb719d0f 100644 --- a/extensions/cli/src/services/SubAgentService.ts +++ b/extensions/cli/src/services/SubAgentService.ts @@ -1,3 +1,5 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + import type { ChatHistoryItem } from "core"; import { streamChatResponse } from "../stream/streamChatResponse.js"; @@ -5,36 +7,10 @@ import { escapeEvents } from "../util/cli.js"; import { logger } from "../util/logger.js"; import { BaseService } from "./BaseService.js"; -import { executionContext } from "./ExecutionContext.js"; import { serviceContainer } from "./ServiceContainer.js"; import type { ToolPermissionServiceState } from "./ToolPermissionService.js"; import { type ModelServiceState, SERVICE_NAMES } from "./types.js"; -/** Types */ - -export interface SubAgentExecutionOptions { - agent: ModelServiceState; - prompt: string; - parentSessionId: string; - abortController: AbortController; -} - -export interface SubAgentResult { - success: boolean; - response: string; - error?: string; -} - -export interface PendingExecution { - executionId: string; - agentName: string; - startTime: number; -} - -export interface SubAgentServiceState { - activeExecutions: Map; -} - /** Service */ export class SubAgentService extends BaseService { @@ -55,7 +31,7 @@ export class SubAgentService extends BaseService { } isInsideSubagent(): boolean { - return executionContext.getStore() !== undefined; + return subAgentExecutionContext.getStore() !== undefined; } private async buildAgentSystemMessage( @@ -171,7 +147,7 @@ export class SubAgentService extends BaseService { escapeEvents.on("user-escape", escapeHandler); try { - const result = await executionContext.run( + const result = await subAgentExecutionContext.run( { executionId, systemMessage, @@ -257,3 +233,40 @@ export class SubAgentService extends BaseService { } export const subAgentService = new SubAgentService(); + +// Subagent execution context + +interface ExecutionContext { + executionId: string; + systemMessage: string; + permissions: ToolPermissionServiceState; +} + +/* Scopes system messages and tool permissions per subagent execution for enabling parallel execution*/ +export const subAgentExecutionContext = + new AsyncLocalStorage(); + +// Types + +export interface SubAgentExecutionOptions { + agent: ModelServiceState; + prompt: string; + parentSessionId: string; + abortController: AbortController; +} + +export interface SubAgentResult { + success: boolean; + response: string; + error?: string; +} + +export interface PendingExecution { + executionId: string; + agentName: string; + startTime: number; +} + +export interface SubAgentServiceState { + activeExecutions: Map; +} diff --git a/extensions/cli/src/stream/handleToolCalls.ts b/extensions/cli/src/stream/handleToolCalls.ts index 4a0b79443b..7310175a05 100644 --- a/extensions/cli/src/stream/handleToolCalls.ts +++ b/extensions/cli/src/stream/handleToolCalls.ts @@ -4,12 +4,12 @@ import { createHistoryItem } from "core/util/messageConversion.js"; import { checkToolPermission } from "src/permissions/permissionChecker.js"; -import { executionContext } from "../services/ExecutionContext.js"; import { SERVICE_NAMES, serviceContainer, services, } from "../services/index.js"; +import { subAgentExecutionContext } from "../services/SubAgentService.js"; import type { ToolPermissionServiceState } from "../services/ToolPermissionService.js"; import { convertToolToChatCompletionTool, @@ -200,7 +200,7 @@ export async function handleToolCalls( export async function getRequestTools(isHeadless: boolean) { const availableTools = await getAllAvailableTools(isHeadless); - const ctx = executionContext.getStore(); + const ctx = subAgentExecutionContext.getStore(); const permissionsState = ctx?.permissions ?? (await serviceContainer.get( diff --git a/extensions/cli/src/stream/streamChatResponse.helpers.ts b/extensions/cli/src/stream/streamChatResponse.helpers.ts index 5998727810..f0110a1e56 100644 --- a/extensions/cli/src/stream/streamChatResponse.helpers.ts +++ b/extensions/cli/src/stream/streamChatResponse.helpers.ts @@ -11,12 +11,12 @@ import { ToolPermissionServiceState } from "src/services/ToolPermissionService.j import { checkToolPermission } from "../permissions/permissionChecker.js"; import { toolPermissionManager } from "../permissions/permissionManager.js"; import { ToolCallRequest, ToolPermissions } from "../permissions/types.js"; -import { executionContext } from "../services/ExecutionContext.js"; import { SERVICE_NAMES, serviceContainer, services, } from "../services/index.js"; +import { subAgentExecutionContext } from "../services/SubAgentService.js"; import { trackSessionUsage } from "../session.js"; import { posthogService } from "../telemetry/posthogService.js"; import { telemetryService } from "../telemetry/telemetryService.js"; @@ -563,7 +563,7 @@ export async function executeStreamedToolCalls( callbacks?.onToolStart?.(call.name, call.arguments); // Check tool permissions using helper - const ctx = executionContext.getStore(); + const ctx = subAgentExecutionContext.getStore(); const permissionState = ctx?.permissions ?? (await serviceContainer.get( diff --git a/extensions/cli/src/stream/streamChatResponse.ts b/extensions/cli/src/stream/streamChatResponse.ts index d028eda6e1..1c2ec01a59 100644 --- a/extensions/cli/src/stream/streamChatResponse.ts +++ b/extensions/cli/src/stream/streamChatResponse.ts @@ -9,8 +9,8 @@ import type { } from "openai/resources.mjs"; import { pruneLastMessage } from "../compaction.js"; -import { executionContext } from "../services/ExecutionContext.js"; import { services } from "../services/index.js"; +import { subAgentExecutionContext } from "../services/SubAgentService.js"; import { posthogService } from "../telemetry/posthogService.js"; import { telemetryService } from "../telemetry/telemetryService.js"; import { applyChatCompletionToolOverrides } from "../tools/applyToolOverrides.js"; @@ -457,7 +457,7 @@ export async function streamChatResponse( logger.debug("Starting conversation iteration"); // Get system message from context if running in subagent, else use global service - const ctx = executionContext.getStore(); + const ctx = subAgentExecutionContext.getStore(); const systemMessage = ctx?.systemMessage ?? (await services.systemMessage.getSystemMessage( From c65c21363d5b1c8c15f1c035de4d9cc3255142e3 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:14:40 +0530 Subject: [PATCH 14/15] add subagent parallel tool --- extensions/cli/src/subagent/index.ts | 21 ++++ extensions/cli/src/tools/allBuiltIns.ts | 6 +- extensions/cli/src/tools/index.tsx | 3 +- extensions/cli/src/tools/subagent.ts | 146 +++++++++++++++++++++++- 4 files changed, 173 insertions(+), 3 deletions(-) diff --git a/extensions/cli/src/subagent/index.ts b/extensions/cli/src/subagent/index.ts index 29f89fadc8..683236b035 100644 --- a/extensions/cli/src/subagent/index.ts +++ b/extensions/cli/src/subagent/index.ts @@ -26,3 +26,24 @@ export const SUBAGENT_TOOL_META: Tool = { }, run: async () => "", }; + +export const SUBAGENT_PARALLEL_TOOL_META: Tool = { + name: "SubagentParallel", + displayName: "Subagent Parallel", + description: + "Invoke multiple subagents in parallel and wait for all to complete.", + readonly: false, + isBuiltIn: true, + parameters: { + type: "object", + required: ["tasks"], + properties: { + tasks: { + type: "array", + description: + "Array of tasks to execute in parallel. Each task specifies a subagent and its prompt.", + }, + }, + }, + run: async () => "", +}; diff --git a/extensions/cli/src/tools/allBuiltIns.ts b/extensions/cli/src/tools/allBuiltIns.ts index a7a501c711..6e7e46261e 100644 --- a/extensions/cli/src/tools/allBuiltIns.ts +++ b/extensions/cli/src/tools/allBuiltIns.ts @@ -1,4 +1,7 @@ -import { SUBAGENT_TOOL_META } from "../subagent/index.js"; +import { + SUBAGENT_PARALLEL_TOOL_META, + SUBAGENT_TOOL_META, +} from "../subagent/index.js"; import { askQuestionTool } from "./askQuestion.js"; import { editTool } from "./edit.js"; @@ -31,6 +34,7 @@ export const ALL_BUILT_IN_TOOLS = [ searchCodeTool, statusTool, SUBAGENT_TOOL_META, + SUBAGENT_PARALLEL_TOOL_META, SKILLS_TOOL_META, uploadArtifactTool, viewDiffTool, diff --git a/extensions/cli/src/tools/index.tsx b/extensions/cli/src/tools/index.tsx index 0f22239627..063f71ed28 100644 --- a/extensions/cli/src/tools/index.tsx +++ b/extensions/cli/src/tools/index.tsx @@ -31,7 +31,7 @@ import { reportFailureTool } from "./reportFailure.js"; import { runTerminalCommandTool } from "./runTerminalCommand.js"; import { checkIfRipgrepIsInstalled, searchCodeTool } from "./searchCode.js"; import { skillsTool } from "./skills.js"; -import { subagentTool } from "./subagent.js"; +import { subagentParallelTool, subagentTool } from "./subagent.js"; import { isBetaUploadArtifactToolEnabled } from "./toolsConfig.js"; import { type Tool, @@ -127,6 +127,7 @@ export async function getAllAvailableTools( } tools.push(await subagentTool()); + tools.push(await subagentParallelTool()); tools.push(await skillsTool()); diff --git a/extensions/cli/src/tools/subagent.ts b/extensions/cli/src/tools/subagent.ts index 3ac3f53426..f82a083426 100644 --- a/extensions/cli/src/tools/subagent.ts +++ b/extensions/cli/src/tools/subagent.ts @@ -7,11 +7,20 @@ import { getSubagent, getAgentNames as getSubagentNames, } from "../subagent/getAgents.js"; -import { SUBAGENT_TOOL_META } from "../subagent/index.js"; +import { + SUBAGENT_PARALLEL_TOOL_META, + SUBAGENT_TOOL_META, +} from "../subagent/index.js"; import { logger } from "../util/logger.js"; import { Tool } from "./types.js"; +interface SubagentTask { + description: string; + prompt: string; + subagent_name: string; +} + export const subagentTool = async (): Promise => { const modelServiceState = await serviceContainer.get( SERVICE_NAMES.MODEL, @@ -99,3 +108,138 @@ export const subagentTool = async (): Promise => { }, }; }; + +export const subagentParallelTool = async (): Promise => { + const modelServiceState = await serviceContainer.get( + SERVICE_NAMES.MODEL, + ); + + const availableAgents = modelServiceState + ? getSubagentNames(modelServiceState).join(", ") + : ""; + + return { + ...SUBAGENT_PARALLEL_TOOL_META, + + description: `Invoke multiple subagents in parallel and wait for all to complete. Use this when you have multiple independent tasks that can be executed concurrently. Available agents: ${availableAgents}`, + + parameters: { + ...SUBAGENT_PARALLEL_TOOL_META.parameters, + properties: { + tasks: { + ...SUBAGENT_PARALLEL_TOOL_META.parameters.properties.tasks, + items: { + type: "object", + required: ["description", "prompt", "subagent_name"], + properties: { + ...SUBAGENT_TOOL_META.parameters.properties, + subagent_name: { + type: "string", + description: `The type of specialized agent to use. Available: ${availableAgents}`, + }, + }, + }, + }, + }, + }, + + preprocess: async (args: any) => { + const { tasks } = args as { tasks: SubagentTask[] }; + + if (!Array.isArray(tasks) || tasks.length === 0) { + throw new Error("tasks must be a non-empty array"); + } + + const previews: { type: string; content: string }[] = []; + + for (const task of tasks) { + const agent = getSubagent(modelServiceState, task.subagent_name); + if (!agent) { + throw new Error( + `Unknown agent type: ${task.subagent_name}. Available agents: ${availableAgents}`, + ); + } + previews.push({ + type: "text", + content: `Spawning ${agent.model.name} to: ${task.description}`, + }); + } + + return { + args, + preview: [ + { + type: "text", + content: `Spawning ${tasks.length} subagents in parallel:\n${previews.map((p) => ` - ${p.content}`).join("\n")}`, + }, + ], + }; + }, + + run: async (args: any, context?: { toolCallId: string }) => { + const { tasks } = args as { tasks: SubagentTask[] }; + + logger.debug("subagent_parallel args", { args, context }); + + const chatHistoryService = services.chatHistory; + const parentSessionId = chatHistoryService.getSessionId(); + if (!parentSessionId) { + throw new Error("No active session found"); + } + + const executeTask = async (task: SubagentTask, index: number) => { + const agent = getSubagent(modelServiceState, task.subagent_name); + if (!agent) { + return { + index, + description: task.description, + success: false, + response: `Unknown agent type: ${task.subagent_name}`, + }; + } + + try { + const result = await subAgentService.executeSubAgent({ + agent, + prompt: task.prompt, + parentSessionId, + abortController: new AbortController(), + }); + + return { + index, + description: task.description, + success: result.success, + response: result.response, + }; + } catch (error) { + return { + index, + description: task.description, + success: false, + response: `Error: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }; + + const results = await Promise.all( + tasks.map((task, index) => executeTask(task, index)), + ); + + logger.debug("subagent_parallel results", { results }); + + const outputParts = results.map((result) => { + return [ + ``, + result.response, + ``, + `status: ${result.success ? "completed" : "failed"}`, + ``, + ``, + ].join("\n"); + }); + + return outputParts.join("\n\n"); + }, + }; +}; From 698e2377f824bd9daa0373028918034b880c371d Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:27:09 +0530 Subject: [PATCH 15/15] show outputs for parallel subagents --- .../cli/src/services/SubAgentService.ts | 5 + .../cli/src/subagent/builtInSubagents.ts | 2 + extensions/cli/src/subagent/index.ts | 2 +- .../cli/src/ui/components/SubAgentOutput.tsx | 128 ++++++++++++------ 4 files changed, 93 insertions(+), 44 deletions(-) diff --git a/extensions/cli/src/services/SubAgentService.ts b/extensions/cli/src/services/SubAgentService.ts index ccdb719d0f..681551c26f 100644 --- a/extensions/cli/src/services/SubAgentService.ts +++ b/extensions/cli/src/services/SubAgentService.ts @@ -84,6 +84,7 @@ export class SubAgentService extends BaseService { this.setState({ activeExecutions }); this.emit("subagentStarted", { + executionId, agentName: subAgent.model?.name, prompt, }); @@ -162,6 +163,7 @@ export class SubAgentService extends BaseService { { onContent: (content: string) => { this.emit("subagentContent", { + executionId, agentName: model?.name, content, type: "content", @@ -169,6 +171,7 @@ export class SubAgentService extends BaseService { }, onToolResult: (result: string) => { this.emit("subagentContent", { + executionId, agentName: model?.name, content: result, type: "toolResult", @@ -195,6 +198,7 @@ export class SubAgentService extends BaseService { }); this.emit("subagentCompleted", { + executionId, agentName: model?.name, success: true, }); @@ -215,6 +219,7 @@ export class SubAgentService extends BaseService { }); this.emit("subagentFailed", { + executionId, agentName: subAgent.model?.name, error: error.message, }); diff --git a/extensions/cli/src/subagent/builtInSubagents.ts b/extensions/cli/src/subagent/builtInSubagents.ts index 2880c7e422..627c1ec105 100644 --- a/extensions/cli/src/subagent/builtInSubagents.ts +++ b/extensions/cli/src/subagent/builtInSubagents.ts @@ -37,6 +37,8 @@ When to use: Use this subagent for any task that doesn't require a specialized subagent, including but not limited to: implementing features, fixing bugs, refactoring, code review, documentation, research, debugging, and analysis. + **Important: You should use this subagent whenever you have independent tasks..** + When handling a task, you will: 1. **Interpret the Request**: Understand what is being asked, whether it's exploration, implementation, review, analysis, or something else entirely. Adapt your approach based on the nature of the task. diff --git a/extensions/cli/src/subagent/index.ts b/extensions/cli/src/subagent/index.ts index 683236b035..a45a811a5f 100644 --- a/extensions/cli/src/subagent/index.ts +++ b/extensions/cli/src/subagent/index.ts @@ -31,7 +31,7 @@ export const SUBAGENT_PARALLEL_TOOL_META: Tool = { name: "SubagentParallel", displayName: "Subagent Parallel", description: - "Invoke multiple subagents in parallel and wait for all to complete.", + "Invoke multiple subagents in parallel to carry out independent tasks.", readonly: false, isBuiltIn: true, parameters: { diff --git a/extensions/cli/src/ui/components/SubAgentOutput.tsx b/extensions/cli/src/ui/components/SubAgentOutput.tsx index 76f7db666c..76f7c1020e 100644 --- a/extensions/cli/src/ui/components/SubAgentOutput.tsx +++ b/extensions/cli/src/ui/components/SubAgentOutput.tsx @@ -5,43 +5,74 @@ import { subAgentService } from "../../services/SubAgentService.js"; import { LoadingAnimation } from "../LoadingAnimation.js"; import { MarkdownRenderer } from "../MarkdownRenderer.js"; +interface SubAgentState { + agentName: string; + contentLines: string[]; +} + export const SubAgentOutput: React.FC = () => { - const [agentName, setAgentName] = useState(undefined); - const [contentLines, setContentLines] = useState([]); - const [isRunning, setIsRunning] = useState(false); + const [agents, setAgents] = useState>({}); useEffect(() => { - const onStarted = (data: { agentName: string }) => { - setAgentName(data.agentName); - setContentLines([]); - setIsRunning(true); + const onStarted = (data: { executionId: string; agentName: string }) => { + setAgents((prev) => ({ + ...prev, + [data.executionId]: { + agentName: data.agentName, + contentLines: [], + }, + })); }; const onContent = (data: { + executionId: string; agentName: string; content: string; type: "content" | "toolResult"; }) => { - setAgentName(data.agentName); - setContentLines((prev) => { + setAgents((prev) => { + const agent = prev[data.executionId]; + if (!agent) return prev; + const newLines = data.content.split("\n"); + let updatedLines: string[]; + if (data.type === "toolResult") { - return [...prev, "", ...newLines]; + updatedLines = [...agent.contentLines, "", ...newLines]; + } else if (agent.contentLines.length === 0) { + updatedLines = newLines; + } else { + const lastLine = agent.contentLines[agent.contentLines.length - 1]; + updatedLines = [ + ...agent.contentLines.slice(0, -1), + lastLine + newLines[0], + ...newLines.slice(1), + ]; } - if (prev.length === 0) { - return newLines; - } - const lastLine = prev[prev.length - 1]; - return [ - ...prev.slice(0, -1), - lastLine + newLines[0], - ...newLines.slice(1), - ]; + + return { + ...prev, + [data.executionId]: { + ...agent, + contentLines: updatedLines, + }, + }; + }); + }; + + const onCompleted = (data: { executionId: string }) => { + setAgents((prev) => { + const { [data.executionId]: _, ...rest } = prev; + return rest; }); }; - const onCompleted = () => setIsRunning(false); - const onFailed = () => setIsRunning(false); + const onFailed = (data: { executionId: string }) => { + setAgents((prev) => { + const { [data.executionId]: _, ...rest } = prev; + return rest; + }); + }; subAgentService.on("subagentStarted", onStarted); subAgentService.on("subagentContent", onContent); @@ -56,35 +87,46 @@ export const SubAgentOutput: React.FC = () => { }; }, []); - if (!isRunning) { + const agentEntries = Object.entries(agents); + + if (agentEntries.length === 0) { return null; } const MAX_OUTPUT_LINES = 15; - const displayContent = - contentLines.length > MAX_OUTPUT_LINES - ? contentLines.slice(-MAX_OUTPUT_LINES).join("\n") - : contentLines.join("\n"); - const hiddenLines = - contentLines.length > MAX_OUTPUT_LINES - ? contentLines.length - MAX_OUTPUT_LINES - : 0; return ( - - - - {" "} - Subagent: {agentName || "unknown"} - - - {contentLines.length > 0 && ( - - {hiddenLines > 0 && ... +{hiddenLines} lines} - - - )} + {agentEntries.map(([executionId, agent]) => { + const displayContent = + agent.contentLines.length > MAX_OUTPUT_LINES + ? agent.contentLines.slice(-MAX_OUTPUT_LINES).join("\n") + : agent.contentLines.join("\n"); + const hiddenLines = + agent.contentLines.length > MAX_OUTPUT_LINES + ? agent.contentLines.length - MAX_OUTPUT_LINES + : 0; + + return ( + + 0 ? 1 : 0}> + + + {" "} + Subagent: {agent.agentName || "unknown"} + + + {agent.contentLines.length > 0 && ( + + {hiddenLines > 0 && ( + ... +{hiddenLines} lines + )} + + + )} + + ); + })} ); };