Skip to content

.NET: Track and update A2A task state - #7998

Draft
SergeyMenshykh wants to merge 1 commit into
microsoft:mainfrom
SergeyMenshykh:sergeymenshykh-track-a2a-task-state
Draft

.NET: Track and update A2A task state#7998
SergeyMenshykh wants to merge 1 commit into
microsoft:mainfrom
SergeyMenshykh:sergeymenshykh-track-a2a-task-state

Conversation

@SergeyMenshykh

@SergeyMenshykh SergeyMenshykh commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

A2A hosting can return either a message or a task. Previously, when it returned a background task, subsequent agent updates were not applied, leaving the task indefinitely in the Working state.

This change uses the agent streaming API for both response types. The server either aggregates updates into a message, returns an initial task and continues updating it, or waits for the stream to finish and returns the completed task.

Description & Review Guide

  • What are the major changes? New A2A messages now run through the streaming agent API. Background-enabled registrations either stream task updates after returning the initial task or aggregate them into a completed task, depending on ReturnImmediately. Background-disabled registrations aggregate the stream into a message. The task aggregation path also preserves response metadata on artifacts, and the four server/client response combinations have dedicated coverage.
  • What is the impact of these changes? A2A tasks receive status and artifact updates instead of remaining indefinitely in Working state, while message responses and non-immediate task responses retain their expected response shapes.
  • What do you want reviewers to focus on? Please focus on the response routing across AgentRunMode and ReturnImmediately, and on task artifact construction in the aggregate path.

Related Issue

Fixes #5362

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Use the streaming agent API for new A2A messages so task responses continue receiving status and artifact updates. Aggregate updates for non-immediate requests and preserve response metadata in completed task artifacts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 82ccdf82-97a9-4d31-a66a-9641edbccd35
Copilot AI balanced review requested due to automatic review settings September 1, 2026 16:58
@SergeyMenshykh
SergeyMenshykh deployed to github-app-auth September 1, 2026 16:58 — with GitHub Actions Active
@SergeyMenshykh
SergeyMenshykh deployed to github-app-auth September 1, 2026 16:58 — with GitHub Actions Active
@SergeyMenshykh
SergeyMenshykh deployed to github-app-auth September 1, 2026 16:58 — with GitHub Actions Active
@agent-framework-automation agent-framework-automation Bot added the .NET Usage: [Issues, PRs], Target: .Net label Sep 1, 2026

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.

Pull request overview

Updates A2A hosting to stream agent responses into task state and artifacts.

Changes:

  • Routes new messages through streaming execution.
  • Supports immediate streaming and aggregated task/message responses.
  • Adds coverage for response modes, empty responses, and metadata.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs Implements streaming and task aggregation.
dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AAgentHandlerTests.cs Tests routing and task lifecycle behavior.
dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs Adds end-to-end server response tests.
Suppressed comments (1)

dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs:102

  • Routing the non-streaming endpoint through this helper drops the run-mode value that the previous path passed as AgentRunOptions.AllowBackgroundResponses: the shared helper calls CreateRunOptions(context) without the result of _runMode. Implementations such as FoundryHostedRequestAgent and ChatClientAgent use this option to activate background/resumable execution, so AllowBackgroundIfSupported now only wraps a foreground run in a task and DisallowBackground no longer explicitly disables it. Evaluate the run mode before creating the options and pass that result into the streaming run.
        await this.HandleNewMessageStreamingAsync(context, eventQueue, aggregateTaskUpdates, cancellationToken).ConfigureAwait(false);

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

Comment on lines +99 to +102
// Aggregate task updates unless the client requests an immediate response.
bool aggregateTaskUpdates = context.Configuration?.ReturnImmediately is not true;

// AIAgent does not support resuming from arbitrary prior tasks.
// Throw explicitly so the client gets a clear error rather than a response
// that silently ignores the referenced task context.
if (context.Message?.ReferenceTaskIds is { Count: > 0 })
{
throw new NotSupportedException("ReferenceTaskIds is not supported. AIAgent cannot resume from arbitrary prior task context.");
}

List<ChatMessage> chatMessages = context.Message is not null ? [context.Message.ToChatMessage()] : [];

