mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 06:16:48 +02:00
feat(tui): add retry and failure debug frames
This commit is contained in:
@@ -39,6 +39,32 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"retrying": {
|
||||
"prompt": "Preview a background subagent retrying an upstream request.",
|
||||
"parts": [
|
||||
{
|
||||
"type": "subagent",
|
||||
"agent": "explore",
|
||||
"description": "Retry provider request while maintaining context",
|
||||
"state": "retrying",
|
||||
"background": true,
|
||||
"message": "Rate limited by provider; retrying after quota window",
|
||||
"attempt": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"failed": {
|
||||
"prompt": "Preview a delegated task failure.",
|
||||
"parts": [
|
||||
{
|
||||
"type": "subagent",
|
||||
"agent": "general",
|
||||
"description": "Fail delegated work after checking upstream status",
|
||||
"state": "error",
|
||||
"error": "Provider returned an authentication error"
|
||||
}
|
||||
]
|
||||
},
|
||||
"completed": {
|
||||
"prompt": "Preview completed foreground and background subagents.",
|
||||
"parts": [
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
Message,
|
||||
Part,
|
||||
Session,
|
||||
SessionStatus,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
ToolState,
|
||||
@@ -39,6 +40,19 @@ const PartInput = Schema.Union([
|
||||
Schema.Struct({ ...SubagentFields, state: Schema.Literal("running") }),
|
||||
Schema.Struct({ ...SubagentFields, state: Schema.Literal("active-background") }),
|
||||
Schema.Struct({ ...SubagentFields, state: Schema.Literal("completed"), background: Schema.optional(Schema.Boolean) }),
|
||||
Schema.Struct({
|
||||
...SubagentFields,
|
||||
state: Schema.Literal("retrying"),
|
||||
background: Schema.optional(Schema.Boolean),
|
||||
message: Schema.String,
|
||||
attempt: Schema.Number,
|
||||
}),
|
||||
Schema.Struct({
|
||||
...SubagentFields,
|
||||
state: Schema.Literal("error"),
|
||||
background: Schema.optional(Schema.Boolean),
|
||||
error: Schema.String,
|
||||
}),
|
||||
])
|
||||
const Frame = Schema.Struct({
|
||||
prompt: Schema.String,
|
||||
@@ -66,7 +80,7 @@ export async function createDebugFrameTransport(input: { file: string; frame: st
|
||||
const created = 1_000_000
|
||||
const root = makeSession(sessionID, input.directory, `${fixture.name}: ${input.frame}`, created)
|
||||
const childSessions = new Map<string, { session: Session; transcript: Transcript }>()
|
||||
const statuses: Record<string, { type: "busy" }> = {}
|
||||
const statuses: Record<string, SessionStatus> = {}
|
||||
const parts = compileParts(frame, sessionID, created, input.directory, childSessions, statuses)
|
||||
const transcript = turn(sessionID, created, frame.prompt, parts)
|
||||
const fetch = createFetch({ root, transcript, childSessions, statuses, directory: input.directory })
|
||||
@@ -81,7 +95,7 @@ function compileParts(
|
||||
created: number,
|
||||
directory: string,
|
||||
children: Map<string, { session: Session; transcript: Transcript }>,
|
||||
statuses: Record<string, { type: "busy" }>,
|
||||
statuses: Record<string, SessionStatus>,
|
||||
) {
|
||||
return frame.parts.map((part, index): Part => {
|
||||
const id = `part_${index.toString().padStart(2, "0")}`
|
||||
@@ -125,24 +139,29 @@ function compileParts(
|
||||
),
|
||||
})
|
||||
if (part.state === "active-background") statuses[childID] = { type: "busy" }
|
||||
if (part.state === "retrying") {
|
||||
statuses[childID] = { type: "retry", attempt: part.attempt, message: part.message, next: created + 1000 }
|
||||
}
|
||||
const state = part.state === "completed" ? "completed" : part.state === "error" ? "error" : "running"
|
||||
const background =
|
||||
part.state === "active-background" ||
|
||||
((part.state === "completed" || part.state === "retrying" || part.state === "error") && part.background === true)
|
||||
return tool(
|
||||
sessionID,
|
||||
assistantID(sessionID),
|
||||
id,
|
||||
"task",
|
||||
toolState(
|
||||
part.state === "running" ? "running" : "completed",
|
||||
state,
|
||||
{
|
||||
description: part.description,
|
||||
subagent_type: part.agent,
|
||||
},
|
||||
{
|
||||
sessionId: childID,
|
||||
...(part.state === "active-background" || (part.state === "completed" && part.background === true)
|
||||
? { background: true }
|
||||
: {}),
|
||||
...(background ? { background: true } : {}),
|
||||
},
|
||||
part.description,
|
||||
part.state === "error" ? part.error : part.description,
|
||||
created,
|
||||
),
|
||||
)
|
||||
@@ -153,7 +172,7 @@ function createFetch(input: {
|
||||
root: Session
|
||||
transcript: Transcript
|
||||
childSessions: Map<string, { session: Session; transcript: Transcript }>
|
||||
statuses: Record<string, { type: "busy" }>
|
||||
statuses: Record<string, SessionStatus>
|
||||
directory: string
|
||||
}) {
|
||||
const provider = {
|
||||
@@ -324,7 +343,9 @@ function toolState(
|
||||
created: number,
|
||||
): ToolState {
|
||||
if (state === "running") return { status: "running", input, metadata, title, time: { start: created } }
|
||||
if (state === "error") return { status: "error", input, error: title, time: { start: created, end: created + 1 } }
|
||||
if (state === "error") {
|
||||
return { status: "error", input, metadata, error: title, time: { start: created, end: created + 1 } }
|
||||
}
|
||||
return {
|
||||
status: "completed",
|
||||
input,
|
||||
|
||||
@@ -47,10 +47,35 @@ describe("TUI debug frames", () => {
|
||||
expect(Object.values(status)).toEqual([{ type: "busy" }])
|
||||
})
|
||||
|
||||
test("compiles retrying and failed subagent states", async () => {
|
||||
const retrying = await createDebugFrameTransport({ file: fixture, frame: "retrying", directory: "/tmp/project" })
|
||||
const retryTranscript = (await (
|
||||
await retrying.fetch(`http://opencode.debug/session/${retrying.sessionID}/message`)
|
||||
).json()) as Array<{ parts: Part[] }>
|
||||
const retryTask = retryTranscript[1]!.parts.find(
|
||||
(part): part is ToolPart => part.type === "tool" && part.tool === "task",
|
||||
)!
|
||||
const retryID = runningMetadata(retryTask).sessionId as string
|
||||
const status = (await (await retrying.fetch("http://opencode.debug/session/status")).json()) as Record<
|
||||
string,
|
||||
{ type: string; attempt?: number }
|
||||
>
|
||||
const failed = await createDebugFrameTransport({ file: fixture, frame: "failed", directory: "/tmp/project" })
|
||||
const failedTranscript = (await (
|
||||
await failed.fetch(`http://opencode.debug/session/${failed.sessionID}/message`)
|
||||
).json()) as Array<{ parts: Part[] }>
|
||||
const failedTask = failedTranscript[1]!.parts.find(
|
||||
(part): part is ToolPart => part.type === "tool" && part.tool === "task",
|
||||
)!
|
||||
|
||||
expect(status[retryID]).toMatchObject({ type: "retry", attempt: 2 })
|
||||
expect(failedTask.state.status).toBe("error")
|
||||
})
|
||||
|
||||
test("reports available frames for unknown selection", async () => {
|
||||
await expect(
|
||||
createDebugFrameTransport({ file: fixture, frame: "missing", directory: "/tmp/project" }),
|
||||
).rejects.toThrow("Available frames: running, active-background, completed")
|
||||
).rejects.toThrow("Available frames: running, active-background, retrying, failed, completed")
|
||||
})
|
||||
|
||||
test("rejects mutations against static debug frames", async () => {
|
||||
@@ -66,3 +91,8 @@ function completedMetadata(part: ToolPart) {
|
||||
if (part.state.status !== "completed") throw new Error("Expected completed task")
|
||||
return part.state.metadata
|
||||
}
|
||||
|
||||
function runningMetadata(part: ToolPart) {
|
||||
if (part.state.status !== "running") throw new Error("Expected running task")
|
||||
return part.state.metadata ?? {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user