Skip to content

Fix agent-openai-agents-sdk multi-turn failure on 2nd+ prompt (id-less assistant history item) - #256

Open
maskebail wants to merge 1 commit into
databricks:mainfrom
maskebail:fix/multi-turn-assistant-history-normalization
Open

Fix agent-openai-agents-sdk multi-turn failure on 2nd+ prompt (id-less assistant history item)#256
maskebail wants to merge 1 commit into
databricks:mainfrom
maskebail:fix/multi-turn-assistant-history-normalization

Conversation

@maskebail

Copy link
Copy Markdown

Summary

Apps built from the agent-openai-agents-sdk template fail on the second and later prompts with:

agents.exceptions.UserError: Unhandled item type or structure:
  {'status': None, 'content': [{'text': ..., 'type': 'output_text'}],
   'role': 'assistant', 'type': 'message'}

The first prompt always succeeds; every follow-up fails. Reproduces on fresh installs.

Root cause

openai-agents >= 0.19 tightened Converter.maybe_response_output_message in chatcmpl_converter.py to require {"id", "content"} on assistant history items:

if (isinstance(item, dict)
    and item.get("type") == "message"
    and item.get("role") == "assistant"
    and {"id", "content"} <= set(item)):   # now requires an id
    return cast(ResponseOutputMessageParam, item)
return None

Failure chain on the 2nd+ prompt:

  1. The built-in chat UI replays the prior assistant turn as an id-less {"type":"message","role":"assistant","content":[output_text]} item.
  2. MLflow's ResponsesAgentRequest strips any client-supplied id during normalization, so the item reaching the converter is guaranteed id-less.
  3. The tightened guard no longer recognizes it; it matches no other branch and hits the catch-all raise UserError("Unhandled item type or structure").

The template pins openai-agents>=0.4.1 with no upper bound and no lockfile, so fresh installs resolve onto a build containing the change.

Fix

Collapse replayed id-less assistant output_text items to the easy-input {"role","content"} form before Runner.run, in both invoke_handler and stream_handler. That form is recognized by maybe_easy_input_message (no id required), so the fix is independent of the installed openai-agents version. Multiple output_text segments are joined with a newline to match the SDK converter byte-for-byte.

The helper lives in a new dependency-free agent_server/history.py so it is unit-testable in isolation.

Why not just pin openai-agents?

A version pin was considered and rejected — pinning <0.18 resolves to 0.17.8, whose older model layer is incompatible with the current openai token-usage schema and fails every prompt (including the 1st) with InputTokensDetails: cache_write_tokens Field required. The code fix above is the correct, version-independent solution.

Maintainers may also want to commit a uv.lock for reproducible installs (kept out of this PR so it's generated in your own resolver environment).

Validation

Verified on a live Databricks Apps deployment (openai-agents 0.19.1, guard active):

Scenario Result
1st prompt
2nd prompt, non-streaming ✅ agent correctly recalls prior turn
2nd prompt, streaming ✅ clean response.completed + [DONE]
2nd prompt, multi-segment assistant history ✅ newline separator preserved

New offline unit tests (tests/test_history_normalization.py, 4 cases, no network) lock in the transformation so the regression can't return silently.

Changes

  • agent_server/history.py — new, dependency-free normalize_history_items()
  • agent_server/agent.py — import + wire both handlers (+3/−2)
  • tests/test_history_normalization.py — regression tests

Apps built from the agent-openai-agents-sdk template fail on the second and
later prompts with:

    agents.exceptions.UserError: Unhandled item type or structure: {...
      'content': [{'text': ..., 'type': 'output_text'}], 'role': 'assistant',
      'type': 'message'}

openai-agents >= 0.19 tightened Converter.maybe_response_output_message in
chatcmpl_converter.py to require {"id","content"} on assistant history items.
The built-in chat UI replays the prior assistant turn as an id-less
{"type":"message","role":"assistant","content":[output_text]} item, and
MLflow's ResponsesAgentRequest strips any client-supplied id during
normalization, so the replayed item fails recognition and falls through to the
catch-all raise. The first prompt has no assistant history to replay, so it
never triggers. The template pins openai-agents>=0.4.1 with no upper bound and
no lockfile, so fresh installs resolve onto a build containing the change.

Fix: collapse replayed id-less assistant output_text items to the easy-input
{"role","content"} form before Runner.run, in both invoke_handler and
stream_handler. That form is recognized by maybe_easy_input_message (no id
required), so the fix is independent of the installed openai-agents version.
Multiple output_text segments are joined with a newline to match the SDK
converter byte-for-byte. The helper lives in a dependency-free
agent_server/history.py so it is unit-testable in isolation.

Note: pinning openai-agents below 0.18 is not a viable workaround -- it
resolves to 0.17.8, whose older model layer is incompatible with the current
openai token-usage schema and fails every prompt with
"InputTokensDetails: cache_write_tokens Field required".

Co-authored-by: Isaac
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