// Decide whether to run in background based on user preferences and agent capabilities
var decisionContext = new A2ARunDecisionContext(context);
var allowBackgroundResponses = await this._runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);

var options = CreateRunOptions(context, allowBackgroundResponses);

AgentResponse response;
try
{
response = await this._hostAgent.RunAsync(
chatMessages,
session: session,
options: options,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
finally
{
await this._hostAgent.SaveSessionAsync(contextId, session, CancellationToken.None).ConfigureAwait(false);
}

if (response.ContinuationToken is null)
{
// Return a lightweight message response (no task lifecycle needed).
var message = CreateMessageFromResponse(contextId, response);
await eventQueue.EnqueueMessageAsync(message, cancellationToken).ConfigureAwait(false);
}
else
{
// Long-running operation: emit task lifecycle events.
var taskUpdater = new TaskUpdater(eventQueue, context.TaskId, contextId);
await taskUpdater.SubmitAsync(cancellationToken).ConfigureAwait(false);

Message? progressMessage = response.Messages.Count > 0
? CreateMessageFromResponse(contextId, response)
: null;

await taskUpdater.StartWorkAsync(progressMessage, cancellationToken).ConfigureAwait(false);
}
await this.HandleNewMessageStreamingAsync(context, eventQueue, aggregateTaskUpdates, cancellationToken).ConfigureAwait(false);
AgentResponse response = await updates.ToAgentResponseAsync(cancellationToken).ConfigureAwait(false);

if (response.Messages.Count == 0)
await updater.SubmitAsync(cancellationToken).ConfigureAwait(false);

@github-actions github-actions Bot 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.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (1 commit(s)): b811d678ea46
Model: gpt-5.6-sol

Overview

The review found 4 verified inline finding(s).

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
4 verified findings remained after source verification (1 high, 3 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AAgentHandler.cs


await taskUpdater.StartWorkAsync(progressMessage, cancellationToken).ConfigureAwait(false);
}
await this.HandleNewMessageStreamingAsync(context, eventQueue, aggregateTaskUpdates, cancellationToken).ConfigureAwait(false);

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.

A2AgentHandler.cs:165-194 leaves AllowBackgroundResponses unset but treats AllowBackgroundIfSupported as an unconditional task decision, so foreground completions become persisted terminal tasks instead of fallback messages; any fix must preserve streamed task updates and ReturnImmediately aggregation semantics.

{
// The server allows background responses and this is either a streaming request or a
// non-streaming request with ReturnImmediately enabled, so emit task updates as they arrive.
await StreamTaskUpdatesAsync(updates, taskUpdater, cancellationToken).ConfigureAwait(false);

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.

For a non-streaming request with ReturnImmediately = true, this new route writes artifacts through ArtifactStreamWriter, which does not copy AgentResponseUpdate.AdditionalProperties. The previous non-streaming response exposed those properties as message metadata, and the new aggregate-task branch explicitly preserves them, so only the immediate task mode silently loses response metadata. Please carry the response metadata onto the streamed artifact updates as well.

if (response.Messages.Count == 0)
await updater.SubmitAsync(cancellationToken).ConfigureAwait(false);

if (response.Messages.ToParts() is { Count: > 0 } parts)

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.

When the stream contains multiple assistant messages with different MessageId values, flattening all messages to one parts list produces a single generated artifact and discards both the message boundaries and IDs. The equivalent streaming path deliberately creates a separate artifact for each message boundary, so ReturnImmediately = false changes the task's logical result shape. Please emit one artifact per aggregated message and retain its message ID where available.

AgentResponse response = await updates.ToAgentResponseAsync(cancellationToken).ConfigureAwait(false);

if (response.Messages.Count == 0)
await updater.SubmitAsync(cancellationToken).ConfigureAwait(false);

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.

After this task is submitted, metadata conversion, artifact enqueueing, or completion can still throw or be canceled, but this aggregate path has no failure/cancellation transition. The request then fails after the server has persisted a permanently Submitted task with no worker left to advance it. Please either prepare the complete terminal event before submission or mirror the streaming path's catch handling so every post-submit exit terminalizes the task.

@SergeyMenshykh
SergeyMenshykh deployed to github-app-auth September 1, 2026 17:16 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Hosting] Track and update A2A Task state after creation

3 participants