Merge pull request #1894 from code-yeongyu/fix/1681-oracle-json-parse

fix: resolve Oracle JSON parse error after promptAsync refactor (#1681)
This commit is contained in:
YeonGyu-Kim 2026-02-17 01:58:21 +09:00 committed by GitHub
commit ada0a233d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 166 additions and 43 deletions

View File

@ -1,12 +1,17 @@
const { describe, test, expect, mock } = require("bun:test") const {
describe: bunDescribe,
test: bunTest,
expect: bunExpect,
mock: bunMock,
} = require("bun:test")
describe("sendSyncPrompt", () => { bunDescribe("sendSyncPrompt", () => {
test("passes question=false via tools parameter", async () => { bunTest("passes question=false via tools parameter", async () => {
//#given //#given
const { sendSyncPrompt } = require("./sync-prompt-sender") const { sendSyncPrompt } = require("./sync-prompt-sender")
let promptArgs: any let promptArgs: any
const promptAsync = mock(async (input: any) => { const promptAsync = bunMock(async (input: any) => {
promptArgs = input promptArgs = input
return { data: {} } return { data: {} }
}) })
@ -33,19 +38,19 @@ describe("sendSyncPrompt", () => {
} }
//#when //#when
await sendSyncPrompt(mockClient as any, input) await sendSyncPrompt(mockClient, input)
//#then //#then
expect(promptAsync).toHaveBeenCalled() bunExpect(promptAsync).toHaveBeenCalled()
expect(promptArgs.body.tools.question).toBe(false) bunExpect(promptArgs.body.tools.question).toBe(false)
}) })
test("applies agent tool restrictions for explore agent", async () => { bunTest("applies agent tool restrictions for explore agent", async () => {
//#given //#given
const { sendSyncPrompt } = require("./sync-prompt-sender") const { sendSyncPrompt } = require("./sync-prompt-sender")
let promptArgs: any let promptArgs: any
const promptAsync = mock(async (input: any) => { const promptAsync = bunMock(async (input: any) => {
promptArgs = input promptArgs = input
return { data: {} } return { data: {} }
}) })
@ -73,19 +78,19 @@ describe("sendSyncPrompt", () => {
} }
//#when //#when
await sendSyncPrompt(mockClient as any, input) await sendSyncPrompt(mockClient, input)
//#then //#then
expect(promptAsync).toHaveBeenCalled() bunExpect(promptAsync).toHaveBeenCalled()
expect(promptArgs.body.tools.call_omo_agent).toBe(false) bunExpect(promptArgs.body.tools.call_omo_agent).toBe(false)
}) })
test("applies agent tool restrictions for librarian agent", async () => { bunTest("applies agent tool restrictions for librarian agent", async () => {
//#given //#given
const { sendSyncPrompt } = require("./sync-prompt-sender") const { sendSyncPrompt } = require("./sync-prompt-sender")
let promptArgs: any let promptArgs: any
const promptAsync = mock(async (input: any) => { const promptAsync = bunMock(async (input: any) => {
promptArgs = input promptArgs = input
return { data: {} } return { data: {} }
}) })
@ -113,19 +118,19 @@ describe("sendSyncPrompt", () => {
} }
//#when //#when
await sendSyncPrompt(mockClient as any, input) await sendSyncPrompt(mockClient, input)
//#then //#then
expect(promptAsync).toHaveBeenCalled() bunExpect(promptAsync).toHaveBeenCalled()
expect(promptArgs.body.tools.call_omo_agent).toBe(false) bunExpect(promptArgs.body.tools.call_omo_agent).toBe(false)
}) })
test("does not restrict call_omo_agent for sisyphus agent", async () => { bunTest("does not restrict call_omo_agent for sisyphus agent", async () => {
//#given //#given
const { sendSyncPrompt } = require("./sync-prompt-sender") const { sendSyncPrompt } = require("./sync-prompt-sender")
let promptArgs: any let promptArgs: any
const promptAsync = mock(async (input: any) => { const promptAsync = bunMock(async (input: any) => {
promptArgs = input promptArgs = input
return { data: {} } return { data: {} }
}) })
@ -153,10 +158,90 @@ describe("sendSyncPrompt", () => {
} }
//#when //#when
await sendSyncPrompt(mockClient as any, input) await sendSyncPrompt(mockClient, input)
//#then //#then
expect(promptAsync).toHaveBeenCalled() bunExpect(promptAsync).toHaveBeenCalled()
expect(promptArgs.body.tools.call_omo_agent).toBe(true) bunExpect(promptArgs.body.tools.call_omo_agent).toBe(true)
})
bunTest("retries with promptSync for oracle when promptAsync fails with unexpected EOF", async () => {
//#given
const { sendSyncPrompt } = require("./sync-prompt-sender")
const promptWithModelSuggestionRetry = bunMock(async () => {
throw new Error("JSON Parse error: Unexpected EOF")
})
const promptSyncWithModelSuggestionRetry = bunMock(async () => {})
const input = {
sessionID: "test-session",
agentToUse: "oracle",
args: {
description: "test task",
prompt: "test prompt",
run_in_background: false,
load_skills: [],
},
systemContent: undefined,
categoryModel: undefined,
toastManager: null,
taskId: undefined,
}
//#when
const result = await sendSyncPrompt(
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
input,
{
promptWithModelSuggestionRetry,
promptSyncWithModelSuggestionRetry,
},
)
//#then
bunExpect(result).toBeNull()
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
})
bunTest("does not retry with promptSync for non-oracle on unexpected EOF", async () => {
//#given
const { sendSyncPrompt } = require("./sync-prompt-sender")
const promptWithModelSuggestionRetry = bunMock(async () => {
throw new Error("JSON Parse error: Unexpected EOF")
})
const promptSyncWithModelSuggestionRetry = bunMock(async () => {})
const input = {
sessionID: "test-session",
agentToUse: "metis",
args: {
description: "test task",
prompt: "test prompt",
run_in_background: false,
load_skills: [],
},
systemContent: undefined,
categoryModel: undefined,
toastManager: null,
taskId: undefined,
}
//#when
const result = await sendSyncPrompt(
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
input,
{
promptWithModelSuggestionRetry,
promptSyncWithModelSuggestionRetry,
},
)
//#then
bunExpect(result).toContain("JSON Parse error: Unexpected EOF")
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(0)
}) })
}) })

View File

@ -1,10 +1,33 @@
import type { DelegateTaskArgs, OpencodeClient } from "./types" import type { DelegateTaskArgs, OpencodeClient } from "./types"
import { isPlanFamily } from "./constants" import { isPlanFamily } from "./constants"
import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" import {
promptSyncWithModelSuggestionRetry,
promptWithModelSuggestionRetry,
} from "../../shared/model-suggestion-retry"
import { formatDetailedError } from "./error-formatting" import { formatDetailedError } from "./error-formatting"
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
import { setSessionTools } from "../../shared/session-tools-store" import { setSessionTools } from "../../shared/session-tools-store"
type SendSyncPromptDeps = {
promptWithModelSuggestionRetry: typeof promptWithModelSuggestionRetry
promptSyncWithModelSuggestionRetry: typeof promptSyncWithModelSuggestionRetry
}
const sendSyncPromptDeps: SendSyncPromptDeps = {
promptWithModelSuggestionRetry,
promptSyncWithModelSuggestionRetry,
}
function isOracleAgent(agentToUse: string): boolean {
return agentToUse.toLowerCase() === "oracle"
}
function isUnexpectedEofError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
const lowered = message.toLowerCase()
return lowered.includes("unexpected eof") || lowered.includes("json parse error")
}
export async function sendSyncPrompt( export async function sendSyncPrompt(
client: OpencodeClient, client: OpencodeClient,
input: { input: {
@ -15,29 +38,44 @@ export async function sendSyncPrompt(
categoryModel: { providerID: string; modelID: string; variant?: string } | undefined categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
toastManager: { removeTask: (id: string) => void } | null | undefined toastManager: { removeTask: (id: string) => void } | null | undefined
taskId: string | undefined taskId: string | undefined
} },
deps: SendSyncPromptDeps = sendSyncPromptDeps
): Promise<string | null> { ): Promise<string | null> {
const allowTask = isPlanFamily(input.agentToUse)
const tools = {
task: allowTask,
call_omo_agent: true,
question: false,
...getAgentToolRestrictions(input.agentToUse),
}
setSessionTools(input.sessionID, tools)
const promptArgs = {
path: { id: input.sessionID },
body: {
agent: input.agentToUse,
system: input.systemContent,
tools,
parts: [{ type: "text", text: input.args.prompt }],
...(input.categoryModel
? { model: { providerID: input.categoryModel.providerID, modelID: input.categoryModel.modelID } }
: {}),
...(input.categoryModel?.variant ? { variant: input.categoryModel.variant } : {}),
},
}
try { try {
const allowTask = isPlanFamily(input.agentToUse) await deps.promptWithModelSuggestionRetry(client, promptArgs)
const tools = {
task: allowTask,
call_omo_agent: true,
question: false,
...getAgentToolRestrictions(input.agentToUse),
}
setSessionTools(input.sessionID, tools)
await promptWithModelSuggestionRetry(client, {
path: { id: input.sessionID },
body: {
agent: input.agentToUse,
system: input.systemContent,
tools,
parts: [{ type: "text", text: input.args.prompt }],
...(input.categoryModel ? { model: { providerID: input.categoryModel.providerID, modelID: input.categoryModel.modelID } } : {}),
...(input.categoryModel?.variant ? { variant: input.categoryModel.variant } : {}),
},
})
} catch (promptError) { } catch (promptError) {
if (isOracleAgent(input.agentToUse) && isUnexpectedEofError(promptError)) {
try {
await deps.promptSyncWithModelSuggestionRetry(client, promptArgs)
return null
} catch (oracleRetryError) {
promptError = oracleRetryError
}
}
if (input.toastManager && input.taskId !== undefined) { if (input.toastManager && input.taskId !== undefined) {
input.toastManager.removeTask(input.taskId) input.toastManager.removeTask(input.taskId)
} }