moeru-ai/airi · error · Error
Responses continuation must contain an item array
Error message
Responses continuation must contain an item array
What it means
When rendering the conversation, an assistant round carries a continuation stamped with protocol 'responses' and matching scope, but its `data` field is not an array. Continuation data holds native Responses `ItemParam[]` from previous steps so stateless multi-step tool loops can be replayed; the renderer reuses it verbatim, so a non-array would silently corrupt the request input.
Solutions
- Ensure round.continuation.data is always an ItemParam[] (the raw Responses items returned by the generation step).
- Delete the stale continuation from the persisted round so renderConversation falls back to projectRound + renderMessage, which re-renders the round portably.
- If migrating persisted state, re-create continuations by replaying or strip all continuations (they are an optimization for stateless replay).
- Verify the scope string matches how it was created (input.scope); mismatched scope entries are ignored, so if you see this, scope matched but data was wrong — check the writer of the continuation.
Example fix
// before
continuation: { protocol: 'responses', scope, data: { items: [...] } }
// after
continuation: { protocol: 'responses', scope, data: [...items] } // ItemParam[] Defensive patterns
Strategy: type-guard
Validate before calling
const usableContinuation = (round, scope) => {
const c = round.continuation
return !!c && c.protocol === 'responses' && c.scope === scope && Array.isArray(c.data) ? c : null
} Type guard
const isResponsesContinuation = (c) => c != null && typeof c === 'object' && c.protocol === 'responses' && Array.isArray(c.data)
Try / catch
try {
await streamResponses({ config, conversation, scope, onEvent })
} catch (e) {
if (e.message === 'Responses continuation must contain an item array') {
// strip invalid continuations from all assistant rounds and retry
conversation.turns.forEach(t => t.type === 'assistant' && t.rounds.forEach(r => { delete r.continuation }))
} else throw e
} Prevention
- Never hand-write continuation objects; only assign data returned by the generation step.
- Version or validate persisted conversation snapshots on load; drop continuations with unknown shapes.
- Remember continuations are a stateless-replay optimization — they are always safe to delete.
When it happens
Trigger: A stored or hand-written continuation object `{ protocol: 'responses', scope: <matching scope>, data: <non-array> }` on an assistant round. Happens when persisted continuation state was written by an older version with a different data shape, when the continuation was deserialized from JSON with data replaced by an object/null, or when custom code assigned the wrong payload to continuation.data.
Common situations: Upgrading the runtime so old persisted conversation snapshots have a stale continuation schema; manually editing or truncating persisted conversation files; a bug in a custom generation wrapper that stores the response object instead of the item array.
Related errors
- Only assistant messages can invoke tools
- property key 'version' wasn't found in the value of key
- Responses file requires exactly one source
- Responses image output requires a URL
- Tool messages require a correlated tool result
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/29efc346fc12098d.
Report an issue: GitHub.
Appendix: source
Thrown at packages/core-agent/src/runtime/responses.ts:84
items.push({ type: 'function_call_output', call_id: segment.callId, output: content.every(part => part.type === 'input_text') ? content.map(part => part.text).join('') : content })
}
else {
parts.push(segment)
}
}
flush()
return items
}
function renderConversation(conversation: Conversation, scope: string): ItemParam[] {
return conversation.turns.flatMap((turn) => {
if (turn.type !== 'assistant')
return renderMessage(projectInput(turn))
return turn.rounds.flatMap((round) => {
const continuation = round.continuation
if (continuation?.protocol === 'responses' && continuation.scope === scope) {
if (!Array.isArray(continuation.data))
throw new Error('Responses continuation must contain an item array')
return continuation.data
}
return projectRound(round).flatMap(renderMessage)
})
})
}
function readToolResultContent(content: Extract<ItemParam, { type: 'function_call_output' }>['output']): InputSegment[] {
if (typeof content === 'string')
return [{ type: 'text', text: content }]
return content.map((part) => {
switch (part.type) {
case 'input_text': return { type: 'text', text: part.text }
case 'input_image':
if (!part.image_url)
throw new Error('Responses image output requires a URL')
return { type: 'image', url: part.image_url, detail: part.detail ?? undefined }
case 'input_file':View on GitHub (pinned to 438a067dde)