fix(session-recovery): harden unavailable tool recovery flow
This commit is contained in:
@@ -124,6 +124,17 @@ describe("detectErrorType", () => {
|
||||
expect(result).toBe("unavailable_tool")
|
||||
})
|
||||
|
||||
it("#given a NoSuchToolError token #when detecting #then returns unavailable_tool", () => {
|
||||
//#given
|
||||
const error = { message: "NoSuchToolError: no such tool invalid" }
|
||||
|
||||
//#when
|
||||
const result = detectErrorType(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("unavailable_tool")
|
||||
})
|
||||
|
||||
it("#given a dummy_tool token in nested error #when detecting #then returns unavailable_tool", () => {
|
||||
//#given
|
||||
const error = {
|
||||
@@ -189,4 +200,15 @@ describe("extractUnavailableToolName", () => {
|
||||
//#then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("#given no such tool error with colon format #when extracting #then returns tool name", () => {
|
||||
//#given
|
||||
const error = { message: "No such tool: invalid_tool" }
|
||||
|
||||
//#when
|
||||
const result = extractUnavailableToolName(error)
|
||||
|
||||
//#then
|
||||
expect(result).toBe("invalid_tool")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ export function extractMessageIndex(error: unknown): number | null {
|
||||
export function extractUnavailableToolName(error: unknown): string | null {
|
||||
try {
|
||||
const message = getErrorMessage(error)
|
||||
const match = message.match(/unavailable tool ['"]?([^'".\s]+)['"]?/)
|
||||
const match = message.match(/(?:unavailable tool|no such tool)[:\s'"]+([^'".\s]+)/)
|
||||
return match ? match[1] : null
|
||||
} catch {
|
||||
return null
|
||||
@@ -90,6 +90,7 @@ export function detectErrorType(error: unknown): RecoveryErrorType {
|
||||
message.includes("unavailable tool") ||
|
||||
message.includes("model tried to call unavailable") ||
|
||||
message.includes("nosuchtoolarror") ||
|
||||
message.includes("nosuchtoolerror") ||
|
||||
message.includes("no such tool")
|
||||
) {
|
||||
return "unavailable_tool"
|
||||
|
||||
@@ -110,11 +110,6 @@ export function createSessionRecoveryHook(ctx: PluginInput, options?: SessionRec
|
||||
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg)
|
||||
} else if (errorType === "unavailable_tool") {
|
||||
success = await recoverUnavailableTool(ctx.client, sessionID, failedMsg)
|
||||
if (success && experimental?.auto_resume) {
|
||||
const lastUser = findLastUserMessage(msgs ?? [])
|
||||
const resumeConfig = extractResumeConfig(lastUser, sessionID)
|
||||
await resumeSession(ctx.client, resumeConfig)
|
||||
}
|
||||
} else if (errorType === "thinking_block_order") {
|
||||
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory, info.error)
|
||||
if (success && experimental?.auto_resume) {
|
||||
|
||||
@@ -7,6 +7,17 @@ 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
|
||||
@@ -80,23 +91,16 @@ export async function recoverUnavailableTool(
|
||||
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.",
|
||||
},
|
||||
content: '{"status":"error","error":"Tool not available. Please continue without this tool."}',
|
||||
}))
|
||||
|
||||
try {
|
||||
const promptAsyncKey = ["prompt", "Async"].join("")
|
||||
const promptAsync = Reflect.get(client.session, promptAsyncKey)
|
||||
if (typeof promptAsync !== "function") {
|
||||
return false
|
||||
}
|
||||
|
||||
await promptAsync({
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user