fix(session-recovery): harden unavailable tool recovery flow
This commit is contained in:
parent
414099534e
commit
49aa5162bb
@ -124,6 +124,17 @@ describe("detectErrorType", () => {
|
|||||||
expect(result).toBe("unavailable_tool")
|
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", () => {
|
it("#given a dummy_tool token in nested error #when detecting #then returns unavailable_tool", () => {
|
||||||
//#given
|
//#given
|
||||||
const error = {
|
const error = {
|
||||||
@ -189,4 +200,15 @@ describe("extractUnavailableToolName", () => {
|
|||||||
//#then
|
//#then
|
||||||
expect(result).toBeNull()
|
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 {
|
export function extractUnavailableToolName(error: unknown): string | null {
|
||||||
try {
|
try {
|
||||||
const message = getErrorMessage(error)
|
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
|
return match ? match[1] : null
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
@ -90,6 +90,7 @@ export function detectErrorType(error: unknown): RecoveryErrorType {
|
|||||||
message.includes("unavailable tool") ||
|
message.includes("unavailable tool") ||
|
||||||
message.includes("model tried to call unavailable") ||
|
message.includes("model tried to call unavailable") ||
|
||||||
message.includes("nosuchtoolarror") ||
|
message.includes("nosuchtoolarror") ||
|
||||||
|
message.includes("nosuchtoolerror") ||
|
||||||
message.includes("no such tool")
|
message.includes("no such tool")
|
||||||
) {
|
) {
|
||||||
return "unavailable_tool"
|
return "unavailable_tool"
|
||||||
|
|||||||
@ -110,11 +110,6 @@ export function createSessionRecoveryHook(ctx: PluginInput, options?: SessionRec
|
|||||||
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg)
|
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg)
|
||||||
} else if (errorType === "unavailable_tool") {
|
} else if (errorType === "unavailable_tool") {
|
||||||
success = await recoverUnavailableTool(ctx.client, sessionID, failedMsg)
|
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") {
|
} else if (errorType === "thinking_block_order") {
|
||||||
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory, info.error)
|
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory, info.error)
|
||||||
if (success && experimental?.auto_resume) {
|
if (success && experimental?.auto_resume) {
|
||||||
|
|||||||
@ -7,6 +7,17 @@ import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
|||||||
|
|
||||||
type Client = ReturnType<typeof createOpencodeClient>
|
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 {
|
interface ToolUsePart {
|
||||||
type: "tool_use"
|
type: "tool_use"
|
||||||
id: string
|
id: string
|
||||||
@ -80,23 +91,16 @@ export async function recoverUnavailableTool(
|
|||||||
const toolResultParts = targetToolUses.map((part) => ({
|
const toolResultParts = targetToolUses.map((part) => ({
|
||||||
type: "tool_result" as const,
|
type: "tool_result" as const,
|
||||||
tool_use_id: part.id,
|
tool_use_id: part.id,
|
||||||
content: {
|
content: '{"status":"error","error":"Tool not available. Please continue without this tool."}',
|
||||||
status: "error",
|
|
||||||
error: "Tool not available. Please continue without this tool.",
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const promptAsyncKey = ["prompt", "Async"].join("")
|
const promptInput: PromptWithToolResultInput = {
|
||||||
const promptAsync = Reflect.get(client.session, promptAsyncKey)
|
|
||||||
if (typeof promptAsync !== "function") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
await promptAsync({
|
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: { parts: toolResultParts },
|
body: { parts: toolResultParts },
|
||||||
})
|
}
|
||||||
|
const promptAsync = client.session.promptAsync as (...args: never[]) => unknown
|
||||||
|
await Reflect.apply(promptAsync, client.session, [promptInput])
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user