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

  1. Ensure round.continuation.data is always an ItemParam[] (the raw Responses items returned by the generation step).
  2. Delete the stale continuation from the persisted round so renderConversation falls back to projectRound + renderMessage, which re-renders the round portably.
  3. If migrating persisted state, re-create continuations by replaying or strip all continuations (they are an optimization for stateless replay).
  4. 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

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


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)