Skip to content

fix: recover agent message text from item/completed when deltas are missing#266

Open
maojindao55 wants to merge 1 commit into
agentclientprotocol:mainfrom
maojindao55:fix/agent-message-completed-fallback
Open

fix: recover agent message text from item/completed when deltas are missing#266
maojindao55 wants to merge 1 commit into
agentclientprotocol:mainfrom
maojindao55:fix/agent-message-completed-fallback

Conversation

@maojindao55

Copy link
Copy Markdown

Problem

Some Responses API providers (e.g. Volcengine Ark / 火山方舟) emit reasoning items in output_item.added without the content/summary array fields that Codex's internal active-item state machine expects. When this happens, the state machine fails to register an active item, and all subsequent streaming deltas — including output_text.delta — are silently dropped with errors like:

ERROR codex_core::util: OutputTextDelta without active item

The model does generate text (confirmed by outputTokens in usage), and Codex successfully surfaces it via the item/completed notification. However, codex-acp ignored agentMessage items in completeItemEvent (return null), so the text was lost entirely. Users on affected providers only saw the reasoning/thinking content, with no actual assistant reply.

Root Cause (confirmed via logs)

Layer TUI mode (codex CLI) app-server mode (codex-acp)
stream_events_utils (SSE parsing) ✅ works ✅ works
codex_core::util (active-item state machine) not used ❌ fails on providers with non-standard reasoning items

The TUI mode bypasses the state machine, so codex CLI works fine. The app-server mode uses it and drops deltas — but the full text is still available in item/completed.

Fix

Mirror the existing reasoning fallback pattern for agentMessage:

  1. Track item IDs that received item/agentMessage/delta in a new seenAgentMessageDeltaItemIds set (in createTextEvent)
  2. In completeItemEvent, if no deltas were seen for the agentMessage item, emit item.text as an agent_message_chunk
  3. If deltas were seen, return null (no duplication)

This mirrors exactly how reasoning items are already handled (seenReasoningDeltaItemIds + createCompletedReasoningEvent).

Impact

  • Providers with correct streaming (OpenAI, etc.): zero impactseenAgentMessageDeltaItemIds contains the item ID, so the completed event returns null as before
  • Providers with missing deltas (Volcengine Ark, potentially others): text is recovered from item/completed instead of being lost

Testing

Added agent-message-fallback.test.ts with two cases:

  • "emits agent message text from item/completed when no deltas streamed" — verifies the fallback fires
  • "does not duplicate agent message when deltas were streamed" — verifies no duplication for standard providers

All 280 existing tests still pass.

Reproduction

Provider config (Volcengine Ark):

model = "doubao-seed-2.0-lite"
model_provider = "volcengine-coding-plan"
model_supports_reasoning_summaries = true

[model_providers.volcengine-coding-plan]
base_url = "https://ark.cn-beijing.volces.com/api/coding/v3"
env_key = "ARK_API_KEY"
wire_api = "responses"

With this config, codex CLI (TUI) outputs text correctly, but any ACP client using codex-acp sees only thinking content and no assistant reply.

…issing

Some Responses API providers (e.g. Volcengine Ark) emit reasoning items in
output_item.added without the content/summary fields that Codex's active-item
state machine expects. When this happens, the state machine fails to register
an active item and all subsequent streaming deltas (including
output_text.delta) are silently dropped.

The model still generates text and Codex surfaces it via the item/completed
notification. However, codex-acp ignored agentMessage items in
completeItemEvent (return null), so the text was lost entirely and only the
reasoning/thinking appeared in the client.

This PR mirrors the existing reasoning fallback pattern for agentMessage:
- Track item IDs that received agentMessage deltas
  (seenAgentMessageDeltaItemIds) in createTextEvent
- In completeItemEvent, if no deltas were seen for the item, emit the text
  from item.text as an agent_message_chunk
- If deltas were seen, skip (no duplication)

This is a purely additive fallback -- providers whose streaming works
correctly (e.g. OpenAI) are unaffected because seenAgentMessageDeltaItemIds
will contain the item ID and the completed event returns null.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant