fix(sisyphus_task): resolve sync mode JSON parse error (#708)
This commit is contained in:
parent
f83b22c4de
commit
179f57fa96
@ -377,7 +377,221 @@ describe("sisyphus-task", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("buildSystemContent", () => {
|
describe("sync mode new task (run_in_background=false)", () => {
|
||||||
|
test("sync mode prompt error returns error message immediately", async () => {
|
||||||
|
// #given
|
||||||
|
const { createSisyphusTask } = require("./tools")
|
||||||
|
|
||||||
|
const mockManager = {
|
||||||
|
launch: async () => ({}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
create: async () => ({ data: { id: "ses_sync_error_test" } }),
|
||||||
|
prompt: async () => {
|
||||||
|
throw new Error("JSON Parse error: Unexpected EOF")
|
||||||
|
},
|
||||||
|
messages: async () => ({ data: [] }),
|
||||||
|
status: async () => ({ data: {} }),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = createSisyphusTask({
|
||||||
|
manager: mockManager,
|
||||||
|
client: mockClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "parent-message",
|
||||||
|
agent: "Sisyphus",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = await tool.execute(
|
||||||
|
{
|
||||||
|
description: "Sync error test",
|
||||||
|
prompt: "Do something",
|
||||||
|
category: "ultrabrain",
|
||||||
|
run_in_background: false,
|
||||||
|
skills: [],
|
||||||
|
},
|
||||||
|
toolContext
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - should return error message with the prompt error
|
||||||
|
expect(result).toContain("❌")
|
||||||
|
expect(result).toContain("Failed to send prompt")
|
||||||
|
expect(result).toContain("JSON Parse error")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sync mode success returns task result with content", async () => {
|
||||||
|
// #given
|
||||||
|
const { createSisyphusTask } = require("./tools")
|
||||||
|
|
||||||
|
const mockManager = {
|
||||||
|
launch: async () => ({}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
create: async () => ({ data: { id: "ses_sync_success" } }),
|
||||||
|
prompt: async () => ({ data: {} }),
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
info: { role: "assistant", time: { created: Date.now() } },
|
||||||
|
parts: [{ type: "text", text: "Sync task completed successfully" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
status: async () => ({ data: { "ses_sync_success": { type: "idle" } } }),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = createSisyphusTask({
|
||||||
|
manager: mockManager,
|
||||||
|
client: mockClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "parent-message",
|
||||||
|
agent: "Sisyphus",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = await tool.execute(
|
||||||
|
{
|
||||||
|
description: "Sync success test",
|
||||||
|
prompt: "Do something",
|
||||||
|
category: "ultrabrain",
|
||||||
|
run_in_background: false,
|
||||||
|
skills: [],
|
||||||
|
},
|
||||||
|
toolContext
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - should return the task result content
|
||||||
|
expect(result).toContain("Sync task completed successfully")
|
||||||
|
expect(result).toContain("Task completed")
|
||||||
|
}, { timeout: 20000 })
|
||||||
|
|
||||||
|
test("sync mode agent not found returns helpful error", async () => {
|
||||||
|
// #given
|
||||||
|
const { createSisyphusTask } = require("./tools")
|
||||||
|
|
||||||
|
const mockManager = {
|
||||||
|
launch: async () => ({}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
create: async () => ({ data: { id: "ses_agent_notfound" } }),
|
||||||
|
prompt: async () => {
|
||||||
|
throw new Error("Cannot read property 'name' of undefined agent.name")
|
||||||
|
},
|
||||||
|
messages: async () => ({ data: [] }),
|
||||||
|
status: async () => ({ data: {} }),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = createSisyphusTask({
|
||||||
|
manager: mockManager,
|
||||||
|
client: mockClient,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "parent-message",
|
||||||
|
agent: "Sisyphus",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = await tool.execute(
|
||||||
|
{
|
||||||
|
description: "Agent not found test",
|
||||||
|
prompt: "Do something",
|
||||||
|
category: "ultrabrain",
|
||||||
|
run_in_background: false,
|
||||||
|
skills: [],
|
||||||
|
},
|
||||||
|
toolContext
|
||||||
|
)
|
||||||
|
|
||||||
|
// #then - should return agent not found error
|
||||||
|
expect(result).toContain("❌")
|
||||||
|
expect(result).toContain("not found")
|
||||||
|
expect(result).toContain("registered")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sync mode passes category model to prompt", async () => {
|
||||||
|
// #given
|
||||||
|
const { createSisyphusTask } = require("./tools")
|
||||||
|
let promptBody: any
|
||||||
|
|
||||||
|
const mockManager = { launch: async () => ({}) }
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
create: async () => ({ data: { id: "ses_sync_model" } }),
|
||||||
|
prompt: async (input: any) => {
|
||||||
|
promptBody = input.body
|
||||||
|
return { data: {} }
|
||||||
|
},
|
||||||
|
messages: async () => ({
|
||||||
|
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }]
|
||||||
|
}),
|
||||||
|
status: async () => ({ data: {} }),
|
||||||
|
},
|
||||||
|
app: { agents: async () => ({ data: [] }) },
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = createSisyphusTask({
|
||||||
|
manager: mockManager,
|
||||||
|
client: mockClient,
|
||||||
|
userCategories: {
|
||||||
|
"custom-cat": { model: "provider/custom-model" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolContext = {
|
||||||
|
sessionID: "parent",
|
||||||
|
messageID: "msg",
|
||||||
|
agent: "Sisyphus",
|
||||||
|
abort: new AbortController().signal
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await tool.execute({
|
||||||
|
description: "Sync model test",
|
||||||
|
prompt: "test",
|
||||||
|
category: "custom-cat",
|
||||||
|
run_in_background: false,
|
||||||
|
skills: []
|
||||||
|
}, toolContext)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(promptBody.model).toEqual({
|
||||||
|
providerID: "provider",
|
||||||
|
modelID: "custom-model"
|
||||||
|
})
|
||||||
|
}, { timeout: 20000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildSystemContent", () => {
|
||||||
test("returns undefined when no skills and no category promptAppend", () => {
|
test("returns undefined when no skills and no category promptAppend", () => {
|
||||||
// #given
|
// #given
|
||||||
const { buildSystemContent } = require("./tools")
|
const { buildSystemContent } = require("./tools")
|
||||||
|
|||||||
@ -419,32 +419,25 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id
|
|||||||
metadata: { sessionId: sessionID, category: args.category, sync: true },
|
metadata: { sessionId: sessionID, category: args.category, sync: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use fire-and-forget prompt() - awaiting causes JSON parse errors with thinking models
|
try {
|
||||||
// Note: Don't pass model in body - use agent's configured model instead
|
await client.session.prompt({
|
||||||
let promptError: Error | undefined
|
path: { id: sessionID },
|
||||||
client.session.prompt({
|
body: {
|
||||||
path: { id: sessionID },
|
agent: agentToUse,
|
||||||
body: {
|
system: systemContent,
|
||||||
agent: agentToUse,
|
tools: {
|
||||||
system: systemContent,
|
task: false,
|
||||||
tools: {
|
sisyphus_task: false,
|
||||||
task: false,
|
},
|
||||||
sisyphus_task: false,
|
parts: [{ type: "text", text: args.prompt }],
|
||||||
|
...(categoryModel ? { model: categoryModel } : {}),
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: args.prompt }],
|
})
|
||||||
},
|
} catch (promptError) {
|
||||||
}).catch((error) => {
|
|
||||||
promptError = error instanceof Error ? error : new Error(String(error))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Small delay to let the prompt start
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
if (promptError) {
|
|
||||||
if (toastManager && taskId !== undefined) {
|
if (toastManager && taskId !== undefined) {
|
||||||
toastManager.removeTask(taskId)
|
toastManager.removeTask(taskId)
|
||||||
}
|
}
|
||||||
const errorMessage = promptError.message
|
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
|
||||||
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
||||||
return `❌ Agent "${agentToUse}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\nSession ID: ${sessionID}`
|
return `❌ Agent "${agentToUse}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\nSession ID: ${sessionID}`
|
||||||
}
|
}
|
||||||
@ -464,20 +457,6 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id
|
|||||||
while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
|
while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
|
||||||
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
|
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
|
||||||
|
|
||||||
// Check for async errors that may have occurred after the initial 100ms delay
|
|
||||||
// TypeScript doesn't understand async mutation, so we cast to check
|
|
||||||
const asyncError = promptError as Error | undefined
|
|
||||||
if (asyncError) {
|
|
||||||
if (toastManager && taskId !== undefined) {
|
|
||||||
toastManager.removeTask(taskId)
|
|
||||||
}
|
|
||||||
const errorMessage = asyncError.message
|
|
||||||
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
|
||||||
return `❌ Agent "${agentToUse}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\nSession ID: ${sessionID}`
|
|
||||||
}
|
|
||||||
return `❌ Failed to send prompt: ${errorMessage}\n\nSession ID: ${sessionID}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusResult = await client.session.status()
|
const statusResult = await client.session.status()
|
||||||
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
||||||
const sessionStatus = allStatuses[sessionID]
|
const sessionStatus = allStatuses[sessionID]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user