| import { feature } from 'bun:bundle' |
| import type { |
| ContentBlockParam, |
| ToolResultBlockParam, |
| ToolUseBlock, |
| } from '@anthropic-ai/sdk/resources/index.mjs' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from 'src/services/analytics/index.js' |
| import { |
| extractMcpToolDetails, |
| extractSkillName, |
| extractToolInputForTelemetry, |
| getFileExtensionForAnalytics, |
| getFileExtensionsFromBashCommand, |
| isToolDetailsLoggingEnabled, |
| mcpToolDetailsForAnalytics, |
| sanitizeToolNameForAnalytics, |
| } from 'src/services/analytics/metadata.js' |
| import { |
| addToToolDuration, |
| getCodeEditToolDecisionCounter, |
| getStatsStore, |
| } from '../../bootstrap/state.js' |
| import { |
| buildCodeEditToolAttributes, |
| isCodeEditingTool, |
| } from '../../hooks/toolPermission/permissionLogging.js' |
| import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' |
| import { |
| findToolByName, |
| type Tool, |
| type ToolProgress, |
| type ToolProgressData, |
| type ToolUseContext, |
| } from '../../Tool.js' |
| import type { BashToolInput } from '../../tools/BashTool/BashTool.js' |
| import { startSpeculativeClassifierCheck } from '../../tools/BashTool/bashPermissions.js' |
| import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' |
| import { FILE_EDIT_TOOL_NAME } from '../../tools/FileEditTool/constants.js' |
| import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js' |
| import { FILE_WRITE_TOOL_NAME } from '../../tools/FileWriteTool/prompt.js' |
| import { NOTEBOOK_EDIT_TOOL_NAME } from '../../tools/NotebookEditTool/constants.js' |
| import { POWERSHELL_TOOL_NAME } from '../../tools/PowerShellTool/toolName.js' |
| import { parseGitCommitId } from '../../tools/shared/gitOperationTracking.js' |
| import { |
| isDeferredTool, |
| TOOL_SEARCH_TOOL_NAME, |
| } from '../../tools/ToolSearchTool/prompt.js' |
| import { getAllBaseTools } from '../../tools.js' |
| import type { HookProgress } from '../../types/hooks.js' |
| import type { |
| AssistantMessage, |
| AttachmentMessage, |
| Message, |
| ProgressMessage, |
| StopHookInfo, |
| } from '../../types/message.js' |
| import { count } from '../../utils/array.js' |
| import { createAttachmentMessage } from '../../utils/attachments.js' |
| import { logForDebugging } from '../../utils/debug.js' |
| import { |
| AbortError, |
| errorMessage, |
| getErrnoCode, |
| ShellError, |
| TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } from '../../utils/errors.js' |
| import { executePermissionDeniedHooks } from '../../utils/hooks.js' |
| import { logError } from '../../utils/log.js' |
| import { |
| CANCEL_MESSAGE, |
| createProgressMessage, |
| createStopHookSummaryMessage, |
| createToolResultStopMessage, |
| createUserMessage, |
| withMemoryCorrectionHint, |
| } from '../../utils/messages.js' |
| import type { |
| PermissionDecisionReason, |
| PermissionResult, |
| } from '../../utils/permissions/PermissionResult.js' |
| import { |
| startSessionActivity, |
| stopSessionActivity, |
| } from '../../utils/sessionActivity.js' |
| import { jsonStringify } from '../../utils/slowOperations.js' |
| import { Stream } from '../../utils/stream.js' |
| import { logOTelEvent } from '../../utils/telemetry/events.js' |
| import { |
| addToolContentEvent, |
| endToolBlockedOnUserSpan, |
| endToolExecutionSpan, |
| endToolSpan, |
| isBetaTracingEnabled, |
| startToolBlockedOnUserSpan, |
| startToolExecutionSpan, |
| startToolSpan, |
| } from '../../utils/telemetry/sessionTracing.js' |
| import { |
| formatError, |
| formatZodValidationError, |
| } from '../../utils/toolErrors.js' |
| import { |
| processPreMappedToolResultBlock, |
| processToolResultBlock, |
| } from '../../utils/toolResultStorage.js' |
| import { |
| extractDiscoveredToolNames, |
| isToolSearchEnabledOptimistic, |
| isToolSearchToolAvailable, |
| } from '../../utils/toolSearch.js' |
| import { |
| McpAuthError, |
| McpToolCallError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } from '../mcp/client.js' |
| import { mcpInfoFromString } from '../mcp/mcpStringUtils.js' |
| import { normalizeNameForMCP } from '../mcp/normalization.js' |
| import type { MCPServerConnection } from '../mcp/types.js' |
| import { |
| getLoggingSafeMcpBaseUrl, |
| getMcpServerScopeFromToolName, |
| isMcpTool, |
| } from '../mcp/utils.js' |
| import { |
| resolveHookPermissionDecision, |
| runPostToolUseFailureHooks, |
| runPostToolUseHooks, |
| runPreToolUseHooks, |
| } from './toolHooks.js' |
|
|
| |
| export const HOOK_TIMING_DISPLAY_THRESHOLD_MS = 500 |
| |
| |
| const SLOW_PHASE_LOG_THRESHOLD_MS = 2000 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function classifyToolError(error: unknown): string { |
| if ( |
| error instanceof TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| ) { |
| return error.telemetryMessage.slice(0, 200) |
| } |
| if (error instanceof Error) { |
| |
| |
| const errnoCode = getErrnoCode(error) |
| if (typeof errnoCode === 'string') { |
| return `Error:${errnoCode}` |
| } |
| |
| |
| if (error.name && error.name !== 'Error' && error.name.length > 3) { |
| return error.name.slice(0, 60) |
| } |
| return 'Error' |
| } |
| return 'UnknownError' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function ruleSourceToOTelSource( |
| ruleSource: string, |
| behavior: 'allow' | 'deny', |
| ): string { |
| switch (ruleSource) { |
| case 'session': |
| return behavior === 'allow' ? 'user_temporary' : 'user_reject' |
| case 'localSettings': |
| case 'userSettings': |
| return behavior === 'allow' ? 'user_permanent' : 'user_reject' |
| default: |
| return 'config' |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function decisionReasonToOTelSource( |
| reason: PermissionDecisionReason | undefined, |
| behavior: 'allow' | 'deny', |
| ): string { |
| if (!reason) { |
| return 'config' |
| } |
| switch (reason.type) { |
| case 'permissionPromptTool': { |
| |
| |
| |
| const toolResult = reason.toolResult as |
| | { decisionClassification?: string } |
| | undefined |
| const classified = toolResult?.decisionClassification |
| if ( |
| classified === 'user_temporary' || |
| classified === 'user_permanent' || |
| classified === 'user_reject' |
| ) { |
| return classified |
| } |
| return behavior === 'allow' ? 'user_temporary' : 'user_reject' |
| } |
| case 'rule': |
| return ruleSourceToOTelSource(reason.rule.source, behavior) |
| case 'hook': |
| return 'hook' |
| case 'mode': |
| case 'classifier': |
| case 'subcommandResults': |
| case 'asyncAgent': |
| case 'sandboxOverride': |
| case 'workingDir': |
| case 'safetyCheck': |
| case 'other': |
| return 'config' |
| default: { |
| const _exhaustive: never = reason |
| return 'config' |
| } |
| } |
| } |
|
|
| function getNextImagePasteId(messages: Message[]): number { |
| let maxId = 0 |
| for (const message of messages) { |
| if (message.type === 'user' && message.imagePasteIds) { |
| for (const id of message.imagePasteIds) { |
| if (id > maxId) maxId = id |
| } |
| } |
| } |
| return maxId + 1 |
| } |
|
|
| export type MessageUpdateLazy<M extends Message = Message> = { |
| message: M |
| contextModifier?: { |
| toolUseID: string |
| modifyContext: (context: ToolUseContext) => ToolUseContext |
| } |
| } |
|
|
| export type McpServerType = |
| | 'stdio' |
| | 'sse' |
| | 'http' |
| | 'ws' |
| | 'sdk' |
| | 'sse-ide' |
| | 'ws-ide' |
| | 'claudeai-proxy' |
| | undefined |
|
|
| function findMcpServerConnection( |
| toolName: string, |
| mcpClients: MCPServerConnection[], |
| ): MCPServerConnection | undefined { |
| if (!toolName.startsWith('mcp__')) { |
| return undefined |
| } |
|
|
| const mcpInfo = mcpInfoFromString(toolName) |
| if (!mcpInfo) { |
| return undefined |
| } |
|
|
| |
| |
| return mcpClients.find( |
| client => normalizeNameForMCP(client.name) === mcpInfo.serverName, |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| function getMcpServerType( |
| toolName: string, |
| mcpClients: MCPServerConnection[], |
| ): McpServerType { |
| const serverConnection = findMcpServerConnection(toolName, mcpClients) |
|
|
| if (serverConnection?.type === 'connected') { |
| |
| return serverConnection.config.type ?? 'stdio' |
| } |
|
|
| return undefined |
| } |
|
|
| |
| |
| |
| |
| function getMcpServerBaseUrlFromToolName( |
| toolName: string, |
| mcpClients: MCPServerConnection[], |
| ): string | undefined { |
| const serverConnection = findMcpServerConnection(toolName, mcpClients) |
| if (serverConnection?.type !== 'connected') { |
| return undefined |
| } |
| return getLoggingSafeMcpBaseUrl(serverConnection.config) |
| } |
|
|
| export async function* runToolUse( |
| toolUse: ToolUseBlock, |
| assistantMessage: AssistantMessage, |
| canUseTool: CanUseToolFn, |
| toolUseContext: ToolUseContext, |
| ): AsyncGenerator<MessageUpdateLazy, void> { |
| const toolName = toolUse.name |
| |
| let tool = findToolByName(toolUseContext.options.tools, toolName) |
|
|
| |
| |
| |
| if (!tool) { |
| const fallbackTool = findToolByName(getAllBaseTools(), toolName) |
| |
| if (fallbackTool && fallbackTool.aliases?.includes(toolName)) { |
| tool = fallbackTool |
| } |
| } |
| const messageId = assistantMessage.message.id |
| const requestId = assistantMessage.requestId |
| const mcpServerType = getMcpServerType( |
| toolName, |
| toolUseContext.options.mcpClients, |
| ) |
| const mcpServerBaseUrl = getMcpServerBaseUrlFromToolName( |
| toolName, |
| toolUseContext.options.mcpClients, |
| ) |
|
|
| |
| if (!tool) { |
| const sanitizedToolName = sanitizeToolNameForAnalytics(toolName) |
| logForDebugging(`Unknown tool ${toolName}: ${toolUse.id}`) |
| logEvent('tengu_tool_use_error', { |
| error: |
| `No such tool available: ${sanitizedToolName}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizedToolName, |
| toolUseID: |
| toolUse.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| isMcp: toolName.startsWith('mcp__'), |
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics(toolName, mcpServerType, mcpServerBaseUrl), |
| }) |
| yield { |
| message: createUserMessage({ |
| content: [ |
| { |
| type: 'tool_result', |
| content: `<tool_use_error>Error: No such tool available: ${toolName}</tool_use_error>`, |
| is_error: true, |
| tool_use_id: toolUse.id, |
| }, |
| ], |
| toolUseResult: `Error: No such tool available: ${toolName}`, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| } |
| return |
| } |
|
|
| const toolInput = toolUse.input as { [key: string]: string } |
| try { |
| if (toolUseContext.abortController.signal.aborted) { |
| logEvent('tengu_tool_use_cancelled', { |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| toolUseID: |
| toolUse.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| isMcp: tool.isMcp ?? false, |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics( |
| tool.name, |
| mcpServerType, |
| mcpServerBaseUrl, |
| ), |
| }) |
| const content = createToolResultStopMessage(toolUse.id) |
| content.content = withMemoryCorrectionHint(CANCEL_MESSAGE) |
| yield { |
| message: createUserMessage({ |
| content: [content], |
| toolUseResult: CANCEL_MESSAGE, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| } |
| return |
| } |
|
|
| for await (const update of streamedCheckPermissionsAndCallTool( |
| tool, |
| toolUse.id, |
| toolInput, |
| toolUseContext, |
| canUseTool, |
| assistantMessage, |
| messageId, |
| requestId, |
| mcpServerType, |
| mcpServerBaseUrl, |
| )) { |
| yield update |
| } |
| } catch (error) { |
| logError(error) |
| const errorMessage = error instanceof Error ? error.message : String(error) |
| const toolInfo = tool ? ` (${tool.name})` : '' |
| const detailedError = `Error calling tool${toolInfo}: ${errorMessage}` |
|
|
| yield { |
| message: createUserMessage({ |
| content: [ |
| { |
| type: 'tool_result', |
| content: `<tool_use_error>${detailedError}</tool_use_error>`, |
| is_error: true, |
| tool_use_id: toolUse.id, |
| }, |
| ], |
| toolUseResult: detailedError, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| } |
| } |
| } |
|
|
| function streamedCheckPermissionsAndCallTool( |
| tool: Tool, |
| toolUseID: string, |
| input: { [key: string]: boolean | string | number }, |
| toolUseContext: ToolUseContext, |
| canUseTool: CanUseToolFn, |
| assistantMessage: AssistantMessage, |
| messageId: string, |
| requestId: string | undefined, |
| mcpServerType: McpServerType, |
| mcpServerBaseUrl: ReturnType<typeof getLoggingSafeMcpBaseUrl>, |
| ): AsyncIterable<MessageUpdateLazy> { |
| |
| |
| |
| |
| |
| const stream = new Stream<MessageUpdateLazy>() |
| checkPermissionsAndCallTool( |
| tool, |
| toolUseID, |
| input, |
| toolUseContext, |
| canUseTool, |
| assistantMessage, |
| messageId, |
| requestId, |
| mcpServerType, |
| mcpServerBaseUrl, |
| progress => { |
| logEvent('tengu_tool_use_progress', { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| isMcp: tool.isMcp ?? false, |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics( |
| tool.name, |
| mcpServerType, |
| mcpServerBaseUrl, |
| ), |
| }) |
| stream.enqueue({ |
| message: createProgressMessage({ |
| toolUseID: progress.toolUseID, |
| parentToolUseID: toolUseID, |
| data: progress.data, |
| }), |
| }) |
| }, |
| ) |
| .then(results => { |
| for (const result of results) { |
| stream.enqueue(result) |
| } |
| }) |
| .catch(error => { |
| stream.error(error) |
| }) |
| .finally(() => { |
| stream.done() |
| }) |
| return stream |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function buildSchemaNotSentHint( |
| tool: Tool, |
| messages: Message[], |
| tools: readonly { name: string }[], |
| ): string | null { |
| |
| |
| |
| |
| if (!isToolSearchEnabledOptimistic()) return null |
| if (!isToolSearchToolAvailable(tools)) return null |
| if (!isDeferredTool(tool)) return null |
| const discovered = extractDiscoveredToolNames(messages) |
| if (discovered.has(tool.name)) return null |
| return ( |
| `\n\nThis tool's schema was not sent to the API — it was not in the discovered-tool set derived from message history. ` + |
| `Without the schema in your prompt, typed parameters (arrays, numbers, booleans) get emitted as strings and the client-side parser rejects them. ` + |
| `Load the tool first: call ${TOOL_SEARCH_TOOL_NAME} with query "select:${tool.name}", then retry this call.` |
| ) |
| } |
|
|
| async function checkPermissionsAndCallTool( |
| tool: Tool, |
| toolUseID: string, |
| input: { [key: string]: boolean | string | number }, |
| toolUseContext: ToolUseContext, |
| canUseTool: CanUseToolFn, |
| assistantMessage: AssistantMessage, |
| messageId: string, |
| requestId: string | undefined, |
| mcpServerType: McpServerType, |
| mcpServerBaseUrl: ReturnType<typeof getLoggingSafeMcpBaseUrl>, |
| onToolProgress: ( |
| progress: ToolProgress<ToolProgressData> | ProgressMessage<HookProgress>, |
| ) => void, |
| ): Promise<MessageUpdateLazy[]> { |
| |
| const parsedInput = tool.inputSchema.safeParse(input) |
| if (!parsedInput.success) { |
| let errorContent = formatZodValidationError(tool.name, parsedInput.error) |
|
|
| const schemaHint = buildSchemaNotSentHint( |
| tool, |
| toolUseContext.messages, |
| toolUseContext.options.tools, |
| ) |
| if (schemaHint) { |
| logEvent('tengu_deferred_tool_schema_not_sent', { |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| isMcp: tool.isMcp ?? false, |
| }) |
| errorContent += schemaHint |
| } |
|
|
| logForDebugging( |
| `${tool.name} tool input error: ${errorContent.slice(0, 200)}`, |
| ) |
| logEvent('tengu_tool_use_error', { |
| error: |
| 'InputValidationError' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| errorDetails: errorContent.slice( |
| 0, |
| 2000, |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| isMcp: tool.isMcp ?? false, |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics(tool.name, mcpServerType, mcpServerBaseUrl), |
| }) |
| return [ |
| { |
| message: createUserMessage({ |
| content: [ |
| { |
| type: 'tool_result', |
| content: `<tool_use_error>InputValidationError: ${errorContent}</tool_use_error>`, |
| is_error: true, |
| tool_use_id: toolUseID, |
| }, |
| ], |
| toolUseResult: `InputValidationError: ${parsedInput.error.message}`, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| }, |
| ] |
| } |
|
|
| |
| const isValidCall = await tool.validateInput?.( |
| parsedInput.data, |
| toolUseContext, |
| ) |
| if (isValidCall?.result === false) { |
| logForDebugging( |
| `${tool.name} tool validation error: ${isValidCall.message?.slice(0, 200)}`, |
| ) |
| logEvent('tengu_tool_use_error', { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| error: |
| isValidCall.message as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| errorCode: isValidCall.errorCode, |
| isMcp: tool.isMcp ?? false, |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics(tool.name, mcpServerType, mcpServerBaseUrl), |
| }) |
| return [ |
| { |
| message: createUserMessage({ |
| content: [ |
| { |
| type: 'tool_result', |
| content: `<tool_use_error>${isValidCall.message}</tool_use_error>`, |
| is_error: true, |
| tool_use_id: toolUseID, |
| }, |
| ], |
| toolUseResult: `Error: ${isValidCall.message}`, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| }, |
| ] |
| } |
| |
| |
| |
| |
| |
| |
| if ( |
| tool.name === BASH_TOOL_NAME && |
| parsedInput.data && |
| 'command' in parsedInput.data |
| ) { |
| const appState = toolUseContext.getAppState() |
| startSpeculativeClassifierCheck( |
| (parsedInput.data as BashToolInput).command, |
| appState.toolPermissionContext, |
| toolUseContext.abortController.signal, |
| toolUseContext.options.isNonInteractiveSession, |
| ) |
| } |
|
|
| const resultingMessages = [] |
|
|
| |
| |
| |
| |
| |
| let processedInput = parsedInput.data |
| if ( |
| tool.name === BASH_TOOL_NAME && |
| processedInput && |
| typeof processedInput === 'object' && |
| '_simulatedSedEdit' in processedInput |
| ) { |
| const { _simulatedSedEdit: _, ...rest } = |
| processedInput as typeof processedInput & { |
| _simulatedSedEdit: unknown |
| } |
| processedInput = rest as typeof processedInput |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| let callInput = processedInput |
| const backfilledClone = |
| tool.backfillObservableInput && |
| typeof processedInput === 'object' && |
| processedInput !== null |
| ? ({ ...processedInput } as typeof processedInput) |
| : null |
| if (backfilledClone) { |
| tool.backfillObservableInput!(backfilledClone as Record<string, unknown>) |
| processedInput = backfilledClone |
| } |
|
|
| let shouldPreventContinuation = false |
| let stopReason: string | undefined |
| let hookPermissionResult: PermissionResult | undefined |
| const preToolHookInfos: StopHookInfo[] = [] |
| const preToolHookStart = Date.now() |
| for await (const result of runPreToolUseHooks( |
| toolUseContext, |
| tool, |
| processedInput, |
| toolUseID, |
| assistantMessage.message.id, |
| requestId, |
| mcpServerType, |
| mcpServerBaseUrl, |
| )) { |
| switch (result.type) { |
| case 'message': |
| if (result.message.message.type === 'progress') { |
| onToolProgress(result.message.message) |
| } else { |
| resultingMessages.push(result.message) |
| const att = result.message.message.attachment |
| if ( |
| att && |
| 'command' in att && |
| att.command !== undefined && |
| 'durationMs' in att && |
| att.durationMs !== undefined |
| ) { |
| preToolHookInfos.push({ |
| command: att.command, |
| durationMs: att.durationMs, |
| }) |
| } |
| } |
| break |
| case 'hookPermissionResult': |
| hookPermissionResult = result.hookPermissionResult |
| break |
| case 'hookUpdatedInput': |
| |
| |
| processedInput = result.updatedInput |
| break |
| case 'preventContinuation': |
| shouldPreventContinuation = result.shouldPreventContinuation |
| break |
| case 'stopReason': |
| stopReason = result.stopReason |
| break |
| case 'additionalContext': |
| resultingMessages.push(result.message) |
| break |
| case 'stop': |
| getStatsStore()?.observe( |
| 'pre_tool_hook_duration_ms', |
| Date.now() - preToolHookStart, |
| ) |
| resultingMessages.push({ |
| message: createUserMessage({ |
| content: [createToolResultStopMessage(toolUseID)], |
| toolUseResult: `Error: ${stopReason}`, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| }) |
| return resultingMessages |
| } |
| } |
| const preToolHookDurationMs = Date.now() - preToolHookStart |
| getStatsStore()?.observe('pre_tool_hook_duration_ms', preToolHookDurationMs) |
| if (preToolHookDurationMs >= SLOW_PHASE_LOG_THRESHOLD_MS) { |
| logForDebugging( |
| `Slow PreToolUse hooks: ${preToolHookDurationMs}ms for ${tool.name} (${preToolHookInfos.length} hooks)`, |
| { level: 'info' }, |
| ) |
| } |
|
|
| |
| |
| if (process.env.USER_TYPE === 'ant' && preToolHookInfos.length > 0) { |
| if (preToolHookDurationMs > HOOK_TIMING_DISPLAY_THRESHOLD_MS) { |
| resultingMessages.push({ |
| message: createStopHookSummaryMessage( |
| preToolHookInfos.length, |
| preToolHookInfos, |
| [], |
| false, |
| undefined, |
| false, |
| 'suggestion', |
| undefined, |
| 'PreToolUse', |
| preToolHookDurationMs, |
| ), |
| }) |
| } |
| } |
|
|
| const toolAttributes: Record<string, string | number | boolean> = {} |
| if (processedInput && typeof processedInput === 'object') { |
| if (tool.name === FILE_READ_TOOL_NAME && 'file_path' in processedInput) { |
| toolAttributes.file_path = String(processedInput.file_path) |
| } else if ( |
| (tool.name === FILE_EDIT_TOOL_NAME || |
| tool.name === FILE_WRITE_TOOL_NAME) && |
| 'file_path' in processedInput |
| ) { |
| toolAttributes.file_path = String(processedInput.file_path) |
| } else if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) { |
| const bashInput = processedInput as BashToolInput |
| toolAttributes.full_command = bashInput.command |
| } |
| } |
|
|
| startToolSpan( |
| tool.name, |
| toolAttributes, |
| isBetaTracingEnabled() ? jsonStringify(processedInput) : undefined, |
| ) |
| startToolBlockedOnUserSpan() |
|
|
| |
| |
| const permissionMode = toolUseContext.getAppState().toolPermissionContext.mode |
| const permissionStart = Date.now() |
|
|
| const resolved = await resolveHookPermissionDecision( |
| hookPermissionResult, |
| tool, |
| processedInput, |
| toolUseContext, |
| canUseTool, |
| assistantMessage, |
| toolUseID, |
| ) |
| const permissionDecision = resolved.decision |
| processedInput = resolved.input |
| const permissionDurationMs = Date.now() - permissionStart |
| |
| |
| |
| |
| if ( |
| permissionDurationMs >= SLOW_PHASE_LOG_THRESHOLD_MS && |
| permissionMode === 'auto' |
| ) { |
| logForDebugging( |
| `Slow permission decision: ${permissionDurationMs}ms for ${tool.name} ` + |
| `(mode=${permissionMode}, behavior=${permissionDecision.behavior})`, |
| { level: 'info' }, |
| ) |
| } |
|
|
| |
| |
| |
| |
| if ( |
| permissionDecision.behavior !== 'ask' && |
| !toolUseContext.toolDecisions?.has(toolUseID) |
| ) { |
| const decision = |
| permissionDecision.behavior === 'allow' ? 'accept' : 'reject' |
| const source = decisionReasonToOTelSource( |
| permissionDecision.decisionReason, |
| permissionDecision.behavior, |
| ) |
| void logOTelEvent('tool_decision', { |
| decision, |
| source, |
| tool_name: sanitizeToolNameForAnalytics(tool.name), |
| }) |
|
|
| |
| if (isCodeEditingTool(tool.name)) { |
| void buildCodeEditToolAttributes( |
| tool, |
| processedInput, |
| decision, |
| source, |
| ).then(attributes => getCodeEditToolDecisionCounter()?.add(1, attributes)) |
| } |
| } |
|
|
| |
| if ( |
| permissionDecision.decisionReason?.type === 'hook' && |
| permissionDecision.decisionReason.hookName === 'PermissionRequest' && |
| permissionDecision.behavior !== 'ask' |
| ) { |
| resultingMessages.push({ |
| message: createAttachmentMessage({ |
| type: 'hook_permission_decision', |
| decision: permissionDecision.behavior, |
| toolUseID, |
| hookEvent: 'PermissionRequest', |
| }), |
| }) |
| } |
|
|
| if (permissionDecision.behavior !== 'allow') { |
| logForDebugging(`${tool.name} tool permission denied`) |
| const decisionInfo = toolUseContext.toolDecisions?.get(toolUseID) |
| endToolBlockedOnUserSpan('reject', decisionInfo?.source || 'unknown') |
| endToolSpan() |
|
|
| logEvent('tengu_tool_use_can_use_tool_rejected', { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics(tool.name, mcpServerType, mcpServerBaseUrl), |
| }) |
| let errorMessage = permissionDecision.message |
| |
| if (shouldPreventContinuation && !errorMessage) { |
| errorMessage = `Execution stopped by PreToolUse hook${stopReason ? `: ${stopReason}` : ''}` |
| } |
|
|
| |
| const messageContent: ContentBlockParam[] = [ |
| { |
| type: 'tool_result', |
| content: errorMessage, |
| is_error: true, |
| tool_use_id: toolUseID, |
| }, |
| ] |
|
|
| |
| const rejectContentBlocks = |
| permissionDecision.behavior === 'ask' |
| ? permissionDecision.contentBlocks |
| : undefined |
| if (rejectContentBlocks?.length) { |
| messageContent.push(...rejectContentBlocks) |
| } |
|
|
| |
| let rejectImageIds: number[] | undefined |
| if (rejectContentBlocks?.length) { |
| const imageCount = count( |
| rejectContentBlocks, |
| (b: ContentBlockParam) => b.type === 'image', |
| ) |
| if (imageCount > 0) { |
| const startId = getNextImagePasteId(toolUseContext.messages) |
| rejectImageIds = Array.from( |
| { length: imageCount }, |
| (_, i) => startId + i, |
| ) |
| } |
| } |
|
|
| resultingMessages.push({ |
| message: createUserMessage({ |
| content: messageContent, |
| imagePasteIds: rejectImageIds, |
| toolUseResult: `Error: ${errorMessage}`, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| }) |
|
|
| |
| |
| if ( |
| feature('TRANSCRIPT_CLASSIFIER') && |
| permissionDecision.decisionReason?.type === 'classifier' && |
| permissionDecision.decisionReason.classifier === 'auto-mode' |
| ) { |
| let hookSaysRetry = false |
| for await (const result of executePermissionDeniedHooks( |
| tool.name, |
| toolUseID, |
| processedInput, |
| permissionDecision.decisionReason.reason ?? 'Permission denied', |
| toolUseContext, |
| permissionMode, |
| toolUseContext.abortController.signal, |
| )) { |
| if (result.retry) hookSaysRetry = true |
| } |
| if (hookSaysRetry) { |
| resultingMessages.push({ |
| message: createUserMessage({ |
| content: |
| 'The PermissionDenied hook indicated this command is now approved. You may retry it if you would like.', |
| isMeta: true, |
| }), |
| }) |
| } |
| } |
|
|
| return resultingMessages |
| } |
| logEvent('tengu_tool_use_can_use_tool_allowed', { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics(tool.name, mcpServerType, mcpServerBaseUrl), |
| }) |
|
|
| |
| |
| if (permissionDecision.updatedInput !== undefined) { |
| processedInput = permissionDecision.updatedInput |
| } |
|
|
| |
| |
| |
| const telemetryToolInput = extractToolInputForTelemetry(processedInput) |
| let toolParameters: Record<string, unknown> = {} |
| if (isToolDetailsLoggingEnabled()) { |
| if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) { |
| const bashInput = processedInput as BashToolInput |
| const commandParts = bashInput.command.trim().split(/\s+/) |
| const bashCommand = commandParts[0] || '' |
|
|
| toolParameters = { |
| bash_command: bashCommand, |
| full_command: bashInput.command, |
| ...(bashInput.timeout !== undefined && { |
| timeout: bashInput.timeout, |
| }), |
| ...(bashInput.description !== undefined && { |
| description: bashInput.description, |
| }), |
| ...('dangerouslyDisableSandbox' in bashInput && { |
| dangerouslyDisableSandbox: bashInput.dangerouslyDisableSandbox, |
| }), |
| } |
| } |
|
|
| const mcpDetails = extractMcpToolDetails(tool.name) |
| if (mcpDetails) { |
| toolParameters.mcp_server_name = mcpDetails.serverName |
| toolParameters.mcp_tool_name = mcpDetails.mcpToolName |
| } |
| const skillName = extractSkillName(tool.name, processedInput) |
| if (skillName) { |
| toolParameters.skill_name = skillName |
| } |
| } |
|
|
| const decisionInfo = toolUseContext.toolDecisions?.get(toolUseID) |
| endToolBlockedOnUserSpan( |
| decisionInfo?.decision || 'unknown', |
| decisionInfo?.source || 'unknown', |
| ) |
| startToolExecutionSpan() |
|
|
| const startTime = Date.now() |
|
|
| startSessionActivity('tool_exec') |
| |
| |
| |
| |
| |
| |
| |
| |
| if ( |
| backfilledClone && |
| processedInput !== callInput && |
| typeof processedInput === 'object' && |
| processedInput !== null && |
| 'file_path' in processedInput && |
| 'file_path' in (callInput as Record<string, unknown>) && |
| (processedInput as Record<string, unknown>).file_path === |
| (backfilledClone as Record<string, unknown>).file_path |
| ) { |
| callInput = { |
| ...processedInput, |
| file_path: (callInput as Record<string, unknown>).file_path, |
| } as typeof processedInput |
| } else if (processedInput !== backfilledClone) { |
| callInput = processedInput |
| } |
| try { |
| const result = await tool.call( |
| callInput, |
| { |
| ...toolUseContext, |
| toolUseId: toolUseID, |
| userModified: permissionDecision.userModified ?? false, |
| }, |
| canUseTool, |
| assistantMessage, |
| progress => { |
| onToolProgress({ |
| toolUseID: progress.toolUseID, |
| data: progress.data, |
| }) |
| }, |
| ) |
| const durationMs = Date.now() - startTime |
| addToToolDuration(durationMs) |
|
|
| |
| if (result.data && typeof result.data === 'object') { |
| const contentAttributes: Record<string, string | number | boolean> = {} |
|
|
| |
| if (tool.name === FILE_READ_TOOL_NAME && 'content' in result.data) { |
| if ('file_path' in processedInput) { |
| contentAttributes.file_path = String(processedInput.file_path) |
| } |
| contentAttributes.content = String(result.data.content) |
| } |
|
|
| |
| if ( |
| (tool.name === FILE_EDIT_TOOL_NAME || |
| tool.name === FILE_WRITE_TOOL_NAME) && |
| 'file_path' in processedInput |
| ) { |
| contentAttributes.file_path = String(processedInput.file_path) |
|
|
| |
| if (tool.name === FILE_EDIT_TOOL_NAME && 'diff' in result.data) { |
| contentAttributes.diff = String(result.data.diff) |
| } |
| |
| if (tool.name === FILE_WRITE_TOOL_NAME && 'content' in processedInput) { |
| contentAttributes.content = String(processedInput.content) |
| } |
| } |
|
|
| |
| if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) { |
| const bashInput = processedInput as BashToolInput |
| contentAttributes.bash_command = bashInput.command |
| |
| if ('output' in result.data) { |
| contentAttributes.output = String(result.data.output) |
| } |
| } |
|
|
| if (Object.keys(contentAttributes).length > 0) { |
| addToolContentEvent('tool.output', contentAttributes) |
| } |
| } |
|
|
| |
| if (typeof result === 'object' && 'structured_output' in result) { |
| |
| resultingMessages.push({ |
| message: createAttachmentMessage({ |
| type: 'structured_output', |
| data: result.structured_output, |
| }), |
| }) |
| } |
|
|
| endToolExecutionSpan({ success: true }) |
| |
| const toolResultStr = |
| result.data && typeof result.data === 'object' |
| ? jsonStringify(result.data) |
| : String(result.data ?? '') |
| endToolSpan(toolResultStr) |
|
|
| |
| |
| const mappedToolResultBlock = tool.mapToolResultToToolResultBlockParam( |
| result.data, |
| toolUseID, |
| ) |
| const mappedContent = mappedToolResultBlock.content |
| const toolResultSizeBytes = !mappedContent |
| ? 0 |
| : typeof mappedContent === 'string' |
| ? mappedContent.length |
| : jsonStringify(mappedContent).length |
|
|
| |
| let fileExtension: ReturnType<typeof getFileExtensionForAnalytics> |
| if (processedInput && typeof processedInput === 'object') { |
| if ( |
| (tool.name === FILE_READ_TOOL_NAME || |
| tool.name === FILE_EDIT_TOOL_NAME || |
| tool.name === FILE_WRITE_TOOL_NAME) && |
| 'file_path' in processedInput |
| ) { |
| fileExtension = getFileExtensionForAnalytics( |
| String(processedInput.file_path), |
| ) |
| } else if ( |
| tool.name === NOTEBOOK_EDIT_TOOL_NAME && |
| 'notebook_path' in processedInput |
| ) { |
| fileExtension = getFileExtensionForAnalytics( |
| String(processedInput.notebook_path), |
| ) |
| } else if (tool.name === BASH_TOOL_NAME && 'command' in processedInput) { |
| const bashInput = processedInput as BashToolInput |
| fileExtension = getFileExtensionsFromBashCommand( |
| bashInput.command, |
| bashInput._simulatedSedEdit?.filePath, |
| ) |
| } |
| } |
|
|
| logEvent('tengu_tool_use_success', { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| isMcp: tool.isMcp ?? false, |
| durationMs, |
| preToolHookDurationMs, |
| toolResultSizeBytes, |
| ...(fileExtension !== undefined && { fileExtension }), |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics(tool.name, mcpServerType, mcpServerBaseUrl), |
| }) |
|
|
| |
| if ( |
| isToolDetailsLoggingEnabled() && |
| (tool.name === BASH_TOOL_NAME || tool.name === POWERSHELL_TOOL_NAME) && |
| 'command' in processedInput && |
| typeof processedInput.command === 'string' && |
| processedInput.command.match(/\bgit\s+commit\b/) && |
| result.data && |
| typeof result.data === 'object' && |
| 'stdout' in result.data |
| ) { |
| const gitCommitId = parseGitCommitId(String(result.data.stdout)) |
| if (gitCommitId) { |
| toolParameters.git_commit_id = gitCommitId |
| } |
| } |
|
|
| |
| const mcpServerScope = isMcpTool(tool) |
| ? getMcpServerScopeFromToolName(tool.name) |
| : null |
|
|
| void logOTelEvent('tool_result', { |
| tool_name: sanitizeToolNameForAnalytics(tool.name), |
| success: 'true', |
| duration_ms: String(durationMs), |
| ...(Object.keys(toolParameters).length > 0 && { |
| tool_parameters: jsonStringify(toolParameters), |
| }), |
| ...(telemetryToolInput && { tool_input: telemetryToolInput }), |
| tool_result_size_bytes: String(toolResultSizeBytes), |
| ...(decisionInfo && { |
| decision_source: decisionInfo.source, |
| decision_type: decisionInfo.decision, |
| }), |
| ...(mcpServerScope && { mcp_server_scope: mcpServerScope }), |
| }) |
|
|
| |
| let toolOutput = result.data |
| const hookResults = [] |
| const toolContextModifier = result.contextModifier |
| const mcpMeta = result.mcpMeta |
|
|
| async function addToolResult( |
| toolUseResult: unknown, |
| preMappedBlock?: ToolResultBlockParam, |
| ) { |
| |
| |
| const toolResultBlock = preMappedBlock |
| ? await processPreMappedToolResultBlock( |
| preMappedBlock, |
| tool.name, |
| tool.maxResultSizeChars, |
| ) |
| : await processToolResultBlock(tool, toolUseResult, toolUseID) |
|
|
| |
| const contentBlocks: ContentBlockParam[] = [toolResultBlock] |
| |
| |
| if ( |
| 'acceptFeedback' in permissionDecision && |
| permissionDecision.acceptFeedback |
| ) { |
| contentBlocks.push({ |
| type: 'text', |
| text: permissionDecision.acceptFeedback, |
| }) |
| } |
|
|
| |
| const allowContentBlocks = |
| 'contentBlocks' in permissionDecision |
| ? permissionDecision.contentBlocks |
| : undefined |
| if (allowContentBlocks?.length) { |
| contentBlocks.push(...allowContentBlocks) |
| } |
|
|
| |
| let allowImageIds: number[] | undefined |
| if (allowContentBlocks?.length) { |
| const imageCount = count( |
| allowContentBlocks, |
| (b: ContentBlockParam) => b.type === 'image', |
| ) |
| if (imageCount > 0) { |
| const startId = getNextImagePasteId(toolUseContext.messages) |
| allowImageIds = Array.from( |
| { length: imageCount }, |
| (_, i) => startId + i, |
| ) |
| } |
| } |
|
|
| resultingMessages.push({ |
| message: createUserMessage({ |
| content: contentBlocks, |
| imagePasteIds: allowImageIds, |
| toolUseResult: |
| toolUseContext.agentId && !toolUseContext.preserveToolUseResults |
| ? undefined |
| : toolUseResult, |
| mcpMeta: toolUseContext.agentId ? undefined : mcpMeta, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| contextModifier: toolContextModifier |
| ? { |
| toolUseID: toolUseID, |
| modifyContext: toolContextModifier, |
| } |
| : undefined, |
| }) |
| } |
|
|
| |
| if (!isMcpTool(tool)) { |
| await addToolResult(toolOutput, mappedToolResultBlock) |
| } |
|
|
| const postToolHookInfos: StopHookInfo[] = [] |
| const postToolHookStart = Date.now() |
| for await (const hookResult of runPostToolUseHooks( |
| toolUseContext, |
| tool, |
| toolUseID, |
| assistantMessage.message.id, |
| processedInput, |
| toolOutput, |
| requestId, |
| mcpServerType, |
| mcpServerBaseUrl, |
| )) { |
| if ('updatedMCPToolOutput' in hookResult) { |
| if (isMcpTool(tool)) { |
| toolOutput = hookResult.updatedMCPToolOutput |
| } |
| } else if (isMcpTool(tool)) { |
| hookResults.push(hookResult) |
| if (hookResult.message.type === 'attachment') { |
| const att = hookResult.message.attachment |
| if ( |
| 'command' in att && |
| att.command !== undefined && |
| 'durationMs' in att && |
| att.durationMs !== undefined |
| ) { |
| postToolHookInfos.push({ |
| command: att.command, |
| durationMs: att.durationMs, |
| }) |
| } |
| } |
| } else { |
| resultingMessages.push(hookResult) |
| if (hookResult.message.type === 'attachment') { |
| const att = hookResult.message.attachment |
| if ( |
| 'command' in att && |
| att.command !== undefined && |
| 'durationMs' in att && |
| att.durationMs !== undefined |
| ) { |
| postToolHookInfos.push({ |
| command: att.command, |
| durationMs: att.durationMs, |
| }) |
| } |
| } |
| } |
| } |
| const postToolHookDurationMs = Date.now() - postToolHookStart |
| if (postToolHookDurationMs >= SLOW_PHASE_LOG_THRESHOLD_MS) { |
| logForDebugging( |
| `Slow PostToolUse hooks: ${postToolHookDurationMs}ms for ${tool.name} (${postToolHookInfos.length} hooks)`, |
| { level: 'info' }, |
| ) |
| } |
|
|
| if (isMcpTool(tool)) { |
| await addToolResult(toolOutput) |
| } |
|
|
| |
| |
| if (process.env.USER_TYPE === 'ant' && postToolHookInfos.length > 0) { |
| if (postToolHookDurationMs > HOOK_TIMING_DISPLAY_THRESHOLD_MS) { |
| resultingMessages.push({ |
| message: createStopHookSummaryMessage( |
| postToolHookInfos.length, |
| postToolHookInfos, |
| [], |
| false, |
| undefined, |
| false, |
| 'suggestion', |
| undefined, |
| 'PostToolUse', |
| postToolHookDurationMs, |
| ), |
| }) |
| } |
| } |
|
|
| |
| if (result.newMessages && result.newMessages.length > 0) { |
| for (const message of result.newMessages) { |
| resultingMessages.push({ message }) |
| } |
| } |
| |
| if (shouldPreventContinuation) { |
| resultingMessages.push({ |
| message: createAttachmentMessage({ |
| type: 'hook_stopped_continuation', |
| message: stopReason || 'Execution stopped by hook', |
| hookName: `PreToolUse:${tool.name}`, |
| toolUseID: toolUseID, |
| hookEvent: 'PreToolUse', |
| }), |
| }) |
| } |
|
|
| |
| for (const hookResult of hookResults) { |
| resultingMessages.push(hookResult) |
| } |
| return resultingMessages |
| } catch (error) { |
| const durationMs = Date.now() - startTime |
| addToToolDuration(durationMs) |
|
|
| endToolExecutionSpan({ |
| success: false, |
| error: errorMessage(error), |
| }) |
| endToolSpan() |
|
|
| |
| |
| if (error instanceof McpAuthError) { |
| toolUseContext.setAppState(prevState => { |
| const serverName = error.serverName |
| const existingClientIndex = prevState.mcp.clients.findIndex( |
| c => c.name === serverName, |
| ) |
| if (existingClientIndex === -1) { |
| return prevState |
| } |
| const existingClient = prevState.mcp.clients[existingClientIndex] |
| |
| if (!existingClient || existingClient.type !== 'connected') { |
| return prevState |
| } |
| const updatedClients = [...prevState.mcp.clients] |
| updatedClients[existingClientIndex] = { |
| name: serverName, |
| type: 'needs-auth' as const, |
| config: existingClient.config, |
| } |
| return { |
| ...prevState, |
| mcp: { |
| ...prevState.mcp, |
| clients: updatedClients, |
| }, |
| } |
| }) |
| } |
|
|
| if (!(error instanceof AbortError)) { |
| const errorMsg = errorMessage(error) |
| logForDebugging( |
| `${tool.name} tool error (${durationMs}ms): ${errorMsg.slice(0, 200)}`, |
| ) |
| if (!(error instanceof ShellError)) { |
| logError(error) |
| } |
| logEvent('tengu_tool_use_error', { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(tool.name), |
| error: classifyToolError( |
| error, |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| isMcp: tool.isMcp ?? false, |
|
|
| queryChainId: toolUseContext.queryTracking |
| ?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryDepth: toolUseContext.queryTracking?.depth, |
| ...(mcpServerType && { |
| mcpServerType: |
| mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(mcpServerBaseUrl && { |
| mcpServerBaseUrl: |
| mcpServerBaseUrl as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...(requestId && { |
| requestId: |
| requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }), |
| ...mcpToolDetailsForAnalytics( |
| tool.name, |
| mcpServerType, |
| mcpServerBaseUrl, |
| ), |
| }) |
| |
| const mcpServerScope = isMcpTool(tool) |
| ? getMcpServerScopeFromToolName(tool.name) |
| : null |
|
|
| void logOTelEvent('tool_result', { |
| tool_name: sanitizeToolNameForAnalytics(tool.name), |
| use_id: toolUseID, |
| success: 'false', |
| duration_ms: String(durationMs), |
| error: errorMessage(error), |
| ...(Object.keys(toolParameters).length > 0 && { |
| tool_parameters: jsonStringify(toolParameters), |
| }), |
| ...(telemetryToolInput && { tool_input: telemetryToolInput }), |
| ...(decisionInfo && { |
| decision_source: decisionInfo.source, |
| decision_type: decisionInfo.decision, |
| }), |
| ...(mcpServerScope && { mcp_server_scope: mcpServerScope }), |
| }) |
| } |
| const content = formatError(error) |
|
|
| |
| const isInterrupt = error instanceof AbortError |
|
|
| |
| const hookMessages: MessageUpdateLazy< |
| AttachmentMessage | ProgressMessage<HookProgress> |
| >[] = [] |
| for await (const hookResult of runPostToolUseFailureHooks( |
| toolUseContext, |
| tool, |
| toolUseID, |
| messageId, |
| processedInput, |
| content, |
| isInterrupt, |
| requestId, |
| mcpServerType, |
| mcpServerBaseUrl, |
| )) { |
| hookMessages.push(hookResult) |
| } |
|
|
| return [ |
| { |
| message: createUserMessage({ |
| content: [ |
| { |
| type: 'tool_result', |
| content, |
| is_error: true, |
| tool_use_id: toolUseID, |
| }, |
| ], |
| toolUseResult: `Error: ${content}`, |
| mcpMeta: toolUseContext.agentId |
| ? undefined |
| : error instanceof |
| McpToolCallError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| ? error.mcpMeta |
| : undefined, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }), |
| }, |
| ...hookMessages, |
| ] |
| } finally { |
| stopSessionActivity('tool_exec') |
| |
| if (decisionInfo) { |
| toolUseContext.toolDecisions?.delete(toolUseID) |
| } |
| } |
| } |
|
|