import type { ContentPart, ToolCall, ToolDescription as Tool } from '#human/llm/message'; import type { LLMRequestTrace } from '#/llm-adapter/contract/request-trace'; import type { ToolInputDisplay } from '#/tool/toolInputDisplay'; export type ExecutableToolOutput = string | ContentPart[]; export const DEFAULT_TOOL_RESULT_MAX_CHARS = 50_000; export const DEFAULT_TOOL_RESULT_MAX_RETAINED_CHARS = 10_000_000; export interface ToolResultSpill { readonly outputPath?: string; readonly totalChars?: number; readonly suffix?: string; } export type ToolDeliveryKind = 'steer'; export interface ToolDeliveryMessage { readonly role: 'user'; readonly content: readonly ContentPart[]; readonly toolCalls?: readonly ToolCall[]; readonly origin?: unknown; } export interface ToolDelivery { readonly kind: ToolDeliveryKind; readonly message: ToolDeliveryMessage; } export interface ExecutableToolSuccessResult { readonly output: ExecutableToolOutput; readonly isError?: false | undefined; readonly stopTurn?: boolean | undefined; readonly stopTurnReason?: string; readonly truncated?: boolean | undefined; readonly note?: string; readonly delivery?: ToolDelivery | undefined; readonly spill?: ToolResultSpill; readonly spillExempt?: true; } export interface ExecutableToolErrorResult { readonly output: ExecutableToolOutput; readonly isError: true; readonly stopTurn?: boolean | undefined; readonly stopTurnReason?: string; readonly truncated?: boolean | undefined; readonly note?: string; readonly delivery?: ToolDelivery | undefined; readonly spill?: ToolResultSpill; readonly spillExempt?: true; } export type ExecutableToolResult = ExecutableToolSuccessResult | ExecutableToolErrorResult; export interface ToolUpdate { kind: 'stdout' | 'stderr' | 'progress' | 'status' | 'custom'; text?: string | undefined; percent?: number | undefined; customKind?: string | undefined; customData?: unknown; replace?: boolean; } export const MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE = 'mcp.oauth.authorization_url'; export interface ExecutableToolContext { readonly turnId: number; readonly toolCallId: string; readonly trace?: LLMRequestTrace; readonly metadata?: unknown; readonly signal: AbortSignal; readonly steerSignal?: AbortSignal; readonly onUpdate?: ((update: ToolUpdate) => void) | undefined; readonly onForegroundTaskStart?: ((taskId: string) => void) | undefined; } export interface RunnableToolExecution { readonly isError?: false | undefined; readonly accesses?: ToolAccesses | undefined; readonly display?: ToolInputDisplay | undefined; readonly description?: string; readonly stopBatchAfterThis?: boolean | undefined; readonly approvalRule: string; readonly matchesRule?: ((ruleArgs: string) => boolean) | undefined; readonly execute: (ctx: ExecutableToolContext) => Promise; } export type ToolExecution = RunnableToolExecution | ExecutableToolErrorResult; export interface ExecutableTool extends Tool { resolveExecution(input: Input): ToolExecution | Promise; } export type ToolSource = 'builtin' | 'user' | 'mcp'; export type ToolDisclosure = 'inline' | 'deferred'; export interface ToolDefinition { readonly name: string; readonly description: string; readonly parameters?: Record; readonly source?: ToolSource; readonly disclosure?: ToolDisclosure; readonly info?: Record; } export interface ToolInfo extends ToolDefinition { readonly source: ToolSource; } export interface AgentTool extends ExecutableTool { readonly _serviceBrand: undefined; } export type ToolResult = ExecutableToolResult & { readonly description?: string; readonly display?: ToolInputDisplay; readonly approvalRule?: string; readonly stopBatchAfterThis?: boolean; }; export type ToolFileAccessOperation = 'read' | 'write' | 'readwrite' | 'search'; export interface ToolFileAccess { readonly kind: 'file'; readonly operation: ToolFileAccessOperation; readonly path: string; readonly recursive?: boolean; } export interface ToolResourceAccessAll { readonly kind: 'all'; } export type ToolResourceAccess = ToolFileAccess | ToolResourceAccessAll; export type ToolAccesses = readonly ToolResourceAccess[]; export const ToolAccesses = { none(): ToolAccesses { return []; }, all(): ToolAccesses { return [{ kind: 'all' }]; }, file( operation: ToolFileAccessOperation, path: string, options: { readonly recursive?: boolean } = {}, ): ToolAccesses { return [{ kind: 'file', operation, path, recursive: options.recursive }]; }, readFile(path: string): ToolAccesses { return ToolAccesses.file('read', path); }, readTree(path: string): ToolAccesses { return ToolAccesses.file('read', path, { recursive: true }); }, writeFile(path: string): ToolAccesses { return ToolAccesses.file('write', path); }, writeTree(path: string): ToolAccesses { return ToolAccesses.file('write', path, { recursive: true }); }, readWriteFile(path: string): ToolAccesses { return ToolAccesses.file('readwrite', path); }, readWriteTree(path: string): ToolAccesses { return ToolAccesses.file('readwrite', path, { recursive: true }); }, searchTree(path: string): ToolAccesses { return ToolAccesses.file('search', path, { recursive: true }); }, conflict(left: ToolAccesses, right: ToolAccesses): boolean { return left.some((leftAccess) => right.some((rightAccess) => resourceAccessesConflict(leftAccess, rightAccess)), ); }, }; function resourceAccessesConflict(left: ToolResourceAccess, right: ToolResourceAccess): boolean { if (left.kind === 'all' || right.kind === 'all') return true; if (!fileOperationsConflict(left.operation, right.operation)) return false; return fileAccessesOverlap(left, right); } function fileOperationsConflict( left: ToolFileAccessOperation, right: ToolFileAccessOperation, ): boolean { return fileOperationWrites(left) || fileOperationWrites(right); } function fileOperationWrites(operation: ToolFileAccessOperation): boolean { switch (operation) { case 'read': case 'search': return false; case 'write': case 'readwrite': return true; } } function fileAccessesOverlap(left: ToolFileAccess, right: ToolFileAccess): boolean { const leftPath = normalizePath(left.path); const rightPath = normalizePath(right.path); if (leftPath === rightPath) return true; const leftPrefix = leftPath.endsWith('/') ? leftPath : `${leftPath}/`; const rightPrefix = rightPath.endsWith('/') ? rightPath : `${rightPath}/`; return ( (left.recursive === true && rightPath.startsWith(leftPrefix)) || (right.recursive === true && leftPath.startsWith(rightPrefix)) ); } function normalizePath(path: string): string { const normalized = path.replaceAll('\\', '/').replaceAll(/\/+/g, '/'); const folded = normalized.toLowerCase(); if (folded.length > 1 && folded.endsWith('/')) { return folded.slice(0, -1); } return folded; } const MCP_NAME_PREFIX = 'mcp__'; export function isMcpToolName(name: string): boolean { return name.startsWith(MCP_NAME_PREFIX); }