793 lines
29 KiB
TypeScript
793 lines
29 KiB
TypeScript
import type {
|
||
ToolResultBlockParam,
|
||
ToolUseBlockParam,
|
||
} from '@anthropic-ai/sdk/resources/index.mjs'
|
||
import type {
|
||
ElicitRequestURLParams,
|
||
ElicitResult,
|
||
} from '@modelcontextprotocol/sdk/types.js'
|
||
import type { UUID } from 'crypto'
|
||
import type { z } from 'zod/v4'
|
||
import type { Command } from './commands.js'
|
||
import type { CanUseToolFn } from './hooks/useCanUseTool.js'
|
||
import type { ThinkingConfig } from './utils/thinking.js'
|
||
|
||
export type ToolInputJSONSchema = {
|
||
[x: string]: unknown
|
||
type: 'object'
|
||
properties?: {
|
||
[x: string]: unknown
|
||
}
|
||
}
|
||
|
||
import type { Notification } from './context/notifications.js'
|
||
import type {
|
||
MCPServerConnection,
|
||
ServerResource,
|
||
} from './services/mcp/types.js'
|
||
import type {
|
||
AgentDefinition,
|
||
AgentDefinitionsResult,
|
||
} from './tools/AgentTool/loadAgentsDir.js'
|
||
import type {
|
||
AssistantMessage,
|
||
AttachmentMessage,
|
||
Message,
|
||
ProgressMessage,
|
||
SystemLocalCommandMessage,
|
||
SystemMessage,
|
||
UserMessage,
|
||
} from './types/message.js'
|
||
// Import permission types from centralized location to break import cycles
|
||
// Import PermissionResult from centralized location to break import cycles
|
||
import type {
|
||
AdditionalWorkingDirectory,
|
||
PermissionMode,
|
||
PermissionResult,
|
||
} from './types/permissions.js'
|
||
// Import tool progress types from centralized location to break import cycles
|
||
import type {
|
||
AgentToolProgress,
|
||
BashProgress,
|
||
MCPProgress,
|
||
REPLToolProgress,
|
||
SkillToolProgress,
|
||
TaskOutputProgress,
|
||
ToolProgressData,
|
||
WebSearchProgress,
|
||
} from './types/tools.js'
|
||
import type { FileStateCache } from './utils/fileStateCache.js'
|
||
import type { DenialTrackingState } from './utils/permissions/denialTracking.js'
|
||
import type { SystemPrompt } from './utils/systemPromptType.js'
|
||
import type { ContentReplacementState } from './utils/toolResultStorage.js'
|
||
|
||
// Re-export progress types for backwards compatibility
|
||
export type {
|
||
AgentToolProgress,
|
||
BashProgress,
|
||
MCPProgress,
|
||
REPLToolProgress,
|
||
SkillToolProgress,
|
||
TaskOutputProgress,
|
||
WebSearchProgress,
|
||
}
|
||
|
||
import type { SpinnerMode } from './components/Spinner.js'
|
||
import type { QuerySource } from './constants/querySource.js'
|
||
import type { SDKStatus } from './entrypoints/agentSdkTypes.js'
|
||
import type { AppState } from './state/AppState.js'
|
||
import type {
|
||
HookProgress,
|
||
PromptRequest,
|
||
PromptResponse,
|
||
} from './types/hooks.js'
|
||
import type { AgentId } from './types/ids.js'
|
||
import type { DeepImmutable } from './types/utils.js'
|
||
import type { AttributionState } from './utils/commitAttribution.js'
|
||
import type { FileHistoryState } from './utils/fileHistory.js'
|
||
import type { Theme, ThemeName } from './utils/theme.js'
|
||
|
||
export type QueryChainTracking = {
|
||
chainId: string
|
||
depth: number
|
||
}
|
||
|
||
export type ValidationResult =
|
||
| { result: true }
|
||
| {
|
||
result: false
|
||
message: string
|
||
errorCode: number
|
||
}
|
||
|
||
export type SetToolJSXFn = (
|
||
args: {
|
||
jsx: React.ReactNode | null
|
||
shouldHidePromptInput: boolean
|
||
shouldContinueAnimation?: true
|
||
showSpinner?: boolean
|
||
isLocalJSXCommand?: boolean
|
||
isImmediate?: boolean
|
||
/** Set to true to clear a local JSX command (e.g., from its onDone callback) */
|
||
clearLocalJSX?: boolean
|
||
} | null,
|
||
) => void
|
||
|
||
// Import tool permission types from centralized location to break import cycles
|
||
import type { ToolPermissionRulesBySource } from './types/permissions.js'
|
||
|
||
// Re-export for backwards compatibility
|
||
export type { ToolPermissionRulesBySource }
|
||
|
||
// Apply DeepImmutable to the imported type
|
||
export type ToolPermissionContext = DeepImmutable<{
|
||
mode: PermissionMode
|
||
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
|
||
alwaysAllowRules: ToolPermissionRulesBySource
|
||
alwaysDenyRules: ToolPermissionRulesBySource
|
||
alwaysAskRules: ToolPermissionRulesBySource
|
||
isBypassPermissionsModeAvailable: boolean
|
||
isAutoModeAvailable?: boolean
|
||
strippedDangerousRules?: ToolPermissionRulesBySource
|
||
/** When true, permission prompts are auto-denied (e.g., background agents that can't show UI) */
|
||
shouldAvoidPermissionPrompts?: boolean
|
||
/** When true, automated checks (classifier, hooks) are awaited before showing the permission dialog (coordinator workers) */
|
||
awaitAutomatedChecksBeforeDialog?: boolean
|
||
/** Stores the permission mode before model-initiated plan mode entry, so it can be restored on exit */
|
||
prePlanMode?: PermissionMode
|
||
}>
|
||
|
||
export const getEmptyToolPermissionContext: () => ToolPermissionContext =
|
||
() => ({
|
||
mode: 'default',
|
||
additionalWorkingDirectories: new Map(),
|
||
alwaysAllowRules: {},
|
||
alwaysDenyRules: {},
|
||
alwaysAskRules: {},
|
||
isBypassPermissionsModeAvailable: false,
|
||
})
|
||
|
||
export type CompactProgressEvent =
|
||
| {
|
||
type: 'hooks_start'
|
||
hookType: 'pre_compact' | 'post_compact' | 'session_start'
|
||
}
|
||
| { type: 'compact_start' }
|
||
| { type: 'compact_end' }
|
||
|
||
export type ToolUseContext = {
|
||
options: {
|
||
commands: Command[]
|
||
debug: boolean
|
||
mainLoopModel: string
|
||
tools: Tools
|
||
verbose: boolean
|
||
thinkingConfig: ThinkingConfig
|
||
mcpClients: MCPServerConnection[]
|
||
mcpResources: Record<string, ServerResource[]>
|
||
isNonInteractiveSession: boolean
|
||
agentDefinitions: AgentDefinitionsResult
|
||
maxBudgetUsd?: number
|
||
/** Custom system prompt that replaces the default system prompt */
|
||
customSystemPrompt?: string
|
||
/** Additional system prompt appended after the main system prompt */
|
||
appendSystemPrompt?: string
|
||
/** Override querySource for analytics tracking */
|
||
querySource?: QuerySource
|
||
/** Optional callback to get the latest tools (e.g., after MCP servers connect mid-query) */
|
||
refreshTools?: () => Tools
|
||
}
|
||
abortController: AbortController
|
||
readFileState: FileStateCache
|
||
getAppState(): AppState
|
||
setAppState(f: (prev: AppState) => AppState): void
|
||
/**
|
||
* Always-shared setAppState for session-scoped infrastructure (background
|
||
* tasks, session hooks). Unlike setAppState, which is no-op for async agents
|
||
* (see createSubagentContext), this always reaches the root store so agents
|
||
* at any nesting depth can register/clean up infrastructure that outlives
|
||
* a single turn. Only set by createSubagentContext; main-thread contexts
|
||
* fall back to setAppState.
|
||
*/
|
||
setAppStateForTasks?: (f: (prev: AppState) => AppState) => void
|
||
/**
|
||
* Optional handler for URL elicitations triggered by tool call errors (-32042).
|
||
* In print/SDK mode, this delegates to structuredIO.handleElicitation.
|
||
* In REPL mode, this is undefined and the queue-based UI path is used.
|
||
*/
|
||
handleElicitation?: (
|
||
serverName: string,
|
||
params: ElicitRequestURLParams,
|
||
signal: AbortSignal,
|
||
) => Promise<ElicitResult>
|
||
setToolJSX?: SetToolJSXFn
|
||
addNotification?: (notif: Notification) => void
|
||
/** Append a UI-only system message to the REPL message list. Stripped at the
|
||
* normalizeMessagesForAPI boundary — the Exclude<> makes that type-enforced. */
|
||
appendSystemMessage?: (
|
||
msg: Exclude<SystemMessage, SystemLocalCommandMessage>,
|
||
) => void
|
||
/** Send an OS-level notification (iTerm2, Kitty, Ghostty, bell, etc.) */
|
||
sendOSNotification?: (opts: {
|
||
message: string
|
||
notificationType: string
|
||
}) => void
|
||
nestedMemoryAttachmentTriggers?: Set<string>
|
||
/**
|
||
* CLAUDE.md paths already injected as nested_memory attachments this
|
||
* session. Dedup for memoryFilesToAttachments — readFileState is an LRU
|
||
* that evicts entries in busy sessions, so its .has() check alone can
|
||
* re-inject the same CLAUDE.md dozens of times.
|
||
*/
|
||
loadedNestedMemoryPaths?: Set<string>
|
||
dynamicSkillDirTriggers?: Set<string>
|
||
/** Skill names surfaced via skill_discovery this session. Telemetry only (feeds was_discovered). */
|
||
discoveredSkillNames?: Set<string>
|
||
userModified?: boolean
|
||
setInProgressToolUseIDs: (f: (prev: Set<string>) => Set<string>) => void
|
||
/** Only wired in interactive (REPL) contexts; SDK/QueryEngine don't set this. */
|
||
setHasInterruptibleToolInProgress?: (v: boolean) => void
|
||
setResponseLength: (f: (prev: number) => number) => void
|
||
/** Ant-only: push a new API metrics entry for OTPS tracking.
|
||
* Called by subagent streaming when a new API request starts. */
|
||
pushApiMetricsEntry?: (ttftMs: number) => void
|
||
setStreamMode?: (mode: SpinnerMode) => void
|
||
onCompactProgress?: (event: CompactProgressEvent) => void
|
||
setSDKStatus?: (status: SDKStatus) => void
|
||
openMessageSelector?: () => void
|
||
updateFileHistoryState: (
|
||
updater: (prev: FileHistoryState) => FileHistoryState,
|
||
) => void
|
||
updateAttributionState: (
|
||
updater: (prev: AttributionState) => AttributionState,
|
||
) => void
|
||
setConversationId?: (id: UUID) => void
|
||
agentId?: AgentId // Only set for subagents; use getSessionId() for session ID. Hooks use this to distinguish subagent calls.
|
||
agentType?: string // Subagent type name. For the main thread's --agent type, hooks fall back to getMainThreadAgentType().
|
||
/** When true, canUseTool must always be called even when hooks auto-approve.
|
||
* Used by speculation for overlay file path rewriting. */
|
||
requireCanUseTool?: boolean
|
||
messages: Message[]
|
||
fileReadingLimits?: {
|
||
maxTokens?: number
|
||
maxSizeBytes?: number
|
||
}
|
||
globLimits?: {
|
||
maxResults?: number
|
||
}
|
||
toolDecisions?: Map<
|
||
string,
|
||
{
|
||
source: string
|
||
decision: 'accept' | 'reject'
|
||
timestamp: number
|
||
}
|
||
>
|
||
queryTracking?: QueryChainTracking
|
||
/** Callback factory for requesting interactive prompts from the user.
|
||
* Returns a prompt callback bound to the given source name.
|
||
* Only available in interactive (REPL) contexts. */
|
||
requestPrompt?: (
|
||
sourceName: string,
|
||
toolInputSummary?: string | null,
|
||
) => (request: PromptRequest) => Promise<PromptResponse>
|
||
toolUseId?: string
|
||
criticalSystemReminder_EXPERIMENTAL?: string
|
||
/** When true, preserve toolUseResult on messages even for subagents.
|
||
* Used by in-process teammates whose transcripts are viewable by the user. */
|
||
preserveToolUseResults?: boolean
|
||
/** Local denial tracking state for async subagents whose setAppState is a
|
||
* no-op. Without this, the denial counter never accumulates and the
|
||
* fallback-to-prompting threshold is never reached. Mutable — the
|
||
* permissions code updates it in place. */
|
||
localDenialTracking?: DenialTrackingState
|
||
/**
|
||
* Per-conversation-thread content replacement state for the tool result
|
||
* budget. When present, query.ts applies the aggregate tool result budget.
|
||
* Main thread: REPL provisions once (never resets — stale UUID keys
|
||
* are inert). Subagents: createSubagentContext clones the parent's state
|
||
* by default (cache-sharing forks need identical decisions), or
|
||
* resumeAgentBackground threads one reconstructed from sidechain records.
|
||
*/
|
||
contentReplacementState?: ContentReplacementState
|
||
/**
|
||
* Parent's rendered system prompt bytes, frozen at turn start.
|
||
* Used by fork subagents to share the parent's prompt cache — re-calling
|
||
* getSystemPrompt() at fork-spawn time can diverge (GrowthBook cold→warm)
|
||
* and bust the cache. See forkSubagent.ts.
|
||
*/
|
||
renderedSystemPrompt?: SystemPrompt
|
||
}
|
||
|
||
// Re-export ToolProgressData from centralized location
|
||
export type { ToolProgressData }
|
||
|
||
export type Progress = ToolProgressData | HookProgress
|
||
|
||
export type ToolProgress<P extends ToolProgressData> = {
|
||
toolUseID: string
|
||
data: P
|
||
}
|
||
|
||
export function filterToolProgressMessages(
|
||
progressMessagesForMessage: ProgressMessage[],
|
||
): ProgressMessage<ToolProgressData>[] {
|
||
return progressMessagesForMessage.filter(
|
||
(msg): msg is ProgressMessage<ToolProgressData> =>
|
||
msg.data?.type !== 'hook_progress',
|
||
)
|
||
}
|
||
|
||
export type ToolResult<T> = {
|
||
data: T
|
||
newMessages?: (
|
||
| UserMessage
|
||
| AssistantMessage
|
||
| AttachmentMessage
|
||
| SystemMessage
|
||
)[]
|
||
// contextModifier is only honored for tools that aren't concurrency safe.
|
||
contextModifier?: (context: ToolUseContext) => ToolUseContext
|
||
/** MCP protocol metadata (structuredContent, _meta) to pass through to SDK consumers */
|
||
mcpMeta?: {
|
||
_meta?: Record<string, unknown>
|
||
structuredContent?: Record<string, unknown>
|
||
}
|
||
}
|
||
|
||
export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = (
|
||
progress: ToolProgress<P>,
|
||
) => void
|
||
|
||
// Type for any schema that outputs an object with string keys
|
||
export type AnyObject = z.ZodType<{ [key: string]: unknown }>
|
||
|
||
/**
|
||
* Checks if a tool matches the given name (primary name or alias).
|
||
*/
|
||
export function toolMatchesName(
|
||
tool: { name: string; aliases?: string[] },
|
||
name: string,
|
||
): boolean {
|
||
return tool.name === name || (tool.aliases?.includes(name) ?? false)
|
||
}
|
||
|
||
/**
|
||
* Finds a tool by name or alias from a list of tools.
|
||
*/
|
||
export function findToolByName(tools: Tools, name: string): Tool | undefined {
|
||
return tools.find(t => toolMatchesName(t, name))
|
||
}
|
||
|
||
export type Tool<
|
||
Input extends AnyObject = AnyObject,
|
||
Output = unknown,
|
||
P extends ToolProgressData = ToolProgressData,
|
||
> = {
|
||
/**
|
||
* Optional aliases for backwards compatibility when a tool is renamed.
|
||
* The tool can be looked up by any of these names in addition to its primary name.
|
||
*/
|
||
aliases?: string[]
|
||
/**
|
||
* One-line capability phrase used by ToolSearch for keyword matching.
|
||
* Helps the model find this tool via keyword search when it's deferred.
|
||
* 3–10 words, no trailing period.
|
||
* Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit).
|
||
*/
|
||
searchHint?: string
|
||
call(
|
||
args: z.infer<Input>,
|
||
context: ToolUseContext,
|
||
canUseTool: CanUseToolFn,
|
||
parentMessage: AssistantMessage,
|
||
onProgress?: ToolCallProgress<P>,
|
||
): Promise<ToolResult<Output>>
|
||
description(
|
||
input: z.infer<Input>,
|
||
options: {
|
||
isNonInteractiveSession: boolean
|
||
toolPermissionContext: ToolPermissionContext
|
||
tools: Tools
|
||
},
|
||
): Promise<string>
|
||
readonly inputSchema: Input
|
||
// Type for MCP tools that can specify their input schema directly in JSON Schema format
|
||
// rather than converting from Zod schema
|
||
readonly inputJSONSchema?: ToolInputJSONSchema
|
||
// Optional because TungstenTool doesn't define this. TODO: Make it required.
|
||
// When we do that, we can also go through and make this a bit more type-safe.
|
||
outputSchema?: z.ZodType<unknown>
|
||
inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
|
||
isConcurrencySafe(input: z.infer<Input>): boolean
|
||
isEnabled(): boolean
|
||
isReadOnly(input: z.infer<Input>): boolean
|
||
/** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */
|
||
isDestructive?(input: z.infer<Input>): boolean
|
||
/**
|
||
* What should happen when the user submits a new message while this tool
|
||
* is running.
|
||
*
|
||
* - `'cancel'` — stop the tool and discard its result
|
||
* - `'block'` — keep running; the new message waits
|
||
*
|
||
* Defaults to `'block'` when not implemented.
|
||
*/
|
||
interruptBehavior?(): 'cancel' | 'block'
|
||
/**
|
||
* Returns information about whether this tool use is a search or read operation
|
||
* that should be collapsed into a condensed display in the UI. Examples include
|
||
* file searching (Grep, Glob), file reading (Read), and bash commands like find,
|
||
* grep, wc, etc.
|
||
*
|
||
* Returns an object indicating whether the operation is a search or read operation:
|
||
* - `isSearch: true` for search operations (grep, find, glob patterns)
|
||
* - `isRead: true` for read operations (cat, head, tail, file read)
|
||
* - `isList: true` for directory-listing operations (ls, tree, du)
|
||
* - All can be false if the operation shouldn't be collapsed
|
||
*/
|
||
isSearchOrReadCommand?(input: z.infer<Input>): {
|
||
isSearch: boolean
|
||
isRead: boolean
|
||
isList?: boolean
|
||
}
|
||
isOpenWorld?(input: z.infer<Input>): boolean
|
||
requiresUserInteraction?(): boolean
|
||
isMcp?: boolean
|
||
isLsp?: boolean
|
||
/**
|
||
* When true, this tool is deferred (sent with defer_loading: true) and requires
|
||
* ToolSearch to be used before it can be called.
|
||
*/
|
||
readonly shouldDefer?: boolean
|
||
/**
|
||
* When true, this tool is never deferred — its full schema appears in the
|
||
* initial prompt even when ToolSearch is enabled. For MCP tools, set via
|
||
* `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on
|
||
* turn 1 without a ToolSearch round-trip.
|
||
*/
|
||
readonly alwaysLoad?: boolean
|
||
/**
|
||
* For MCP tools: the server and tool names as received from the MCP server (unnormalized).
|
||
* Present on all MCP tools regardless of whether `name` is prefixed (mcp__server__tool)
|
||
* or unprefixed (CLAUDE_AGENT_SDK_MCP_NO_PREFIX mode).
|
||
*/
|
||
mcpInfo?: { serverName: string; toolName: string }
|
||
readonly name: string
|
||
/**
|
||
* Maximum size in characters for tool result before it gets persisted to disk.
|
||
* When exceeded, the result is saved to a file and Claude receives a preview
|
||
* with the file path instead of the full content.
|
||
*
|
||
* Set to Infinity for tools whose output must never be persisted (e.g. Read,
|
||
* where persisting creates a circular Read→file→Read loop and the tool
|
||
* already self-bounds via its own limits).
|
||
*/
|
||
maxResultSizeChars: number
|
||
/**
|
||
* When true, enables strict mode for this tool, which causes the API to
|
||
* more strictly adhere to tool instructions and parameter schemas.
|
||
* Only applied when the tengu_tool_pear is enabled.
|
||
*/
|
||
readonly strict?: boolean
|
||
|
||
/**
|
||
* Called on copies of tool_use input before observers see it (SDK stream,
|
||
* transcript, canUseTool, PreToolUse/PostToolUse hooks). Mutate in place
|
||
* to add legacy/derived fields. Must be idempotent. The original API-bound
|
||
* input is never mutated (preserves prompt cache). Not re-applied when a
|
||
* hook/permission returns a fresh updatedInput — those own their shape.
|
||
*/
|
||
backfillObservableInput?(input: Record<string, unknown>): void
|
||
|
||
/**
|
||
* Determines if this tool is allowed to run with this input in the current context.
|
||
* It informs the model of why the tool use failed, and does not directly display any UI.
|
||
* @param input
|
||
* @param context
|
||
*/
|
||
validateInput?(
|
||
input: z.infer<Input>,
|
||
context: ToolUseContext,
|
||
): Promise<ValidationResult>
|
||
|
||
/**
|
||
* Determines if the user is asked for permission. Only called after validateInput() passes.
|
||
* General permission logic is in permissions.ts. This method contains tool-specific logic.
|
||
* @param input
|
||
* @param context
|
||
*/
|
||
checkPermissions(
|
||
input: z.infer<Input>,
|
||
context: ToolUseContext,
|
||
): Promise<PermissionResult>
|
||
|
||
// Optional method for tools that operate on a file path
|
||
getPath?(input: z.infer<Input>): string
|
||
|
||
/**
|
||
* Prepare a matcher for hook `if` conditions (permission-rule patterns like
|
||
* "git *" from "Bash(git *)"). Called once per hook-input pair; any
|
||
* expensive parsing happens here. Returns a closure that is called per
|
||
* hook pattern. If not implemented, only tool-name-level matching works.
|
||
*/
|
||
preparePermissionMatcher?(
|
||
input: z.infer<Input>,
|
||
): Promise<(pattern: string) => boolean>
|
||
|
||
prompt(options: {
|
||
getToolPermissionContext: () => Promise<ToolPermissionContext>
|
||
tools: Tools
|
||
agents: AgentDefinition[]
|
||
allowedAgentTypes?: string[]
|
||
}): Promise<string>
|
||
userFacingName(input: Partial<z.infer<Input>> | undefined): string
|
||
userFacingNameBackgroundColor?(
|
||
input: Partial<z.infer<Input>> | undefined,
|
||
): keyof Theme | undefined
|
||
/**
|
||
* Transparent wrappers (e.g. REPL) delegate all rendering to their progress
|
||
* handler, which emits native-looking blocks for each inner tool call.
|
||
* The wrapper itself shows nothing.
|
||
*/
|
||
isTransparentWrapper?(): boolean
|
||
/**
|
||
* Returns a short string summary of this tool use for display in compact views.
|
||
* @param input The tool input
|
||
* @returns A short string summary, or null to not display
|
||
*/
|
||
getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null
|
||
/**
|
||
* Returns a human-readable present-tense activity description for spinner display.
|
||
* Example: "Reading src/foo.ts", "Running bun test", "Searching for pattern"
|
||
* @param input The tool input
|
||
* @returns Activity description string, or null to fall back to tool name
|
||
*/
|
||
getActivityDescription?(
|
||
input: Partial<z.infer<Input>> | undefined,
|
||
): string | null
|
||
/**
|
||
* Returns a compact representation of this tool use for the auto-mode
|
||
* security classifier. Examples: `ls -la` for Bash, `/tmp/x: new content`
|
||
* for Edit. Return '' to skip this tool in the classifier transcript
|
||
* (e.g. tools with no security relevance). May return an object to avoid
|
||
* double-encoding when the caller JSON-wraps the value.
|
||
*/
|
||
toAutoClassifierInput(input: z.infer<Input>): unknown
|
||
mapToolResultToToolResultBlockParam(
|
||
content: Output,
|
||
toolUseID: string,
|
||
): ToolResultBlockParam
|
||
/**
|
||
* Optional. When omitted, the tool result renders nothing (same as returning
|
||
* null). Omit for tools whose results are surfaced elsewhere (e.g., TodoWrite
|
||
* updates the todo panel, not the transcript).
|
||
*/
|
||
renderToolResultMessage?(
|
||
content: Output,
|
||
progressMessagesForMessage: ProgressMessage<P>[],
|
||
options: {
|
||
style?: 'condensed'
|
||
theme: ThemeName
|
||
tools: Tools
|
||
verbose: boolean
|
||
isTranscriptMode?: boolean
|
||
isBriefOnly?: boolean
|
||
/** Original tool_use input, when available. Useful for compact result
|
||
* summaries that reference what was requested (e.g. "Sent to #foo"). */
|
||
input?: unknown
|
||
},
|
||
): React.ReactNode
|
||
/**
|
||
* Flattened text of what renderToolResultMessage shows IN TRANSCRIPT
|
||
* MODE (verbose=true, isTranscriptMode=true). For transcript search
|
||
* indexing: the index counts occurrences in this string, the highlight
|
||
* overlay scans the actual screen buffer. For count ≡ highlight, this
|
||
* must return the text that ends up visible — not the model-facing
|
||
* serialization from mapToolResultToToolResultBlockParam (which adds
|
||
* system-reminders, persisted-output wrappers).
|
||
*
|
||
* Chrome can be skipped (under-count is fine). "Found 3 files in 12ms"
|
||
* isn't worth indexing. Phantoms are not fine — text that's claimed
|
||
* here but doesn't render is a count≠highlight bug.
|
||
*
|
||
* Optional: omitted → field-name heuristic in transcriptSearch.ts.
|
||
* Drift caught by test/utils/transcriptSearch.renderFidelity.test.tsx
|
||
* which renders sample outputs and flags text that's indexed-but-not-
|
||
* rendered (phantom) or rendered-but-not-indexed (under-count warning).
|
||
*/
|
||
extractSearchText?(out: Output): string
|
||
/**
|
||
* Render the tool use message. Note that `input` is partial because we render
|
||
* the message as soon as possible, possibly before tool parameters have fully
|
||
* streamed in.
|
||
*/
|
||
renderToolUseMessage(
|
||
input: Partial<z.infer<Input>>,
|
||
options: { theme: ThemeName; verbose: boolean; commands?: Command[] },
|
||
): React.ReactNode
|
||
/**
|
||
* Returns true when the non-verbose rendering of this output is truncated
|
||
* (i.e., clicking to expand would reveal more content). Gates
|
||
* click-to-expand in fullscreen — only messages where verbose actually
|
||
* shows more get a hover/click affordance. Unset means never truncated.
|
||
*/
|
||
isResultTruncated?(output: Output): boolean
|
||
/**
|
||
* Renders an optional tag to display after the tool use message.
|
||
* Used for additional metadata like timeout, model, resume ID, etc.
|
||
* Returns null to not display anything.
|
||
*/
|
||
renderToolUseTag?(input: Partial<z.infer<Input>>): React.ReactNode
|
||
/**
|
||
* Optional. When omitted, no progress UI is shown while the tool runs.
|
||
*/
|
||
renderToolUseProgressMessage?(
|
||
progressMessagesForMessage: ProgressMessage<P>[],
|
||
options: {
|
||
tools: Tools
|
||
verbose: boolean
|
||
terminalSize?: { columns: number; rows: number }
|
||
inProgressToolCallCount?: number
|
||
isTranscriptMode?: boolean
|
||
},
|
||
): React.ReactNode
|
||
renderToolUseQueuedMessage?(): React.ReactNode
|
||
/**
|
||
* Optional. When omitted, falls back to <FallbackToolUseRejectedMessage />.
|
||
* Only define this for tools that need custom rejection UI (e.g., file edits
|
||
* that show the rejected diff).
|
||
*/
|
||
renderToolUseRejectedMessage?(
|
||
input: z.infer<Input>,
|
||
options: {
|
||
columns: number
|
||
messages: Message[]
|
||
style?: 'condensed'
|
||
theme: ThemeName
|
||
tools: Tools
|
||
verbose: boolean
|
||
progressMessagesForMessage: ProgressMessage<P>[]
|
||
isTranscriptMode?: boolean
|
||
},
|
||
): React.ReactNode
|
||
/**
|
||
* Optional. When omitted, falls back to <FallbackToolUseErrorMessage />.
|
||
* Only define this for tools that need custom error UI (e.g., search tools
|
||
* that show "File not found" instead of the raw error).
|
||
*/
|
||
renderToolUseErrorMessage?(
|
||
result: ToolResultBlockParam['content'],
|
||
options: {
|
||
progressMessagesForMessage: ProgressMessage<P>[]
|
||
tools: Tools
|
||
verbose: boolean
|
||
isTranscriptMode?: boolean
|
||
},
|
||
): React.ReactNode
|
||
|
||
/**
|
||
* Renders multiple parallel instances of this tool as a group.
|
||
* @returns React node to render, or null to fall back to individual rendering
|
||
*/
|
||
/**
|
||
* Renders multiple tool uses as a group (non-verbose mode only).
|
||
* In verbose mode, individual tool uses render at their original positions.
|
||
* @returns React node to render, or null to fall back to individual rendering
|
||
*/
|
||
renderGroupedToolUse?(
|
||
toolUses: Array<{
|
||
param: ToolUseBlockParam
|
||
isResolved: boolean
|
||
isError: boolean
|
||
isInProgress: boolean
|
||
progressMessages: ProgressMessage<P>[]
|
||
result?: {
|
||
param: ToolResultBlockParam
|
||
output: unknown
|
||
}
|
||
}>,
|
||
options: {
|
||
shouldAnimate: boolean
|
||
tools: Tools
|
||
},
|
||
): React.ReactNode | null
|
||
}
|
||
|
||
/**
|
||
* A collection of tools. Use this type instead of `Tool[]` to make it easier
|
||
* to track where tool sets are assembled, passed, and filtered across the codebase.
|
||
*/
|
||
export type Tools = readonly Tool[]
|
||
|
||
/**
|
||
* Methods that `buildTool` supplies a default for. A `ToolDef` may omit these;
|
||
* the resulting `Tool` always has them.
|
||
*/
|
||
type DefaultableToolKeys =
|
||
| 'isEnabled'
|
||
| 'isConcurrencySafe'
|
||
| 'isReadOnly'
|
||
| 'isDestructive'
|
||
| 'checkPermissions'
|
||
| 'toAutoClassifierInput'
|
||
| 'userFacingName'
|
||
|
||
/**
|
||
* Tool definition accepted by `buildTool`. Same shape as `Tool` but with the
|
||
* defaultable methods optional — `buildTool` fills them in so callers always
|
||
* see a complete `Tool`.
|
||
*/
|
||
export type ToolDef<
|
||
Input extends AnyObject = AnyObject,
|
||
Output = unknown,
|
||
P extends ToolProgressData = ToolProgressData,
|
||
> = Omit<Tool<Input, Output, P>, DefaultableToolKeys> &
|
||
Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>
|
||
|
||
/**
|
||
* Type-level spread mirroring `{ ...TOOL_DEFAULTS, ...def }`. For each
|
||
* defaultable key: if D provides it (required), D's type wins; if D omits
|
||
* it or has it optional (inherited from Partial<> in the constraint), the
|
||
* default fills in. All other keys come from D verbatim — preserving arity,
|
||
* optional presence, and literal types exactly as `satisfies Tool` did.
|
||
*/
|
||
type BuiltTool<D> = Omit<D, DefaultableToolKeys> & {
|
||
[K in DefaultableToolKeys]-?: K extends keyof D
|
||
? undefined extends D[K]
|
||
? ToolDefaults[K]
|
||
: D[K]
|
||
: ToolDefaults[K]
|
||
}
|
||
|
||
/**
|
||
* Build a complete `Tool` from a partial definition, filling in safe defaults
|
||
* for the commonly-stubbed methods. All tool exports should go through this so
|
||
* that defaults live in one place and callers never need `?.() ?? default`.
|
||
*
|
||
* Defaults (fail-closed where it matters):
|
||
* - `isEnabled` → `true`
|
||
* - `isConcurrencySafe` → `false` (assume not safe)
|
||
* - `isReadOnly` → `false` (assume writes)
|
||
* - `isDestructive` → `false`
|
||
* - `checkPermissions` → `{ behavior: 'allow', updatedInput }` (defer to general permission system)
|
||
* - `toAutoClassifierInput` → `''` (skip classifier — security-relevant tools must override)
|
||
* - `userFacingName` → `name`
|
||
*/
|
||
const TOOL_DEFAULTS = {
|
||
isEnabled: () => true,
|
||
isConcurrencySafe: (_input?: unknown) => false,
|
||
isReadOnly: (_input?: unknown) => false,
|
||
isDestructive: (_input?: unknown) => false,
|
||
checkPermissions: (
|
||
input: { [key: string]: unknown },
|
||
_ctx?: ToolUseContext,
|
||
): Promise<PermissionResult> =>
|
||
Promise.resolve({ behavior: 'allow', updatedInput: input }),
|
||
toAutoClassifierInput: (_input?: unknown) => '',
|
||
userFacingName: (_input?: unknown) => '',
|
||
}
|
||
|
||
// The defaults type is the ACTUAL shape of TOOL_DEFAULTS (optional params so
|
||
// both 0-arg and full-arg call sites type-check — stubs varied in arity and
|
||
// tests relied on that), not the interface's strict signatures.
|
||
type ToolDefaults = typeof TOOL_DEFAULTS
|
||
|
||
// D infers the concrete object-literal type from the call site. The
|
||
// constraint provides contextual typing for method parameters; `any` in
|
||
// constraint position is structural and never leaks into the return type.
|
||
// BuiltTool<D> mirrors runtime `{...TOOL_DEFAULTS, ...def}` at the type level.
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
type AnyToolDef = ToolDef<any, any, any>
|
||
|
||
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
|
||
// The runtime spread is straightforward; the `as` bridges the gap between
|
||
// the structural-any constraint and the precise BuiltTool<D> return. The
|
||
// type semantics are proven by the 0-error typecheck across all 60+ tools.
|
||
return {
|
||
...TOOL_DEFAULTS,
|
||
userFacingName: () => def.name,
|
||
...def,
|
||
} as BuiltTool<D>
|
||
}
|