import { isAbsolute, resolve } from "path" import type { ToolCall, ToolCallContent, ToolCallLocation, ToolCallUpdate, ToolKind } from "@agentclientprotocol/sdk" export type ToolInput = Record export type ToolAttachment = { readonly mime?: string readonly url?: string readonly [key: string]: unknown } export type CompletedToolState = { readonly status: "completed" readonly input: ToolInput readonly output: string readonly metadata?: unknown readonly attachments?: ReadonlyArray } export type RunningToolState = { readonly status: "running" readonly input: ToolInput readonly title?: string } export type ErrorToolState = { readonly status: "error" readonly input: ToolInput readonly error: string readonly metadata?: unknown } export type ImageAttachment = { readonly mimeType: string readonly data: string } export function toToolKind(toolName: string): ToolKind { const tool = toolName.toLocaleLowerCase() switch (tool) { case "bash": case "shell": return "execute" case "webfetch": return "fetch" case "edit": case "apply_patch": case "patch": case "write": return "edit" case "grep": case "glob": case "context": case "context7_resolve_library_id": case "context7_get_library_docs": return "search" case "read": return "read" case "task": return "think" default: return "other" } } export function toLocations(toolName: string, input: ToolInput, cwd?: string): ToolCallLocation[] { const tool = toolName.toLocaleLowerCase() switch (tool) { case "bash": case "shell": { const workdir = shellWorkdir(input, cwd) return workdir ? [{ path: workdir }] : [] } case "read": case "edit": case "write": return locationFrom(input.filePath ?? input.filepath) case "external_directory": return locationFrom(input.filePath ?? input.filepath, input.parentDir, input.directories) case "grep": case "glob": case "context": case "context7_resolve_library_id": case "context7_get_library_docs": return locationFrom(input.path) default: return [] } } export function completedToolContent(toolName: string, state: CompletedToolState): ToolCallContent[] { const text = toolName.toLocaleLowerCase() === "read" ? (readDisplayText(state.metadata) ?? state.output) : state.output const content: ToolCallContent[] = [ { type: "content", content: { type: "text", text, }, }, ] if (toToolKind(toolName) === "edit") { content.push(...diffContent(state.input)) } content.push(...imageContents(state.attachments ?? [])) return content } export function pendingToolCall(input: { readonly toolCallId: string readonly toolName: string readonly state: { readonly input: ToolInput; readonly title?: string } readonly cwd?: string }): ToolCall { return { toolCallId: input.toolCallId, title: toolTitle(input.toolName, input.state.input, input.state.title), kind: toToolKind(input.toolName), status: "pending", locations: toLocations(input.toolName, input.state.input, input.cwd), rawInput: rawInput(input.toolName, input.state.input, input.cwd), } } export function runningToolUpdate(input: { readonly toolCallId: string readonly toolName: string readonly state: RunningToolState readonly output?: string readonly cwd?: string }): ToolCallUpdate { const content = input.output ? [ { type: "content" as const, content: { type: "text" as const, text: input.output, }, }, ] : undefined return { toolCallId: input.toolCallId, status: "in_progress", kind: toToolKind(input.toolName), title: toolTitle(input.toolName, input.state.input, input.state.title), locations: toLocations(input.toolName, input.state.input, input.cwd), rawInput: rawInput(input.toolName, input.state.input, input.cwd), ...(content ? { content } : {}), } } export function duplicateRunningToolUpdate(input: { readonly toolCallId: string readonly toolName: string readonly state: RunningToolState readonly cwd?: string }): ToolCallUpdate { return { toolCallId: input.toolCallId, status: "in_progress", kind: toToolKind(input.toolName), title: toolTitle(input.toolName, input.state.input, input.state.title), locations: toLocations(input.toolName, input.state.input, input.cwd), rawInput: rawInput(input.toolName, input.state.input, input.cwd), } } export function completedToolUpdate(input: { readonly toolCallId: string readonly toolName: string readonly state: CompletedToolState & { readonly title?: string } readonly cwd?: string }): ToolCallUpdate { return { toolCallId: input.toolCallId, status: "completed", ...(input.state.title ? { title: input.state.title } : {}), content: completedToolContent(input.toolName, input.state), rawOutput: completedToolRawOutput(input.state), } } export function errorToolUpdate(input: { readonly toolCallId: string readonly toolName: string readonly state: ErrorToolState readonly cwd?: string }): ToolCallUpdate { return { toolCallId: input.toolCallId, status: "failed", kind: toToolKind(input.toolName), title: toolTitle(input.toolName, input.state.input, undefined), locations: toLocations(input.toolName, input.state.input, input.cwd), rawInput: rawInput(input.toolName, input.state.input, input.cwd), content: [ { type: "content", content: { type: "text", text: input.state.error, }, }, ], rawOutput: { error: input.state.error, metadata: input.state.metadata, }, } } export function completedToolRawOutput(state: CompletedToolState) { return { output: state.output, ...(state.metadata !== undefined ? { metadata: state.metadata } : {}), ...(state.attachments?.length ? { attachments: state.attachments } : {}), } } export function imageContents(attachments: ReadonlyArray): ToolCallContent[] { return extractImageAttachments(attachments).map((attachment): ToolCallContent => { return { type: "content", content: { type: "image", mimeType: attachment.mimeType, data: attachment.data, }, } }) } export function extractImageAttachments(attachments: ReadonlyArray): ImageAttachment[] { return attachments.flatMap((attachment): ImageAttachment[] => { const data = dataUrlImage(attachment) return data ? [data] : [] }) } export function shellOutputSnapshot(state: { readonly metadata?: unknown }) { if (!state.metadata || typeof state.metadata !== "object") return undefined return stringValue((state.metadata as Record).output) } // For shell tools, surface the actual command as the title so it stays visible // before output lands; non-shell tools keep their model-provided title. function toolTitle(toolName: string, input: ToolInput, fallback: string | undefined) { if (isShell(toolName)) return shellCommand(input) ?? fallback ?? toolName return fallback || toolName } // Enrich shell rawInput with the resolved working directory so clients can show // where the command runs, unless the model already specified one. function rawInput(toolName: string, input: ToolInput, cwd?: string): ToolInput { if (!isShell(toolName)) return input if (input.cwd || input.workdir) return input const workdir = shellWorkdir(input, cwd) return workdir ? { ...input, cwd: workdir } : input } function shellWorkdir(input: ToolInput, cwd?: string) { const explicit = stringValue(input.workdir) ?? stringValue(input.cwd) return resolvePath(explicit, cwd) ?? cwd } function resolvePath(value: string | undefined, cwd?: string) { if (!value) return undefined if (isAbsolute(value)) return value return resolve(cwd ?? process.cwd(), value) } function shellCommand(input: ToolInput) { return stringValue(input.command) ?? stringValue(input.cmd) } function isShell(toolName: string) { const tool = toolName.toLocaleLowerCase() return tool === "bash" || tool === "shell" } export const mapToolKind = toToolKind export const extractLocations = toLocations export const buildCompletedToolContent = completedToolContent export const buildCompletedRawOutput = completedToolRawOutput export const extractShellOutputSnapshot = shellOutputSnapshot export const buildPendingToolCall = pendingToolCall export const buildRunningToolUpdate = runningToolUpdate export const buildDuplicateRunningToolUpdate = duplicateRunningToolUpdate export const buildCompletedToolUpdate = completedToolUpdate export const buildErrorToolUpdate = errorToolUpdate function locationFrom(...values: unknown[]): ToolCallLocation[] { return Array.from( new Set( values.flatMap((value): string[] => { if (Array.isArray(value)) { return value.filter((item): item is string => typeof item === "string" && item.length > 0) } const path = stringValue(value) return path ? [path] : [] }), ), (path) => ({ path }), ) } function diffContent(input: ToolInput): ToolCallContent[] { const oldText = stringValue(input.oldString) const newText = stringValue(input.newString) ?? stringValue(input.content) if (oldText === undefined || newText === undefined) return [] return [ { type: "diff", path: stringValue(input.filePath) ?? "", oldText, newText, }, ] } function readDisplayText(metadata: unknown) { if (!metadata || typeof metadata !== "object") return undefined const display = (metadata as Record).display if (!display || typeof display !== "object") return undefined const info = display as Record if (info.type === "file") return stringValue(info.text) if (info.type === "directory" && Array.isArray(info.entries)) { return info.entries.filter((item): item is string => typeof item === "string").join("\n") } return undefined } function dataUrlImage(attachment: ToolAttachment) { const match = stringValue(attachment.url)?.match(/^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/) const mime = match?.[1] ?? stringValue(attachment.mime) if (!mime?.startsWith("image/")) return undefined const data = match?.[2] if (data === undefined) return undefined return { mimeType: mime, data } } function stringValue(value: unknown) { return typeof value === "string" ? value : undefined }