Tool.ts
29 KB793 lines
src/Tool.ts
1import type {2 ToolResultBlockParam,3 ToolUseBlockParam,4} from '@anthropic-ai/sdk/resources/index.mjs'5import type {6 ElicitRequestURLParams,7 ElicitResult,8} from '@modelcontextprotocol/sdk/types.js'9import type { UUID } from 'crypto'10import type { z } from 'zod/v4'11import type { Command } from './commands.js'12import type { CanUseToolFn } from './hooks/useCanUseTool.js'13import type { ThinkingConfig } from './utils/thinking.js'1415export type ToolInputJSONSchema = {16 [x: string]: unknown17 type: 'object'18 properties?: {19 [x: string]: unknown20 }21}2223import type { Notification } from './context/notifications.js'24import type {25 MCPServerConnection,26 ServerResource,27} from './services/mcp/types.js'28import type {29 AgentDefinition,30 AgentDefinitionsResult,31} from './tools/AgentTool/loadAgentsDir.js'32import 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 cycles42// Import PermissionResult from centralized location to break import cycles43import type {44 AdditionalWorkingDirectory,45 PermissionMode,46 PermissionResult,47} from './types/permissions.js'48// Import tool progress types from centralized location to break import cycles49import type {50 AgentToolProgress,51 BashProgress,52 MCPProgress,53 REPLToolProgress,54 SkillToolProgress,55 TaskOutputProgress,56 ToolProgressData,57 WebSearchProgress,58} from './types/tools.js'59import type { FileStateCache } from './utils/fileStateCache.js'60import type { DenialTrackingState } from './utils/permissions/denialTracking.js'61import type { SystemPrompt } from './utils/systemPromptType.js'62import type { ContentReplacementState } from './utils/toolResultStorage.js'6364// Re-export progress types for backwards compatibility65export type {66 AgentToolProgress,67 BashProgress,68 MCPProgress,69 REPLToolProgress,70 SkillToolProgress,71 TaskOutputProgress,72 WebSearchProgress,73}7475import type { SpinnerMode } from './components/Spinner.js'76import type { QuerySource } from './constants/querySource.js'77import type { SDKStatus } from './entrypoints/agentSdkTypes.js'78import type { AppState } from './state/AppState.js'79import type {80 HookProgress,81 PromptRequest,82 PromptResponse,83} from './types/hooks.js'84import type { AgentId } from './types/ids.js'85import type { DeepImmutable } from './types/utils.js'86import type { AttributionState } from './utils/commitAttribution.js'87import type { FileHistoryState } from './utils/fileHistory.js'88import type { Theme, ThemeName } from './utils/theme.js'8990export type QueryChainTracking = {91 chainId: string92 depth: number93}9495export type ValidationResult =96 | { result: true }97 | {98 result: false99 message: string100 errorCode: number101 }102103export type SetToolJSXFn = (104 args: {105 jsx: React.ReactNode | null106 shouldHidePromptInput: boolean107 shouldContinueAnimation?: true108 showSpinner?: boolean109 isLocalJSXCommand?: boolean110 isImmediate?: boolean111 /** Set to true to clear a local JSX command (e.g., from its onDone callback) */112 clearLocalJSX?: boolean113 } | null,114) => void115116// Import tool permission types from centralized location to break import cycles117import type { ToolPermissionRulesBySource } from './types/permissions.js'118119// Re-export for backwards compatibility120export type { ToolPermissionRulesBySource }121122// Apply DeepImmutable to the imported type123export type ToolPermissionContext = DeepImmutable<{124 mode: PermissionMode125 additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>126 alwaysAllowRules: ToolPermissionRulesBySource127 alwaysDenyRules: ToolPermissionRulesBySource128 alwaysAskRules: ToolPermissionRulesBySource129 isBypassPermissionsModeAvailable: boolean130 isAutoModeAvailable?: boolean131 strippedDangerousRules?: ToolPermissionRulesBySource132 /** When true, permission prompts are auto-denied (e.g., background agents that can't show UI) */133 shouldAvoidPermissionPrompts?: boolean134 /** When true, automated checks (classifier, hooks) are awaited before showing the permission dialog (coordinator workers) */135 awaitAutomatedChecksBeforeDialog?: boolean136 /** Stores the permission mode before model-initiated plan mode entry, so it can be restored on exit */137 prePlanMode?: PermissionMode138}>139140export const getEmptyToolPermissionContext: () => ToolPermissionContext =141 () => ({142 mode: 'default',143 additionalWorkingDirectories: new Map(),144 alwaysAllowRules: {},145 alwaysDenyRules: {},146 alwaysAskRules: {},147 isBypassPermissionsModeAvailable: false,148 })149150export type CompactProgressEvent =151 | {152 type: 'hooks_start'153 hookType: 'pre_compact' | 'post_compact' | 'session_start'154 }155 | { type: 'compact_start' }156 | { type: 'compact_end' }157158export type ToolUseContext = {159 options: {160 commands: Command[]161 debug: boolean162 mainLoopModel: string163 tools: Tools164 verbose: boolean165 thinkingConfig: ThinkingConfig166 mcpClients: MCPServerConnection[]167 mcpResources: Record<string, ServerResource[]>168 isNonInteractiveSession: boolean169 agentDefinitions: AgentDefinitionsResult170 maxBudgetUsd?: number171 /** Custom system prompt that replaces the default system prompt */172 customSystemPrompt?: string173 /** Additional system prompt appended after the main system prompt */174 appendSystemPrompt?: string175 /** Override querySource for analytics tracking */176 querySource?: QuerySource177 /** Optional callback to get the latest tools (e.g., after MCP servers connect mid-query) */178 refreshTools?: () => Tools179 }180 abortController: AbortController181 readFileState: FileStateCache182 getAppState(): AppState183 setAppState(f: (prev: AppState) => AppState): void184 /**185 * Always-shared setAppState for session-scoped infrastructure (background186 * tasks, session hooks). Unlike setAppState, which is no-op for async agents187 * (see createSubagentContext), this always reaches the root store so agents188 * at any nesting depth can register/clean up infrastructure that outlives189 * a single turn. Only set by createSubagentContext; main-thread contexts190 * fall back to setAppState.191 */192 setAppStateForTasks?: (f: (prev: AppState) => AppState) => void193 /**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?: SetToolJSXFn204 addNotification?: (notif: Notification) => void205 /** Append a UI-only system message to the REPL message list. Stripped at the206 * normalizeMessagesForAPI boundary — the Exclude<> makes that type-enforced. */207 appendSystemMessage?: (208 msg: Exclude<SystemMessage, SystemLocalCommandMessage>,209 ) => void210 /** Send an OS-level notification (iTerm2, Kitty, Ghostty, bell, etc.) */211 sendOSNotification?: (opts: {212 message: string213 notificationType: string214 }) => void215 nestedMemoryAttachmentTriggers?: Set<string>216 /**217 * CLAUDE.md paths already injected as nested_memory attachments this218 * session. Dedup for memoryFilesToAttachments — readFileState is an LRU219 * that evicts entries in busy sessions, so its .has() check alone can220 * 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?: boolean227 setInProgressToolUseIDs: (f: (prev: Set<string>) => Set<string>) => void228 /** Only wired in interactive (REPL) contexts; SDK/QueryEngine don't set this. */229 setHasInterruptibleToolInProgress?: (v: boolean) => void230 setResponseLength: (f: (prev: number) => number) => void231 /** 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) => void234 setStreamMode?: (mode: SpinnerMode) => void235 onCompactProgress?: (event: CompactProgressEvent) => void236 setSDKStatus?: (status: SDKStatus) => void237 openMessageSelector?: () => void238 updateFileHistoryState: (239 updater: (prev: FileHistoryState) => FileHistoryState,240 ) => void241 updateAttributionState: (242 updater: (prev: AttributionState) => AttributionState,243 ) => void244 setConversationId?: (id: UUID) => void245 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?: boolean250 messages: Message[]251 fileReadingLimits?: {252 maxTokens?: number253 maxSizeBytes?: number254 }255 globLimits?: {256 maxResults?: number257 }258 toolDecisions?: Map<259 string,260 {261 source: string262 decision: 'accept' | 'reject'263 timestamp: number264 }265 >266 queryTracking?: QueryChainTracking267 /** 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?: string275 criticalSystemReminder_EXPERIMENTAL?: string276 /** When true, preserve toolUseResult on messages even for subagents.277 * Used by in-process teammates whose transcripts are viewable by the user. */278 preserveToolUseResults?: boolean279 /** Local denial tracking state for async subagents whose setAppState is a280 * no-op. Without this, the denial counter never accumulates and the281 * fallback-to-prompting threshold is never reached. Mutable — the282 * permissions code updates it in place. */283 localDenialTracking?: DenialTrackingState284 /**285 * Per-conversation-thread content replacement state for the tool result286 * budget. When present, query.ts applies the aggregate tool result budget.287 * Main thread: REPL provisions once (never resets — stale UUID keys288 * are inert). Subagents: createSubagentContext clones the parent's state289 * by default (cache-sharing forks need identical decisions), or290 * resumeAgentBackground threads one reconstructed from sidechain records.291 */292 contentReplacementState?: ContentReplacementState293 /**294 * Parent's rendered system prompt bytes, frozen at turn start.295 * Used by fork subagents to share the parent's prompt cache — re-calling296 * getSystemPrompt() at fork-spawn time can diverge (GrowthBook cold→warm)297 * and bust the cache. See forkSubagent.ts.298 */299 renderedSystemPrompt?: SystemPrompt300}301302// Re-export ToolProgressData from centralized location303export type { ToolProgressData }304305export type Progress = ToolProgressData | HookProgress306307export type ToolProgress<P extends ToolProgressData> = {308 toolUseID: string309 data: P310}311312export 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}320321export type ToolResult<T> = {322 data: T323 newMessages?: (324 | UserMessage325 | AssistantMessage326 | AttachmentMessage327 | SystemMessage328 )[]329 // contextModifier is only honored for tools that aren't concurrency safe.330 contextModifier?: (context: ToolUseContext) => ToolUseContext331 /** 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}337338export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = (339 progress: ToolProgress<P>,340) => void341342// Type for any schema that outputs an object with string keys343export type AnyObject = z.ZodType<{ [key: string]: unknown }>344345/**346 * Checks if a tool matches the given name (primary name or alias).347 */348export function toolMatchesName(349 tool: { name: string; aliases?: string[] },350 name: string,351): boolean {352 return tool.name === name || (tool.aliases?.includes(name) ?? false)353}354355/**356 * Finds a tool by name or alias from a list of tools.357 */358export function findToolByName(tools: Tools, name: string): Tool | undefined {359 return tools.find(t => toolMatchesName(t, name))360}361362export 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?: string379 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: boolean390 toolPermissionContext: ToolPermissionContext391 tools: Tools392 },393 ): Promise<string>394 readonly inputSchema: Input395 // Type for MCP tools that can specify their input schema directly in JSON Schema format396 // rather than converting from Zod schema397 readonly inputJSONSchema?: ToolInputJSONSchema398 // 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>): boolean402 isConcurrencySafe(input: z.infer<Input>): boolean403 isEnabled(): boolean404 isReadOnly(input: z.infer<Input>): boolean405 /** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */406 isDestructive?(input: z.infer<Input>): boolean407 /**408 * What should happen when the user submits a new message while this tool409 * is running.410 *411 * - `'cancel'` — stop the tool and discard its result412 * - `'block'` — keep running; the new message waits413 *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 operation419 * that should be collapsed into a condensed display in the UI. Examples include420 * 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 collapsed428 */429 isSearchOrReadCommand?(input: z.infer<Input>): {430 isSearch: boolean431 isRead: boolean432 isList?: boolean433 }434 isOpenWorld?(input: z.infer<Input>): boolean435 requiresUserInteraction?(): boolean436 isMcp?: boolean437 isLsp?: boolean438 /**439 * When true, this tool is deferred (sent with defer_loading: true) and requires440 * ToolSearch to be used before it can be called.441 */442 readonly shouldDefer?: boolean443 /**444 * When true, this tool is never deferred — its full schema appears in the445 * initial prompt even when ToolSearch is enabled. For MCP tools, set via446 * `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on447 * turn 1 without a ToolSearch round-trip.448 */449 readonly alwaysLoad?: boolean450 /**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: string457 /**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 preview460 * 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 tool464 * already self-bounds via its own limits).465 */466 maxResultSizeChars: number467 /**468 * When true, enables strict mode for this tool, which causes the API to469 * more strictly adhere to tool instructions and parameter schemas.470 * Only applied when the tengu_tool_pear is enabled.471 */472 readonly strict?: boolean473474 /**475 * Called on copies of tool_use input before observers see it (SDK stream,476 * transcript, canUseTool, PreToolUse/PostToolUse hooks). Mutate in place477 * to add legacy/derived fields. Must be idempotent. The original API-bound478 * input is never mutated (preserves prompt cache). Not re-applied when a479 * hook/permission returns a fresh updatedInput — those own their shape.480 */481 backfillObservableInput?(input: Record<string, unknown>): void482483 /**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 input487 * @param context488 */489 validateInput?(490 input: z.infer<Input>,491 context: ToolUseContext,492 ): Promise<ValidationResult>493494 /**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 input498 * @param context499 */500 checkPermissions(501 input: z.infer<Input>,502 context: ToolUseContext,503 ): Promise<PermissionResult>504505 // Optional method for tools that operate on a file path506 getPath?(input: z.infer<Input>): string507508 /**509 * Prepare a matcher for hook `if` conditions (permission-rule patterns like510 * "git *" from "Bash(git *)"). Called once per hook-input pair; any511 * expensive parsing happens here. Returns a closure that is called per512 * hook pattern. If not implemented, only tool-name-level matching works.513 */514 preparePermissionMatcher?(515 input: z.infer<Input>,516 ): Promise<(pattern: string) => boolean>517518 prompt(options: {519 getToolPermissionContext: () => Promise<ToolPermissionContext>520 tools: Tools521 agents: AgentDefinition[]522 allowedAgentTypes?: string[]523 }): Promise<string>524 userFacingName(input: Partial<z.infer<Input>> | undefined): string525 userFacingNameBackgroundColor?(526 input: Partial<z.infer<Input>> | undefined,527 ): keyof Theme | undefined528 /**529 * Transparent wrappers (e.g. REPL) delegate all rendering to their progress530 * handler, which emits native-looking blocks for each inner tool call.531 * The wrapper itself shows nothing.532 */533 isTransparentWrapper?(): boolean534 /**535 * Returns a short string summary of this tool use for display in compact views.536 * @param input The tool input537 * @returns A short string summary, or null to not display538 */539 getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null540 /**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 input544 * @returns Activity description string, or null to fall back to tool name545 */546 getActivityDescription?(547 input: Partial<z.infer<Input>> | undefined,548 ): string | null549 /**550 * Returns a compact representation of this tool use for the auto-mode551 * security classifier. Examples: `ls -la` for Bash, `/tmp/x: new content`552 * for Edit. Return '' to skip this tool in the classifier transcript553 * (e.g. tools with no security relevance). May return an object to avoid554 * double-encoding when the caller JSON-wraps the value.555 */556 toAutoClassifierInput(input: z.infer<Input>): unknown557 mapToolResultToToolResultBlockParam(558 content: Output,559 toolUseID: string,560 ): ToolResultBlockParam561 /**562 * Optional. When omitted, the tool result renders nothing (same as returning563 * null). Omit for tools whose results are surfaced elsewhere (e.g., TodoWrite564 * updates the todo panel, not the transcript).565 */566 renderToolResultMessage?(567 content: Output,568 progressMessagesForMessage: ProgressMessage<P>[],569 options: {570 style?: 'condensed'571 theme: ThemeName572 tools: Tools573 verbose: boolean574 isTranscriptMode?: boolean575 isBriefOnly?: boolean576 /** Original tool_use input, when available. Useful for compact result577 * summaries that reference what was requested (e.g. "Sent to #foo"). */578 input?: unknown579 },580 ): React.ReactNode581 /**582 * Flattened text of what renderToolResultMessage shows IN TRANSCRIPT583 * MODE (verbose=true, isTranscriptMode=true). For transcript search584 * indexing: the index counts occurrences in this string, the highlight585 * overlay scans the actual screen buffer. For count ≡ highlight, this586 * must return the text that ends up visible — not the model-facing587 * serialization from mapToolResultToToolResultBlockParam (which adds588 * 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 claimed592 * 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.tsx596 * 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): string600 /**601 * Render the tool use message. Note that `input` is partial because we render602 * the message as soon as possible, possibly before tool parameters have fully603 * streamed in.604 */605 renderToolUseMessage(606 input: Partial<z.infer<Input>>,607 options: { theme: ThemeName; verbose: boolean; commands?: Command[] },608 ): React.ReactNode609 /**610 * Returns true when the non-verbose rendering of this output is truncated611 * (i.e., clicking to expand would reveal more content). Gates612 * click-to-expand in fullscreen — only messages where verbose actually613 * shows more get a hover/click affordance. Unset means never truncated.614 */615 isResultTruncated?(output: Output): boolean616 /**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.ReactNode622 /**623 * Optional. When omitted, no progress UI is shown while the tool runs.624 */625 renderToolUseProgressMessage?(626 progressMessagesForMessage: ProgressMessage<P>[],627 options: {628 tools: Tools629 verbose: boolean630 terminalSize?: { columns: number; rows: number }631 inProgressToolCallCount?: number632 isTranscriptMode?: boolean633 },634 ): React.ReactNode635 renderToolUseQueuedMessage?(): React.ReactNode636 /**637 * Optional. When omitted, falls back to <FallbackToolUseRejectedMessage />.638 * Only define this for tools that need custom rejection UI (e.g., file edits639 * that show the rejected diff).640 */641 renderToolUseRejectedMessage?(642 input: z.infer<Input>,643 options: {644 columns: number645 messages: Message[]646 style?: 'condensed'647 theme: ThemeName648 tools: Tools649 verbose: boolean650 progressMessagesForMessage: ProgressMessage<P>[]651 isTranscriptMode?: boolean652 },653 ): React.ReactNode654 /**655 * Optional. When omitted, falls back to <FallbackToolUseErrorMessage />.656 * Only define this for tools that need custom error UI (e.g., search tools657 * 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: Tools664 verbose: boolean665 isTranscriptMode?: boolean666 },667 ): React.ReactNode668669 /**670 * Renders multiple parallel instances of this tool as a group.671 * @returns React node to render, or null to fall back to individual rendering672 */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 rendering677 */678 renderGroupedToolUse?(679 toolUses: Array<{680 param: ToolUseBlockParam681 isResolved: boolean682 isError: boolean683 isInProgress: boolean684 progressMessages: ProgressMessage<P>[]685 result?: {686 param: ToolResultBlockParam687 output: unknown688 }689 }>,690 options: {691 shouldAnimate: boolean692 tools: Tools693 },694 ): React.ReactNode | null695}696697/**698 * A collection of tools. Use this type instead of `Tool[]` to make it easier699 * to track where tool sets are assembled, passed, and filtered across the codebase.700 */701export type Tools = readonly Tool[]702703/**704 * Methods that `buildTool` supplies a default for. A `ToolDef` may omit these;705 * the resulting `Tool` always has them.706 */707type DefaultableToolKeys =708 | 'isEnabled'709 | 'isConcurrencySafe'710 | 'isReadOnly'711 | 'isDestructive'712 | 'checkPermissions'713 | 'toAutoClassifierInput'714 | 'userFacingName'715716/**717 * Tool definition accepted by `buildTool`. Same shape as `Tool` but with the718 * defaultable methods optional — `buildTool` fills them in so callers always719 * see a complete `Tool`.720 */721export 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>>727728/**729 * Type-level spread mirroring `{ ...TOOL_DEFAULTS, ...def }`. For each730 * defaultable key: if D provides it (required), D's type wins; if D omits731 * it or has it optional (inherited from Partial<> in the constraint), the732 * default fills in. All other keys come from D verbatim — preserving arity,733 * optional presence, and literal types exactly as `satisfies Tool` did.734 */735type BuiltTool<D> = Omit<D, DefaultableToolKeys> & {736 [K in DefaultableToolKeys]-?: K extends keyof D737 ? undefined extends D[K]738 ? ToolDefaults[K]739 : D[K]740 : ToolDefaults[K]741}742743/**744 * Build a complete `Tool` from a partial definition, filling in safe defaults745 * for the commonly-stubbed methods. All tool exports should go through this so746 * 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 */757const 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}770771// The defaults type is the ACTUAL shape of TOOL_DEFAULTS (optional params so772// both 0-arg and full-arg call sites type-check — stubs varied in arity and773// tests relied on that), not the interface's strict signatures.774type ToolDefaults = typeof TOOL_DEFAULTS775776// D infers the concrete object-literal type from the call site. The777// constraint provides contextual typing for method parameters; `any` in778// 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-any781type AnyToolDef = ToolDef<any, any, any>782783export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {784 // The runtime spread is straightforward; the `as` bridges the gap between785 // the structural-any constraint and the precise BuiltTool<D> return. The786 // 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}793