/ Tool.ts
Tool.ts
  1  import type {
  2    ToolResultBlockParam,
  3    ToolUseBlockParam,
  4  } from '@anthropic-ai/sdk/resources/index.mjs'
  5  import type {
  6    ElicitRequestURLParams,
  7    ElicitResult,
  8  } from '@modelcontextprotocol/sdk/types.js'
  9  import type { UUID } from 'crypto'
 10  import type { z } from 'zod/v4'
 11  import type { Command } from './commands.js'
 12  import type { CanUseToolFn } from './hooks/useCanUseTool.js'
 13  import type { ThinkingConfig } from './utils/thinking.js'
 14  
 15  export type ToolInputJSONSchema = {
 16    [x: string]: unknown
 17    type: 'object'
 18    properties?: {
 19      [x: string]: unknown
 20    }
 21  }
 22  
 23  import type { Notification } from './context/notifications.js'
 24  import type {
 25    MCPServerConnection,
 26    ServerResource,
 27  } from './services/mcp/types.js'
 28  import type {
 29    AgentDefinition,
 30    AgentDefinitionsResult,
 31  } from './tools/AgentTool/loadAgentsDir.js'
 32  import type {
 33    AssistantMessage,
 34    AttachmentMessage,
 35    Message,
 36    ProgressMessage,
 37    SystemLocalCommandMessage,
 38    SystemMessage,
 39    UserMessage,
 40  } from './types/message.js'
 41  // Import permission types from centralized location to break import cycles
 42  // Import PermissionResult from centralized location to break import cycles
 43  import type {
 44    AdditionalWorkingDirectory,
 45    PermissionMode,
 46    PermissionResult,
 47  } from './types/permissions.js'
 48  // Import tool progress types from centralized location to break import cycles
 49  import type {
 50    AgentToolProgress,
 51    BashProgress,
 52    MCPProgress,
 53    REPLToolProgress,
 54    SkillToolProgress,
 55    TaskOutputProgress,
 56    ToolProgressData,
 57    WebSearchProgress,
 58  } from './types/tools.js'
 59  import type { FileStateCache } from './utils/fileStateCache.js'
 60  import type { DenialTrackingState } from './utils/permissions/denialTracking.js'
 61  import type { SystemPrompt } from './utils/systemPromptType.js'
 62  import type { ContentReplacementState } from './utils/toolResultStorage.js'
 63  
 64  // Re-export progress types for backwards compatibility
 65  export type {
 66    AgentToolProgress,
 67    BashProgress,
 68    MCPProgress,
 69    REPLToolProgress,
 70    SkillToolProgress,
 71    TaskOutputProgress,
 72    WebSearchProgress,
 73  }
 74  
 75  import type { SpinnerMode } from './components/Spinner.js'
 76  import type { QuerySource } from './constants/querySource.js'
 77  import type { SDKStatus } from './entrypoints/agentSdkTypes.js'
 78  import type { AppState } from './state/AppState.js'
 79  import type {
 80    HookProgress,
 81    PromptRequest,
 82    PromptResponse,
 83  } from './types/hooks.js'
 84  import type { AgentId } from './types/ids.js'
 85  import type { DeepImmutable } from './types/utils.js'
 86  import type { AttributionState } from './utils/commitAttribution.js'
 87  import type { FileHistoryState } from './utils/fileHistory.js'
 88  import type { Theme, ThemeName } from './utils/theme.js'
 89  
 90  export type QueryChainTracking = {
 91    chainId: string
 92    depth: number
 93  }
 94  
 95  export type ValidationResult =
 96    | { result: true }
 97    | {
 98        result: false
 99        message: string
100        errorCode: number
101      }
102  
103  export type SetToolJSXFn = (
104    args: {
105      jsx: React.ReactNode | null
106      shouldHidePromptInput: boolean
107      shouldContinueAnimation?: true
108      showSpinner?: boolean
109      isLocalJSXCommand?: boolean
110      isImmediate?: boolean
111      /** Set to true to clear a local JSX command (e.g., from its onDone callback) */
112      clearLocalJSX?: boolean
113    } | null,
114  ) => void
115  
116  // Import tool permission types from centralized location to break import cycles
117  import type { ToolPermissionRulesBySource } from './types/permissions.js'
118  
119  // Re-export for backwards compatibility
120  export type { ToolPermissionRulesBySource }
121  
122  // Apply DeepImmutable to the imported type
123  export type ToolPermissionContext = DeepImmutable<{
124    mode: PermissionMode
125    additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
126    alwaysAllowRules: ToolPermissionRulesBySource
127    alwaysDenyRules: ToolPermissionRulesBySource
128    alwaysAskRules: ToolPermissionRulesBySource
129    isBypassPermissionsModeAvailable: boolean
130    isAutoModeAvailable?: boolean
131    strippedDangerousRules?: ToolPermissionRulesBySource
132    /** When true, permission prompts are auto-denied (e.g., background agents that can't show UI) */
133    shouldAvoidPermissionPrompts?: boolean
134    /** When true, automated checks (classifier, hooks) are awaited before showing the permission dialog (coordinator workers) */
135    awaitAutomatedChecksBeforeDialog?: boolean
136    /** Stores the permission mode before model-initiated plan mode entry, so it can be restored on exit */
137    prePlanMode?: PermissionMode
138  }>
139  
140  export const getEmptyToolPermissionContext: () => ToolPermissionContext =
141    () => ({
142      mode: 'default',
143      additionalWorkingDirectories: new Map(),
144      alwaysAllowRules: {},
145      alwaysDenyRules: {},
146      alwaysAskRules: {},
147      isBypassPermissionsModeAvailable: false,
148    })
149  
150  export type CompactProgressEvent =
151    | {
152        type: 'hooks_start'
153        hookType: 'pre_compact' | 'post_compact' | 'session_start'
154      }
155    | { type: 'compact_start' }
156    | { type: 'compact_end' }
157  
158  export type ToolUseContext = {
159    options: {
160      commands: Command[]
161      debug: boolean
162      mainLoopModel: string
163      tools: Tools
164      verbose: boolean
165      thinkingConfig: ThinkingConfig
166      mcpClients: MCPServerConnection[]
167      mcpResources: Record<string, ServerResource[]>
168      isNonInteractiveSession: boolean
169      agentDefinitions: AgentDefinitionsResult
170      maxBudgetUsd?: number
171      /** Custom system prompt that replaces the default system prompt */
172      customSystemPrompt?: string
173      /** Additional system prompt appended after the main system prompt */
174      appendSystemPrompt?: string
175      /** Override querySource for analytics tracking */
176      querySource?: QuerySource
177      /** Optional callback to get the latest tools (e.g., after MCP servers connect mid-query) */
178      refreshTools?: () => Tools
179    }
180    abortController: AbortController
181    readFileState: FileStateCache
182    getAppState(): AppState
183    setAppState(f: (prev: AppState) => AppState): void
184    /**
185     * Always-shared setAppState for session-scoped infrastructure (background
186     * tasks, session hooks). Unlike setAppState, which is no-op for async agents
187     * (see createSubagentContext), this always reaches the root store so agents
188     * at any nesting depth can register/clean up infrastructure that outlives
189     * a single turn. Only set by createSubagentContext; main-thread contexts
190     * fall back to setAppState.
191     */
192    setAppStateForTasks?: (f: (prev: AppState) => AppState) => void
193    /**
194     * Optional handler for URL elicitations triggered by tool call errors (-32042).
195     * In print/SDK mode, this delegates to structuredIO.handleElicitation.
196     * In REPL mode, this is undefined and the queue-based UI path is used.
197     */
198    handleElicitation?: (
199      serverName: string,
200      params: ElicitRequestURLParams,
201      signal: AbortSignal,
202    ) => Promise<ElicitResult>
203    setToolJSX?: SetToolJSXFn
204    addNotification?: (notif: Notification) => void
205    /** Append a UI-only system message to the REPL message list. Stripped at the
206     *  normalizeMessagesForAPI boundary — the Exclude<> makes that type-enforced. */
207    appendSystemMessage?: (
208      msg: Exclude<SystemMessage, SystemLocalCommandMessage>,
209    ) => void
210    /** Send an OS-level notification (iTerm2, Kitty, Ghostty, bell, etc.) */
211    sendOSNotification?: (opts: {
212      message: string
213      notificationType: string
214    }) => void
215    nestedMemoryAttachmentTriggers?: Set<string>
216    /**
217     * CLAUDE.md paths already injected as nested_memory attachments this
218     * session. Dedup for memoryFilesToAttachments — readFileState is an LRU
219     * that evicts entries in busy sessions, so its .has() check alone can
220     * re-inject the same CLAUDE.md dozens of times.
221     */
222    loadedNestedMemoryPaths?: Set<string>
223    dynamicSkillDirTriggers?: Set<string>
224    /** Skill names surfaced via skill_discovery this session. Telemetry only (feeds was_discovered). */
225    discoveredSkillNames?: Set<string>
226    userModified?: boolean
227    setInProgressToolUseIDs: (f: (prev: Set<string>) => Set<string>) => void
228    /** Only wired in interactive (REPL) contexts; SDK/QueryEngine don't set this. */
229    setHasInterruptibleToolInProgress?: (v: boolean) => void
230    setResponseLength: (f: (prev: number) => number) => void
231    /** Ant-only: push a new API metrics entry for OTPS tracking.
232     *  Called by subagent streaming when a new API request starts. */
233    pushApiMetricsEntry?: (ttftMs: number) => void
234    setStreamMode?: (mode: SpinnerMode) => void
235    onCompactProgress?: (event: CompactProgressEvent) => void
236    setSDKStatus?: (status: SDKStatus) => void
237    openMessageSelector?: () => void
238    updateFileHistoryState: (
239      updater: (prev: FileHistoryState) => FileHistoryState,
240    ) => void
241    updateAttributionState: (
242      updater: (prev: AttributionState) => AttributionState,
243    ) => void
244    setConversationId?: (id: UUID) => void
245    agentId?: AgentId // Only set for subagents; use getSessionId() for session ID. Hooks use this to distinguish subagent calls.
246    agentType?: string // Subagent type name. For the main thread's --agent type, hooks fall back to getMainThreadAgentType().
247    /** When true, canUseTool must always be called even when hooks auto-approve.
248     *  Used by speculation for overlay file path rewriting. */
249    requireCanUseTool?: boolean
250    messages: Message[]
251    fileReadingLimits?: {
252      maxTokens?: number
253      maxSizeBytes?: number
254    }
255    globLimits?: {
256      maxResults?: number
257    }
258    toolDecisions?: Map<
259      string,
260      {
261        source: string
262        decision: 'accept' | 'reject'
263        timestamp: number
264      }
265    >
266    queryTracking?: QueryChainTracking
267    /** Callback factory for requesting interactive prompts from the user.
268     * Returns a prompt callback bound to the given source name.
269     * Only available in interactive (REPL) contexts. */
270    requestPrompt?: (
271      sourceName: string,
272      toolInputSummary?: string | null,
273    ) => (request: PromptRequest) => Promise<PromptResponse>
274    toolUseId?: string
275    criticalSystemReminder_EXPERIMENTAL?: string
276    /** When true, preserve toolUseResult on messages even for subagents.
277     * Used by in-process teammates whose transcripts are viewable by the user. */
278    preserveToolUseResults?: boolean
279    /** Local denial tracking state for async subagents whose setAppState is a
280     *  no-op. Without this, the denial counter never accumulates and the
281     *  fallback-to-prompting threshold is never reached. Mutable — the
282     *  permissions code updates it in place. */
283    localDenialTracking?: DenialTrackingState
284    /**
285     * Per-conversation-thread content replacement state for the tool result
286     * budget. When present, query.ts applies the aggregate tool result budget.
287     * Main thread: REPL provisions once (never resets — stale UUID keys
288     * are inert). Subagents: createSubagentContext clones the parent's state
289     * by default (cache-sharing forks need identical decisions), or
290     * resumeAgentBackground threads one reconstructed from sidechain records.
291     */
292    contentReplacementState?: ContentReplacementState
293    /**
294     * Parent's rendered system prompt bytes, frozen at turn start.
295     * Used by fork subagents to share the parent's prompt cache — re-calling
296     * getSystemPrompt() at fork-spawn time can diverge (GrowthBook cold→warm)
297     * and bust the cache. See forkSubagent.ts.
298     */
299    renderedSystemPrompt?: SystemPrompt
300  }
301  
302  // Re-export ToolProgressData from centralized location
303  export type { ToolProgressData }
304  
305  export type Progress = ToolProgressData | HookProgress
306  
307  export type ToolProgress<P extends ToolProgressData> = {
308    toolUseID: string
309    data: P
310  }
311  
312  export function filterToolProgressMessages(
313    progressMessagesForMessage: ProgressMessage[],
314  ): ProgressMessage<ToolProgressData>[] {
315    return progressMessagesForMessage.filter(
316      (msg): msg is ProgressMessage<ToolProgressData> =>
317        msg.data?.type !== 'hook_progress',
318    )
319  }
320  
321  export type ToolResult<T> = {
322    data: T
323    newMessages?: (
324      | UserMessage
325      | AssistantMessage
326      | AttachmentMessage
327      | SystemMessage
328    )[]
329    // contextModifier is only honored for tools that aren't concurrency safe.
330    contextModifier?: (context: ToolUseContext) => ToolUseContext
331    /** MCP protocol metadata (structuredContent, _meta) to pass through to SDK consumers */
332    mcpMeta?: {
333      _meta?: Record<string, unknown>
334      structuredContent?: Record<string, unknown>
335    }
336  }
337  
338  export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = (
339    progress: ToolProgress<P>,
340  ) => void
341  
342  // Type for any schema that outputs an object with string keys
343  export type AnyObject = z.ZodType<{ [key: string]: unknown }>
344  
345  /**
346   * Checks if a tool matches the given name (primary name or alias).
347   */
348  export function toolMatchesName(
349    tool: { name: string; aliases?: string[] },
350    name: string,
351  ): boolean {
352    return tool.name === name || (tool.aliases?.includes(name) ?? false)
353  }
354  
355  /**
356   * Finds a tool by name or alias from a list of tools.
357   */
358  export function findToolByName(tools: Tools, name: string): Tool | undefined {
359    return tools.find(t => toolMatchesName(t, name))
360  }
361  
362  export type Tool<
363    Input extends AnyObject = AnyObject,
364    Output = unknown,
365    P extends ToolProgressData = ToolProgressData,
366  > = {
367    /**
368     * Optional aliases for backwards compatibility when a tool is renamed.
369     * The tool can be looked up by any of these names in addition to its primary name.
370     */
371    aliases?: string[]
372    /**
373     * One-line capability phrase used by ToolSearch for keyword matching.
374     * Helps the model find this tool via keyword search when it's deferred.
375     * 3–10 words, no trailing period.
376     * Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit).
377     */
378    searchHint?: string
379    call(
380      args: z.infer<Input>,
381      context: ToolUseContext,
382      canUseTool: CanUseToolFn,
383      parentMessage: AssistantMessage,
384      onProgress?: ToolCallProgress<P>,
385    ): Promise<ToolResult<Output>>
386    description(
387      input: z.infer<Input>,
388      options: {
389        isNonInteractiveSession: boolean
390        toolPermissionContext: ToolPermissionContext
391        tools: Tools
392      },
393    ): Promise<string>
394    readonly inputSchema: Input
395    // Type for MCP tools that can specify their input schema directly in JSON Schema format
396    // rather than converting from Zod schema
397    readonly inputJSONSchema?: ToolInputJSONSchema
398    // Optional because TungstenTool doesn't define this. TODO: Make it required.
399    // When we do that, we can also go through and make this a bit more type-safe.
400    outputSchema?: z.ZodType<unknown>
401    inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
402    isConcurrencySafe(input: z.infer<Input>): boolean
403    isEnabled(): boolean
404    isReadOnly(input: z.infer<Input>): boolean
405    /** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */
406    isDestructive?(input: z.infer<Input>): boolean
407    /**
408     * What should happen when the user submits a new message while this tool
409     * is running.
410     *
411     * - `'cancel'` — stop the tool and discard its result
412     * - `'block'`  — keep running; the new message waits
413     *
414     * Defaults to `'block'` when not implemented.
415     */
416    interruptBehavior?(): 'cancel' | 'block'
417    /**
418     * Returns information about whether this tool use is a search or read operation
419     * that should be collapsed into a condensed display in the UI. Examples include
420     * file searching (Grep, Glob), file reading (Read), and bash commands like find,
421     * grep, wc, etc.
422     *
423     * Returns an object indicating whether the operation is a search or read operation:
424     * - `isSearch: true` for search operations (grep, find, glob patterns)
425     * - `isRead: true` for read operations (cat, head, tail, file read)
426     * - `isList: true` for directory-listing operations (ls, tree, du)
427     * - All can be false if the operation shouldn't be collapsed
428     */
429    isSearchOrReadCommand?(input: z.infer<Input>): {
430      isSearch: boolean
431      isRead: boolean
432      isList?: boolean
433    }
434    isOpenWorld?(input: z.infer<Input>): boolean
435    requiresUserInteraction?(): boolean
436    isMcp?: boolean
437    isLsp?: boolean
438    /**
439     * When true, this tool is deferred (sent with defer_loading: true) and requires
440     * ToolSearch to be used before it can be called.
441     */
442    readonly shouldDefer?: boolean
443    /**
444     * When true, this tool is never deferred — its full schema appears in the
445     * initial prompt even when ToolSearch is enabled. For MCP tools, set via
446     * `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on
447     * turn 1 without a ToolSearch round-trip.
448     */
449    readonly alwaysLoad?: boolean
450    /**
451     * For MCP tools: the server and tool names as received from the MCP server (unnormalized).
452     * Present on all MCP tools regardless of whether `name` is prefixed (mcp__server__tool)
453     * or unprefixed (CLAUDE_AGENT_SDK_MCP_NO_PREFIX mode).
454     */
455    mcpInfo?: { serverName: string; toolName: string }
456    readonly name: string
457    /**
458     * Maximum size in characters for tool result before it gets persisted to disk.
459     * When exceeded, the result is saved to a file and Claude receives a preview
460     * with the file path instead of the full content.
461     *
462     * Set to Infinity for tools whose output must never be persisted (e.g. Read,
463     * where persisting creates a circular Read→file→Read loop and the tool
464     * already self-bounds via its own limits).
465     */
466    maxResultSizeChars: number
467    /**
468     * When true, enables strict mode for this tool, which causes the API to
469     * more strictly adhere to tool instructions and parameter schemas.
470     * Only applied when the tengu_tool_pear is enabled.
471     */
472    readonly strict?: boolean
473  
474    /**
475     * Called on copies of tool_use input before observers see it (SDK stream,
476     * transcript, canUseTool, PreToolUse/PostToolUse hooks). Mutate in place
477     * to add legacy/derived fields. Must be idempotent. The original API-bound
478     * input is never mutated (preserves prompt cache). Not re-applied when a
479     * hook/permission returns a fresh updatedInput — those own their shape.
480     */
481    backfillObservableInput?(input: Record<string, unknown>): void
482  
483    /**
484     * Determines if this tool is allowed to run with this input in the current context.
485     * It informs the model of why the tool use failed, and does not directly display any UI.
486     * @param input
487     * @param context
488     */
489    validateInput?(
490      input: z.infer<Input>,
491      context: ToolUseContext,
492    ): Promise<ValidationResult>
493  
494    /**
495     * Determines if the user is asked for permission. Only called after validateInput() passes.
496     * General permission logic is in permissions.ts. This method contains tool-specific logic.
497     * @param input
498     * @param context
499     */
500    checkPermissions(
501      input: z.infer<Input>,
502      context: ToolUseContext,
503    ): Promise<PermissionResult>
504  
505    // Optional method for tools that operate on a file path
506    getPath?(input: z.infer<Input>): string
507  
508    /**
509     * Prepare a matcher for hook `if` conditions (permission-rule patterns like
510     * "git *" from "Bash(git *)"). Called once per hook-input pair; any
511     * expensive parsing happens here. Returns a closure that is called per
512     * hook pattern. If not implemented, only tool-name-level matching works.
513     */
514    preparePermissionMatcher?(
515      input: z.infer<Input>,
516    ): Promise<(pattern: string) => boolean>
517  
518    prompt(options: {
519      getToolPermissionContext: () => Promise<ToolPermissionContext>
520      tools: Tools
521      agents: AgentDefinition[]
522      allowedAgentTypes?: string[]
523    }): Promise<string>
524    userFacingName(input: Partial<z.infer<Input>> | undefined): string
525    userFacingNameBackgroundColor?(
526      input: Partial<z.infer<Input>> | undefined,
527    ): keyof Theme | undefined
528    /**
529     * Transparent wrappers (e.g. REPL) delegate all rendering to their progress
530     * handler, which emits native-looking blocks for each inner tool call.
531     * The wrapper itself shows nothing.
532     */
533    isTransparentWrapper?(): boolean
534    /**
535     * Returns a short string summary of this tool use for display in compact views.
536     * @param input The tool input
537     * @returns A short string summary, or null to not display
538     */
539    getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null
540    /**
541     * Returns a human-readable present-tense activity description for spinner display.
542     * Example: "Reading src/foo.ts", "Running bun test", "Searching for pattern"
543     * @param input The tool input
544     * @returns Activity description string, or null to fall back to tool name
545     */
546    getActivityDescription?(
547      input: Partial<z.infer<Input>> | undefined,
548    ): string | null
549    /**
550     * Returns a compact representation of this tool use for the auto-mode
551     * security classifier. Examples: `ls -la` for Bash, `/tmp/x: new content`
552     * for Edit. Return '' to skip this tool in the classifier transcript
553     * (e.g. tools with no security relevance). May return an object to avoid
554     * double-encoding when the caller JSON-wraps the value.
555     */
556    toAutoClassifierInput(input: z.infer<Input>): unknown
557    mapToolResultToToolResultBlockParam(
558      content: Output,
559      toolUseID: string,
560    ): ToolResultBlockParam
561    /**
562     * Optional. When omitted, the tool result renders nothing (same as returning
563     * null). Omit for tools whose results are surfaced elsewhere (e.g., TodoWrite
564     * updates the todo panel, not the transcript).
565     */
566    renderToolResultMessage?(
567      content: Output,
568      progressMessagesForMessage: ProgressMessage<P>[],
569      options: {
570        style?: 'condensed'
571        theme: ThemeName
572        tools: Tools
573        verbose: boolean
574        isTranscriptMode?: boolean
575        isBriefOnly?: boolean
576        /** Original tool_use input, when available. Useful for compact result
577         * summaries that reference what was requested (e.g. "Sent to #foo"). */
578        input?: unknown
579      },
580    ): React.ReactNode
581    /**
582     * Flattened text of what renderToolResultMessage shows IN TRANSCRIPT
583     * MODE (verbose=true, isTranscriptMode=true). For transcript search
584     * indexing: the index counts occurrences in this string, the highlight
585     * overlay scans the actual screen buffer. For count ≡ highlight, this
586     * must return the text that ends up visible — not the model-facing
587     * serialization from mapToolResultToToolResultBlockParam (which adds
588     * system-reminders, persisted-output wrappers).
589     *
590     * Chrome can be skipped (under-count is fine). "Found 3 files in 12ms"
591     * isn't worth indexing. Phantoms are not fine — text that's claimed
592     * here but doesn't render is a count≠highlight bug.
593     *
594     * Optional: omitted → field-name heuristic in transcriptSearch.ts.
595     * Drift caught by test/utils/transcriptSearch.renderFidelity.test.tsx
596     * which renders sample outputs and flags text that's indexed-but-not-
597     * rendered (phantom) or rendered-but-not-indexed (under-count warning).
598     */
599    extractSearchText?(out: Output): string
600    /**
601     * Render the tool use message. Note that `input` is partial because we render
602     * the message as soon as possible, possibly before tool parameters have fully
603     * streamed in.
604     */
605    renderToolUseMessage(
606      input: Partial<z.infer<Input>>,
607      options: { theme: ThemeName; verbose: boolean; commands?: Command[] },
608    ): React.ReactNode
609    /**
610     * Returns true when the non-verbose rendering of this output is truncated
611     * (i.e., clicking to expand would reveal more content). Gates
612     * click-to-expand in fullscreen — only messages where verbose actually
613     * shows more get a hover/click affordance. Unset means never truncated.
614     */
615    isResultTruncated?(output: Output): boolean
616    /**
617     * Renders an optional tag to display after the tool use message.
618     * Used for additional metadata like timeout, model, resume ID, etc.
619     * Returns null to not display anything.
620     */
621    renderToolUseTag?(input: Partial<z.infer<Input>>): React.ReactNode
622    /**
623     * Optional. When omitted, no progress UI is shown while the tool runs.
624     */
625    renderToolUseProgressMessage?(
626      progressMessagesForMessage: ProgressMessage<P>[],
627      options: {
628        tools: Tools
629        verbose: boolean
630        terminalSize?: { columns: number; rows: number }
631        inProgressToolCallCount?: number
632        isTranscriptMode?: boolean
633      },
634    ): React.ReactNode
635    renderToolUseQueuedMessage?(): React.ReactNode
636    /**
637     * Optional. When omitted, falls back to <FallbackToolUseRejectedMessage />.
638     * Only define this for tools that need custom rejection UI (e.g., file edits
639     * that show the rejected diff).
640     */
641    renderToolUseRejectedMessage?(
642      input: z.infer<Input>,
643      options: {
644        columns: number
645        messages: Message[]
646        style?: 'condensed'
647        theme: ThemeName
648        tools: Tools
649        verbose: boolean
650        progressMessagesForMessage: ProgressMessage<P>[]
651        isTranscriptMode?: boolean
652      },
653    ): React.ReactNode
654    /**
655     * Optional. When omitted, falls back to <FallbackToolUseErrorMessage />.
656     * Only define this for tools that need custom error UI (e.g., search tools
657     * that show "File not found" instead of the raw error).
658     */
659    renderToolUseErrorMessage?(
660      result: ToolResultBlockParam['content'],
661      options: {
662        progressMessagesForMessage: ProgressMessage<P>[]
663        tools: Tools
664        verbose: boolean
665        isTranscriptMode?: boolean
666      },
667    ): React.ReactNode
668  
669    /**
670     * Renders multiple parallel instances of this tool as a group.
671     * @returns React node to render, or null to fall back to individual rendering
672     */
673    /**
674     * Renders multiple tool uses as a group (non-verbose mode only).
675     * In verbose mode, individual tool uses render at their original positions.
676     * @returns React node to render, or null to fall back to individual rendering
677     */
678    renderGroupedToolUse?(
679      toolUses: Array<{
680        param: ToolUseBlockParam
681        isResolved: boolean
682        isError: boolean
683        isInProgress: boolean
684        progressMessages: ProgressMessage<P>[]
685        result?: {
686          param: ToolResultBlockParam
687          output: unknown
688        }
689      }>,
690      options: {
691        shouldAnimate: boolean
692        tools: Tools
693      },
694    ): React.ReactNode | null
695  }
696  
697  /**
698   * A collection of tools. Use this type instead of `Tool[]` to make it easier
699   * to track where tool sets are assembled, passed, and filtered across the codebase.
700   */
701  export type Tools = readonly Tool[]
702  
703  /**
704   * Methods that `buildTool` supplies a default for. A `ToolDef` may omit these;
705   * the resulting `Tool` always has them.
706   */
707  type DefaultableToolKeys =
708    | 'isEnabled'
709    | 'isConcurrencySafe'
710    | 'isReadOnly'
711    | 'isDestructive'
712    | 'checkPermissions'
713    | 'toAutoClassifierInput'
714    | 'userFacingName'
715  
716  /**
717   * Tool definition accepted by `buildTool`. Same shape as `Tool` but with the
718   * defaultable methods optional — `buildTool` fills them in so callers always
719   * see a complete `Tool`.
720   */
721  export type ToolDef<
722    Input extends AnyObject = AnyObject,
723    Output = unknown,
724    P extends ToolProgressData = ToolProgressData,
725  > = Omit<Tool<Input, Output, P>, DefaultableToolKeys> &
726    Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>
727  
728  /**
729   * Type-level spread mirroring `{ ...TOOL_DEFAULTS, ...def }`. For each
730   * defaultable key: if D provides it (required), D's type wins; if D omits
731   * it or has it optional (inherited from Partial<> in the constraint), the
732   * default fills in. All other keys come from D verbatim — preserving arity,
733   * optional presence, and literal types exactly as `satisfies Tool` did.
734   */
735  type BuiltTool<D> = Omit<D, DefaultableToolKeys> & {
736    [K in DefaultableToolKeys]-?: K extends keyof D
737      ? undefined extends D[K]
738        ? ToolDefaults[K]
739        : D[K]
740      : ToolDefaults[K]
741  }
742  
743  /**
744   * Build a complete `Tool` from a partial definition, filling in safe defaults
745   * for the commonly-stubbed methods. All tool exports should go through this so
746   * that defaults live in one place and callers never need `?.() ?? default`.
747   *
748   * Defaults (fail-closed where it matters):
749   * - `isEnabled` → `true`
750   * - `isConcurrencySafe` → `false` (assume not safe)
751   * - `isReadOnly` → `false` (assume writes)
752   * - `isDestructive` → `false`
753   * - `checkPermissions` → `{ behavior: 'allow', updatedInput }` (defer to general permission system)
754   * - `toAutoClassifierInput` → `''` (skip classifier — security-relevant tools must override)
755   * - `userFacingName` → `name`
756   */
757  const TOOL_DEFAULTS = {
758    isEnabled: () => true,
759    isConcurrencySafe: (_input?: unknown) => false,
760    isReadOnly: (_input?: unknown) => false,
761    isDestructive: (_input?: unknown) => false,
762    checkPermissions: (
763      input: { [key: string]: unknown },
764      _ctx?: ToolUseContext,
765    ): Promise<PermissionResult> =>
766      Promise.resolve({ behavior: 'allow', updatedInput: input }),
767    toAutoClassifierInput: (_input?: unknown) => '',
768    userFacingName: (_input?: unknown) => '',
769  }
770  
771  // The defaults type is the ACTUAL shape of TOOL_DEFAULTS (optional params so
772  // both 0-arg and full-arg call sites type-check — stubs varied in arity and
773  // tests relied on that), not the interface's strict signatures.
774  type ToolDefaults = typeof TOOL_DEFAULTS
775  
776  // D infers the concrete object-literal type from the call site. The
777  // constraint provides contextual typing for method parameters; `any` in
778  // constraint position is structural and never leaks into the return type.
779  // BuiltTool<D> mirrors runtime `{...TOOL_DEFAULTS, ...def}` at the type level.
780  // eslint-disable-next-line @typescript-eslint/no-explicit-any
781  type AnyToolDef = ToolDef<any, any, any>
782  
783  export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
784    // The runtime spread is straightforward; the `as` bridges the gap between
785    // the structural-any constraint and the precise BuiltTool<D> return. The
786    // type semantics are proven by the 0-error typecheck across all 60+ tools.
787    return {
788      ...TOOL_DEFAULTS,
789      userFacingName: () => def.name,
790      ...def,
791    } as BuiltTool<D>
792  }