Download src/tools/AgentTool/agentToolUtils.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 22.7 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/AgentTool/agentToolUtils.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/AgentTool/agentToolUtils.ts
-
curl -L -o agentToolUtils.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/AgentTool/agentToolUtils.ts
22.7 kB
| import { feature } from 'bun:bundle' | |
| import { z } from 'zod/v4' | |
| import { clearInvokedSkillsForAgent } from '../../bootstrap/state.js' | |
| import { | |
| ALL_AGENT_DISALLOWED_TOOLS, | |
| ASYNC_AGENT_ALLOWED_TOOLS, | |
| CUSTOM_AGENT_DISALLOWED_TOOLS, | |
| IN_PROCESS_TEAMMATE_ALLOWED_TOOLS, | |
| } from '../../constants/tools.js' | |
| import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js' | |
| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| logEvent, | |
| } from '../../services/analytics/index.js' | |
| import { clearDumpState } from '../../services/api/dumpPrompts.js' | |
| import type { AppState } from '../../state/AppState.js' | |
| import type { | |
| Tool, | |
| ToolPermissionContext, | |
| Tools, | |
| ToolUseContext, | |
| } from '../../Tool.js' | |
| import { toolMatchesName } from '../../Tool.js' | |
| import { | |
| completeAgentTask as completeAsyncAgent, | |
| createActivityDescriptionResolver, | |
| createProgressTracker, | |
| enqueueAgentNotification, | |
| failAgentTask as failAsyncAgent, | |
| getProgressUpdate, | |
| getTokenCountFromTracker, | |
| isLocalAgentTask, | |
| killAsyncAgent, | |
| type ProgressTracker, | |
| updateAgentProgress as updateAsyncAgentProgress, | |
| updateProgressFromMessage, | |
| } from '../../tasks/LocalAgentTask/LocalAgentTask.js' | |
| import { asAgentId } from '../../types/ids.js' | |
| import type { Message as MessageType } from '../../types/message.js' | |
| import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' | |
| import { logForDebugging } from '../../utils/debug.js' | |
| import { isInProtectedNamespace } from '../../utils/envUtils.js' | |
| import { AbortError, errorMessage } from '../../utils/errors.js' | |
| import type { CacheSafeParams } from '../../utils/forkedAgent.js' | |
| import { lazySchema } from '../../utils/lazySchema.js' | |
| import { | |
| extractTextContent, | |
| getLastAssistantMessage, | |
| } from '../../utils/messages.js' | |
| import type { PermissionMode } from '../../utils/permissions/PermissionMode.js' | |
| import { permissionRuleValueFromString } from '../../utils/permissions/permissionRuleParser.js' | |
| import { | |
| buildTranscriptForClassifier, | |
| classifyYoloAction, | |
| } from '../../utils/permissions/yoloClassifier.js' | |
| import { emitTaskProgress as emitTaskProgressEvent } from '../../utils/task/sdkProgress.js' | |
| import { isInProcessTeammate } from '../../utils/teammateContext.js' | |
| import { getTokenCountFromUsage } from '../../utils/tokens.js' | |
| import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../ExitPlanModeTool/constants.js' | |
| import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from './constants.js' | |
| import type { AgentDefinition } from './loadAgentsDir.js' | |
| export type ResolvedAgentTools = { | |
| hasWildcard: boolean | |
| validTools: string[] | |
| invalidTools: string[] | |
| resolvedTools: Tools | |
| allowedAgentTypes?: string[] | |
| } | |
| export function filterToolsForAgent({ | |
| tools, | |
| isBuiltIn, | |
| isAsync = false, | |
| permissionMode, | |
| }: { | |
| tools: Tools | |
| isBuiltIn: boolean | |
| isAsync?: boolean | |
| permissionMode?: PermissionMode | |
| }): Tools { | |
| return tools.filter(tool => { | |
| // Allow MCP tools for all agents | |
| if (tool.name.startsWith('mcp__')) { | |
| return true | |
| } | |
| // Allow ExitPlanMode for agents in plan mode (e.g., in-process teammates) | |
| // This bypasses both the ALL_AGENT_DISALLOWED_TOOLS and async tool filters | |
| if ( | |
| toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME) && | |
| permissionMode === 'plan' | |
| ) { | |
| return true | |
| } | |
| if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) { | |
| return false | |
| } | |
| if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) { | |
| return false | |
| } | |
| if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) { | |
| if (isAgentSwarmsEnabled() && isInProcessTeammate()) { | |
| // Allow AgentTool for in-process teammates to spawn sync subagents. | |
| // Validation in AgentTool.call() prevents background agents and teammate spawning. | |
| if (toolMatchesName(tool, AGENT_TOOL_NAME)) { | |
| return true | |
| } | |
| // Allow task tools for in-process teammates to coordinate via shared task list | |
| if (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| return true | |
| }) | |
| } | |
| /** | |
| * Resolves and validates agent tools against available tools | |
| * Handles wildcard expansion and validation in one place | |
| */ | |
| export function resolveAgentTools( | |
| agentDefinition: Pick< | |
| AgentDefinition, | |
| 'tools' | 'disallowedTools' | 'source' | 'permissionMode' | |
| >, | |
| availableTools: Tools, | |
| isAsync = false, | |
| isMainThread = false, | |
| ): ResolvedAgentTools { | |
| const { | |
| tools: agentTools, | |
| disallowedTools, | |
| source, | |
| permissionMode, | |
| } = agentDefinition | |
| // When isMainThread is true, skip filterToolsForAgent entirely — the main | |
| // thread's tool pool is already properly assembled by useMergedTools(), so | |
| // the sub-agent disallow lists shouldn't apply. | |
| const filteredAvailableTools = isMainThread | |
| ? availableTools | |
| : filterToolsForAgent({ | |
| tools: availableTools, | |
| isBuiltIn: source === 'built-in', | |
| isAsync, | |
| permissionMode, | |
| }) | |
| // Create a set of disallowed tool names for quick lookup | |
| const disallowedToolSet = new Set( | |
| disallowedTools?.map(toolSpec => { | |
| const { toolName } = permissionRuleValueFromString(toolSpec) | |
| return toolName | |
| }) ?? [], | |
| ) | |
| // Filter available tools based on disallowed list | |
| const allowedAvailableTools = filteredAvailableTools.filter( | |
| tool => !disallowedToolSet.has(tool.name), | |
| ) | |
| // If tools is undefined or ['*'], allow all tools (after filtering disallowed) | |
| const hasWildcard = | |
| agentTools === undefined || | |
| (agentTools.length === 1 && agentTools[0] === '*') | |
| if (hasWildcard) { | |
| return { | |
| hasWildcard: true, | |
| validTools: [], | |
| invalidTools: [], | |
| resolvedTools: allowedAvailableTools, | |
| } | |
| } | |
| const availableToolMap = new Map<string, Tool>() | |
| for (const tool of allowedAvailableTools) { | |
| availableToolMap.set(tool.name, tool) | |
| } | |
| const validTools: string[] = [] | |
| const invalidTools: string[] = [] | |
| const resolved: Tool[] = [] | |
| const resolvedToolsSet = new Set<Tool>() | |
| let allowedAgentTypes: string[] | undefined | |
| for (const toolSpec of agentTools) { | |
| // Parse the tool spec to extract the base tool name and any permission pattern | |
| const { toolName, ruleContent } = permissionRuleValueFromString(toolSpec) | |
| // Special case: Agent tool carries allowedAgentTypes metadata in its spec | |
| if (toolName === AGENT_TOOL_NAME) { | |
| if (ruleContent) { | |
| // Parse comma-separated agent types: "worker, researcher" → ["worker", "researcher"] | |
| allowedAgentTypes = ruleContent.split(',').map(s => s.trim()) | |
| } | |
| // For sub-agents, Agent is excluded by filterToolsForAgent — mark the spec | |
| // valid for allowedAgentTypes tracking but skip tool resolution. | |
| if (!isMainThread) { | |
| validTools.push(toolSpec) | |
| continue | |
| } | |
| // For main thread, filtering was skipped so Agent is in availableToolMap — | |
| // fall through to normal resolution below. | |
| } | |
| const tool = availableToolMap.get(toolName) | |
| if (tool) { | |
| validTools.push(toolSpec) | |
| if (!resolvedToolsSet.has(tool)) { | |
| resolved.push(tool) | |
| resolvedToolsSet.add(tool) | |
| } | |
| } else { | |
| invalidTools.push(toolSpec) | |
| } | |
| } | |
| return { | |
| hasWildcard: false, | |
| validTools, | |
| invalidTools, | |
| resolvedTools: resolved, | |
| allowedAgentTypes, | |
| } | |
| } | |
| export const agentToolResultSchema = lazySchema(() => | |
| z.object({ | |
| agentId: z.string(), | |
| // Optional: older persisted sessions won't have this (resume replays | |
| // results verbatim without re-validation). Used to gate the sync | |
| // result trailer — one-shot built-ins skip the SendMessage hint. | |
| agentType: z.string().optional(), | |
| content: z.array(z.object({ type: z.literal('text'), text: z.string() })), | |
| totalToolUseCount: z.number(), | |
| totalDurationMs: z.number(), | |
| totalTokens: z.number(), | |
| usage: z.object({ | |
| input_tokens: z.number(), | |
| output_tokens: z.number(), | |
| cache_creation_input_tokens: z.number().nullable(), | |
| cache_read_input_tokens: z.number().nullable(), | |
| server_tool_use: z | |
| .object({ | |
| web_search_requests: z.number(), | |
| web_fetch_requests: z.number(), | |
| }) | |
| .nullable(), | |
| service_tier: z.enum(['standard', 'priority', 'batch']).nullable(), | |
| cache_creation: z | |
| .object({ | |
| ephemeral_1h_input_tokens: z.number(), | |
| ephemeral_5m_input_tokens: z.number(), | |
| }) | |
| .nullable(), | |
| }), | |
| }), | |
| ) | |
| export type AgentToolResult = z.input<ReturnType<typeof agentToolResultSchema>> | |
| export function countToolUses(messages: MessageType[]): number { | |
| let count = 0 | |
| for (const m of messages) { | |
| if (m.type === 'assistant') { | |
| for (const block of m.message.content) { | |
| if (block.type === 'tool_use') { | |
| count++ | |
| } | |
| } | |
| } | |
| } | |
| return count | |
| } | |
| export function finalizeAgentTool( | |
| agentMessages: MessageType[], | |
| agentId: string, | |
| metadata: { | |
| prompt: string | |
| resolvedAgentModel: string | |
| isBuiltInAgent: boolean | |
| startTime: number | |
| agentType: string | |
| isAsync: boolean | |
| }, | |
| ): AgentToolResult { | |
| const { | |
| prompt, | |
| resolvedAgentModel, | |
| isBuiltInAgent, | |
| startTime, | |
| agentType, | |
| isAsync, | |
| } = metadata | |
| const lastAssistantMessage = getLastAssistantMessage(agentMessages) | |
| if (lastAssistantMessage === undefined) { | |
| throw new Error('No assistant messages found') | |
| } | |
| // Extract text content from the agent's response. If the final assistant | |
| // message is a pure tool_use block (loop exited mid-turn), fall back to | |
| // the most recent assistant message that has text content. | |
| let content = lastAssistantMessage.message.content.filter( | |
| _ => _.type === 'text', | |
| ) | |
| if (content.length === 0) { | |
| for (let i = agentMessages.length - 1; i >= 0; i--) { | |
| const m = agentMessages[i]! | |
| if (m.type !== 'assistant') continue | |
| const textBlocks = m.message.content.filter(_ => _.type === 'text') | |
| if (textBlocks.length > 0) { | |
| content = textBlocks | |
| break | |
| } | |
| } | |
| } | |
| const totalTokens = getTokenCountFromUsage(lastAssistantMessage.message.usage) | |
| const totalToolUseCount = countToolUses(agentMessages) | |
| logEvent('tengu_agent_tool_completed', { | |
| agent_type: | |
| agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| model: | |
| resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| prompt_char_count: prompt.length, | |
| response_char_count: content.length, | |
| assistant_message_count: agentMessages.length, | |
| total_tool_uses: totalToolUseCount, | |
| duration_ms: Date.now() - startTime, | |
| total_tokens: totalTokens, | |
| is_built_in_agent: isBuiltInAgent, | |
| is_async: isAsync, | |
| }) | |
| // Signal to inference that this subagent's cache chain can be evicted. | |
| const lastRequestId = lastAssistantMessage.requestId | |
| if (lastRequestId) { | |
| logEvent('tengu_cache_eviction_hint', { | |
| scope: | |
| 'subagent_end' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| last_request_id: | |
| lastRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| } | |
| return { | |
| agentId, | |
| agentType, | |
| content, | |
| totalDurationMs: Date.now() - startTime, | |
| totalTokens, | |
| totalToolUseCount, | |
| usage: lastAssistantMessage.message.usage, | |
| } | |
| } | |
| /** | |
| * Returns the name of the last tool_use block in an assistant message, | |
| * or undefined if the message is not an assistant message with tool_use. | |
| */ | |
| export function getLastToolUseName(message: MessageType): string | undefined { | |
| if (message.type !== 'assistant') return undefined | |
| const block = message.message.content.findLast(b => b.type === 'tool_use') | |
| return block?.type === 'tool_use' ? block.name : undefined | |
| } | |
| export function emitTaskProgress( | |
| tracker: ProgressTracker, | |
| taskId: string, | |
| toolUseId: string | undefined, | |
| description: string, | |
| startTime: number, | |
| lastToolName: string, | |
| ): void { | |
| const progress = getProgressUpdate(tracker) | |
| emitTaskProgressEvent({ | |
| taskId, | |
| toolUseId, | |
| description: progress.lastActivity?.activityDescription ?? description, | |
| startTime, | |
| totalTokens: progress.tokenCount, | |
| toolUses: progress.toolUseCount, | |
| lastToolName, | |
| }) | |
| } | |
| export async function classifyHandoffIfNeeded({ | |
| agentMessages, | |
| tools, | |
| toolPermissionContext, | |
| abortSignal, | |
| subagentType, | |
| totalToolUseCount, | |
| }: { | |
| agentMessages: MessageType[] | |
| tools: Tools | |
| toolPermissionContext: AppState['toolPermissionContext'] | |
| abortSignal: AbortSignal | |
| subagentType: string | |
| totalToolUseCount: number | |
| }): Promise<string | null> { | |
| if (feature('TRANSCRIPT_CLASSIFIER')) { | |
| if (toolPermissionContext.mode !== 'auto') return null | |
| const agentTranscript = buildTranscriptForClassifier(agentMessages, tools) | |
| if (!agentTranscript) return null | |
| const classifierResult = await classifyYoloAction( | |
| agentMessages, | |
| { | |
| role: 'user', | |
| content: [ | |
| { | |
| type: 'text', | |
| text: "Sub-agent has finished and is handing back control to the main agent. Review the sub-agent's work based on the block rules and let the main agent know if any file is dangerous (the main agent will see the reason).", | |
| }, | |
| ], | |
| }, | |
| tools, | |
| toolPermissionContext as ToolPermissionContext, | |
| abortSignal, | |
| ) | |
| const handoffDecision = classifierResult.unavailable | |
| ? 'unavailable' | |
| : classifierResult.shouldBlock | |
| ? 'blocked' | |
| : 'allowed' | |
| logEvent('tengu_auto_mode_decision', { | |
| decision: | |
| handoffDecision as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| toolName: | |
| // Use legacy name for analytics continuity across the Task→Agent rename | |
| LEGACY_AGENT_TOOL_NAME as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| inProtectedNamespace: isInProtectedNamespace(), | |
| classifierModel: | |
| classifierResult.model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| agentType: | |
| subagentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| toolUseCount: totalToolUseCount, | |
| isHandoff: true, | |
| // For handoff, the relevant agent completion is the subagent's final | |
| // assistant message — the last thing the classifier transcript shows | |
| // before the handoff review prompt. | |
| agentMsgId: getLastAssistantMessage(agentMessages)?.message | |
| .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| classifierStage: | |
| classifierResult.stage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| classifierStage1RequestId: | |
| classifierResult.stage1RequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| classifierStage1MsgId: | |
| classifierResult.stage1MsgId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| classifierStage2RequestId: | |
| classifierResult.stage2RequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| classifierStage2MsgId: | |
| classifierResult.stage2MsgId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| if (classifierResult.shouldBlock) { | |
| // When classifier is unavailable, still propagate the sub-agent's | |
| // results but with a warning so the parent agent can verify the work. | |
| if (classifierResult.unavailable) { | |
| logForDebugging( | |
| 'Handoff classifier unavailable, allowing sub-agent output with warning', | |
| { level: 'warn' }, | |
| ) | |
| return `Note: The safety classifier was unavailable when reviewing this sub-agent's work. Please carefully verify the sub-agent's actions and output before acting on them.` | |
| } | |
| logForDebugging( | |
| `Handoff classifier flagged sub-agent output: ${classifierResult.reason}`, | |
| { level: 'warn' }, | |
| ) | |
| return `SECURITY WARNING: This sub-agent performed actions that may violate security policy. Reason: ${classifierResult.reason}. Review the sub-agent's actions carefully before acting on its output.` | |
| } | |
| } | |
| return null | |
| } | |
| /** | |
| * Extract a partial result string from an agent's accumulated messages. | |
| * Used when an async agent is killed to preserve what it accomplished. | |
| * Returns undefined if no text content is found. | |
| */ | |
| export function extractPartialResult( | |
| messages: MessageType[], | |
| ): string | undefined { | |
| for (let i = messages.length - 1; i >= 0; i--) { | |
| const m = messages[i]! | |
| if (m.type !== 'assistant') continue | |
| const text = extractTextContent(m.message.content, '\n') | |
| if (text) { | |
| return text | |
| } | |
| } | |
| return undefined | |
| } | |
| type SetAppState = (f: (prev: AppState) => AppState) => void | |
| /** | |
| * Drives a background agent from spawn to terminal notification. | |
| * Shared between AgentTool's async-from-start path and resumeAgentBackground. | |
| */ | |
| export async function runAsyncAgentLifecycle({ | |
| taskId, | |
| abortController, | |
| makeStream, | |
| metadata, | |
| description, | |
| toolUseContext, | |
| rootSetAppState, | |
| agentIdForCleanup, | |
| enableSummarization, | |
| getWorktreeResult, | |
| }: { | |
| taskId: string | |
| abortController: AbortController | |
| makeStream: ( | |
| onCacheSafeParams: ((p: CacheSafeParams) => void) | undefined, | |
| ) => AsyncGenerator<MessageType, void> | |
| metadata: Parameters<typeof finalizeAgentTool>[2] | |
| description: string | |
| toolUseContext: ToolUseContext | |
| rootSetAppState: SetAppState | |
| agentIdForCleanup: string | |
| enableSummarization: boolean | |
| getWorktreeResult: () => Promise<{ | |
| worktreePath?: string | |
| worktreeBranch?: string | |
| }> | |
| }): Promise<void> { | |
| let stopSummarization: (() => void) | undefined | |
| const agentMessages: MessageType[] = [] | |
| try { | |
| const tracker = createProgressTracker() | |
| const resolveActivity = createActivityDescriptionResolver( | |
| toolUseContext.options.tools, | |
| ) | |
| const onCacheSafeParams = enableSummarization | |
| ? (params: CacheSafeParams) => { | |
| const { stop } = startAgentSummarization( | |
| taskId, | |
| asAgentId(taskId), | |
| params, | |
| rootSetAppState, | |
| ) | |
| stopSummarization = stop | |
| } | |
| : undefined | |
| for await (const message of makeStream(onCacheSafeParams)) { | |
| agentMessages.push(message) | |
| // Append immediately when UI holds the task (retain). Bootstrap reads | |
| // disk in parallel and UUID-merges the prefix — disk-write-before-yield | |
| // means live is always a suffix of disk, so merge is order-correct. | |
| rootSetAppState(prev => { | |
| const t = prev.tasks[taskId] | |
| if (!isLocalAgentTask(t) || !t.retain) return prev | |
| const base = t.messages ?? [] | |
| return { | |
| ...prev, | |
| tasks: { | |
| ...prev.tasks, | |
| [taskId]: { ...t, messages: [...base, message] }, | |
| }, | |
| } | |
| }) | |
| updateProgressFromMessage( | |
| tracker, | |
| message, | |
| resolveActivity, | |
| toolUseContext.options.tools, | |
| ) | |
| updateAsyncAgentProgress( | |
| taskId, | |
| getProgressUpdate(tracker), | |
| rootSetAppState, | |
| ) | |
| const lastToolName = getLastToolUseName(message) | |
| if (lastToolName) { | |
| emitTaskProgress( | |
| tracker, | |
| taskId, | |
| toolUseContext.toolUseId, | |
| description, | |
| metadata.startTime, | |
| lastToolName, | |
| ) | |
| } | |
| } | |
| stopSummarization?.() | |
| const agentResult = finalizeAgentTool(agentMessages, taskId, metadata) | |
| // Mark task completed FIRST so TaskOutput(block=true) unblocks | |
| // immediately. classifyHandoffIfNeeded (API call) and getWorktreeResult | |
| // (git exec) are notification embellishments that can hang — they must | |
| // not gate the status transition (gh-20236). | |
| completeAsyncAgent(agentResult, rootSetAppState) | |
| let finalMessage = extractTextContent(agentResult.content, '\n') | |
| if (feature('TRANSCRIPT_CLASSIFIER')) { | |
| const handoffWarning = await classifyHandoffIfNeeded({ | |
| agentMessages, | |
| tools: toolUseContext.options.tools, | |
| toolPermissionContext: | |
| toolUseContext.getAppState().toolPermissionContext, | |
| abortSignal: abortController.signal, | |
| subagentType: metadata.agentType, | |
| totalToolUseCount: agentResult.totalToolUseCount, | |
| }) | |
| if (handoffWarning) { | |
| finalMessage = `${handoffWarning}\n\n${finalMessage}` | |
| } | |
| } | |
| const worktreeResult = await getWorktreeResult() | |
| enqueueAgentNotification({ | |
| taskId, | |
| description, | |
| status: 'completed', | |
| setAppState: rootSetAppState, | |
| finalMessage, | |
| usage: { | |
| totalTokens: getTokenCountFromTracker(tracker), | |
| toolUses: agentResult.totalToolUseCount, | |
| durationMs: agentResult.totalDurationMs, | |
| }, | |
| toolUseId: toolUseContext.toolUseId, | |
| ...worktreeResult, | |
| }) | |
| } catch (error) { | |
| stopSummarization?.() | |
| if (error instanceof AbortError) { | |
| // killAsyncAgent is a no-op if TaskStop already set status='killed' — | |
| // but only this catch handler has agentMessages, so the notification | |
| // must fire unconditionally. Transition status BEFORE worktree cleanup | |
| // so TaskOutput unblocks even if git hangs (gh-20236). | |
| killAsyncAgent(taskId, rootSetAppState) | |
| logEvent('tengu_agent_tool_terminated', { | |
| agent_type: | |
| metadata.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| model: | |
| metadata.resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| duration_ms: Date.now() - metadata.startTime, | |
| is_async: true, | |
| is_built_in_agent: metadata.isBuiltInAgent, | |
| reason: | |
| 'user_kill_async' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| const worktreeResult = await getWorktreeResult() | |
| const partialResult = extractPartialResult(agentMessages) | |
| enqueueAgentNotification({ | |
| taskId, | |
| description, | |
| status: 'killed', | |
| setAppState: rootSetAppState, | |
| toolUseId: toolUseContext.toolUseId, | |
| finalMessage: partialResult, | |
| ...worktreeResult, | |
| }) | |
| return | |
| } | |
| const msg = errorMessage(error) | |
| failAsyncAgent(taskId, msg, rootSetAppState) | |
| const worktreeResult = await getWorktreeResult() | |
| enqueueAgentNotification({ | |
| taskId, | |
| description, | |
| status: 'failed', | |
| error: msg, | |
| setAppState: rootSetAppState, | |
| toolUseId: toolUseContext.toolUseId, | |
| ...worktreeResult, | |
| }) | |
| } finally { | |
| clearInvokedSkillsForAgent(agentIdForCleanup) | |
| clearDumpState(agentIdForCleanup) | |
| } | |
| } | |