import type { createOpencodeClient } from "@opencode-ai/sdk" import { extractUnavailableToolName } from "./detect-error-type" import { readParts } from "./storage" import type { MessageData } from "./types" import { normalizeSDKResponse } from "../../shared" import { isSqliteBackend } from "../../shared/opencode-storage-detection" import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" type Client = ReturnType interface ToolResultPart { type: "tool_result" toolUseId: string tool_use_id?: string isError?: boolean content: Array<{ type: "text"; text: string }> } interface PromptWithToolResultInput { path: { id: string } body: { parts: ToolResultPart[] } } type ClientWithPromptAsync = Client & { session: Client["session"] & { promptAsync: (input: PromptWithToolResultInput) => Promise } } function hasPromptAsync(client: Client): client is ClientWithPromptAsync { const promptAsync = (client.session as { promptAsync?: unknown }).promptAsync return typeof promptAsync === "function" } interface ToolUsePart { type: "tool_use" id: string name: string } interface MessagePart { type: string id?: string name?: string } function extractToolUseParts(parts: MessagePart[]): ToolUsePart[] { return parts.filter( (part): part is ToolUsePart => part.type === "tool_use" && typeof part.id === "string" && typeof part.name === "string" ) } async function readPartsFromSDKFallback( client: Client, sessionID: string, messageID: string ): Promise { try { const response = await client.session.messages({ path: { id: sessionID } }) const messages = normalizeSDKResponse(response, [] as MessageData[], { preferResponseOnMissingData: true }) const target = messages.find((message) => message.info?.id === messageID) if (!target?.parts) return [] return target.parts.map((part) => ({ type: part.type === "tool" ? "tool_use" : part.type, id: "callID" in part ? (part as { callID?: string }).callID : part.id, name: "name" in part && typeof part.name === "string" ? part.name : ("tool" in part && typeof (part as { tool?: unknown }).tool === "string" ? (part as { tool: string }).tool : undefined), })) } catch { return [] } } export async function recoverUnavailableTool( client: Client, sessionID: string, failedAssistantMsg: MessageData ): Promise { let parts = failedAssistantMsg.parts || [] if (parts.length === 0 && failedAssistantMsg.info?.id) { if (isSqliteBackend()) { parts = await readPartsFromSDKFallback(client, sessionID, failedAssistantMsg.info.id) } else { const storedParts = readParts(failedAssistantMsg.info.id) parts = storedParts.map((part) => ({ type: part.type === "tool" ? "tool_use" : part.type, id: "callID" in part ? (part as { callID?: string }).callID : part.id, name: "tool" in part && typeof part.tool === "string" ? part.tool : undefined, })) } } const toolUseParts = extractToolUseParts(parts) if (toolUseParts.length === 0) { return false } const unavailableToolName = extractUnavailableToolName(failedAssistantMsg.info?.error) const matchingToolUses = unavailableToolName ? toolUseParts.filter((part) => part.name.toLowerCase() === unavailableToolName) : [] const targetToolUses = matchingToolUses.length > 0 ? matchingToolUses : toolUseParts const toolResultParts = targetToolUses.map((part) => ({ type: "tool_result" as const, toolUseId: part.id, tool_use_id: part.id, isError: true, content: [{ type: "text" as const, text: '{"status":"error","error":"Tool not available. Please continue without this tool."}' }], })) try { const promptInput: PromptWithToolResultInput = { path: { id: sessionID }, body: { parts: toolResultParts }, } if (!hasPromptAsync(client)) { return false } const promptResult = await promptAsyncAfterSessionIdle({ client, sessionID, source: "session-recovery-unavailable-tool", input: promptInput, }) return promptResult.status === "dispatched" } catch { return false } }