fix(opencode): settle pending tool errors

This commit is contained in:
Aiden Cline 2026-07-04 02:03:43 +00:00
parent b44bc0ad07
commit 42d4995cee
2 changed files with 61 additions and 3 deletions

View file

@ -185,7 +185,8 @@ const layer = Layer.effect(
const failToolCall = Effect.fn("SessionProcessor.failToolCall")(function* (toolCallID: string, error: unknown) {
const match = yield* readToolCall(toolCallID)
if (!match || match.part.state.status !== "running") return false
if (!match || (match.part.state.status !== "pending" && match.part.state.status !== "running")) return false
const start = match.part.state.status === "running" ? match.part.state.time.start : Date.now()
yield* session.updatePart({
...match.part,
state: {
@ -193,8 +194,8 @@ const layer = Layer.effect(
input: match.part.state.input,
error: errorMessage(error),
// Keep metadata streamed while running so failures retain progress detail (e.g. execute's child calls).
metadata: match.part.state.metadata,
time: { start: match.part.state.time.start, end: Date.now() },
metadata: match.part.state.status === "running" ? match.part.state.metadata : undefined,
time: { start, end: Date.now() },
},
})
if (error instanceof PermissionV1.RejectedError || error instanceof Question.RejectedError) {

View file

@ -226,6 +226,22 @@ const fragmentFailureLLM = Layer.succeed(
const fragmentFailureEnv = LayerNode.compile(root, [...replacements, [LLM.node, fragmentFailureLLM]])
const itFragmentFailure = testEffect(fragmentFailureEnv)
const pendingToolErrorLLM = Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () =>
Stream.make(
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id: "call-pending-error", name: "lookup" }),
LLMEvent.toolError({ id: "call-pending-error", name: "lookup", message: "lookup failed" }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
),
}),
)
const pendingToolErrorEnv = LayerNode.compile(root, [...replacements, [LLM.node, pendingToolErrorLLM]])
const itPendingToolError = testEffect(pendingToolErrorEnv)
const boot = Effect.fn("test.boot")(function* () {
const processors = yield* SessionProcessor.Service
const session = yield* Session.Service
@ -834,6 +850,47 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
),
)
itPendingToolError.live("session.processor settles tool errors received while input is pending", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()
const chat = yield* session.create({})
const parent = yield* user(chat.id, "pending tool error")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
expect(
yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "pending tool error" }],
tools: {},
}),
).toBe("continue")
const parts = yield* MessageV2.parts(msg.id)
const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
expect(call?.state.status).toBe("error")
if (call?.state.status !== "error") return
expect(call.state.error).toBe("lookup failed")
expect(call.state.metadata?.interrupted).toBeUndefined()
}),
{ config: cfg },
),
)
it.live("session.processor effect tests record aborted errors and idle state", () =>
provideTmpdirServer(
({ dir, llm }) =>