Skip to content

Avoid messages for response-only updates - #1127

Open
Kirill (llirik0) wants to merge 2 commits into
microsoft:mainfrom
llirik0:fix/metadata-only-response-updates
Open

Kirill (llirik0) wants to merge 2 commits into
microsoft:mainfrom
llirik0:fix/metadata-only-response-updates

Conversation

@llirik0

@llirik0 Kirill (llirik0) commented Sep 20, 2026 •

Copy link
Copy Markdown

Summary

Prevent response-only metadata and provider lifecycle updates from creating or modifying messages when collected into a Response.

  • Response.Update treats raw-only provider payloads as response metadata, not as evidence of a message. Genuine message updates retain their raw payloads.
  • Agent.setAuthor decorates only message-bearing updates. OpenAI, AG-UI, and A2A lifecycle paths avoid introducing message discriminators for response-only events.
  • OpenAI lifecycle updates clear their message ID and inherited role without resetting content-stream state. Regressions cover incomplete and completed-without-usage events arriving after a real message, including timestamp and raw-payload isolation.
  • ToUpdates preserves response metadata when no messages exist. Its assistant-role fallback is limited to otherwise unidentifiable metadata-only messages; it is justified by the collector's existing assistant default, not a claim of identical .NET serialization behavior.

Rebase and #1133

Rebased onto main at 1ac53ce01a1c3a715f9fa318c2b9564fbfedfbf2, resolving conflicts while retaining upstream ConversationID propagation and MessageID-based AdditionalProperties scoping. #1133 remains open; this update preserves the scoping already present on main rather than introducing a competing rule.

Validation

On the updated candidate:

  • go test ./...
  • go test -race ./...
  • go vet ./...
  • golangci-lint run
  • Catalog: golangci-lint run --config ../.golangci.yml
  • git diff --check

All passed. The new post-message lifecycle regressions failed before the role-clearing fix and passed afterward.

@github-actions github-actions Bot added area:agent Changes files in the agent area size:medium At most 100 changed lines across at most 5 files kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure labels Sep 20, 2026
@github-actions

This comment has been minimized.

@llirik0

Copy link
Copy Markdown
Author

Kirill (Kirill (@llirik0)) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

@llirik0
Kirill (llirik0) marked this pull request as ready for review September 20, 2026 20:41
@llirik0
Kirill (llirik0) requested a review from a team as a code owner September 20, 2026 20:41
Copilot AI lite review requested due to automatic review settings September 20, 2026 20:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Message-only metadata can be lost during response round trips.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Prevents response-only metadata updates from creating empty assistant messages during response round trips.

Changes:

  • Gates message mutation on message-related update fields.
  • Adds metadata round-trip tests.
File Description
agent/​response.go Updates response aggregation behavior.
agent/​response_test.go Tests metadata-only round trips.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agent/response.go Resolved
@github-actions github-actions Bot added size:large At most 300 changed lines across at most 10 files and removed size:medium At most 100 changed lines across at most 5 files labels Sep 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: user-visible behavior

Changed Go contract: agent.Response.Update (skips creating/mutating a message when a ResponseUpdate carries no message-related data — no MessageID, AuthorName, Role, Contents, or RawRepresentation) and agent.Response.ToUpdates (defaults an empty per-message Role to message.RoleAssistant when converting to updates). No exported types/signatures were added, removed, or changed — same public methods, same shapes.

Upstream evidence reviewed:

  • Python python/packages/core/agent_framework/_types.py, _process_update (~line 2038): creates a new Message("assistant", []) whenever response.messages is empty or the update signals a new message boundary, without checking whether the update actually carries message data.
  • Python _finalize_response (~line 2278) → _coalesce_function_call_occurrences (~line 2287): after all updates are applied, filters response.messages to [m for m in response.messages if m.contents], discarding any message left empty — this is how Python avoids surfacing an empty message from metadata-only updates (AgentResponse.from_updates / from_update_generator always call _finalize_response before returning).
  • .NET dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs, ToAgentResponseUpdates() (~line 265): emits one update per message, using Role = message.Role (no default-to-assistant substitution), plus a single "extra" metadata update only when AdditionalProperties or Usage is set. AgentResponseExtensions.ToAgentResponse delegates message reconstruction to Microsoft.Extensions.AI's ToChatResponse(), whose empty-message handling isn't vendored in this repo, so its exact behavior couldn't be directly inspected here.

Result: aligned. Go's new guard in Update achieves the same end state as Python's post-hoc empty-message filter in _finalize_response — a response built purely from response-level metadata (continuation token, additional properties) round-trips through ToUpdates/Update without producing a spurious empty assistant message. The mechanism differs (guard-on-write vs. filter-on-finalize) but that's an implementation detail, not an observable divergence, and no new exported API surface was introduced. The Role default-to-assistant addition in ToUpdates is a minor Go-specific normalization (guards against emitting an update with an empty Role for an existing message) and doesn't conflict with either upstream implementation. No parity concerns found; public-api-change label not applicable since no exported API surface changed.

