Python: Preserve refusals as marked text - #7992
Python: Preserve refusals as marked text#7992Eduard van Valkenburg (eavanvalkenburg) wants to merge 6 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds durable typed refusal content across Python core, providers, hosting, and DevUI.
Changes:
- Introduces
Content.from_refusal(...)with serialization, aggregation, and display support. - Preserves native refusal shapes for OpenAI-compatible paths and text fallbacks elsewhere.
- Adds cross-package tests, UI support, documentation, and ADR 0036.
Reviewed changes
Copilot reviewed 49 out of 51 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
python/packages/openai/tests/openai/test_openai_chat_completion_client.py |
Tests Chat Completions refusal conversion. |
python/packages/openai/tests/openai/test_openai_chat_client.py |
Tests Responses refusal conversion. |
python/packages/openai/AGENTS.md |
Documents OpenAI refusal handling. |
python/packages/openai/agent_framework_openai/_chat_completion_client.py |
Parses and replays Chat Completions refusals. |
python/packages/openai/agent_framework_openai/_chat_client.py |
Preserves Responses refusal parts and deltas. |
python/packages/hosting-responses/tests/hosting_responses/test_parsing.py |
Tests hosted refusal parsing and streaming. |
python/packages/hosting-responses/README.md |
Documents refusal round-tripping. |
python/packages/hosting-responses/agent_framework_hosting_responses/_parsing.py |
Converts hosted refusal payloads and events. |
python/packages/hosting-a2a/tests/hosting_a2a/test_conversion.py |
Tests refusal text fallback. |
python/packages/hosting-a2a/agent_framework_hosting_a2a/_conversion.py |
Maps refusals to A2A text. |
python/packages/gemini/tests/test_gemini_client.py |
Tests Gemini refusal fallback. |
python/packages/gemini/agent_framework_gemini/_chat_client.py |
Maps refusals to Gemini text parts. |
python/packages/foundry_hosting/tests/test_responses.py |
Tests Foundry refusal events and parsing. |
python/packages/foundry_hosting/README.md |
Documents Foundry refusal support. |
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py |
Streams and converts native refusal content. |
python/packages/devui/tests/devui/test_mapper.py |
Tests refusal event mapping. |
python/packages/devui/tests/devui/test_conversations.py |
Tests refusal conversation persistence. |
python/packages/devui/frontend/src/types/openai.ts |
Adds OpenAI refusal event and content types. |
python/packages/devui/frontend/src/types/agent-framework.ts |
Adds framework refusal types and guard. |
python/packages/devui/frontend/src/stores/devuiStore.ts |
Handles refusal deltas in debug state. |
python/packages/devui/frontend/src/services/streaming-state.ts |
Persists streaming refusal previews. |
python/packages/devui/frontend/src/services/api.ts |
Tracks refusal deltas during streaming. |
python/packages/devui/frontend/src/components/layout/debug-panel.tsx |
Displays refusal stream events. |
python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx |
Renders workflow refusal output. |
python/packages/devui/frontend/src/components/features/workflow/execution-timeline.tsx |
Includes refusal text in timelines. |
python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIContentRenderer.tsx |
Renders refusal message parts. |
python/packages/devui/frontend/src/components/features/agent/agent-view.tsx |
Handles streamed and restored refusals. |
python/packages/devui/AGENTS.md |
Documents DevUI refusal mapping. |
python/packages/devui/agent_framework_devui/models/__init__.py |
Exports refusal protocol models. |
python/packages/devui/agent_framework_devui/_mapper.py |
Emits native refusal stream events. |
python/packages/devui/agent_framework_devui/_conversations.py |
Stores and retrieves refusal parts. |
python/packages/core/tests/core/test_types.py |
Tests refusal semantics and aggregation. |
python/packages/core/tests/core/test_sessions.py |
Tests serialized refusal history. |
python/packages/core/AGENTS.md |
Documents refusal content support. |
python/packages/core/agent_framework/_types.py |
Defines the core refusal representation. |
python/packages/chatkit/tests/test_streaming.py |
Tests ChatKit refusal fallback. |
python/packages/chatkit/agent_framework_chatkit/_streaming.py |
Streams refusals as ChatKit text. |
python/packages/bedrock/tests/test_bedrock_client.py |
Tests Bedrock refusal fallback. |
python/packages/bedrock/agent_framework_bedrock/_chat_client.py |
Maps refusals to Bedrock text blocks. |
python/packages/anthropic/tests/test_anthropic_client.py |
Tests Anthropic refusal fallback. |
python/packages/anthropic/agent_framework_anthropic/_chat_client.py |
Maps refusals to Anthropic text blocks. |
python/packages/ag-ui/tests/ag_ui/test_run.py |
Tests AG-UI refusal conversion. |
python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py |
Includes refusals in workflow text. |
python/packages/ag-ui/agent_framework_ag_ui/_run_common.py |
Emits refusals as AG-UI text events. |
python/packages/a2a/tests/test_a2a_executor.py |
Tests executor refusal conversion. |
python/packages/a2a/tests/test_a2a_agent.py |
Tests agent refusal conversion. |
python/packages/a2a/agent_framework_a2a/_agent.py |
Maps refusals to A2A text parts. |
python/packages/a2a/agent_framework_a2a/_a2a_executor.py |
Handles refusal output in A2A execution. |
docs/decisions/0036-python-refusal-content.md |
Records the refusal representation decision. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
MAF Automated Review — Iteration 1
Result: Findings reported
Scope: full PR (2 commit(s)): 941cc0120512, 35f30a931d9a
Model: gpt-5.6-sol
Overview
The review found 5 verified inline finding(s).
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
5 verified findings remained after source verification (2 high, 3 medium) across 4 files. Details are attached to the affected lines below.
Affected areas: python/packages/devui/frontend/src/services/streaming-state.ts, python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py, python/packages/hosting-responses/agent_framework_hosting_responses/_parsing.py, python/packages/openai/agent_framework_openai/_chat_completion_client.py
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
/review |
There was a problem hiding this comment.
MAF Automated Review — Iteration 2
Result: No findings
Scope: 34 net-new commit(s): dc7714295fff, 52a376453994, 34d1ac70b2fc, 178f365dc69e, 44c8cf6a2012, 6ed829cf713e, f7e78c689bb9, 341d0ec35bbc, 947d933f2385, de02975ee606, 6d532cf77e26, edfe115ea06b, 1aca2a95e3b9, 4c0bff8b77d5, a617392cc79b, 5996105a1bf2, d2a934d53530, 018dea570b84, 11d628d60ff2, 6a0773ba2180, 8b0aea17663c, 2955888d5512, 9ca957148efd, 0822dd7495d9, 575b408e0ec8, 362972afb701, b03792465563, 486b5d0267f8, ab9ce3d4649d, e2f7db207cb6, 8ea6f8345dbd, baf0ea5252eb, 5715b358b324, 60dbe8f701b9
Model: gpt-5.6-sol
Overview
The change consistently represents refusals as text carrying an experimental marker, preserves that marker through serialization and storage, excludes refusals from structured-output parsing, and reconstructs native refusal shapes at OpenAI, hosting, Foundry, and DevUI boundaries. The added tests cover visibility, persistence, provider round-tripping, and refusal streaming. After reconciling the proposed risks against those guards, the supported mixed-content issue duplicates existing feedback and the remaining candidates lack a demonstrated publishable impact.
Reviewed the supplied incremental change set across correctness, security/reliability, architecture, and failure behavior.
No publishable findings remained after source verification for this scope.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f80e09e6-3b73-4404-818b-3a3ab0f60de2
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f80e09e6-3b73-4404-818b-3a3ab0f60de2
|
/review |
There was a problem hiding this comment.
MAF Automated Review — Iteration 3
Result: Findings reported
Scope: 2 net-new commit(s): c2955458c083, 4e4bc65cbdc7
Model: gpt-5.6-sol
Overview
The change preserves refusal text as serializable marked content and adds broad coverage for structured-output exclusion, coalescing boundaries, provider replay, and mixed text/refusal streams. The new Hosting Responses lifecycle is substantially more complete, but its independently assigned message indices diverge from the final response whenever non-text output precedes streamed text, breaking a core Responses stream contract.
Reviewed the supplied incremental change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 high) across 1 file. Details are attached to the affected lines below.
Affected areas: python/packages/hosting-responses/agent_framework_hosting_responses/_parsing.py
|
|
||
| def open_message(requested_message_id: str | None) -> str: | ||
| nonlocal message_id, output_index | ||
| output_index += 1 |
There was a problem hiding this comment.
When a stream update contains a function call before text or refusal, this counter still assigns the message index 0 because the streaming loop skips non-text content, while responses_from_run places the function call at index 0 and the message at index 1. The lifecycle events then identify a different output item than response.completed, so strict Responses clients can reject or corrupt the mixed-output stream. Please reserve indices for preceding non-text output or emit every output item's lifecycle in source order so streamed and final indices match.
Motivation & Context
Native provider refusals are currently flattened into ordinary Python text content. That keeps refusal wording visible, but loses the provider semantic across durable history, native provider replay, Responses-compatible hosting, and DevUI conversation/UI state. This change preserves that signal without adding a stable content discriminator before its broader usage and semantics are understood.
Description & Review Guide
Content(type="text")and carry the experimental, serializableadditional_properties["model_output_kind"] == "refusal"marker. Structured-output extraction skips marked refusal text, while normal message/response text and coalescing remain unchanged. OpenAI Responses, Chat Completions, Foundry hosting, Hosting Responses, and DevUI inspect the marker to reconstruct native refusal fields, parts, and streaming events. ADR 0039 records the metadata choice and the Microsoft.Extensions.AIErrorContent(ErrorCode="Refusal")alternative.Related Issue
N/A — follow-up from the Responses conversion consistency sweep
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.