CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
nonInteractiveHelpers.ts631 linesDownload Raw Back to utils
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import type {8  Config,9  ToolResultDisplay,10  AgentResultDisplay,11  OutputUpdateHandler,12  ToolCallRequestInfo,13  ToolCallResponseInfo,14  SessionMetrics,15  McpToolProgressData,16} from '@qwen-code/qwen-code-core';17import {18  ApprovalMode,19  OutputFormat,20  ToolErrorType,21  createDebugLogger,22  getArenaSystemReminder,23  getMCPServerStatus,24  getPlanModeSystemReminder,25} from '@qwen-code/qwen-code-core';26import type { Part, PartListUnion } from '@google/genai';27import type {28  CLIUserMessage,29  Usage,30  PermissionMode,31  CLISystemMessage,32} from '../nonInteractive/types.js';33import type {34  JsonOutputAdapterInterface,35  MessageEmitter,36} from '../nonInteractive/io/BaseJsonOutputAdapter.js';37import { computeSessionStats } from '../ui/utils/computeStats.js';38import { getAvailableCommands } from '../nonInteractiveCliCommands.js';39 40const debugLogger = createDebugLogger('NON_INTERACTIVE');41 42/**43 * Normalizes various part list formats into a consistent Part[] array.44 *45 * @param parts - Input parts in various formats (string, Part, Part[], or null)46 * @returns Normalized array of Part objects47 */48export function normalizePartList(parts: PartListUnion | null): Part[] {49  if (!parts) {50    return [];51  }52 53  if (typeof parts === 'string') {54    return [{ text: parts }];55  }56 57  if (Array.isArray(parts)) {58    return parts.map((part) =>59      typeof part === 'string' ? { text: part } : (part as Part),60    );61  }62 63  return [parts as Part];64}65 66/**67 * Extracts user message parts from a CLI protocol message.68 *69 * @param message - User message sourced from the CLI protocol layer70 * @returns Extracted parts or null if the message lacks textual content71 */72export function extractPartsFromUserMessage(73  message: CLIUserMessage | undefined,74): PartListUnion | null {75  if (!message) {76    return null;77  }78 79  const content = message.message?.content;80  if (typeof content === 'string') {81    return content;82  }83 84  if (Array.isArray(content)) {85    const parts: Part[] = [];86    for (const block of content) {87      if (!block || typeof block !== 'object' || !('type' in block)) {88        continue;89      }90      if (block.type === 'text' && 'text' in block && block.text) {91        parts.push({ text: block.text });92      } else {93        parts.push({ text: JSON.stringify(block) });94      }95    }96    return parts.length > 0 ? parts : null;97  }98 99  return null;100}101 102/**103 * Computes Usage information from SessionMetrics using computeSessionStats.104 * Aggregates token usage across all models in the session.105 *106 * @param metrics - Session metrics from uiTelemetryService107 * @returns Usage object with token counts108 */109export function computeUsageFromMetrics(metrics: SessionMetrics): Usage {110  const stats = computeSessionStats(metrics);111  const { models } = metrics;112 113  // Sum up output tokens (candidates) and total tokens across all models114  const totalOutputTokens = Object.values(models).reduce(115    (acc, model) => acc + model.tokens.candidates,116    0,117  );118  const totalTokens = Object.values(models).reduce(119    (acc, model) => acc + model.tokens.total,120    0,121  );122 123  const usage: Usage = {124    input_tokens: stats.totalPromptTokens,125    output_tokens: totalOutputTokens,126    cache_read_input_tokens: stats.totalCachedTokens,127  };128 129  // Only include total_tokens if it's greater than 0130  if (totalTokens > 0) {131    usage.total_tokens = totalTokens;132  }133 134  return usage;135}136 137export function buildInitialSystemReminders(config: Config): Part[] {138  const reminders: Part[] = [];139 140  if (config.getApprovalMode() === ApprovalMode.PLAN) {141    reminders.push({ text: getPlanModeSystemReminder(config.getSdkMode?.()) });142  }143 144  const arenaManager = config.getArenaManager?.();145  if (arenaManager) {146    try {147      const sessionDir = arenaManager.getArenaSessionDir();148      const configPath = `${sessionDir}/config.json`;149      reminders.push({ text: getArenaSystemReminder(configPath) });150    } catch {151      // Arena config not yet initialized; match the regular send path.152    }153  }154 155  return reminders;156}157 158export function insertAfterFunctionResponses(159  parts: Part[],160  additions: Part[],161): Part[] {162  const firstNonFunctionResponse = parts.findIndex(163    (part) => !part.functionResponse,164  );165  const insertAt =166    firstNonFunctionResponse === -1 ? parts.length : firstNonFunctionResponse;167  return [...parts.slice(0, insertAt), ...additions, ...parts.slice(insertAt)];168}169 170/**171 * Load slash command names using getAvailableCommands172 *173 * @param config - Config instance174 * @returns Promise resolving to array of slash command names175 */176async function loadSlashCommandNames(config: Config): Promise<string[]> {177  const controller = new AbortController();178  try {179    const commands = await getAvailableCommands(180      config,181      controller.signal,182      'non_interactive',183    );184 185    // Extract command names and sort186    return commands.map((cmd) => cmd.name).sort();187  } catch (error) {188    debugLogger.error(189      '[buildSystemMessage] Failed to load slash commands:',190      error,191    );192    return [];193  } finally {194    controller.abort();195  }196}197 198/**199 * Build system message for SDK200 *201 * Constructs a system initialization message including tools, MCP servers,202 * and model configuration. System messages are independent of the control203 * system and are sent before every turn regardless of whether control204 * system is available.205 *206 * Note: Control capabilities are NOT included in system messages. They207 * are only included in the initialize control response, which is handled208 * separately by SystemController.209 *210 * @param config - Config instance211 * @param sessionId - Session identifier212 * @param permissionMode - Current permission/approval mode213 * @returns Promise resolving to CLISystemMessage214 */215export async function buildSystemMessage(216  config: Config,217  sessionId: string,218  permissionMode: PermissionMode,219): Promise<CLISystemMessage> {220  const toolRegistry = config.getToolRegistry();221  const tools = toolRegistry ? toolRegistry.getAllToolNames() : [];222 223  const mcpServers = config.getMcpServers();224  const mcpServerList = mcpServers225    ? Object.keys(mcpServers).map((name) => ({226        name,227        status: getMCPServerStatus(name),228      }))229    : [];230 231  // Load slash commands available in ACP mode232  const slashCommands = await loadSlashCommandNames(config);233 234  // Load subagent names from config235  let agentNames: string[] = [];236  try {237    const subagentManager = config.getSubagentManager();238    const subagents = await subagentManager.listSubagents();239    agentNames = subagents.map((subagent) => subagent.name);240  } catch (error) {241    debugLogger.error('[buildSystemMessage] Failed to load subagents:', error);242  }243 244  const systemMessage: CLISystemMessage = {245    type: 'system',246    subtype: 'init',247    uuid: sessionId,248    session_id: sessionId,249    cwd: config.getTargetDir(),250    tools,251    mcp_servers: mcpServerList,252    model: config.getModel(),253    permission_mode: permissionMode,254    slash_commands: slashCommands,255    qwen_code_version: config.getCliVersion() || 'unknown',256    agents: agentNames,257  };258 259  return systemMessage;260}261 262function isMcpToolProgressData(263  output: ToolResultDisplay,264): output is McpToolProgressData {265  return (266    typeof output === 'object' &&267    output !== null &&268    'type' in output &&269    (output as McpToolProgressData).type === 'mcp_tool_progress'270  );271}272 273/**274 * Creates a generic output update handler for tools with canUpdateOutput=true.275 * This handler forwards MCP progress data (McpToolProgressData) as tool_progress276 * stream events via the adapter. Progress events are only emitted when the adapter277 * supports partial messages (i.e., includePartialMessages is true).278 *279 * @param request - Tool call request info280 * @param adapter - The adapter instance for emitting messages281 * @returns An object containing the output update handler282 */283export function createToolProgressHandler(284  request: ToolCallRequestInfo,285  adapter: MessageEmitter,286): {287  handler: OutputUpdateHandler;288} {289  const handler: OutputUpdateHandler = (290    _callId: string,291    output: ToolResultDisplay,292  ) => {293    if (isMcpToolProgressData(output)) {294      adapter.emitToolProgress(request, output);295    }296  };297 298  return { handler };299}300 301/**302 * Creates an output update handler specifically for Agent tool subagent execution.303 * This handler monitors AgentResultDisplay updates and converts them to protocol messages304 * using the unified adapter's subagent APIs. All emitted messages will have parent_tool_use_id set to305 * the agent tool's callId.306 *307 * @param config - Config instance for getting output format308 * @param agentToolCallId - The agent tool's callId to use as parent_tool_use_id for all subagent messages309 * @param adapter - The unified adapter instance (JsonOutputAdapter or StreamJsonOutputAdapter)310 * @returns An object containing the output update handler311 */312export function createAgentToolProgressHandler(313  config: Config,314  agentToolCallId: string,315  adapter: JsonOutputAdapterInterface,316): {317  handler: OutputUpdateHandler;318} {319  // Track previous AgentResultDisplay states per tool call to detect changes320  const previousTaskStates = new Map<string, AgentResultDisplay>();321  // Track which tool call IDs have already emitted tool_use to prevent duplicates322  const emittedToolUseIds = new Set<string>();323  // Track which tool call IDs have already emitted tool_result to prevent duplicates324  const emittedToolResultIds = new Set<string>();325 326  /**327   * Builds a ToolCallRequestInfo object from a tool call.328   *329   * @param toolCall - The tool call information330   * @returns ToolCallRequestInfo object331   */332  const buildRequest = (333    toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],334  ): ToolCallRequestInfo => ({335    callId: toolCall.callId,336    name: toolCall.name,337    args: toolCall.args || {},338    isClientInitiated: true,339    prompt_id: '',340    response_id: undefined,341  });342 343  /**344   * Builds a ToolCallResponseInfo object from a tool call.345   *346   * @param toolCall - The tool call information347   * @returns ToolCallResponseInfo object348   */349  const buildResponse = (350    toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],351  ): ToolCallResponseInfo => ({352    callId: toolCall.callId,353    error:354      toolCall.status === 'failed'355        ? new Error(toolCall.error || 'Tool execution failed')356        : undefined,357    errorType:358      toolCall.status === 'failed' ? ToolErrorType.EXECUTION_FAILED : undefined,359    resultDisplay: toolCall.resultDisplay,360    responseParts: toolCall.responseParts || [],361  });362 363  /**364   * Checks if a tool call has result content that should be emitted.365   *366   * @param toolCall - The tool call information367   * @returns True if the tool call has result content to emit368   */369  const hasResultContent = (370    toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],371  ): boolean => {372    // Check resultDisplay string373    if (374      typeof toolCall.resultDisplay === 'string' &&375      toolCall.resultDisplay.trim().length > 0376    ) {377      return true;378    }379 380    // Check responseParts - only check existence, don't parse for performance381    if (toolCall.responseParts && toolCall.responseParts.length > 0) {382      return true;383    }384 385    // Failed status should always emit result386    return toolCall.status === 'failed';387  };388 389  /**390   * Emits tool_use for a tool call if it hasn't been emitted yet.391   *392   * @param toolCall - The tool call information393   * @param fallbackStatus - Optional fallback status if toolCall.status should be overridden394   */395  const emitToolUseIfNeeded = (396    toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],397    fallbackStatus?: 'executing' | 'awaiting_approval',398  ): void => {399    if (emittedToolUseIds.has(toolCall.callId)) {400      return;401    }402 403    const toolCallToEmit: NonNullable<AgentResultDisplay['toolCalls']>[number] =404      fallbackStatus405        ? {406            ...toolCall,407            status: fallbackStatus,408          }409        : toolCall;410 411    if (412      toolCallToEmit.status === 'executing' ||413      toolCallToEmit.status === 'awaiting_approval'414    ) {415      if (adapter.processSubagentToolCall) {416        adapter.processSubagentToolCall(toolCallToEmit, agentToolCallId);417        emittedToolUseIds.add(toolCall.callId);418      }419    }420  };421 422  /**423   * Emits tool_result for a tool call if it hasn't been emitted yet and has content.424   *425   * @param toolCall - The tool call information426   */427  const emitToolResultIfNeeded = (428    toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],429  ): void => {430    if (emittedToolResultIds.has(toolCall.callId)) {431      return;432    }433 434    if (!hasResultContent(toolCall)) {435      return;436    }437 438    // Mark as emitted even if we skip, to prevent duplicate emits439    emittedToolResultIds.add(toolCall.callId);440 441    const request = buildRequest(toolCall);442    const response = buildResponse(toolCall);443    // For subagent tool results, we need to pass parentToolUseId444    // The adapter implementations accept an optional parentToolUseId parameter445    if (446      'emitToolResult' in adapter &&447      typeof adapter.emitToolResult === 'function'448    ) {449      adapter.emitToolResult(request, response, agentToolCallId);450    } else {451      adapter.emitToolResult(request, response);452    }453  };454 455  /**456   * Processes a tool call, ensuring tool_use and tool_result are emitted exactly once.457   *458   * @param toolCall - The tool call information459   * @param previousCall - The previous state of the tool call (if any)460   */461  const processToolCall = (462    toolCall: NonNullable<AgentResultDisplay['toolCalls']>[number],463    previousCall?: NonNullable<AgentResultDisplay['toolCalls']>[number],464  ): void => {465    const isCompleted =466      toolCall.status === 'success' || toolCall.status === 'failed';467    const isExecuting =468      toolCall.status === 'executing' ||469      toolCall.status === 'awaiting_approval';470    const wasExecuting =471      previousCall &&472      (previousCall.status === 'executing' ||473        previousCall.status === 'awaiting_approval');474 475    // Emit tool_use if needed476    if (isExecuting) {477      // Normal case: tool call is executing or awaiting approval478      emitToolUseIfNeeded(toolCall);479    } else if (isCompleted && !emittedToolUseIds.has(toolCall.callId)) {480      // Edge case: tool call appeared with result already (shouldn't happen normally,481      // but handle it gracefully by emitting tool_use with 'executing' status first)482      emitToolUseIfNeeded(toolCall, 'executing');483    } else if (wasExecuting && isCompleted) {484      // Status changed from executing to completed - ensure tool_use was emitted485      emitToolUseIfNeeded(toolCall, 'executing');486    }487 488    // Emit tool_result if tool call is completed489    if (isCompleted) {490      emitToolResultIfNeeded(toolCall);491    }492  };493 494  const outputUpdateHandler = (495    callId: string,496    outputChunk: ToolResultDisplay,497  ) => {498    // Only process AgentResultDisplay (Task tool updates)499    if (500      typeof outputChunk === 'object' &&501      outputChunk !== null &&502      'type' in outputChunk &&503      outputChunk.type === 'task_execution'504    ) {505      const taskDisplay = outputChunk as AgentResultDisplay;506      const previous = previousTaskStates.get(callId);507 508      // Only process if adapter supports subagent APIs509      if (510        !adapter.processSubagentToolCall ||511        !adapter.emitSubagentErrorResult512      ) {513        previousTaskStates.set(callId, taskDisplay);514        return;515      }516 517      if (taskDisplay.toolCalls) {518        if (!previous || !previous.toolCalls) {519          // First time seeing tool calls - process all initial ones520          for (const toolCall of taskDisplay.toolCalls) {521            processToolCall(toolCall);522          }523        } else {524          // Compare with previous state to find new/changed tool calls525          for (const toolCall of taskDisplay.toolCalls) {526            const previousCall = previous.toolCalls.find(527              (tc) => tc.callId === toolCall.callId,528            );529            processToolCall(toolCall, previousCall);530          }531        }532      }533 534      // Handle task-level errors (status: 'failed', 'cancelled')535      if (536        taskDisplay.status === 'failed' ||537        taskDisplay.status === 'cancelled'538      ) {539        const previousStatus = previous?.status;540        // Only emit error result if status changed to failed/cancelled541        if (542          previousStatus !== 'failed' &&543          previousStatus !== 'cancelled' &&544          previousStatus !== undefined545        ) {546          const errorMessage =547            taskDisplay.terminateReason ||548            (taskDisplay.status === 'cancelled'549              ? 'Task was cancelled'550              : 'Task execution failed');551          // Use subagent adapter's emitSubagentErrorResult method552          adapter.emitSubagentErrorResult(errorMessage, 0, agentToolCallId);553        }554      }555 556      // Handle subagent initial message (prompt) in non-interactive mode with json/stream-json output557      // Emit when this is the first update (previous is undefined) and task starts558      if (559        !previous &&560        taskDisplay.taskPrompt &&561        !config.isInteractive() &&562        (config.getOutputFormat() === OutputFormat.JSON ||563          config.getOutputFormat() === OutputFormat.STREAM_JSON)564      ) {565        // Emit the user message with the correct parent_tool_use_id566        adapter.emitUserMessage(567          [{ text: taskDisplay.taskPrompt }],568          agentToolCallId,569        );570      }571 572      // Update previous state573      previousTaskStates.set(callId, taskDisplay);574    }575  };576 577  // No longer need to attach adapter to handler - task.ts uses AgentResultDisplay.message instead578 579  return {580    handler: outputUpdateHandler,581  };582}583 584/**585 * Converts function response parts to a string representation.586 * Handles functionResponse parts specially by extracting their output content.587 *588 * @param parts - Array of Part objects to convert589 * @returns String representation of the parts590 */591export function functionResponsePartsToString(parts: Part[]): string {592  return parts593    .map((part) => {594      if ('functionResponse' in part) {595        const content = part.functionResponse?.response?.['output'] ?? '';596        return content;597      }598      return JSON.stringify(part);599    })600    .join('');601}602 603/**604 * Extracts content from a tool call response for inclusion in tool_result blocks.605 * Uses functionResponsePartsToString to properly handle functionResponse parts,606 * which correctly extracts output content from functionResponse objects rather607 * than simply concatenating text or JSON.stringify.608 *609 * @param response - Tool call response information610 * @returns String content for the tool_result block, or undefined if no content available611 */612export function toolResultContent(613  response: ToolCallResponseInfo,614): string | undefined {615  if (616    typeof response.resultDisplay === 'string' &&617    response.resultDisplay.trim().length > 0618  ) {619    return response.resultDisplay;620  }621  if (response.responseParts && response.responseParts.length > 0) {622    // Always use functionResponsePartsToString to properly handle623    // functionResponse parts that contain output content624    return functionResponsePartsToString(response.responseParts);625  }626  if (response.error) {627    return response.error.message;628  }629  return undefined;630}631 
basant307/AI_Governance_Project · CoolFace