Generated by Go API Consistency Review Agent · copilot · auto · 82.2 AIC · ⌖ 7.41 AIC · ⊞ 9.2K · ◷

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Metadata-only updates can still produce empty assistant messages during collection; the agent-level path needs correction and regression coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
Resolved since last review (1)

Comment thread agent/response.go Resolved
@PratikDhanave

Copy link
Copy Markdown
Contributor

Reviewed this closely since it touches the same Response.Update/ToUpdates seam I was in for #1133. The core change is right and matches M.E.AI ChatResponseExtensions.ProcessUpdate: a response-metadata-only update should not materialize an empty message. Tests are a good round-trip shape. A few notes:

Composition with #1133 (worth the maintainers knowing before merge order is decided). #1133 scopes AdditionalProperties in Response.Update to the message when the update carries a MessageID and to the response otherwise. These two PRs are complementary and both needed:

So they dont conflict, but whichever merges second will want a trivial rebase in the same block. No action needed here beyond awareness.

Question — RawRepresentation != nil in the guard. Including it means a provider update that carries only a raw representation (no id/role/contents) will still create an empty-content message. Within ToUpdates this cant happen (the extra response-level update has RawRepresentation == nil), so the round-trip tests pass — but for a live streaming provider that emits a final raw-only event, is materializing a message the intended behavior, or should raw-only also be treated as response-level? Might be worth a comment on the guard either way.

Question — the ToUpdates role default (msg.Role == "" -> RoleAssistant). This is a separate behavior tweak bundled into the same PR. It looks reasonable, but does it mirror what .NET does for an unroled message on ToChatResponseUpdates, and is it needed for the metadata-message case specifically? A one-line rationale in the code or PR body would help future readers see why it rides along.

Nothing blocking from my perspective — the direction is correct and well-tested. 👍

@llirik0
Kirill (llirik0) force-pushed the fix/metadata-only-response-updates branch from 2c0b680 to fd164b1 Compare September 25, 2026 18:31
@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/agui Changes files in the provider / agui area area:provider/openai Changes files in the provider / openai area labels Sep 25, 2026
@llirik0
Kirill (llirik0) force-pushed the fix/metadata-only-response-updates branch from fd164b1 to 8669b06 Compare September 25, 2026 18:59
@github-actions github-actions Bot added area:provider/a2a Changes files in the provider / a2a area size:xlarge More than 300 changed lines or 10 files and removed size:large At most 300 changed lines across at most 10 files labels Sep 25, 2026
@llirik0
Kirill (llirik0) force-pushed the fix/metadata-only-response-updates branch from 8669b06 to 5c92893 Compare October 19, 2026 14:38
@llirik0

Copy link
Copy Markdown
Author

Rebased onto current main and resolved the conflicts. Thanks PratikDhanave (@PratikDhanave) for the detailed review. Raw-only updates are now explicitly response-level; regressions also cover lifecycle events arriving after a real OpenAI message, so inherited roles cannot leak lifecycle timestamps or raw payloads into that message. The ToUpdates assistant fallback is limited to otherwise unidentifiable metadata-only messages, with the collector-default rationale documented in code. The rebase preserves the MessageID-based AdditionalProperties scoping now present on main; #1133 is still open. Full tests, race tests, vet, and root/catalog lint pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Content-bearing updates can corrupt tool-message grouping, while metadata round trips and empty artifact handling can lose data.

4 open issues
1 resolved since last review

Review effort: Max

Comment thread agent/response.go
Comment on lines +193 to +195
if len(resp.Messages) == 0 {
extra.FinishReason = resp.FinishReason
extra.RawRepresentation = resp.RawRepresentation
}
update := newResponseUpdate(task, cloneMetadata(task.Metadata), string(task.ID), messageID, message.RoleAssistant, contents)
role := message.Role("")
if messageID != "" || len(contents) > 0 {
Comment on lines 1135 to 1136
CreatedAt: time.Unix(int64(resp.CreatedAt), 0),
Role: message.RoleAssistant,
}
case responses.ResponseOutputItemDoneEvent:
// Create update for all output item done events
u = createUpdate(message.RoleAssistant, nil)
u = createUpdate("", nil)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Previously reported message-preservation issues in A2A task snapshots and OpenAI tool rounds remain unresolved.

4 open issues

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area area:provider/a2a Changes files in the provider / a2a area area:provider/agui Changes files in the provider / agui area area:provider/openai Changes files in the provider / openai area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure size:xlarge More than 300 changed lines or 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants