fix(agent): surface empty assistant answers instead of silent stops (#1830)
The support chat showed 'Tänker' and then went quiet with no answer and no error. Root cause: the 2026-08-21 RIP-3 cutover moved general.help to the single-call POST /api/agent/ask, whose 1500-token default cap made stop_reason max_tokens routine on tool-loop turns. The empty answer then passed unlogged through the service, the route answered 200 with an empty string, and the console appended an invisible empty bubble. Fixes, single-call path: - ask-service: default maxTokens 1500 -> 5400 (the streaming chat's reply ceiling); an empty final answer now logs model + usage and throws the typed EmptyModelAnswerError instead of passing through. - /api/agent/ask: maxDuration 300, logger, empty answer maps to 502 with 'Assistenten gav inget svar. Försök igen.'; an empty assistant turn is never persisted (the question stays, so retry works). - AskConsole: a 200 with an empty answer shows the error box instead of appending an invisible bubble. - anthropic-family: serialized tool results are bounded at 40000 chars (mirrors run-turn) so one big read cannot eat the output budget; the step-exhausted fallback keeps tools declared with tool_choice none, because replaying tool_use/tool_result without tools is an API 400. Fixes, streaming path (same silent class): - /api/agent/invoke: maxDuration 300 so deep thinking turns are not killed mid-stream at the platform default cap. - run-turn: stop_reason max_tokens with no visible text emits an error event, not a bare turn_complete. - AgentChat: an NDJSON stream that ends without turn_complete or error (and was not aborted) shows 'Anslutningen bröts innan svaret blev klart. Försök igen.' The lib/ai request-shape tests were updated deliberately for the fallback change; general.help stays on the single-call runtime (founder decision, not reverted). Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
78525bd391
commit
6aecbdc9b7
@@ -458,6 +458,11 @@ export default function AgentChat({
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
// Whether the stream delivered a terminal event (turn_complete or error).
|
||||
// A serverless function killed at its duration cap closes the stream
|
||||
// cleanly (done: true, nothing thrown), which used to look identical to
|
||||
// success: "Tänker" collapsed with no answer and no error.
|
||||
let sawTerminalEvent = false
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
@@ -480,6 +485,9 @@ export default function AgentChat({
|
||||
// First user-visible event lazily mounts the bubble. `conversation`
|
||||
// is a metadata event with no visible payload so it does not.
|
||||
const ev = parsed as { kind?: string } | null
|
||||
if (ev && (ev.kind === 'turn_complete' || ev.kind === 'error')) {
|
||||
sawTerminalEvent = true
|
||||
}
|
||||
if (
|
||||
ev &&
|
||||
typeof ev.kind === 'string' &&
|
||||
@@ -491,6 +499,12 @@ export default function AgentChat({
|
||||
handleEvent(parsed)
|
||||
}
|
||||
}
|
||||
// The stream ended without throwing. If no terminal event arrived and
|
||||
// the user did not abort, the function was cut off mid-turn (duration
|
||||
// cap, proxy drop): say so instead of presenting silence as success.
|
||||
if (!sawTerminalEvent && !signal.aborted) {
|
||||
setErrorMessage('Anslutningen bröts innan svaret blev klart. Försök igen.')
|
||||
}
|
||||
} catch (err) {
|
||||
if (!signal.aborted) {
|
||||
setErrorMessage(err instanceof Error ? getUserErrorMessage(err) : 'Streamen avbröts.')
|
||||
|
||||
@@ -185,6 +185,14 @@ export default function AskConsole({
|
||||
conversationIdRef.current = convId
|
||||
onConversationCreated?.(convId)
|
||||
}
|
||||
if (answer.trim().length === 0) {
|
||||
// Belt and braces with the server's 502 guard: a 200 whose answer is
|
||||
// empty must not append an invisible bubble ("Tänker" collapses and
|
||||
// nothing appears). Same message the server sends on 502; the thread
|
||||
// id (if one was created) is kept above so a retry lands in it.
|
||||
setError('Assistenten gav inget svar. Försök igen.')
|
||||
return
|
||||
}
|
||||
setMessages((prev) => [...prev, { role: 'assistant', text: answer }])
|
||||
} catch {
|
||||
setError('Kunde inte nå assistenten. Kontrollera anslutningen och försök igen.')
|
||||
|
||||
Reference in New Issue
Block a user