mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(acp): forward provider retry status to clients (#50752)
This commit is contained in:
@@ -4,6 +4,7 @@ import type {
|
||||
OpenCodeClient,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageInfo,
|
||||
SessionStructuredError,
|
||||
} from "@opencode/client/promise"
|
||||
import type { ACPConnection } from "./connection"
|
||||
import { partsToContentChunks, type ReplayPart } from "./content"
|
||||
@@ -40,6 +41,13 @@ export type TurnStart =
|
||||
|
||||
export const ChildSessionUpdatesCapability = "opencode/child-session-updates"
|
||||
export const ChildSessionUpdateMethod = "opencode/session/child_update"
|
||||
const RetryMeta = "opencode/retry"
|
||||
|
||||
type RetryStatus = {
|
||||
readonly attempt: number
|
||||
readonly nextRetryAt: string
|
||||
readonly error: SessionStructuredError
|
||||
}
|
||||
|
||||
type ChildSessionUpdateBase = {
|
||||
readonly rootSessionId: string
|
||||
@@ -97,6 +105,7 @@ export async function streamTurn(input: {
|
||||
let finish: SessionMessageAssistant["finish"]
|
||||
let executionError: { readonly type: string; readonly message: string } | undefined
|
||||
const tools = new Map<string, ToolState>()
|
||||
const retries = new Map<string, RetryStatus>()
|
||||
const children = new Map<string, ChildSession>()
|
||||
const openChildren = new Set<string>()
|
||||
let handedOff = false
|
||||
@@ -188,6 +197,18 @@ export async function streamTurn(input: {
|
||||
|
||||
if (event.type === "session.step.started") {
|
||||
if (!child) assistantMessageID = event.data.assistantMessageID
|
||||
if (retries.delete(eventSessionID))
|
||||
await send({ sessionUpdate: "session_info_update", _meta: { [RetryMeta]: null } })
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.retry.scheduled") {
|
||||
const retry = {
|
||||
attempt: event.data.attempt,
|
||||
nextRetryAt: new Date(event.data.at).toISOString(),
|
||||
error: event.data.error,
|
||||
}
|
||||
retries.set(eventSessionID, retry)
|
||||
await send({ sessionUpdate: "session_info_update", _meta: { [RetryMeta]: retry } })
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.text.delta") {
|
||||
@@ -381,6 +402,7 @@ export async function streamTurn(input: {
|
||||
terminal,
|
||||
control.cancelled,
|
||||
finish,
|
||||
retries.get(input.sessionID),
|
||||
)
|
||||
} catch (error) {
|
||||
streamController.abort()
|
||||
@@ -560,6 +582,7 @@ function response(
|
||||
terminal: "succeeded" | "failed" | "interrupted",
|
||||
cancelled: boolean,
|
||||
finish: SessionMessageAssistant["finish"],
|
||||
retry?: RetryStatus,
|
||||
): PromptResponse {
|
||||
const error = assistant?.error ?? executionError
|
||||
if (error?.type === "provider.auth") throw new ACPError.AuthRequiredError()
|
||||
@@ -582,7 +605,8 @@ function response(
|
||||
}
|
||||
: undefined
|
||||
const stopReason = resolveStopReason({ terminal, cancelled, finish, error: error?.type })
|
||||
return { stopReason, ...(usage ? { usage } : {}), _meta: {} }
|
||||
// Only an interrupt during backoff leaves a retry pending. Interruption clears the projected retry, so report it here.
|
||||
return { stopReason, ...(usage ? { usage } : {}), _meta: retry ? { [RetryMeta]: retry } : {} }
|
||||
}
|
||||
|
||||
function resolveStopReason(input: {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { PromptResponse, SessionNotification } from "@agentclientprotocol/sdk"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createAcpFixture, expectOk, initialize, newSession } from "./subprocess"
|
||||
|
||||
describe("acp retry subprocess", () => {
|
||||
test("reports provider retries during the turn and on cancellation", async () => {
|
||||
await using fixture = await createAcpFixture({
|
||||
respond: () =>
|
||||
Response.json(
|
||||
{ error: { message: "rate-limited upstream", type: "rate_limit_error" } },
|
||||
// A long backoff keeps the retry pending until the cancel lands.
|
||||
{ status: 429, headers: { "retry-after": "30" } },
|
||||
),
|
||||
})
|
||||
const acp = fixture.spawn()
|
||||
await initialize(acp)
|
||||
const session = await newSession(acp, fixture.home)
|
||||
|
||||
const prompt = acp.send<PromptResponse>("session/prompt", {
|
||||
sessionId: session.sessionId,
|
||||
prompt: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
const scheduled = await acp.waitForNotification<SessionNotification>(
|
||||
"session/update",
|
||||
(params) => params.sessionId === session.sessionId && params.update.sessionUpdate === "session_info_update",
|
||||
)
|
||||
const retry = scheduled.params.update._meta?.["opencode/retry"]
|
||||
expect(retry).toMatchObject({ attempt: 2, error: { message: expect.stringContaining("rate-limited upstream") } })
|
||||
|
||||
await acp.notify("session/cancel", { sessionId: session.sessionId })
|
||||
const response = expectOk(await prompt.response)
|
||||
expect(response.stopReason).toBe("cancelled")
|
||||
expect(response._meta?.["opencode/retry"]).toEqual(retry)
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -70,7 +70,10 @@ description: Verifier compatibility skill.
|
||||
`
|
||||
|
||||
export async function createAcpFixture(
|
||||
options: { readonly skill?: string; readonly respond?: (request: unknown) => string | Promise<string> } = {},
|
||||
options: {
|
||||
readonly skill?: string
|
||||
readonly respond?: (request: unknown) => string | Response | Promise<string | Response>
|
||||
} = {},
|
||||
) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cli-acp-"))
|
||||
const home = path.join(root, "workspace")
|
||||
@@ -93,7 +96,9 @@ export async function createAcpFixture(
|
||||
}
|
||||
const body: unknown = await request.json().catch(() => undefined)
|
||||
requests.push(body)
|
||||
return new Response(completion(await (options.respond?.(body) ?? "accepted")), {
|
||||
const result = await (options.respond?.(body) ?? "accepted")
|
||||
if (result instanceof Response) return result
|
||||
return new Response(completion(result), {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user