basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import { randomUUID } from 'node:crypto';8import { BaseDeclarativeTool, BaseToolInvocation, Kind } from '../tools.js';9import { ToolNames, ToolDisplayNames } from '../tool-names.js';10import { EXCLUDED_TOOLS_FOR_SUBAGENTS } from '../../agents/runtime/agent-core.js';11import type {12 ToolResult,13 ToolResultDisplay,14 AgentResultDisplay,15} from '../tools.js';16import { ToolConfirmationOutcome } from '../tools.js';17import type {18 ToolCallConfirmationDetails,19 ToolConfirmationPayload,20} from '../tools.js';21import type { PermissionDecision } from '../../permissions/types.js';22import type { SubagentManager } from '../../subagents/subagent-manager.js';23import type { SubagentConfig } from '../../subagents/types.js';24import { BUBBLE_APPROVAL_MODE } from '../../subagents/types.js';25import { AgentTerminateMode } from '../../agents/runtime/agent-types.js';26import type {27 PromptConfig,28 ToolConfig,29} from '../../agents/runtime/agent-types.js';30import {31 AgentHeadless,32 ContextState,33} from '../../agents/runtime/agent-headless.js';34import type { AgentExternalInput } from '../../agents/runtime/agent-types.js';35import type { Content, FunctionDeclaration } from '@google/genai';36import {37 FORK_AGENT,38 FORK_DEFAULT_MAX_TURNS,39 FORK_SUBAGENT_TYPE,40 FORK_PLACEHOLDER_RESULT,41 buildForkedMessages,42 buildChildMessage,43 buildWorktreeNotice,44 isForkSubagentEnabled,45 runInForkContext,46} from './fork-subagent.js';47import {48 generateAgentWorktreeSlug,49 GitWorktreeService,50 writeWorktreeSessionMarker,51} from '../../services/gitWorktreeService.js';52import { FileDiscoveryService } from '../../services/fileDiscoveryService.js';53import { WorkspaceContext } from '../../utils/workspaceContext.js';54import {55 childLaunchDepth,56 getCurrentAgentId,57 isTopLevelSession,58 runWithAgentContext,59 spawnBlockReason,60} from '../../agents/runtime/agent-context.js';61import { trace, context as otelContext } from '@opentelemetry/api';62import {63 endSubagentSpan,64 runInSubagentSpanContext,65 startSubagentSpan,66 type SubagentInvocationKind,67 type SubagentSpanMetadata,68} from '../../telemetry/index.js';69import {70 AgentEventEmitter,71 AgentEventType,72} from '../../agents/runtime/agent-events.js';73import type {74 AgentToolCallEvent,75 AgentToolResultEvent,76 AgentFinishEvent,77 AgentErrorEvent,78 AgentApprovalRequestEvent,79 AgentUsageEvent,80} from '../../agents/runtime/agent-events.js';81import {82 BuiltinAgentRegistry,83 DEFAULT_BUILTIN_SUBAGENT_TYPE,84} from '../../subagents/builtin-agents.js';85import { createDebugLogger } from '../../utils/debugLogger.js';86import { PermissionMode } from '../../hooks/types.js';87import type { StopHookOutput } from '../../hooks/types.js';88import {89 appendStopHookBlockingCapWarning,90 formatStopHookBlockingCapWarning,91} from '../../hooks/stopHookCap.js';92import { toModelVisibleSubagentResult } from '../../agents/subagent-result.js';93import {94 ApprovalMode,95 Config,96 normalizeMaxSubagentDepth,97} from '../../config/config.js';98import { createDenialState } from '../../permissions/denialTracking.js';99import { isTeammate } from '../../agents/team/identity.js';100import { isSubagentLikeExecutionContext } from '../../agents/runtime/subagent-plan-tool-policy.js';101import {102 getAgentJsonlPath,103 getAgentMetaPath,104 attachJsonlTranscriptWriter,105 patchAgentMeta,106 writeAgentMeta,107 type AgentPersistedCliFlags,108} from '../../agents/agent-transcript.js';109import type { BackgroundSlotReservation } from '../../agents/background-tasks.js';110import { getGitBranch } from '../../utils/gitUtils.js';111 112// Memoize git branch per cwd for the agent-launch path. `getGitBranch`113// shells out to `git rev-parse` synchronously; caching avoids the per-launch114// execSync on a path that runs every time a subagent (foreground or115// background) starts. Branches don't change within a process under normal116// use; the transcript annotation is best-effort audit metadata, so a stale117// value after a user `git checkout` mid-session is acceptable.118const gitBranchCache = new Map<string, string | undefined>();119function getCachedGitBranch(cwd: string): string | undefined {120 if (gitBranchCache.has(cwd)) return gitBranchCache.get(cwd);121 const branch = getGitBranch(cwd);122 gitBranchCache.set(cwd, branch);123 return branch;124}125 126function persistBackgroundCancellation(127 metaPath: string,128 persistedStatus: 'running' | 'cancelled',129): void {130 patchAgentMeta(metaPath, {131 status: persistedStatus,132 lastUpdatedAt: new Date().toISOString(),133 lastError: undefined,134 });135}136 137function createLocalExternalInputQueue(): {138 enqueue: (input: AgentExternalInput) => boolean;139 drain: () => AgentExternalInput[];140 wait: (signal: AbortSignal) => Promise<AgentExternalInput[]>;141 wake: () => void;142} {143 const inputs: AgentExternalInput[] = [];144 const waiters = new Set<() => void>();145 146 const drain = () => inputs.splice(0);147 const wakeWaiters = () => {148 const pending = Array.from(waiters);149 for (const waiter of pending) {150 waiter();151 }152 };153 154 return {155 enqueue(input: AgentExternalInput): boolean {156 inputs.push(input);157 wakeWaiters();158 return true;159 },160 drain,161 wake(): void {162 wakeWaiters();163 },164 wait(signal: AbortSignal): Promise<AgentExternalInput[]> {165 const immediate = drain();166 if (immediate.length > 0 || signal.aborted) {167 return Promise.resolve(immediate);168 }169 170 return new Promise<AgentExternalInput[]>((resolve) => {171 const cleanup = () => {172 waiters.delete(onWake);173 signal.removeEventListener('abort', onAbort);174 };175 const onWake = () => {176 cleanup();177 resolve(drain());178 };179 const onAbort = () => {180 cleanup();181 resolve([]);182 };183 waiters.add(onWake);184 signal.addEventListener('abort', onAbort, { once: true });185 if (signal.aborted) {186 cleanup();187 resolve([]);188 return;189 }190 });191 },192 };193}194 195export interface AgentParams {196 description: string;197 prompt: string;198 subagent_type?: string;199 run_in_background?: boolean;200 /** When set, spawn as a named teammate via TeamManager instead of a one-shot subagent. */201 name?: string;202 /** Start a named teammate in plan mode and require leader approval. */203 plan_mode_required?: boolean;204 /**205 * When set to `'worktree'`, spins up a temporary git worktree under206 * `<projectRoot>/.qwen/worktrees/agent-<7hex>` and instructs the agent to207 * confine all file operations to that path. After the agent completes:208 * - if no changes were made, the worktree is auto-removed;209 * - if changes were made, the worktree is preserved and its path/branch210 * are returned in the agent's result.211 */212 isolation?: 'worktree';213}214 215const debugLogger = createDebugLogger('AGENT');216 217const TEAM_AGENT_NAME_PROPERTY = {218 type: 'string',219 description:220 'When provided, spawn as a named teammate via the active team ' +221 'instead of a one-shot subagent. Requires an active team context.',222};223 224const TEAM_AGENT_PLAN_REQUIRED_PROPERTY = {225 type: 'boolean',226 description:227 'When true, the named teammate starts in plan mode and must call ' +228 'exit_plan_mode to request leader approval before executing. Only valid ' +229 'with a named teammate in an active team.',230};231 232/**233 * Maps ApprovalMode to PermissionMode for hook events.234 */235function approvalModeToPermissionMode(mode: ApprovalMode): PermissionMode {236 switch (mode) {237 case ApprovalMode.YOLO:238 return PermissionMode.Yolo;239 case ApprovalMode.AUTO_EDIT:240 return PermissionMode.AutoEdit;241 case ApprovalMode.AUTO:242 return PermissionMode.Auto;243 case ApprovalMode.PLAN:244 return PermissionMode.Plan;245 case ApprovalMode.DEFAULT:246 default:247 return PermissionMode.Default;248 }249}250 251/**252 * Resolves the effective permission mode for a sub-agent.253 *254 * Rules (matching claw-code):255 * - Permissive parent modes (yolo, auto-edit) always win256 * - Otherwise, the agent definition's mode applies if set257 * - Default fallback is auto-edit (sub-agents need autonomy)258 */259export function resolveSubagentApprovalMode(260 parentApprovalMode: ApprovalMode,261 agentApprovalMode?: string,262 isTrustedFolder?: boolean,263): PermissionMode {264 // Permissive parent modes always win. AUTO is permissive in the sense265 // that the sub-agent should inherit classifier-mediated approval rather266 // than degrading to DEFAULT (which would force every sub-agent tool call267 // through manual confirmation — unusable in headless sub-agent contexts).268 if (269 parentApprovalMode === ApprovalMode.YOLO ||270 parentApprovalMode === ApprovalMode.AUTO_EDIT ||271 parentApprovalMode === ApprovalMode.AUTO272 ) {273 return approvalModeToPermissionMode(parentApprovalMode);274 }275 276 // The subagent-only `bubble` mode is not an ApprovalMode enum member; it277 // resolves to Default run behavior (tool calls require confirmation). The278 // background launch path is what turns deny into surface-to-parent. Handle279 // it explicitly rather than relying on approvalModeToPermissionMode's280 // `default:` fall-through, so adding a real ApprovalMode.BUBBLE later can't281 // silently change this.282 if (agentApprovalMode === BUBBLE_APPROVAL_MODE) {283 return PermissionMode.Default;284 }285 286 // Agent definition's mode applies if set287 if (agentApprovalMode) {288 const resolved = approvalModeToPermissionMode(289 agentApprovalMode as ApprovalMode,290 );291 // Privileged modes require trusted folder. AUTO is privileged because292 // its LLM classifier can auto-approve shell / network / agent calls293 // without user prompts; allowing an untrusted-repo sub-agent definition294 // to opt into AUTO would let the repo silently grant itself classifier-295 // mediated automation.296 if (297 !isTrustedFolder &&298 (resolved === PermissionMode.Yolo ||299 resolved === PermissionMode.AutoEdit ||300 resolved === PermissionMode.Auto)301 ) {302 return approvalModeToPermissionMode(parentApprovalMode);303 }304 return resolved;305 }306 307 // Default: match parent mode. In plan mode, stay in plan.308 // In default mode in trusted folders, auto-edit for autonomy.309 if (parentApprovalMode === ApprovalMode.PLAN) {310 return PermissionMode.Plan;311 }312 if (isTrustedFolder) {313 return PermissionMode.AutoEdit;314 }315 return approvalModeToPermissionMode(parentApprovalMode);316}317 318/**319 * Maps PermissionMode back to ApprovalMode.320 */321function permissionModeToApprovalMode(mode: PermissionMode): ApprovalMode {322 switch (mode) {323 case PermissionMode.Yolo:324 return ApprovalMode.YOLO;325 case PermissionMode.AutoEdit:326 return ApprovalMode.AUTO_EDIT;327 case PermissionMode.Auto:328 return ApprovalMode.AUTO;329 case PermissionMode.Plan:330 return ApprovalMode.PLAN;331 case PermissionMode.Default:332 default:333 return ApprovalMode.DEFAULT;334 }335}336 337/**338 * Marker that signals "this Config wrapper has rebuilt its own tool339 * registry so bound EditTool / WriteFileTool / ReadFileTool resolve to340 * the wrapper instead of the parent". Stored as a Symbol-keyed property341 * so that JavaScript's normal property lookup (which walks the342 * prototype chain) lets a downstream wrapper detect a rebuild that343 * happened on any ancestor without manually walking the chain.344 *345 * `Symbol.for` is used so the marker survives bundle-deduping; two346 * independent imports of this module observe the same Symbol identity.347 */348export const TOOL_REGISTRY_REBUILT: unique symbol = Symbol.for(349 'qwen-code:tool-registry-rebuilt',350);351 352/**353 * `true` if any Config in this wrapper's prototype chain has already354 * rebuilt its tool registry via {@link rebuildToolRegistryOnOverride}.355 *356 * Used by spawn sites that may be called with a wrapper-on-wrapper357 * argument (e.g. `subagent-manager.ts:buildSubagentContextOverride`358 * receiving `bgConfig = Object.create(agentConfig)` from the359 * background-agent path) to skip a redundant rebuild.360 */361export function hasRebuiltToolRegistry(config: Config): boolean {362 // eslint-disable-next-line @typescript-eslint/no-explicit-any363 return (config as any)[TOOL_REGISTRY_REBUILT] === true;364}365 366/**367 * Rebuilds the tool registry on `override` so core tools resolve368 * `this.config` to `override` instead of `base`. Used by both369 * {@link createApprovalModeOverride} and370 * `subagent-manager.ts:buildSubagentContextOverride` to avoid371 * duplicated rebuild logic.372 *373 * - `override.createToolRegistry(...)` runs on the override (so the374 * lazy factories close over `this = override`).375 * - Discovered tools (MCP / command-discovered) are copied from `base`376 * rather than re-discovered, since discovery is expensive.377 * - The {@link TOOL_REGISTRY_REBUILT} marker is set so wrapper-of-wrapper378 * layers downstream skip the rebuild via {@link hasRebuiltToolRegistry}.379 */380export async function rebuildToolRegistryOnOverride(381 override: Config,382 base: Config,383): Promise<void> {384 // eslint-disable-next-line @typescript-eslint/no-explicit-any385 const ov = override as any;386 const agentRegistry = await ov.createToolRegistry(undefined, {387 skipDiscovery: true,388 forSubAgent: true,389 });390 agentRegistry.copyDiscoveredToolsFrom(base.getToolRegistry());391 ov.getToolRegistry = () => agentRegistry;392 ov[TOOL_REGISTRY_REBUILT] = true;393}394 395/**396 * Handle returned by {@link createApprovalModeOverride}.397 *398 * The `cleanup` callback MUST be invoked in a `finally` block after the399 * sub-agent lifecycle ends. It restores the parent PermissionManager's400 * dangerous allow rules if and only if this override was responsible401 * for stripping them — see {@link createApprovalModeOverride} below402 * for the cases.403 */404export interface ApprovalModeOverrideHandle {405 config: Config;406 cleanup: () => void;407}408 409export interface ApprovalModeOverrideOptions {410 persistedCliFlags?: AgentPersistedCliFlags;411}412 413function hasOwn(value: object, key: PropertyKey): boolean {414 return Object.prototype.hasOwnProperty.call(value, key);415}416 417function applyPersistedCliFlagOverrides(418 override: Config,419 flags: AgentPersistedCliFlags | undefined,420): void {421 if (!flags) return;422 // eslint-disable-next-line @typescript-eslint/no-explicit-any423 const ov = override as any;424 if (flags.bare !== undefined) {425 ov.getBareMode = () => flags.bare;426 }427 if (flags.safeMode !== undefined) {428 ov.isSafeMode = () => flags.safeMode;429 }430 if (hasOwn(flags, 'sandbox')) {431 const sandbox = flags.sandbox ?? undefined;432 ov.getSandbox = () => sandbox;433 }434 if (flags.screenReader !== undefined) {435 ov.getScreenReader = () => flags.screenReader;436 }437 if (flags.model !== undefined) {438 ov.getModel = () => flags.model;439 }440 if (flags.maxSessionTurns !== undefined) {441 ov.getMaxSessionTurns = () => flags.maxSessionTurns;442 }443 if (flags.maxToolCalls !== undefined) {444 ov.getMaxToolCalls = () => flags.maxToolCalls;445 }446 if (flags.maxSubagentDepth !== undefined) {447 // Re-normalize across the serialization boundary: this codebase only448 // ever persists a normalized 1-100 integer, but the sidecar is a plain449 // JSON file — a malformed or hand-edited copy (out-of-range numbers,450 // `1e309` → Infinity, or a literal null) must not bypass the nesting451 // cap for resumed agents. Same semantics as the Config constructor.452 const maxSubagentDepth = normalizeMaxSubagentDepth(flags.maxSubagentDepth);453 ov.getMaxSubagentDepth = () => maxSubagentDepth;454 }455}456 457function capturePersistedCliFlags(458 config: Config,459 resolvedApprovalMode: ApprovalMode,460): AgentPersistedCliFlags {461 return {462 approvalMode: resolvedApprovalMode,463 bare: config.getBareMode(),464 safeMode: config.isSafeMode(),465 sandbox: config.getSandbox() ?? null,466 screenReader: config.getScreenReader(),467 model: config.getModel(),468 maxSessionTurns: config.getMaxSessionTurns(),469 maxToolCalls: config.getMaxToolCalls(),470 maxSubagentDepth: config.getMaxSubagentDepth(),471 };472}473 474/**475 * Creates a Config override with a different approval mode.476 *477 * Uses prototype delegation (Object.create) to avoid mutating the parent478 * config, then delegates to {@link rebuildToolRegistryOnOverride} so the479 * override's tool registry has core tools bound to the override rather480 * than to the parent. Without that rebuild, the parent's cached tool481 * instances continue to resolve `this.config` to the parent, defeating482 * per-Config isolation of FileReadCache / approval mode for any code483 * path that goes through the bound tool.484 *485 * Returns `{ config, cleanup }`. Callers MUST invoke `cleanup` in a486 * `finally` block after the override is no longer in use, otherwise487 * the parent's PermissionManager may leak a strip across the sub-agent488 * boundary (see strip lifecycle below).489 *490 * Strip lifecycle for AUTO overrides:491 * - parent not in AUTO, override starts in AUTO: this function strips492 * the PARENT's PM (shared via prototype chain — the override cannot493 * have its own PM without a much bigger refactor).494 * - parent already in AUTO, override starts in AUTO: parent's495 * `setApprovalMode` already stripped on its own entry, so this496 * function does not strip again.497 * - override enters/leaves AUTO later: `setApprovalMode` reuses Config's498 * normal state transition, but suppresses AUTO strip/restore while the499 * parent is already in AUTO because the parent owns that strip lifecycle.500 * `cleanup` only restores if the child finishes still in AUTO while the501 * parent is not in AUTO.502 */503export async function createApprovalModeOverride(504 base: Config,505 mode: ApprovalMode,506 options: ApprovalModeOverrideOptions = {},507): Promise<ApprovalModeOverrideHandle> {508 // eslint-disable-next-line @typescript-eslint/no-explicit-any509 const override = Object.create(base) as any;510 const baseApprovalMode = base.getApprovalMode();511 // These own properties intentionally mirror Config's TS-private field names.512 // Config prototype methods read/write them at runtime on this override object.513 override.approvalMode = mode;514 override.getApprovalMode = Config.prototype.getApprovalMode;515 override.prePlanMode =516 mode === ApprovalMode.PLAN517 ? baseApprovalMode === ApprovalMode.PLAN518 ? base.getPrePlanMode()519 : baseApprovalMode520 : undefined;521 const basePlanGateState =522 mode === ApprovalMode.PLAN ? base.getPlanGateState() : undefined;523 override.planGateState = basePlanGateState524 ? {525 ...basePlanGateState,526 lastFindings: [...basePlanGateState.lastFindings],527 }528 : undefined;529 override.planGateEntryCounter = override.planGateState?.entryId ?? 0;530 override.autoModeDenialState = createDenialState();531 override.setApprovalMode = (532 nextMode: ApprovalMode,533 setOptions?: Parameters<Config['setApprovalMode']>[1],534 ): void => {535 if (base.getApprovalMode() !== ApprovalMode.AUTO) {536 Config.prototype.setApprovalMode.call(537 override as Config,538 nextMode,539 setOptions,540 );541 return;542 }543 544 const hadOwnPermissionManager = Object.prototype.hasOwnProperty.call(545 override,546 'permissionManager',547 );548 const ownPermissionManager = override.permissionManager;549 override.permissionManager = null;550 try {551 Config.prototype.setApprovalMode.call(552 override as Config,553 nextMode,554 setOptions,555 );556 } finally {557 if (hadOwnPermissionManager) {558 override.permissionManager = ownPermissionManager;559 } else {560 delete override.permissionManager;561 }562 }563 };564 applyPersistedCliFlagOverrides(override as Config, options.persistedCliFlags);565 await rebuildToolRegistryOnOverride(override as Config, base);566 567 const cleanup = () => {568 if (569 (override as Config).getApprovalMode() === ApprovalMode.AUTO &&570 base.getApprovalMode() !== ApprovalMode.AUTO571 ) {572 base.getPermissionManager?.()?.restoreDangerousRules();573 }574 };575 576 if (mode === ApprovalMode.AUTO) {577 const baseWasAuto = base.getApprovalMode() === ApprovalMode.AUTO;578 if (!baseWasAuto) {579 // This override is bringing AUTO into a non-AUTO parent. Strip580 // dangerous allow rules so the sub-agent's classifier actually581 // gates them. Cleanup handles restore if the child finishes in AUTO.582 base.getPermissionManager?.()?.stripDangerousRulesForAutoMode();583 }584 // baseWasAuto: parent's setApprovalMode already stripped; cleanup585 // will not restore while the parent remains in AUTO.586 }587 588 return { config: override as Config, cleanup };589}590 591/**592 * Agent tool that enables primary agents to delegate tasks to specialized agents.593 * The tool dynamically loads available agents and includes them in its description594 * for the model to choose from.595 */596export class AgentTool extends BaseDeclarativeTool<AgentParams, ToolResult> {597 static readonly Name: string = ToolNames.AGENT;598 599 override get maxOutputChars(): number {600 return 32_000;601 }602 603 override get truncateKeep(): 'tail' {604 return 'tail';605 }606 607 private subagentManager: SubagentManager;608 private availableSubagents: SubagentConfig[] =609 BuiltinAgentRegistry.getBuiltinAgents();610 private readonly removeChangeListener: () => void;611 612 constructor(private readonly config: Config) {613 // Initialize with a basic schema first614 const initialSchema = {615 type: 'object',616 properties: {617 description: {618 type: 'string',619 description: 'A short (3-5 word) description of the task',620 },621 prompt: {622 type: 'string',623 description: 'The task for the agent to perform',624 },625 subagent_type: {626 type: 'string',627 description: 'The type of specialized agent to use for this task',628 },629 run_in_background: {630 type: 'boolean',631 description:632 'Set to true to run this agent in the background. You will be notified when it completes. Top-level session only: from within a sub-agent the task runs in the foreground and returns its result inline.',633 },634 ...(config.isAgentTeamEnabled()635 ? {636 name: TEAM_AGENT_NAME_PROPERTY,637 plan_mode_required: TEAM_AGENT_PLAN_REQUIRED_PROPERTY,638 }639 : {}),640 isolation: {641 type: 'string',642 enum: ['worktree'],643 description:644 "Isolation mode. 'worktree' creates a temporary git worktree under <projectRoot>/.qwen/worktrees/agent-<7hex> so the agent works on an isolated copy of the repo. The worktree is auto-removed if the agent makes no changes; otherwise the worktree path and branch are returned in the result.",645 },646 },647 required: ['description', 'prompt'],648 additionalProperties: false,649 $schema: 'http://json-schema.org/draft-07/schema#',650 };651 652 super(653 AgentTool.Name,654 ToolDisplayNames.AGENT,655 'Launch a new agent to handle complex, multi-step tasks autonomously.\n\nThe Agent tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n',656 Kind.Agent,657 initialSchema,658 true, // isOutputMarkdown659 true, // canUpdateOutput - Enable live output updates for real-time progress660 );661 662 this.subagentManager = config.getSubagentManager();663 this.removeChangeListener = this.subagentManager.addChangeListener(() => {664 void this.refreshSubagents();665 });666 667 // Initialize the tool asynchronously668 this.refreshSubagents();669 }670 671 dispose(): void {672 this.removeChangeListener();673 }674 675 /**676 * Asynchronously initializes the tool by loading available subagents677 * and updating the description and schema.678 */679 async refreshSubagents(): Promise<void> {680 try {681 this.availableSubagents = await this.subagentManager.listSubagents();682 this.updateDescriptionAndSchema();683 } catch (error) {684 debugLogger.warn('Failed to load agents for Agent tool:', error);685 this.availableSubagents = BuiltinAgentRegistry.getBuiltinAgents();686 this.updateDescriptionAndSchema();687 } finally {688 // Update the client with the new tools689 const geminiClient = this.config.getGeminiClient();690 if (geminiClient) {691 await geminiClient.setTools();692 }693 }694 }695 696 /**697 * Updates the tool's description and schema based on available subagents.698 */699 private updateDescriptionAndSchema(): void {700 let subagentDescriptions = '';701 if (this.availableSubagents.length === 0) {702 subagentDescriptions =703 'No subagents are currently configured. You can create subagents using the /agents command.';704 } else {705 subagentDescriptions = this.availableSubagents706 .map((subagent) => `- **${subagent.name}**: ${subagent.description}`)707 .join('\n');708 }709 710 // Only advertise team coordination when the experimental711 // feature is on; otherwise the model is steered toward a712 // `team_create` tool that isn't registered.713 const teamGuidance = this.config.isAgentTeamEnabled()714 ? `**For tasks requiring multiple agents to coordinate, communicate, or work as a team**: Use ${ToolNames.TEAM_CREATE} first to create a team, then spawn teammates using the Agent tool with the \`name\` parameter (the active team is selected automatically). Teams enable message passing between agents, shared task lists, and coordinated workflows. If the user asks for agents to collaborate, review each other's work, or produce a consolidated result — create a team.`715 : '';716 717 const baseDescription = `Launch a new agent to handle complex, multi-step tasks autonomously.718The Agent tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.719 720Available agent types and the tools they have access to:721${subagentDescriptions}722 723${724 isForkSubagentEnabled(this.config)725 ? `When using the Agent tool, specify a subagent_type to select which agent type to use. If omitted, the general-purpose agent is used and returns its result to you inline. A fork (\`subagent_type: "fork"\`) runs detached and fire-and-forget — its result does NOT come back to you, so use it ONLY for work whose output you won't need. When you need the agent's findings back (review, audit, aggregation, verification), use a regular subagent, never a fork.`726 : `When using the Agent tool, specify a subagent_type parameter to select which agent type to use. If omitted, the general-purpose agent is used.`727}728 729When NOT to use the Agent tool:730- If you want to read a specific file path, use the ${ToolNames.READ_FILE} tool or the ${ToolNames.GLOB} tool instead of the ${ToolNames.AGENT} tool, to find the match more quickly731- If you are searching for a specific class definition like "class Foo", use the ${ToolNames.GREP} tool instead, to find the match more quickly732- If you are searching for code within a specific file or set of 2-3 files, use the ${ToolNames.READ_FILE} tool instead of the ${ToolNames.AGENT} tool, to find the match more quickly733- Other tasks that are not related to the agent descriptions above734 735${teamGuidance}736 737Usage notes:738- Always include a short description (3-5 words) summarizing what the agent will do739- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses740- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.741- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.742- The agent's outputs should generally be trusted743- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent744- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.745- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Agent tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.746- You can optionally set \`run_in_background: true\` to run the agent in the background. You will be notified when it completes. Use this when you have genuinely independent work to do in parallel and don't need the agent's results before you can proceed.747- You can optionally set \`isolation: "worktree"\` to run the agent in a temporary git worktree, giving it an isolated copy of the repository. The worktree is automatically cleaned up if the agent makes no changes; if changes are made, the worktree path and branch are returned in the result so you can review or merge them.748${749 isForkSubagentEnabled(this.config)750 ? `751## When to fork752 753A fork (\`subagent_type: "fork"\`) runs detached and fire-and-forget: it inherits your full context, but its findings do NOT come back to you in a form you can act on. **Never fork work whose output you need** — reviews, audits, parallel investigations you must aggregate, verification, anything where you have to read or combine the results. For all of that, launch regular awaitable subagents instead (omit \`subagent_type\` for general-purpose, or name a specific type); each returns its result to you inline, and several in one message still run concurrently. Omitting \`subagent_type\` does NOT fork.754 755Fork only when you genuinely won't need the result back — a detached background chore the user asked you to kick off and move on from. The criterion is qualitative: "will I need to read this output?" If yes, don't fork.756 757Forks are cheap because they share your prompt cache. Don't set \`model\` on a fork — a different model can't reuse the parent's cache. Pass a short \`name\` (one or two words, lowercase) so the user can track the fork.758 759**Don't peek.** The tool result includes an output — do not read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it. Reading the transcript mid-flight pulls the fork's tool noise into your context, which defeats the point of forking.760 761**Don't race.** After launching, you know nothing about what the fork found. Never fabricate or predict fork results in any format — not as prose, summary, or structured output. The notification arrives as a user-role message in a later turn; it is never something you write yourself. If the user asks a follow-up before the notification lands, tell them the fork is still running — give status, not a guess.762 763**Writing a fork prompt.** Since the fork inherits your context, the prompt is a *directive* — what to do, not what the situation is. Be specific about scope: what's in, what's out, what another agent is handling. Don't re-explain background.764`765 : ''766}767## Writing the prompt768 769${isForkSubagentEnabled(this.config) ? 'When spawning a fresh agent (with a `subagent_type`), it starts with zero context. ' : ''}Brief the agent like a smart colleague who just walked into the room — it has not seen this conversation, does not know what you've tried, and does not understand why this task matters.770- Explain what you're trying to accomplish and why.771- Describe what you've already learned or ruled out.772- Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.773- If you need a short response, say so explicitly.774- For lookups, provide the exact target. For investigations, provide the actual question rather than an over-prescribed sequence of steps.775 776${isForkSubagentEnabled(this.config) ? 'For fresh agents, terse' : 'Terse'} command-style prompts produce shallow, generic work.777 778**Never delegate understanding.** Do not write prompts like "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood the task: include relevant file paths, constraints, what specifically needs to be learned or changed, and what is out of scope.779 780After launching an agent, do not fabricate or predict what it found before it returns. If the user asks a follow-up before the result arrives, provide status rather than guessing.781 782Example usage:783 784<example_agent_descriptions>785"test-runner": use this agent after you are done writing code to run tests786</example_agent_descriptions>787 788<example>789user: "Please write a function that checks if a number is prime"790assistant: I'm going to use the Write tool to write the following code:791<code>792function isPrime(n) {793 if (n <= 1) return false794 for (let i = 2; i * i <= n; i++) {795 if (n % i === 0) return false796 }797 return true798}799</code>800<commentary>801Since a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests802</commentary>803assistant: Uses the ${ToolNames.AGENT} tool to launch the test-runner agent804</example>805`;806 807 // Update description using object property assignment since it's readonly808 (this as { description: string }).description = baseDescription;809 810 // Generate dynamic schema with enum of available subagent names811 const subagentNames = this.availableSubagents.map((s) => s.name);812 813 // Update the parameter schema by modifying the existing object814 const schema = this.parameterSchema as {815 properties?: {816 subagent_type?: {817 enum?: string[];818 };819 name?: typeof TEAM_AGENT_NAME_PROPERTY;820 plan_mode_required?: typeof TEAM_AGENT_PLAN_REQUIRED_PROPERTY;821 };822 };823 if (schema.properties && schema.properties.subagent_type) {824 // Only real, loadable subagents are advertised in the enum. `fork` is a825 // deliberate pseudo-type, NOT listed here: dangling it as a casual option826 // led the model to fork result-bearing work (e.g. review agents), whose827 // findings a fork never returns. Forking stays reachable for intentional828 // use — validation accepts `subagent_type: "fork"` and `/fork` passes it829 // directly — it just isn't offered as a default pick.830 if (subagentNames.length > 0) {831 schema.properties.subagent_type.enum = subagentNames;832 } else {833 delete schema.properties.subagent_type.enum;834 }835 }836 if (schema.properties) {837 if (this.config.isAgentTeamEnabled()) {838 schema.properties.name = TEAM_AGENT_NAME_PROPERTY;839 schema.properties.plan_mode_required =840 TEAM_AGENT_PLAN_REQUIRED_PROPERTY;841 } else {842 delete schema.properties.name;843 delete schema.properties.plan_mode_required;844 }845 }846 }847 848 override validateToolParams(params: AgentParams): string | null {849 // Validate required fields850 if (851 !params.description ||852 typeof params.description !== 'string' ||853 params.description.trim() === ''854 ) {855 return 'Parameter "description" must be a non-empty string.';856 }857 858 if (859 !params.prompt ||860 typeof params.prompt !== 'string' ||861 params.prompt.trim() === ''862 ) {863 return 'Parameter "prompt" must be a non-empty string.';864 }865 866 if (params.subagent_type !== undefined) {867 if (868 typeof params.subagent_type !== 'string' ||869 params.subagent_type.trim() === ''870 ) {871 return 'Parameter "subagent_type" must be a non-empty string.';872 }873 // Validate that the subagent exists (case-insensitive). `fork` is an874 // explicit pseudo-type resolved by the dispatch logic (not a loadable875 // subagent), so accept it regardless of the registered list; when876 // forking is unavailable, dispatch falls back to general-purpose.877 const lowerType = params.subagent_type.toLowerCase();878 if (lowerType !== FORK_SUBAGENT_TYPE) {879 const subagentExists = this.availableSubagents.some(880 (subagent) => subagent.name.toLowerCase() === lowerType,881 );882 883 if (!subagentExists) {884 const availableNames = this.availableSubagents.map((s) => s.name);885 return `Subagent "${params.subagent_type}" not found. Available subagents: ${availableNames.join(', ')}`;886 }887 }888 }889 890 if (params.isolation !== undefined) {891 if (params.isolation !== 'worktree') {892 return 'Parameter "isolation" must be "worktree" when set.';893 }894 // Isolation puts the agent in a separate git worktree. A fork reuses895 // the parent's conversation context and working tree, so it can't be896 // isolated; and the general-purpose default is only worth isolating897 // when asked for explicitly. Require an explicit, non-fork subagent_type.898 if (899 !params.subagent_type ||900 params.subagent_type.toLowerCase() === FORK_SUBAGENT_TYPE901 ) {902 return 'Parameter "isolation" requires an explicit subagent_type (and cannot be "fork").';903 }904 }905 906 if (params.plan_mode_required !== undefined) {907 if (typeof params.plan_mode_required !== 'boolean') {908 return 'Parameter "plan_mode_required" must be a boolean when set.';909 }910 if (params.plan_mode_required) {911 if (912 !params.name ||913 typeof params.name !== 'string' ||914 params.name.trim() === ''915 ) {916 return 'Parameter "plan_mode_required" requires a named teammate via "name".';917 }918 if (!this.config.getTeamManager()) {919 return 'Parameter "plan_mode_required" requires an active team.';920 }921 }922 }923 924 return null;925 }926 927 protected createInvocation(params: AgentParams) {928 return new AgentToolInvocation(this.config, this.subagentManager, params);929 }930 931 override toAutoClassifierInput(params: AgentParams): Record<string, unknown> {932 // Forward the full prompt (no truncation). The earlier 200-char preview933 // hid any attack payload after character 200 from the classifier while934 // the sub-agent itself received the full text — same shape of attack935 // surface as truncating a shell command. Shell tools forward the full936 // command for the same reason.937 return {938 subagent_type: params.subagent_type,939 prompt: params.prompt ?? '',940 };941 }942 943 getAvailableSubagentNames(): string[] {944 return this.availableSubagents.map((subagent) => subagent.name);945 }946}947 948/**949 * Callback the body of `runWithSubagentSpan` invokes to publish its terminal950 * state. Without this, both `runSubagentWithHooks` and `bgBody` swallow their951 * own errors before returning, leaving the wrapper's catch block dead and952 * every span ending as `status='completed'` regardless of actual outcome.953 * Review wenshao @ #4410.954 */955type SubagentOutcomeSink = (metadata: SubagentSpanMetadata) => void;956 957/**958 * Map `AgentTerminateMode` + signal/error state to the span's status taxonomy.959 * Mirrors the foreground/background display logic: GOAL → success, CANCELLED960 * (or signal abort) → user-initiated stop, everything else → failure.961 */962function deriveSubagentOutcomeMetadata(opts: {963 terminateMode: AgentTerminateMode;964 signalAborted: boolean;965 resultSummaryPresent: boolean;966}): SubagentSpanMetadata {967 const { terminateMode, signalAborted, resultSummaryPresent } = opts;968 if (signalAborted || terminateMode === AgentTerminateMode.CANCELLED) {969 return {970 status: 'cancelled',971 terminateReason: signalAborted ? 'signal_aborted' : 'subagent_cancelled',972 resultSummaryPresent,973 };974 }975 // SHUTDOWN is a graceful arena/team-session-end, not a failure — group it976 // with cancellations so dashboards don't count it against subagent error977 // rate. Review wenshao @ #4410.978 if (terminateMode === AgentTerminateMode.SHUTDOWN) {979 return {980 status: 'cancelled',981 terminateReason: 'subagent_shutdown',982 resultSummaryPresent,983 };984 }985 if (terminateMode === AgentTerminateMode.GOAL) {986 return { status: 'completed', resultSummaryPresent };987 }988 // Non-throwing failure paths (ERROR / MAX_TURNS / TIMEOUT) — populate989 // `error`/`errorType` so endSubagentSpan sets standard OTel exception990 // attributes instead of a generic `'subagent failed'` placeholder.991 // Otherwise dashboards relying on `exception.message`/`error.type` see992 // no signal for these (reachable) outcomes. wenshao @ #4410.993 return {994 status: 'failed',995 terminateReason: String(terminateMode).toLowerCase(),996 error: `subagent terminated with mode: ${terminateMode}`,997 errorType: terminateMode,998 resultSummaryPresent,999 };1000}1001 1002function deriveSubagentExceptionMetadata(1003 error: unknown,1004 signalAborted: boolean,1005): SubagentSpanMetadata {1006 return {1007 status: signalAborted ? 'aborted' : 'failed',1008 error: error instanceof Error ? error.message : String(error),1009 errorType:1010 error instanceof Error ? error.constructor.name : 'NonErrorThrown',1011 terminateReason: signalAborted ? 'signal_aborted' : 'exception',1012 // Exception path always lacks a subagent-produced summary (we never got1013 // through getFinalText()). Setting this explicitly keeps attribute1014 // shape symmetric with the success-path derive so dashboards filtering1015 // on result_summary_present don't silently exclude failed runs.1016 // Review wenshao @ #4410.1017 resultSummaryPresent: false,1018 };1019}1020 1021class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {1022 readonly eventEmitter: AgentEventEmitter = new AgentEventEmitter();1023 private currentDisplay: AgentResultDisplay | null = null;1024 private currentToolCalls: AgentResultDisplay['toolCalls'] = [];1025 private callId?: string;1026 1027 constructor(1028 private readonly config: Config,1029 private readonly subagentManager: SubagentManager,1030 params: AgentParams,1031 ) {1032 super(params);1033 }1034 1035 // Background agents carry the tool-use id through to completion notifications.1036 setCallId(callId: string): void {1037 this.callId = callId;1038 }1039 1040 /**1041 * Updates the current display state and calls updateOutput if provided1042 */1043 private updateDisplay(1044 updates: Partial<AgentResultDisplay>,1045 updateOutput?: (output: ToolResultDisplay) => void,1046 ): void {1047 if (!this.currentDisplay) return;1048 1049 this.currentDisplay = {1050 ...this.currentDisplay,1051 ...updates,1052 };1053 1054 if (updateOutput) {1055 updateOutput(this.currentDisplay);1056 }1057 }1058 1059 private registerOwnedMonitorNotifications(1060 agentId: string,1061 enqueue: (input: AgentExternalInput) => boolean,1062 wake: () => void,1063 ): () => void {1064 const monitorRegistry = this.config.getMonitorRegistry();1065 monitorRegistry.setAgentNotificationCallback(1066 agentId,1067 (_displayText, modelText) =>1068 void enqueue({ kind: 'notification', text: modelText }),1069 );1070 monitorRegistry.setAgentLifecycleCallback(agentId, wake);1071 1072 return () => {1073 monitorRegistry.cancelRunningForOwner(agentId, { notify: false });1074 monitorRegistry.setAgentNotificationCallback(agentId, undefined);1075 monitorRegistry.setAgentLifecycleCallback(agentId, undefined);1076 };1077 }1078 1079 /**1080 * Sets up event listeners for real-time subagent progress updates1081 */1082 private setupEventListeners(1083 updateOutput?: (output: ToolResultDisplay) => void,1084 ): void {1085 let pendingConfirmationCallId: string | undefined;1086 const preserveProtocolPayloads = !this.config.isInteractive();1087 1088 this.eventEmitter.on(AgentEventType.START, () => {1089 this.updateDisplay({ status: 'running' }, updateOutput);1090 });1091 1092 this.eventEmitter.on(AgentEventType.TOOL_CALL, (...args: unknown[]) => {1093 const event = args[0] as AgentToolCallEvent;1094 const newToolCall = {1095 callId: event.callId,1096 name: event.name,1097 status: 'executing' as const,1098 ...(preserveProtocolPayloads ? { args: event.args } : {}),1099 description: event.description,1100 };1101 this.currentToolCalls!.push(newToolCall);1102 1103 this.updateDisplay(1104 {1105 toolCalls: [...this.currentToolCalls!],1106 },1107 updateOutput,1108 );1109 });1110 1111 this.eventEmitter.on(AgentEventType.TOOL_RESULT, (...args: unknown[]) => {1112 const event = args[0] as AgentToolResultEvent;1113 const toolCallIndex = this.currentToolCalls!.findIndex(1114 (call) => call.callId === event.callId,1115 );1116 if (toolCallIndex >= 0) {1117 this.currentToolCalls![toolCallIndex] = {1118 ...this.currentToolCalls![toolCallIndex],1119 status: event.success ? 'success' : 'failed',1120 error: event.error,1121 ...(preserveProtocolPayloads && event.responseParts !== undefined1122 ? { responseParts: event.responseParts }1123 : {}),1124 ...(typeof event.resultDisplay === 'string'1125 ? { resultDisplay: event.resultDisplay }1126 : {}),1127 };1128 1129 // When a tool result arrives for the tool that had a pending1130 // confirmation, clear the stale prompt. This handles the case where1131 // the IDE diff-tab accept resolved the tool via CoreToolScheduler's1132 // IDE confirmation handler, which bypasses the UI's onConfirm wrapper.1133 const clearPending =1134 pendingConfirmationCallId === event.callId1135 ? { pendingConfirmation: undefined }1136 : {};1137 if (pendingConfirmationCallId === event.callId) {1138 pendingConfirmationCallId = undefined;1139 }1140 1141 this.updateDisplay(1142 {1143 toolCalls: [...this.currentToolCalls!],1144 ...clearPending,1145 },1146 updateOutput,1147 );1148 }1149 });1150 1151 this.eventEmitter.on(AgentEventType.FINISH, (...args: unknown[]) => {1152 const event = args[0] as AgentFinishEvent;1153 this.updateDisplay(1154 {1155 status: event.terminateReason === 'GOAL' ? 'completed' : 'failed',1156 terminateReason: event.terminateReason,1157 },1158 updateOutput,1159 );1160 });1161 1162 this.eventEmitter.on(AgentEventType.ERROR, (...args: unknown[]) => {1163 const event = args[0] as AgentErrorEvent;1164 this.updateDisplay(1165 {1166 status: 'failed',1167 terminateReason: event.error,1168 },1169 updateOutput,1170 );1171 });1172 1173 // Track real-time token consumption from subagent API calls.1174 // Each USAGE_METADATA event carries per-round usage, so we accumulate1175 // output tokens across rounds. We use candidatesTokenCount (output-only)1176 // to stay consistent with the main stream's chars/4 output-token estimate.1177 let accumulatedOutputTokens = 0;1178 this.eventEmitter.on(1179 AgentEventType.USAGE_METADATA,1180 (...args: unknown[]) => {1181 const event = args[0] as AgentUsageEvent;1182 const outputTokens = event.usage?.candidatesTokenCount ?? 0;1183 if (outputTokens > 0) {1184 accumulatedOutputTokens += outputTokens;1185 this.updateDisplay(1186 { tokenCount: accumulatedOutputTokens },1187 updateOutput,1188 );1189 }1190 },1191 );1192 1193 // Indicate when a tool call is waiting for approval1194 this.eventEmitter.on(1195 AgentEventType.TOOL_WAITING_APPROVAL,1196 (...args: unknown[]) => {1197 const event = args[0] as AgentApprovalRequestEvent;1198 const idx = this.currentToolCalls!.findIndex(1199 (c) => c.callId === event.callId,1200 );