basant307/AI_Governance_Project
045
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 