CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
agent.ts3490 linesDownload Raw Back to agent
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        );

Showing the first 1,200 of 3490 lines. Download the file for the rest.

basant307/AI_Governance_Project · CoolFace