-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix(session-recovery): handle unavailable_tool (dummy_tool) errors #2005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
43b8884
b404bcd
e6883a4
4140995
49aa516
b48804e
e21bbed
4aec627
d618678
fbe7e61
1970d6d
d08fa72
db9df55
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import type { createOpencodeClient } from "@opencode-ai/sdk" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Duplicate code: Prompt for AI agents |
||
| 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" | ||
|
|
||
| type Client = ReturnType<typeof createOpencodeClient> | ||
|
|
||
| interface ToolResultPart { | ||
| type: "tool_result" | ||
| tool_use_id: string | ||
| content: string | ||
| } | ||
|
|
||
| interface PromptWithToolResultInput { | ||
| path: { id: string } | ||
| body: { parts: ToolResultPart[] } | ||
| } | ||
|
|
||
| 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<MessagePart[]> { | ||
| 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<boolean> { | ||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P0: Custom agent: Opencode Compatibility Return a Prompt for AI agents |
||
|
|
||
| const toolResultParts = targetToolUses.map((part) => ({ | ||
| type: "tool_result" as const, | ||
| tool_use_id: part.id, | ||
| content: '{"status":"error","error":"Tool not available. Please continue without this tool."}', | ||
| })) | ||
|
|
||
| try { | ||
| const promptInput: PromptWithToolResultInput = { | ||
| path: { id: sessionID }, | ||
| body: { parts: toolResultParts }, | ||
| } | ||
| const promptAsync = client.session.promptAsync as (...args: never[]) => unknown | ||
| await Reflect.apply(promptAsync, client.session, [promptInput]) | ||
| return true | ||
| } catch { | ||
| return false | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.