feat(a2a): request_input, notify_owner and an artifact chat channel for server sessions

This commit is contained in:
EugeneTes
2026-10-07 12:49:09 +00:00
parent ab4af2cc21
commit 2c9f18a7bd
2 changed files with 95 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
using A2A;
using Volcanic.AgenticCore.Abstractions.Ui;
namespace Volcanic.AgenticCore.A2A.Server.Agent;
/// <summary>The <c>send</c> tool's channel in A2A sessions: every file becomes an artifact of the running task.</summary>
internal sealed class A2AChatChannel(long maxFileBytes) : IChatChannel
{
private volatile TaskUpdater? _updater;
public ChatChannelLimits Limits => new(maxFileBytes, maxFileBytes, 1024);
public void Bind(TaskUpdater? updater) => _updater = updater;
public Task<ChatDelivery> SendTextAsync(string markdown, CancellationToken cancellationToken) =>
EmitAsync([Part.FromText(markdown)], null, null, cancellationToken);
public async Task<ChatDelivery> SendFileAsync(ChatFile file, CancellationToken cancellationToken)
{
var name = Path.GetFileName(file.Path);
var bytes = await File.ReadAllBytesAsync(file.Path, cancellationToken).ConfigureAwait(false);
var delivery = await EmitAsync([Part.FromRaw(bytes, MediaTypes.For(file.Path), name)], name, file.Caption, cancellationToken).ConfigureAwait(false);
return delivery with { SentAs = ChatFileKind.Document };
}
private async Task<ChatDelivery> EmitAsync(List<Part> parts, string? name, string? description, CancellationToken ct)
{
var updater = _updater ?? throw new ChatDeliveryException("No A2A task is running, so there is nobody to send this to.");
var id = Guid.NewGuid().ToString("N");
await updater.AddArtifactAsync(parts, id, name, description, lastChunk: true, append: false, ct).ConfigureAwait(false);
return new ChatDelivery(null, id);
}
}

View File

@@ -0,0 +1,62 @@
using System.ComponentModel;
using Volcanic.AgenticCore.A2A.Server.Callers;
using Volcanic.AgenticCore.Abstractions.Extensions;
using Volcanic.AgenticCore.Abstractions.Json;
using Volcanic.AgenticCore.Abstractions.Messages;
using Volcanic.AgenticCore.Abstractions.Tools;
namespace Volcanic.AgenticCore.A2A.Server.Agent;
public sealed record RequestInputArgs([property: Description("The question for the external agent")] string Question);
public sealed record NotifyOwnerArgs([property: Description("A short message for your owner")] string Text);
/// <summary>
/// Added to every A2A server session: <c>request_input</c> (ends the run; the task becomes input-required),
/// <c>notify_owner</c> (when the host has an <see cref="IA2AOwnerNotifier"/>), and the <c>a2a.inbound</c> event-bus
/// message the client extension reads its hop count from.
/// </summary>
internal sealed class A2ASessionExtension(A2ACaller caller, IA2AOwnerNotifier? notifier) : IExtension
{
/// <summary>Event-bus channel; data is a JsonElement <c>{"hops": n, "caller": "name"}</c>.</summary>
public const string InboundChannel = "a2a.inbound";
private IExtensionApi? _api;
private volatile string? _pendingQuestion;
public string Name => "a2a-session";
/// <summary>The question passed to <c>request_input</c> during the current run, if any.</summary>
public string? PendingQuestion => _pendingQuestion;
public void BeginRun(int hops)
{
_pendingQuestion = null;
_api?.Events.Emit(InboundChannel, VolcanicJson.ToElement(new { hops, caller = caller.Name }));
}
public ValueTask ActivateAsync(IExtensionApi api, CancellationToken cancellationToken)
{
_api = api;
api.RegisterTool(new DelegateTool<RequestInputArgs>(
"request_input",
"Ask the external agent that sent this A2A message a question and wait for its answer. Ends your turn; its reply arrives as the next message.",
(args, _, _) =>
{
_pendingQuestion = args.Question;
return Task.FromResult(new ToolResult([new TextContent("Question sent to the caller; your turn ends here.")], Terminate: true));
},
promptSnippet: "request_input: ask the external A2A caller a question (ends your turn)"));
if (notifier is not null)
api.RegisterTool(new DelegateTool<NotifyOwnerArgs>(
"notify_owner",
"Send a short message to your owner (the person whose assistant you are) about this A2A conversation.",
async (args, _, ct) =>
{
await notifier.NotifyAsync(caller, args.Text, ct).ConfigureAwait(false);
return ToolResult.Text("delivered");
},
promptSnippet: "notify_owner: tell your owner something about this A2A conversation"));
return ValueTask.CompletedTask;
}
}