Cursor SDK / Composer 2.5: Finished Runs With Empty Assistant Output

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

Summary

We are seeing intermittent production Cursor Cloud Agent runs where the SDK reports the run as finished, and usage/enrichment shows nonzero output tokens, but no assistant output is available from any SDK surface.

Stream Replay Detail

For run-f37134be-xxxxxxxxxxx-97b323f7c873, replaying the stream via Agent.getRun(…).stream() produced only:

[
  { "type": "status", "status": "FINISHED" },
  { "type": "status", "status": "FINISHED" }
]

There were no assistant events.

For a nearby healthy run, the same stream replay included many assistant events and run.wait().result contained non-empty text.

Pattern

This is intermittent and cross-surface:

  • It is not tied to a single worker pod.
  • Bad runs consistently have very low stream event counts, usually 2-4.
  • The run is marked finished, not error.
  • Usage can show nonzero output tokens even though no assistant text is available.

Impact

Our production integrations treat finished as success. When finished runs have empty assistant output, users receive blank replies.

Questions

  • Is this related to Composer 2.5 / Composer 2.5 Fast capacity or model routing?
  • Should SDK clients treat finished + undefined result + no assistantMessage as a retryable failure?
  • Does @cursor/sdk 1.0.23 include any fix for this exact finished state, or only better error surfacing for status: “error” runs?

Steps to Reproduce

run.status is finished
run.wait() resolves with status: “finished”
run.wait().result is undefined
run.conversation() contains the user turn but no assistantMessage
Replaying run.stream() returns only FINISHED status events and no assistant events

Expected Behavior

Expected Behavior

If a run finishes successfully and reports nonzero output tokens, the assistant response should be available through at least one SDK surface:

run.wait().result
run.conversation() as an assistantMessage
run.stream() assistant events

If no assistant response can be produced, the run should not be marked as finished; it should return error with actionable error details.

Operating System

Linux

Version Information

SDK: @cursor/sdk 1.0.19 at the time of the affected production runs
Runtime: Cursor Cloud Agents
Node: >=22
Model reported in our task analytics: composer-2.5+standard

For AI issues: which model did you use?

composer-2.5

Does this stop you from using Cursor

Yes - Cursor is unusable

Hey there,
Cloud run can legitimately reach finished without a final assistant-text turn, for example when the last turn ends on tool calls or the model produces no closing text. When that happens there’s no assistant text to surface, so run.wait().result is undefined, conversation() has only the user turn, and a replayed stream() carries just the terminal status events. The nonzero output tokens are expected alongside this, since usage accrues from the model’s work during the run (tool-call turns included), independent of whether a final assistant message was produced.

On your three questions:

  1. Composer 2.5 capacity / routing? Not the cause. This isn’t specific to Composer 2.5, and it isn’t a routing or capacity effect. Capacity or routing problems surface as error runs or slow responses, not as a finished run with empty output. What you’re hitting is the general “finished with no final assistant text” case.

  2. Treat finished + undefined result + no assistantMessage as retryable? Yes, that’s a sound defensive pattern. Since a run can finish today without producing final text, an empty result on a finished run is a reasonable signal to re-run rather than show a blank reply to your users. Guarding on status === "finished" with no result and no assistant turn in conversation(), then retrying, is the right call.

  3. Does 1.0.23 fix this exact finished state? Not this specific case. 1.0.23 improves error detail on runs that end in status: "error", and 1.0.22 added usage surfacing on wait() and the stream, so both are worth upgrading to. But no SDK version will surface assistant text for a run that didn’t produce any, so the finished + empty-output outcome won’t change with the upgrade.

To dig into your specific runs, could you share, for a couple of the bad ones: the full (unredacted) requestId and run/agent id, rough timestamps in UTC, and whether those runs use subagents? The requestId on the RunResult is the cleanest way for us to trace a run on our side. With that we can confirm whether the model genuinely produced no final text, or whether the response wasn’t surfaced correctly on our end, and track it accordingly.

Sure. Here are some of the runs. BTW I forgot to mention - all of those are within self-hosted agents
Also - switching model to default seems to resolved that issue. But we prefer to stay on composer 2.5


run-637f4a1d-4903-43fb-b5fb-98b5dfdf1be9
bc-cf90ee67-aefb-41fb-9af2-eedd1745f7af
Stream events: 4
Tool calls: 0
Finished: 2026-07-08 08:43:33 UTC
run-c9b3158b-7adf-46de-9064-71a660b5e8e4
bc-4cd2fa6e-5317-4cce-80b0-b90974c3920f
Stream events: 2
Tool calls: 0
Finished: 2026-07-08 08:17:11 UTC

Just tried to switch back to composer-2.5 and getting pretty much the same output
Having it set to default model - works fine

UPD: We’ve switched back to composer-2.5 as it seems that it is no longer producing empty responses. No changes on our side except pointing to composer-2.5 instead of default.

I understand it was advised this issue was not relevant to a specific model but it really looked so, if that helps investigation.

That lines up with what you were seeing. Since nothing changed on your side and default kept working the whole time, this looks like a temporary, model-side dip on composer-2.5 around that window rather than anything in your integration, and it’s since cleared. So your read that it looked model-specific was fair.

A couple of things worth keeping handy:

  1. Fast vs standard variant. In the SDK, composer-2.5 without explicit params resolves to the Fast variant. If you’d rather pin the standard (non-Fast) variant while staying on composer-2.5, pass the fast parameter:
model: { id: "composer-2.5", params: [{ id: "fast", value: "false" }] }

You can confirm the variants and params available to your account with Cursor.models.list(). Details are in the Cursor SDK docs.

  1. Keep the retry guard. Treating a finished run that has no result and no assistant turn as a signal to re-run, instead of surfacing a blank reply, is still the right safety net for the occasional empty finish.

If it comes back, grab the requestId from the RunResult plus a rough UTC timestamp for a bad run and drop it here. That’s the cleanest way for us to trace a specific run and confirm whether it was genuinely empty output or a surfacing issue on our end.