fix(delegate-task): start sync prompts asynchronously
This commit is contained in:
@@ -74,6 +74,8 @@ export async function promptWithModelSuggestionRetry(
|
||||
source: "model-suggestion-retry",
|
||||
settleMs: 0,
|
||||
...(options.queueBehavior ? { queueBehavior: options.queueBehavior } : {}),
|
||||
...(options.checkStatus !== undefined ? { checkStatus: options.checkStatus } : {}),
|
||||
...(options.checkToolState !== undefined ? { checkToolState: options.checkToolState } : {}),
|
||||
})
|
||||
if (promptResult.status === "failed") {
|
||||
if (timeoutContext.wasTimedOut()) {
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface PromptTimeoutArgs {
|
||||
export interface PromptRetryOptions {
|
||||
timeoutMs?: number
|
||||
queueBehavior?: "enqueue" | "defer"
|
||||
checkStatus?: boolean
|
||||
checkToolState?: boolean
|
||||
}
|
||||
|
||||
export const PROMPT_TIMEOUT_MS = 120000
|
||||
|
||||
@@ -5,7 +5,7 @@ import { publishToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||
import { getMessageDir, normalizeSDKResponse } from "../../shared"
|
||||
import { promptSyncWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||
import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||
import { resolveMessageContext } from "../../features/hook-message-injector"
|
||||
import { formatDuration } from "./time-formatter"
|
||||
import { syncContinuationDeps, type SyncContinuationDeps } from "./sync-continuation-deps"
|
||||
@@ -160,7 +160,7 @@ export async function executeSyncContinuation(
|
||||
}
|
||||
setSessionTools(continuationID, tools)
|
||||
|
||||
await promptSyncWithModelSuggestionRetry(client, {
|
||||
await promptWithModelSuggestionRetry(client, {
|
||||
path: { id: continuationID },
|
||||
body: {
|
||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||
|
||||
@@ -4,17 +4,17 @@ import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
|
||||
import type { OpencodeClient } from "./types"
|
||||
import { sendSyncPrompt } from "./sync-prompt-sender"
|
||||
import {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
} from "../../shared/model-suggestion-retry"
|
||||
|
||||
type PromptSyncRetryClient = Parameters<typeof promptSyncWithModelSuggestionRetry>[0]
|
||||
type PromptSyncRetryArgs = Parameters<typeof promptSyncWithModelSuggestionRetry>[1]
|
||||
type PromptRetryClient = Parameters<typeof promptWithModelSuggestionRetry>[0]
|
||||
type PromptRetryArgs = Parameters<typeof promptWithModelSuggestionRetry>[1]
|
||||
|
||||
describe("sendSyncPrompt session routing", () => {
|
||||
test("#given a sync child session directory #when sending the prompt #then prompt uses that OpenCode directory route", async () => {
|
||||
// given
|
||||
const promptCalls: PromptSyncRetryArgs[] = []
|
||||
const promptSyncWithRetry = mock(async (_client: PromptSyncRetryClient, input: PromptSyncRetryArgs) => {
|
||||
const promptCalls: PromptRetryArgs[] = []
|
||||
const promptWithRetry = mock(async (_client: PromptRetryClient, input: PromptRetryArgs) => {
|
||||
promptCalls.push(input)
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("sendSyncPrompt session routing", () => {
|
||||
taskId: undefined,
|
||||
},
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry: promptSyncWithRetry,
|
||||
promptWithModelSuggestionRetry: promptWithRetry,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -48,9 +48,9 @@ describe("sendSyncPrompt session routing", () => {
|
||||
|
||||
test("#given oracle prompt returns unexpected EOF #when sending the prompt #then the sync route keeps the same directory route", async () => {
|
||||
// given
|
||||
const promptSyncCalls: PromptSyncRetryArgs[] = []
|
||||
const promptSyncWithRetry = mock(async (_client: PromptSyncRetryClient, input: PromptSyncRetryArgs) => {
|
||||
promptSyncCalls.push(input)
|
||||
const promptCalls: PromptRetryArgs[] = []
|
||||
const promptWithRetry = mock(async (_client: PromptRetryClient, input: PromptRetryArgs) => {
|
||||
promptCalls.push(input)
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
|
||||
@@ -73,13 +73,13 @@ describe("sendSyncPrompt session routing", () => {
|
||||
taskId: undefined,
|
||||
},
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry: promptSyncWithRetry,
|
||||
promptWithModelSuggestionRetry: promptWithRetry,
|
||||
},
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBeNull()
|
||||
expect(promptSyncCalls).toHaveLength(1)
|
||||
expect(promptSyncCalls[0]?.query).toEqual({ directory: "/parent/project" })
|
||||
expect(promptCalls).toHaveLength(1)
|
||||
expect(promptCalls[0]?.query).toEqual({ directory: "/parent/project" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,6 +16,45 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
clearSessionPromptParams("test-session")
|
||||
})
|
||||
|
||||
bunTest("#given sync task result is polled separately #when sending the child prompt #then it starts with promptAsync instead of holding the sync stream", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const prompt = bunMock(async () => {
|
||||
throw new Error("sync prompt stream should not be used")
|
||||
})
|
||||
const promptAsync = bunMock(async () => undefined)
|
||||
const mockClient = {
|
||||
session: {
|
||||
prompt,
|
||||
promptAsync,
|
||||
},
|
||||
}
|
||||
|
||||
const input = {
|
||||
sessionID: "test-session",
|
||||
agentToUse: "sisyphus-junior",
|
||||
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(mockClient, input)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toBeNull()
|
||||
bunExpect(prompt).toHaveBeenCalledTimes(0)
|
||||
bunExpect(promptAsync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
bunTest("passes question=false via tools parameter", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
@@ -234,7 +273,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
let promptArgs: any
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
const promptWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
promptArgs = input
|
||||
})
|
||||
|
||||
@@ -267,12 +306,12 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { prompt: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptArgs.body.model).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5.4",
|
||||
@@ -299,7 +338,7 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
let promptArgs: any
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
const promptWithModelSuggestionRetry = bunMock(async (_client: any, input: any) => {
|
||||
promptArgs = input
|
||||
})
|
||||
|
||||
@@ -328,19 +367,19 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { prompt: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptArgs.body.temperature).toBe(0.25)
|
||||
})
|
||||
bunTest("#given oracle promptSync returns unexpected EOF #when sending a sync prompt #then the prompt is treated as started without retrying promptAsync", async () => {
|
||||
bunTest("#given oracle prompt starter returns unexpected EOF #when sending a sync prompt #then the prompt is treated as started", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
|
||||
@@ -364,20 +403,20 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toBeNull()
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
bunTest("returns non-oracle unexpected EOF without retrying promptAsync", async () => {
|
||||
bunTest("returns non-oracle unexpected EOF from the prompt starter", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("JSON Parse error: Unexpected EOF")
|
||||
})
|
||||
|
||||
@@ -401,20 +440,59 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toContain("Unexpected EOF")
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
bunTest("#given oracle promptSync is blocked by the prompt gate #when sending a sync prompt #then the gate error is preserved", async () => {
|
||||
bunTest("#given prompt starter rejects an invalid payload #when sending a sync prompt #then the task error is surfaced and toast is removed", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptSyncWithModelSuggestionRetry = bunMock(async () => {
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("Bad request: parts is required")
|
||||
})
|
||||
const removeTask = bunMock(() => undefined)
|
||||
|
||||
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: { removeTask },
|
||||
taskId: "task-123",
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await sendSyncPrompt(
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toContain("Bad request: parts is required")
|
||||
bunExpect(removeTask).toHaveBeenCalledWith("task-123")
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
bunTest("#given oracle prompt starter is blocked by the prompt gate #when sending a sync prompt #then the gate error is preserved", async () => {
|
||||
//#given
|
||||
const { sendSyncPrompt } = require("./sync-prompt-sender")
|
||||
|
||||
const promptWithModelSuggestionRetry = bunMock(async () => {
|
||||
throw new Error("prompt skipped by gate: reserved")
|
||||
})
|
||||
|
||||
@@ -438,12 +516,12 @@ bunDescribe("sendSyncPrompt", () => {
|
||||
{ session: { promptAsync: bunMock(async () => ({ data: {} })) } },
|
||||
input,
|
||||
{
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
},
|
||||
)
|
||||
|
||||
//#then
|
||||
bunExpect(result).toContain("prompt skipped by gate")
|
||||
bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,10 +3,10 @@ import { stripInvisibleAgentCharacters } from "../../shared/agent-display-names"
|
||||
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||
import { createInternalAgentTextPart } from "../../shared/internal-initiator-marker"
|
||||
import {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
} from "../../shared/model-suggestion-retry"
|
||||
import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers"
|
||||
import { routePromptSyncRetry } from "../../shared/session-route"
|
||||
import { routePromptRetry } from "../../shared/session-route"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
import { isPlanFamily } from "./constants"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
@@ -14,11 +14,11 @@ import { buildTaskPrompt } from "./prompt-builder"
|
||||
import type { DelegatedModelConfig, DelegateTaskArgs, OpencodeClient } from "./types"
|
||||
|
||||
type SendSyncPromptDeps = {
|
||||
promptSyncWithModelSuggestionRetry: typeof promptSyncWithModelSuggestionRetry
|
||||
promptWithModelSuggestionRetry: typeof promptWithModelSuggestionRetry
|
||||
}
|
||||
|
||||
const sendSyncPromptDeps: SendSyncPromptDeps = {
|
||||
promptSyncWithModelSuggestionRetry,
|
||||
promptWithModelSuggestionRetry,
|
||||
}
|
||||
|
||||
function buildPromptGenerationParams(model: DelegatedModelConfig | undefined): Record<string, unknown> {
|
||||
@@ -101,8 +101,10 @@ export async function sendSyncPrompt(
|
||||
}
|
||||
|
||||
try {
|
||||
await deps.promptSyncWithModelSuggestionRetry(client, routePromptSyncRetry(promptArgs, input.directory), {
|
||||
await deps.promptWithModelSuggestionRetry(client, routePromptRetry(promptArgs, input.directory), {
|
||||
queueBehavior: "defer",
|
||||
checkStatus: false,
|
||||
checkToolState: false,
|
||||
})
|
||||
} catch (promptError) {
|
||||
if (isOracleAgent(input.agentToUse) && isUnexpectedEofError(promptError)) {
|
||||
|
||||
@@ -1117,7 +1117,7 @@ describe("sisyphus-task", () => {
|
||||
})
|
||||
}, { timeout: 20000 })
|
||||
|
||||
test("DEFAULT_CATEGORIES explicit high model passes to sync session.prompt WITHOUT userCategories", async () => {
|
||||
test("DEFAULT_CATEGORIES explicit high model passes to sync prompt request WITHOUT userCategories", async () => {
|
||||
// given - NO userCategories, testing DEFAULT_CATEGORIES for sync mode
|
||||
const { createDelegateTask } = require("./tools")
|
||||
let promptBody: any
|
||||
@@ -2617,7 +2617,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// then - should run sync, NOT forced to background
|
||||
expect(launchCalled).toBe(false) // manager.launch should NOT be called
|
||||
expect(promptCalled).toBe(true) // sync mode uses session.prompt
|
||||
expect(promptCalled).toBe(true)
|
||||
expect(result).not.toContain("UNSTABLE AGENT MODE")
|
||||
}, { timeout: 20000 })
|
||||
|
||||
@@ -3296,6 +3296,7 @@ describe("sisyphus-task", () => {
|
||||
prompt: async () => {
|
||||
return { data: {} }
|
||||
},
|
||||
promptAsync: async () => ({ data: {} }),
|
||||
messages: async () => ({
|
||||
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }]
|
||||
}),
|
||||
@@ -4026,7 +4027,7 @@ describe("sisyphus-task", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("sync mode passes matched agent model to session.prompt", async () => {
|
||||
test("sync mode passes matched agent model to prompt request", async () => {
|
||||
// given - agent with model registered, using subagent_type with run_in_background=false
|
||||
const { createDelegateTask } = require("./tools")
|
||||
let promptBody: any
|
||||
@@ -4083,7 +4084,6 @@ describe("sisyphus-task", () => {
|
||||
toolContext
|
||||
)
|
||||
|
||||
// then - matched agent's model should be passed to session.prompt
|
||||
expect(promptBody.model).toEqual({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-opus-4-7",
|
||||
|
||||
Reference in New Issue
Block a user