feat: Expose wrapped Request/Response API (#471)

* Allows creation of ExternalRequest objects directly to control the requestId
* Allows receiving ExternalResponse objects rather than unwrapped Data
* Normalizing naming
This commit is contained in:
Jacob Alber
2025-08-22 18:07:28 -04:00
committed by GitHub
Unverified
parent 6e9d35830f
commit b26d9c95fe
5 changed files with 34 additions and 9 deletions
@@ -25,9 +25,15 @@ public abstract class Executor : IIdentified
/// </summary>
/// <param name="id">A optional unique identifier for the executor. If <c>null</c>, a type-tagged
/// UUID will be generated.</param>
protected Executor(string? id = null)
protected Executor(string? id = null) : this(ExecutorOptions.Default, id)
{
}
private readonly ExecutorOptions _options;
internal Executor(ExecutorOptions options, string? id = null)
{
this.Id = id ?? $"{this.GetType().Name}/{Guid.NewGuid():N}";
this._options = options;
}
/// <summary>
@@ -96,7 +102,7 @@ public abstract class Executor : IIdentified
}
// If we had a real return type, raise it as a SendMessage; TODO: Should we have a way to disable this behaviour?
if (result.Result != null && ExecutorOptions.Default.AutoSendMessageHandlerResultObject)
if (result.Result != null && this._options.AutoSendMessageHandlerResultObject)
{
await context.SendMessageAsync(result.Result).ConfigureAwait(false);
}
@@ -110,7 +110,7 @@ public sealed class ExecutorIsh :
{
Type.Unbound => throw new InvalidOperationException($"Executor with ID '{this.Id}' is unbound."),
Type.Executor => () => this._executorValue!,
Type.InputPort => () => new RequestInputExecutor(this._inputPortValue!),
Type.InputPort => () => new RequestInfoExecutor(this._inputPortValue!),
Type.Agent => () => new AIAgentHostExecutor(this._aiAgentValue!),
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
};
@@ -12,7 +12,7 @@ public sealed class ExecutorOptions
/// </summary>
public static ExecutorOptions Default { get; } = new();
private ExecutorOptions() { }
internal ExecutorOptions() { }
/// <summary>
/// If <see langword="true"/>, the result of a message handler that returns a value will be sent as a message to the workflow.
@@ -34,7 +34,7 @@ internal class InProcessRunnerContext<TExternalInput> : IRunnerContext
this._executors[executorId] = executor = provider();
if (executor is RequestInputExecutor requestInputExecutor)
if (executor is RequestInfoExecutor requestInputExecutor)
{
requestInputExecutor.AttachRequestSink(this);
}
@@ -7,22 +7,40 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Specialized;
internal class RequestInputExecutor : Executor
internal class RequestInfoExecutor : Executor
{
private InputPort Port { get; }
private IExternalRequestSink? RequestSink { get; set; }
public RequestInputExecutor(InputPort port) : base(port.Id)
private static ExecutorOptions DefaultOptions => new()
{
// We need to be able to return the ExternalRequest/Result objects so they can be bubbled up
// through the event system, but we do not want to forward the Request message.
AutoSendMessageHandlerResultObject = false
};
private readonly bool _allowWrapped;
public RequestInfoExecutor(InputPort port, bool allowWrapped = true) : base(RequestInfoExecutor.DefaultOptions, port.Id)
{
this.Port = port;
this._allowWrapped = allowWrapped;
}
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder
routeBuilder = routeBuilder
// Handle incoming requests (as raw request payloads)
.AddHandler(this.Port.Request, this.HandleAsync)
.AddHandler(typeof(object), this.HandleAsync)
.AddHandler(typeof(object), this.HandleAsync);
if (this._allowWrapped)
{
routeBuilder = routeBuilder
.AddHandler<ExternalRequest, ExternalRequest>((request, context) => this.HandleAsync(request.Data, context));
}
return routeBuilder
// Handle incoming responses (as wrapped Response object)
.AddHandler<ExternalResponse, ExternalResponse>(this.HandleAsync);
}
@@ -53,6 +71,7 @@ internal class RequestInputExecutor : Executor
$"Message type {message.Data.GetType().Name} is not assignable to the response type {this.Port.Response.Name} of input port {this.Port.Id}.");
}
await context.SendMessageAsync(message).ConfigureAwait(false);
await context.SendMessageAsync(message.Data).ConfigureAwait(false);
return message;