Skip to content

Commit 0a2ccc3

Browse files
authored
Merge pull request #156 from browser-use/finish-reason-resample
fix(opencode): resample turns with an unmapped finish reason
2 parents e634099 + 1fdb393 commit 0a2ccc3

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1108,9 +1108,22 @@ const layer = Layer.effect(
11081108
(part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
11091109
) ?? false
11101110

1111+
// "unknown" is every mapper's fallback for a finish reason we could not
1112+
// interpret, and the AI SDK reports a stream that closed without any
1113+
// finish chunk at all as "other", which maps here too. Neither means the
1114+
// model was done, so resample the turn like "tool-calls" instead of
1115+
// exiting as a clean completion. Exiting here silently truncated runs
1116+
// mid-task: the error check below already excludes "unknown", so nothing
1117+
// was recorded anywhere.
1118+
if (lastAssistant?.finish === "unknown")
1119+
yield* Effect.logWarning("resampling turn that ended with an unmapped finish reason", {
1120+
"session.id": sessionID,
1121+
messageID: lastAssistant.id,
1122+
})
1123+
11111124
if (
11121125
lastAssistant?.finish &&
1113-
!["tool-calls"].includes(lastAssistant.finish) &&
1126+
!["tool-calls", "unknown"].includes(lastAssistant.finish) &&
11141127
!hasToolCalls &&
11151128
lastAssistant.parentID === lastUser.id
11161129
) {

packages/opencode/test/session/prompt.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,36 @@ it.instance("loop continues when finish is tool-calls", () =>
930930
}),
931931
)
932932

933+
it.instance("loop resamples when the provider closes a stream without a finish reason", () =>
934+
Effect.gen(function* () {
935+
const { llm } = yield* useServerConfig(providerCfg)
936+
const prompt = yield* SessionPrompt.Service
937+
const sessions = yield* Session.Service
938+
const session = yield* sessions.create({
939+
title: "Pinned",
940+
permission: [{ permission: "*", pattern: "*", action: "allow" }],
941+
})
942+
yield* prompt.prompt({
943+
sessionID: session.id,
944+
agent: "build",
945+
noReply: true,
946+
parts: [{ type: "text", text: "hello" }],
947+
})
948+
// No finish chunk: the AI SDK reports this as finishReason "other", which we
949+
// map to "unknown". The loop must resample instead of ending the run.
950+
yield* llm.push(reply().item())
951+
yield* llm.text("second")
952+
953+
const result = yield* prompt.loop({ sessionID: session.id })
954+
expect(yield* llm.calls).toBe(2)
955+
expect(result.info.role).toBe("assistant")
956+
if (result.info.role === "assistant") {
957+
expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
958+
expect(result.info.finish).toBe("stop")
959+
}
960+
}),
961+
)
962+
933963
it.instance("glob tool keeps instance context during prompt runs", () =>
934964
Effect.gen(function* () {
935965
const { dir, llm } = yield* useServerConfig(providerCfg)

0 commit comments

Comments
 (0)