--stream-partial-output: assistant events have multiple undocumented forms — how should consumers parse them?

Context

We’re building automation on top of cursor agent --print --output-format stream-json --stream-partial-output and need to correctly parse the output.

Problem

In a single session, assistant events appear in at least 4 different forms, but the documentation only describes one:

// Form 1: no model_call_id, has timestamp_ms

{"type":"assistant","message":{"content":\[{"type":"text","text":"正在读取..."}\]},"timestamp_ms":1774846267524}

// Form 2: has model_call_id, has timestamp_ms — same text as Form 1

{"type":"assistant","message":{"content":\[{"type":"text","text":"正在读取..."}\]},"model_call_id":"a594...","timestamp_ms":1774846267693}

// Form 3: has timestamp_ms, small delta text fragments

{"type":"assistant","message":{"content":\[{"type":"text","text":"RCH\]\\n\\n\`offbo"}\]},"timestamp_ms":1774846272384}

// Form 4: no timestamp_ms, full cumulative text of the entire response

{"type":"assistant","message":{"content":\[{"type":"text","text":"<entire response here>"}\]}}

The documentation only says:

Concatenate all message.content[].text values to reconstruct the complete response.

Following this literally produces duplicated text, because Form 1 & 2 carry the same content, and Form 4 repeats everything again.

Questions

  1. Is there an official parsing reference or sample parser for --stream-partial-output that handles all these forms?

  2. What is the intended semantic for each form? Specifically:

    • Should Form 1 (no model_call_id) be skipped in favor of Form 2?

    • Should Form 4 (no timestamp_ms) always be skipped?

    • Are there other forms we haven’t encountered yet?

Hey @weiji! Great questions!

When you use --stream-partial-output, the CLI sends you each chunk of text as it arrives from the model. Those are the assistant events with timestamp_ms (your Forms 1 and 3, which I believe are actually the same form). That’s the stream you want for real-time display, and you can concatenate them to build the response as it’s generated.

The CLI also emits buffered copies of the same text at certain boundaries. You’ll see one before each tool call (those have a model_call_id field, your Form 2), and one right at the end before the result event (that one has no timestamp_ms , your Form 4).

If you don’t need real-time streaming and just want the final answer, you can skip all assistant events entirely and use the type: "result" event at the end of the stream — its result field has the complete, deduplicated response.

Here’s an annotated example showing every event type in action:

# Setup
{"type":"system","subtype":"init","model":"Opus 4.6 1M Thinking",...}
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]},...}

# Thinking
{"type":"thinking","subtype":"delta","text":"The user",...}
  ... more thinking deltas ...
{"type":"thinking","subtype":"completed",...}

# Text before tool call
{"type":"assistant","message":{"content":[{"type":"text","text":"I will"}]},
    "timestamp_ms":1774949076041}                                                ✅ delta
{"type":"assistant","message":{"content":[{"type":"text","text":" read the file now."}]},
    "timestamp_ms":1774949076077}                                                ✅ delta
{"type":"assistant","message":{"content":[{"type":"text","text":"I will read the file now."}]},
    "model_call_id":"5955bb18-...","timestamp_ms":1774949076838}                 ❌ flush before tool call

# Tool call
{"type":"tool_call","subtype":"started","call_id":"toolu_vrtx_...","tool_call":{...},
    "model_call_id":"5955bb18-...","timestamp_ms":1774949076838}
{"type":"tool_call","subtype":"completed","call_id":"toolu_vrtx_...","tool_call":{...},
    "model_call_id":"5955bb18-...","timestamp_ms":1774949077019}

# Text after tool call
{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]},
    "timestamp_ms":1774949078199}                                                ✅ delta
{"type":"assistant","message":{"content":[{"type":"text","text":" world!"}]},
    "timestamp_ms":1774949078261}                                                ✅ delta
{"type":"assistant","message":{"content":[{"type":"text","text":"Hello world!"}]}}
                                                                                 ❌ final flush (no timestamp)

# Final result
{"type":"result","subtype":"success",
    "result":"I will read the file now.Hello world!",...}                         ✅ canonical answer

Cheatsheet:

type timestamp_ms model_call_id Action
assistant :white_check_mark: :cross_mark: Use — streaming delta
assistant :white_check_mark: :white_check_mark: Skip — duplicate before tool call
assistant :cross_mark: :cross_mark: Skip — duplicate at end
result Use — canonical final answer

We’ll look into updating the docs, but please let us know if we missed something here or if you run into anything else! This was my takeaway after looking into the code and running a few tests. Possible I missed something.

Hey again @weiji!

Would love to know if this information was helpful / matched what you’re seeing. :slight_smile:

Hi Colin,

Sorry for the delay getting back to you!

The breakdown you provided was super helpful—we were definitely getting tripped up by those buffered flushes in Form 2 and 4. Your tip about using timestamp_ms as the filter for deltas and relying on the result event for the final state worked like a charm.

It’s all running smoothly now. Really appreciate you digging into the source code to clear that up for us!