Skip to content

Responses API phase metadata is not exposed on streaming delta events #1117

@BillSong96

Description

@BillSong96

Summary

When consuming the Responses API via the SDK (wireApi: 'responses'),
the phase field that distinguishes commentary messages from
final_answer messages is only available on the terminal
assistant.message event, never on assistant.message_delta. This
makes it impossible to route streamed content to the correct UI
channel (chain-of-thought vs main response bubble) in real time — the
consumer only learns each item's phase after the item has completed,
by which point all of its deltas have already streamed.

Environment

  • @github/copilot-sdk v0.2.2
  • Provider: OpenAI (via Microsoft CAPI BYOK routing)
  • Model: gpt-5.4-reasoning
  • body.reasoning.summary = 'auto'
  • Reasoning effort: medium
  • Streaming enabled (streaming: true)
  • Repro application: OfficeAgent Excel Agent (containerized), system
    prompt includes commentary-phase instructions adapted from
    augloop-workflows PR 5072180.

Observed SDK event shapes

From a single turn in which the model emitted one commentary message
(followed by a tool call) and one final_answer message. Raw payloads
captured by subscribing via session.on(...) and writing
JSON.stringify(event, null, 2) to disk inside each callback.

assistant.message (terminal) — commentary item

{
  "type": "assistant.message",
  "data": {
    "messageId": "99d9313c-c175-4c5d-ae14-262aa3e26bea",
    "content": "<|im_sep|>**...**...",
    "toolRequests": [ { "toolCallId": "call_1qwiuJtCN6cZIekcHSf8VVdG", ... } ],
    "interactionId": "84a1875a-d161-4f16-a2c5-f49b41037714",
    "reasoningOpaque": "rs_0e2f...",
    "reasoningText": "**Modifying table column**...",
    "encryptedContent": "...",
    "phase": "commentary",          // <-- phase IS exposed here
    "outputTokens": 513
  }
}

The terminal assistant.message for the final_answer item has the
same shape (minus reasoningOpaque / reasoningText /
encryptedContent) and carries phase: "final_answer".

assistant.message_delta — streamed tokens for the SAME commentary item

{
  "type": "assistant.message_delta",
  "data": {
    "messageId": "99d9313c-c175-4c5d-ae14-262aa3e26bea",  // same ID as commentary above
    "deltaContent": "<"
    // no `phase` field                                    <-- phase NOT exposed
  }
}

45 such delta events were emitted for the commentary messageId before
the terminal event arrived, none of them carrying phase.

Aggregate counts for one turn

  132 AssistantMessageDelta  ["messageId","deltaContent"]    phase=absent    messageId=91c9aebd...  (streamed final_answer)
   45 AssistantMessageDelta  ["messageId","deltaContent"]    phase=absent    messageId=99d9313c...  (streamed commentary)
    1 AssistantMessage       [..., "phase", ...]             phase=commentary    messageId=99d9313c...
    1 AssistantMessage       [..., "phase", ...]             phase=final_answer  messageId=91c9aebd...

Across both items, 177 message_delta events streamed before the
consumer could know their phase. Two assistant.message events at
item completion then revealed the phase — too late for streaming
routing.

Why this blocks us

Our streaming UI has two distinct channels:

  • a chain-of-thought panel that shows reasoning while the model is
    working, and
  • the main assistant response bubble that shows the final answer.

To route content correctly in real time, we need to know the phase of
each streamed token at the moment it arrives. Today
assistant.message_delta is phase-blind; the consumer can only learn
the phase retroactively when assistant.message fires at item
completion, by which point the deltas have already been forwarded to
the wrong channel.

The non-container code path (direct raw-SSE consumption, not using
the SDK) solves this by handling response.output_item.added up
front — that event tells the consumer "the next stream of tokens
belongs to item X with phase Y", and subsequent
response.output_text.delta events can be routed by looking up the
phase associated with the item's ID. This pattern is implemented in
augloop-workflows PR 5072180 (internal). We cannot apply the same
pattern through the SDK because response.output_item.added is
consumed internally and no equivalent event is re-emitted to SDK
consumers (see "Verification" below).

Verification: no SDK event carries an item-added preamble

To rule out the possibility that the SDK surfaces
response.output_item.added under a different event name that we
simply haven't subscribed to, we added a wildcard
session.on(handler) subscription and re-ran the same turn. The SDK
emitted 21 distinct event types in total. None of them carry an
{itemId, phase} preamble:

  • assistant.turn_start fires per-turn with
    data: {turnId, interactionId} only — no item information.
  • assistant.turn_end fires per-turn with data: {turnId} only.
  • pending_messages.modified fires with an empty data: {} object,
    so consumers can observe a change occurred but not what changed.
  • All other event types are either per-delta streaming events
    (phase-blind, as above), per-tool, per-hook, or session-level
    lifecycle (session.info, session.idle, session.usage_info,
    session.tools_updated, session.skills_loaded).

Full event-type list is attached as
evidence/all-sdk-event-types.log; the three non-obvious candidates
above have their actual payloads in evidence/ruled-out/.

Ask

Two options, in order of preference:

Option A (minimum) — surface phase on assistant.message_delta

Include data.phase on every assistant.message_delta event,
carrying the phase of the item the delta belongs to. Consumers can
then filter deltas by phase without waiting for the terminal event.

Option B (ideal) — surface response.output_item.added as a discrete SDK event

Emit a new event type (e.g. assistant.item_added) when a new output
item begins, carrying { itemId, itemType, phase }. Subsequent
assistant.message_delta events only need to carry messageId
(already present); consumers maintain an itemId -> phase map and
route deltas by lookup. This matches the raw-SSE model and avoids
duplicating phase information on every delta.

Either option unblocks phase-aware streaming routing.

Evidence bundle

The companion folder contains:

  • evidence/commentary-item/ — terminal commentary event plus 3
    delta events for the same messageId.
  • evidence/final-answer-item/ — terminal final_answer event plus 3
    delta events for the same messageId.
  • evidence/all-sdk-event-types.log — full list of the 21 SDK event
    types observed via wildcard subscription.
  • evidence/ruled-out/ — actual payloads of turn_start,
    turn_end, and pending_messages.modified, confirmed not to
    carry item-level metadata.
  • event-shape-summary.log — aggregate counts for the turn.

evidences.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions