Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions samples/AgentServer/EchoAgentWithTasks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,49 @@ private async Task ProcessMessageAsync(AgentTask task, CancellationToken cancell
// Check for target-state metadata to determine task behavior
TaskState targetState = GetTargetStateFromMetadata(lastMessage.Metadata) ?? TaskState.Completed;

// This is a short-lived task - complete it immediately
await _taskManager!.ReturnArtifactAsync(task.Id, new Artifact()
// Demonstrate different artifact update patterns based on message content
if (messageText.StartsWith("stream:", StringComparison.OrdinalIgnoreCase))
{
Parts = [new TextPart() {
Text = $"Echo: {messageText}"
}]
}, cancellationToken);
// Demonstrate streaming with UpdateArtifactAsync by sending chunks
var content = messageText.Substring(7).Trim(); // Remove "stream:" prefix
var chunks = content.Split(' ');

for (int i = 0; i < chunks.Length; i++)
{
bool isLastChunk = i == chunks.Length - 1;
await _taskManager!.UpdateArtifactAsync(task.Id, new Artifact()
{
Parts = [new TextPart() { Text = $"Echo chunk {i + 1}: {chunks[i]}" }]
}, append: i > 0, lastChunk: isLastChunk, cancellationToken);
}
}
else if (messageText.StartsWith("append:", StringComparison.OrdinalIgnoreCase))
{
// Demonstrate appending to existing artifacts
var content = messageText.Substring(7).Trim(); // Remove "append:" prefix

// First, create an initial artifact (append=false for new artifact)
await _taskManager!.UpdateArtifactAsync(task.Id, new Artifact()
{
Parts = [new TextPart() { Text = $"Initial echo: {content}" }]
}, append: false, cancellationToken: cancellationToken);

// Then append additional content (append=true to add to existing)
await _taskManager!.UpdateArtifactAsync(task.Id, new Artifact()
{
Parts = [new TextPart() { Text = $" | Appended: {content.ToUpper(System.Globalization.CultureInfo.InvariantCulture)}" }]
}, append: true, lastChunk: true, cancellationToken);
}
else
{
// Default behavior: use ReturnArtifactAsync for simple, complete responses
await _taskManager!.ReturnArtifactAsync(task.Id, new Artifact()
{
Parts = [new TextPart() {
Text = $"Echo: {messageText}"
}]
}, cancellationToken);
}

await _taskManager!.UpdateStatusAsync(
task.Id,
Expand All @@ -57,7 +93,7 @@ private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken can
return Task.FromResult(new AgentCard()
{
Name = "Echo Agent",
Description = "Agent which will echo every message it receives.",
Description = "Agent which will echo every message it receives. Supports special commands: 'stream: <text>' for chunked responses, 'append: <text>' for appending to artifacts, or regular text for simple echo.",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
Expand Down
15 changes: 15 additions & 0 deletions src/A2A/Server/ITaskManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ public interface ITaskManager
/// <returns>A task representing the asynchronous operation.</returns>
Task ReturnArtifactAsync(string taskId, Artifact artifact, CancellationToken cancellationToken = default);

/// <summary>
/// Updates an artifact for a task, either by adding a new artifact or appending to the last one.
/// </summary>
/// <remarks>
/// When append is true, the artifact's parts are added to the last artifact in the task's collection.
/// When append is false or there are no existing artifacts, a new artifact is added to the collection.
/// </remarks>
/// <param name="taskId">The ID of the task to update the artifact for.</param>
/// <param name="artifact">The artifact containing parts to add.</param>
/// <param name="append">Whether to append to the last artifact (true) or create a new one (false).</param>
/// <param name="lastChunk">Whether this is the last chunk of the artifact.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateArtifactAsync(string taskId, Artifact artifact, bool append = false, bool? lastChunk = null, CancellationToken cancellationToken = default);
Copy link
Collaborator

Choose a reason for hiding this comment

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

@copilot This method is not used yet (except in tests). Show me how it is supposed to be used on agent implementations in samples/ folder. This should play nice with the current ReturnArtifactAsync method implementation so observe how it is being used in samples/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Enhanced the EchoAgentWithTasks sample to demonstrate how UpdateArtifactAsync should be used in agent implementations. The sample now shows three patterns:

  1. Simple responses: ReturnArtifactAsync for complete, single artifacts (default behavior)
  2. Streaming responses: UpdateArtifactAsync with chunked data (use stream: <text> command)
  3. Incremental building: UpdateArtifactAsync with append functionality (use append: <text> command)

The key difference from ReturnArtifactAsync is that UpdateArtifactAsync supports:

  • append parameter to add to existing artifacts vs creating new ones
  • lastChunk parameter for streaming scenarios
  • More granular control over artifact construction

This demonstrates the proper usage patterns alongside the existing ReturnArtifactAsync usage in the samples. Commit: 8a8af29


/// <summary>
/// Updates the status of a task and optionally adds a message to its history.
/// </summary>
Expand Down
95 changes: 93 additions & 2 deletions src/A2A/Server/TaskManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,97 @@ public async Task ReturnArtifactAsync(string taskId, Artifact artifact, Cancella
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
// TODO: Implement UpdateArtifact method
}

/// <inheritdoc />
public async Task UpdateArtifactAsync(string taskId, Artifact artifact, bool append = false, bool? lastChunk = null, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (string.IsNullOrEmpty(taskId))
{
throw new A2AException(nameof(taskId), A2AErrorCode.InvalidParams);
}
else if (artifact is null)
{
throw new A2AException(nameof(artifact), A2AErrorCode.InvalidParams);
}

using var activity = ActivitySource.StartActivity("UpdateArtifact", ActivityKind.Server);
activity?.SetTag("task.id", taskId);
activity?.SetTag("artifact.append", append);
activity?.SetTag("artifact.lastChunk", lastChunk);

try
{
var task = await _taskStore.GetTaskAsync(taskId, cancellationToken).ConfigureAwait(false);
if (task != null)
{
activity?.SetTag("task.found", true);

task.Artifacts ??= [];

if (append && task.Artifacts.Count > 0)
{
// Append to the last artifact by adding parts to it
var lastArtifact = task.Artifacts[^1];

Choose a reason for hiding this comment

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

Hello @brandonh-msft. Why are you taking the last artifact from the task ?
Is this a limitation from the A2A protocol, meaning we can only append to the last artifact from the task ?
If this is not a limitation, you could use the artifact object received from the method params and look for a corresponding artifactId ?


// Add all parts from the new artifact to the last artifact
foreach (var part in artifact.Parts)
{
lastArtifact.Parts.Add(part);
}

activity?.SetTag("event.type", "artifact_append");
await _taskStore.SetTaskAsync(task, cancellationToken).ConfigureAwait(false);

// Notify with the updated artifact and append=true
_taskUpdateEventEnumerators.TryGetValue(task.Id, out var enumerator);
if (enumerator is not null)
{
var taskUpdateEvent = new TaskArtifactUpdateEvent
{
TaskId = task.Id,
Artifact = lastArtifact,
Append = true,
LastChunk = lastChunk
};
enumerator.NotifyEvent(taskUpdateEvent);
}
}
else
{
// Create a new artifact (either append=false or no existing artifacts)
task.Artifacts.Add(artifact);
activity?.SetTag("event.type", "artifact_new");
await _taskStore.SetTaskAsync(task, cancellationToken).ConfigureAwait(false);

// Notify with the new artifact and append=false
_taskUpdateEventEnumerators.TryGetValue(task.Id, out var enumerator);
if (enumerator is not null)
{
var taskUpdateEvent = new TaskArtifactUpdateEvent
{
TaskId = task.Id,
Artifact = artifact,
Append = false, // Always false when creating a new artifact
LastChunk = lastChunk
};
enumerator.NotifyEvent(taskUpdateEvent);
}
}
}
else
{
activity?.SetTag("task.found", false);
activity?.SetStatus(ActivityStatusCode.Error, "Task not found");
throw new A2AException("Task not found.", A2AErrorCode.TaskNotFound);
}
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}
Loading