fix(session): reject empty assistant responses
This commit is contained in:
parent
f2f5eb6f16
commit
f57f68e1c8
2 changed files with 71 additions and 1 deletions
|
|
@ -1083,6 +1083,7 @@ const layer = Layer.effect(
|
|||
const ctx = yield* InstanceState.context
|
||||
let structured: unknown
|
||||
let step = 0
|
||||
let retriedEmptyResponse = false
|
||||
const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
|
||||
|
||||
while (true) {
|
||||
|
|
@ -1107,12 +1108,17 @@ const layer = Layer.effect(
|
|||
lastAssistantMsg?.parts.some(
|
||||
(part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
|
||||
) ?? false
|
||||
const hasVisibleOutput =
|
||||
lastAssistantMsg?.parts.some(
|
||||
(part) => part.type === "tool" || (part.type === "text" && part.text.trim().length > 0),
|
||||
) ?? false
|
||||
|
||||
if (
|
||||
lastAssistant?.finish &&
|
||||
!["tool-calls"].includes(lastAssistant.finish) &&
|
||||
!hasToolCalls &&
|
||||
lastUser.id < lastAssistant.id
|
||||
lastUser.id < lastAssistant.id &&
|
||||
!(retriedEmptyResponse && !hasVisibleOutput)
|
||||
) {
|
||||
const orphan = lastAssistantMsg?.parts.find(
|
||||
(part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
|
||||
|
|
@ -1314,6 +1320,28 @@ const layer = Layer.effect(
|
|||
yield* sessions.updateMessage(handle.message)
|
||||
return "break" as const
|
||||
}
|
||||
|
||||
const parts = yield* MessageV2.parts(handle.message.id).pipe(
|
||||
Effect.provideService(Database.Service, database),
|
||||
)
|
||||
const hasVisibleOutput = parts.some(
|
||||
(part) => part.type === "tool" || (part.type === "text" && part.text.trim().length > 0),
|
||||
)
|
||||
if (!hasVisibleOutput) {
|
||||
if (!retriedEmptyResponse) {
|
||||
retriedEmptyResponse = true
|
||||
yield* Effect.logWarning("retrying empty assistant response", {
|
||||
"session.id": sessionID,
|
||||
messageID: handle.message.id,
|
||||
})
|
||||
return "continue" as const
|
||||
}
|
||||
const error = new NamedError.Unknown({ message: "Model returned an empty response after retry" })
|
||||
handle.message.error = error.toObject()
|
||||
yield* sessions.updateMessage(handle.message)
|
||||
yield* events.publish(Session.Event.Error, { sessionID, error: handle.message.error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (result === "stop") return "break" as const
|
||||
|
|
|
|||
|
|
@ -2201,6 +2201,48 @@ it.instance("does not loop empty assistant turns for a simple reply", () =>
|
|||
}),
|
||||
)
|
||||
|
||||
it.instance("retries a reasoning-only assistant response once", () =>
|
||||
Effect.gen(function* () {
|
||||
const { llm } = yield* useServerConfig(providerCfg)
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Empty response retry" })
|
||||
|
||||
yield* llm.push(reply().reason("thinking").stop(), reply().text("done").stop())
|
||||
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "Answer me" }],
|
||||
})
|
||||
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
expect(result.parts.some((part) => part.type === "text" && part.text === "done")).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("fails after two reasoning-only assistant responses", () =>
|
||||
Effect.gen(function* () {
|
||||
const { llm } = yield* useServerConfig(providerCfg)
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({ title: "Empty response failure" })
|
||||
|
||||
yield* llm.push(reply().reason("thinking").stop(), reply().reason("still thinking").stop())
|
||||
|
||||
const exit = yield* prompt
|
||||
.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
parts: [{ type: "text", text: "Answer me" }],
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("records aborted errors when prompt is cancelled mid-stream", () =>
|
||||
Effect.gen(function* () {
|
||||
const { llm } = yield* useServerConfig(providerCfg)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue