mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'feature-foundry-agents' into feature-declarative-agents-dotnet
This commit is contained in:
@@ -32,6 +32,7 @@ public static class DevUIExtensions
|
||||
{
|
||||
var group = endpoints.MapGroup("");
|
||||
group.MapDevUI(pattern: "/devui");
|
||||
group.MapMeta();
|
||||
group.MapEntities();
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(EntityInfo))]
|
||||
[JsonSerializable(typeof(DiscoveryResponse))]
|
||||
[JsonSerializable(typeof(MetaResponse))]
|
||||
[JsonSerializable(typeof(EnvVarRequirement))]
|
||||
[JsonSerializable(typeof(List<EntityInfo>))]
|
||||
[JsonSerializable(typeof(List<JsonElement>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, bool>))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Server metadata response for the /meta endpoint.
|
||||
/// Provides information about the DevUI server configuration, capabilities, and requirements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This response is used by the frontend to:
|
||||
/// - Determine the UI mode (developer vs user interface)
|
||||
/// - Check server capabilities (tracing, OpenAI proxy support)
|
||||
/// - Verify authentication requirements
|
||||
/// - Display framework and version information
|
||||
/// </remarks>
|
||||
internal sealed record MetaResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the UI interface mode.
|
||||
/// "developer" shows debug tools and advanced features, "user" shows a simplified interface.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ui_mode")]
|
||||
public string UiMode { get; init; } = "developer";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DevUI version string.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = "0.1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backend framework identifier.
|
||||
/// Always "agent_framework" for Agent Framework implementations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("framework")]
|
||||
public string Framework { get; init; } = "agent_framework";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backend runtime/language.
|
||||
/// "dotnet" for .NET implementations, "python" for Python implementations.
|
||||
/// Used by frontend for deployment guides and feature availability.
|
||||
/// </summary>
|
||||
[JsonPropertyName("runtime")]
|
||||
public string Runtime { get; init; } = "dotnet";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server capabilities dictionary.
|
||||
/// Key-value pairs indicating which optional features are enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Standard capability keys:
|
||||
/// - "tracing": Whether trace events are emitted for debugging
|
||||
/// - "openai_proxy": Whether the server can proxy requests to OpenAI
|
||||
/// </remarks>
|
||||
[JsonPropertyName("capabilities")]
|
||||
public Dictionary<string, bool> Capabilities { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Bearer token authentication is required for API access.
|
||||
/// When true, clients must include "Authorization: Bearer {token}" header in requests.
|
||||
/// </summary>
|
||||
[JsonPropertyName("auth_required")]
|
||||
public bool AuthRequired { get; init; }
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Agents.AI.DevUI.Entities;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
@@ -56,79 +58,19 @@ internal static class EntitiesApiExtensions
|
||||
{
|
||||
var entities = new List<EntityInfo>();
|
||||
|
||||
// Discover agents from the agent catalog
|
||||
if (agentCatalog is not null)
|
||||
// Discover agents
|
||||
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (agent.GetType().Name == "WorkflowHostAgent")
|
||||
{
|
||||
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
|
||||
// and workflows are handled below.
|
||||
continue;
|
||||
}
|
||||
|
||||
entities.Add(new EntityInfo(
|
||||
Id: agent.Name ?? agent.Id,
|
||||
Type: "agent",
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
});
|
||||
}
|
||||
entities.Add(agentInfo);
|
||||
}
|
||||
|
||||
// Discover workflows from the workflow catalog
|
||||
if (workflowCatalog is not null)
|
||||
// Discover workflows
|
||||
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
entities.Add(new EntityInfo(
|
||||
Id: workflow.Name ?? workflow.StartExecutorId,
|
||||
Type: "workflow",
|
||||
Name: workflow.Name ?? workflow.StartExecutorId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "string",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
});
|
||||
}
|
||||
entities.Add(workflowInfo);
|
||||
}
|
||||
|
||||
return Results.Json(new DiscoveryResponse(entities), EntitiesJsonContext.Default.DiscoveryResponse);
|
||||
return Results.Json(new DiscoveryResponse([.. entities]), EntitiesJsonContext.Default.DiscoveryResponse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -141,93 +83,26 @@ internal static class EntitiesApiExtensions
|
||||
|
||||
private static async Task<IResult> GetEntityInfoAsync(
|
||||
string entityId,
|
||||
string? type,
|
||||
AgentCatalog? agentCatalog,
|
||||
WorkflowCatalog? workflowCatalog,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to find the entity among discovered agents
|
||||
if (agentCatalog is not null)
|
||||
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (agent.GetType().Name == "WorkflowHostAgent")
|
||||
{
|
||||
// HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows,
|
||||
// and workflows are handled below.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(agent.Name, entityId, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(agent.Id, entityId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var entityInfo = new EntityInfo(
|
||||
Id: agent.Name ?? agent.Id,
|
||||
Type: "agent",
|
||||
Name: agent.Name ?? agent.Id,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
};
|
||||
|
||||
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the entity among discovered workflows
|
||||
if (workflowCatalog is not null)
|
||||
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
if (string.Equals(workflowId, entityId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
var entityInfo = new EntityInfo(
|
||||
Id: workflowId,
|
||||
Type: "workflow",
|
||||
Name: workflow.Name ?? workflow.StartExecutorId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "Input",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
};
|
||||
|
||||
return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,4 +116,123 @@ internal static class EntitiesApiExtensions
|
||||
title: "Error getting entity info");
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<EntityInfo> DiscoverAgentsAsync(
|
||||
AgentCatalog? agentCatalog,
|
||||
string? entityIdFilter,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (agentCatalog is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// If filtering by entity ID, skip non-matching agents
|
||||
if (entityIdFilter is not null &&
|
||||
!string.Equals(agent.Name, entityIdFilter, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(agent.Id, entityIdFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return CreateAgentEntityInfo(agent);
|
||||
|
||||
// If we found the entity we're looking for, we're done
|
||||
if (entityIdFilter is not null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<EntityInfo> DiscoverWorkflowsAsync(
|
||||
WorkflowCatalog? workflowCatalog,
|
||||
string? entityIdFilter,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (workflowCatalog is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
|
||||
// If filtering by entity ID, skip non-matching workflows
|
||||
if (entityIdFilter is not null && !string.Equals(workflowId, entityIdFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return CreateWorkflowEntityInfo(workflow);
|
||||
|
||||
// If we found the entity we're looking for, we're done
|
||||
if (entityIdFilter is not null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
|
||||
{
|
||||
var entityId = agent.Name ?? agent.Id;
|
||||
return new EntityInfo(
|
||||
Id: entityId,
|
||||
Type: "agent",
|
||||
Name: entityId,
|
||||
Description: agent.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: null,
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory"
|
||||
};
|
||||
}
|
||||
|
||||
private static EntityInfo CreateWorkflowEntityInfo(Workflow workflow)
|
||||
{
|
||||
// Extract executor IDs from the workflow structure
|
||||
var executorIds = new HashSet<string> { workflow.StartExecutorId };
|
||||
var reflectedEdges = workflow.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a default input schema (string type)
|
||||
var defaultInputSchema = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "string"
|
||||
};
|
||||
|
||||
var workflowId = workflow.Name ?? workflow.StartExecutorId;
|
||||
return new EntityInfo(
|
||||
Id: workflowId,
|
||||
Type: "workflow",
|
||||
Name: workflowId,
|
||||
Description: workflow.Description,
|
||||
Framework: "agent-framework",
|
||||
Tools: [.. executorIds],
|
||||
Metadata: []
|
||||
)
|
||||
{
|
||||
Source = "in_memory",
|
||||
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
|
||||
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
|
||||
InputTypeName = "string",
|
||||
StartExecutorId = workflow.StartExecutorId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DevUI.Entities;
|
||||
|
||||
namespace Microsoft.Agents.AI.DevUI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for mapping the server metadata endpoint to an <see cref="IEndpointRouteBuilder"/>.
|
||||
/// </summary>
|
||||
internal static class MetaApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps the HTTP API endpoint for retrieving server metadata.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <returns>The <see cref="IEndpointConventionBuilder"/> for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// This extension method registers the following endpoint:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>GET /meta - Retrieve server metadata including UI mode, version, capabilities, and auth requirements</description></item>
|
||||
/// </list>
|
||||
/// The endpoint is compatible with the Python DevUI frontend and provides essential
|
||||
/// configuration information needed for proper frontend initialization.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapMeta(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
return endpoints.MapGet("/meta", GetMeta)
|
||||
.WithName("GetMeta")
|
||||
.WithSummary("Get server metadata and configuration")
|
||||
.WithDescription("Returns server metadata including UI mode, version, framework identifier, capabilities, and authentication requirements. Used by the frontend for initialization and feature detection.")
|
||||
.Produces<MetaResponse>(StatusCodes.Status200OK, contentType: "application/json");
|
||||
}
|
||||
|
||||
private static IResult GetMeta()
|
||||
{
|
||||
// TODO: Consider making these configurable via IOptions<DevUIOptions>
|
||||
// For now, using sensible defaults that match Python DevUI behavior
|
||||
|
||||
var meta = new MetaResponse
|
||||
{
|
||||
UiMode = "developer", // Could be made configurable to support "user" mode
|
||||
Version = "0.1.0", // TODO: Extract from assembly version attribute
|
||||
Framework = "agent_framework",
|
||||
Runtime = "dotnet", // .NET runtime for deployment guides
|
||||
Capabilities = new Dictionary<string, bool>
|
||||
{
|
||||
// Tracing capability - will be enabled when trace event support is added
|
||||
["tracing"] = false,
|
||||
|
||||
// OpenAI proxy capability - not currently supported in .NET DevUI
|
||||
["openai_proxy"] = false,
|
||||
|
||||
// Deployment capability - not currently supported in .NET DevUI
|
||||
["deployment"] = false
|
||||
},
|
||||
AuthRequired = false // Could be made configurable based on authentication middleware
|
||||
};
|
||||
|
||||
return Results.Json(meta, EntitiesJsonContext.Default.MetaResponse);
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ internal static class OpenAIHostingJsonUtilities
|
||||
[JsonSerializable(typeof(MCPApprovalRequestItemResource))]
|
||||
[JsonSerializable(typeof(MCPApprovalResponseItemResource))]
|
||||
[JsonSerializable(typeof(MCPCallItemResource))]
|
||||
[JsonSerializable(typeof(ExecutorActionItemResource))]
|
||||
[JsonSerializable(typeof(List<ItemResource>))]
|
||||
// ItemParam types
|
||||
[JsonSerializable(typeof(ItemParam))]
|
||||
|
||||
+89
-1
@@ -45,6 +45,9 @@ internal static class AgentRunResponseUpdateExtensions
|
||||
var updateEnumerator = updates.GetAsyncEnumerator(cancellationToken);
|
||||
await using var _ = updateEnumerator.ConfigureAwait(false);
|
||||
|
||||
// Track active item IDs by executor ID to pair invoked/completed/failed events
|
||||
Dictionary<string, string> executorItemIds = [];
|
||||
|
||||
AgentRunResponseUpdate? previousUpdate = null;
|
||||
StreamingEventGenerator? generator = null;
|
||||
while (await updateEnumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
@@ -55,7 +58,92 @@ internal static class AgentRunResponseUpdateExtensions
|
||||
// Special-case for agent framework workflow events.
|
||||
if (update.RawRepresentation is WorkflowEvent workflowEvent)
|
||||
{
|
||||
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
|
||||
// Convert executor events to standard OpenAI output_item events
|
||||
if (workflowEvent is ExecutorInvokedEvent invokedEvent)
|
||||
{
|
||||
var itemId = IdGenerator.NewId(prefix: "item");
|
||||
// Store the item ID for this executor so we can reuse it for completion/failure
|
||||
executorItemIds[invokedEvent.ExecutorId] = itemId;
|
||||
|
||||
var item = new ExecutorActionItemResource
|
||||
{
|
||||
Id = itemId,
|
||||
ExecutorId = invokedEvent.ExecutorId,
|
||||
Status = "in_progress",
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
yield return new StreamingOutputItemAdded
|
||||
{
|
||||
SequenceNumber = seq.Increment(),
|
||||
OutputIndex = outputIndex,
|
||||
Item = item
|
||||
};
|
||||
}
|
||||
else if (workflowEvent is ExecutorCompletedEvent completedEvent)
|
||||
{
|
||||
// Reuse the item ID from the invoked event, or generate a new one if not found
|
||||
var itemId = executorItemIds.TryGetValue(completedEvent.ExecutorId, out var existingId)
|
||||
? existingId
|
||||
: IdGenerator.NewId(prefix: "item");
|
||||
|
||||
// Remove from tracking as this executor run is now complete
|
||||
executorItemIds.Remove(completedEvent.ExecutorId);
|
||||
JsonElement? resultData = null;
|
||||
if (completedEvent.Data != null && JsonSerializer.IsReflectionEnabledByDefault)
|
||||
{
|
||||
resultData = JsonSerializer.SerializeToElement(
|
||||
completedEvent.Data,
|
||||
OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
||||
}
|
||||
|
||||
var item = new ExecutorActionItemResource
|
||||
{
|
||||
Id = itemId,
|
||||
ExecutorId = completedEvent.ExecutorId,
|
||||
Status = "completed",
|
||||
Result = resultData,
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
yield return new StreamingOutputItemDone
|
||||
{
|
||||
SequenceNumber = seq.Increment(),
|
||||
OutputIndex = outputIndex,
|
||||
Item = item
|
||||
};
|
||||
}
|
||||
else if (workflowEvent is ExecutorFailedEvent failedEvent)
|
||||
{
|
||||
// Reuse the item ID from the invoked event, or generate a new one if not found
|
||||
var itemId = executorItemIds.TryGetValue(failedEvent.ExecutorId, out var existingId)
|
||||
? existingId
|
||||
: IdGenerator.NewId(prefix: "item");
|
||||
|
||||
// Remove from tracking as this executor run has now failed
|
||||
executorItemIds.Remove(failedEvent.ExecutorId);
|
||||
|
||||
var item = new ExecutorActionItemResource
|
||||
{
|
||||
Id = itemId,
|
||||
ExecutorId = failedEvent.ExecutorId,
|
||||
Status = "failed",
|
||||
Error = failedEvent.Data?.ToString(),
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
yield return new StreamingOutputItemDone
|
||||
{
|
||||
SequenceNumber = seq.Increment(),
|
||||
OutputIndex = outputIndex,
|
||||
Item = item
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// For other workflow events (not executor-specific), keep the old format as fallback
|
||||
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -45,6 +45,7 @@ internal sealed class ItemResourceConverter : JsonConverter<ItemResource>
|
||||
MCPApprovalRequestItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalRequestItemResource),
|
||||
MCPApprovalResponseItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalResponseItemResource),
|
||||
MCPCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPCallItemResource),
|
||||
ExecutorActionItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ExecutorActionItemResource),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
@@ -106,6 +107,9 @@ internal sealed class ItemResourceConverter : JsonConverter<ItemResource>
|
||||
case MCPCallItemResource mcpCall:
|
||||
JsonSerializer.Serialize(writer, mcpCall, OpenAIHostingJsonContext.Default.MCPCallItemResource);
|
||||
break;
|
||||
case ExecutorActionItemResource executorAction:
|
||||
JsonSerializer.Serialize(writer, executorAction, OpenAIHostingJsonContext.Default.ExecutorActionItemResource);
|
||||
break;
|
||||
default:
|
||||
throw new JsonException($"Unknown item type: {value.GetType().Name}");
|
||||
}
|
||||
|
||||
@@ -888,3 +888,47 @@ internal sealed class MCPCallItemResource : ItemResource
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An executor action item resource for workflow execution visualization.
|
||||
/// </summary>
|
||||
internal sealed class ExecutorActionItemResource : ItemResource
|
||||
{
|
||||
/// <summary>
|
||||
/// The constant item type identifier for executor action items.
|
||||
/// </summary>
|
||||
public const string ItemType = "executor_action";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Type => ItemType;
|
||||
|
||||
/// <summary>
|
||||
/// The executor identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("executor_id")]
|
||||
public required string ExecutorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The execution status: "in_progress", "completed", "failed", or "cancelled".
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The executor result data (for completed status).
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public JsonElement? Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The error message (for failed status).
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The creation timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public long CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
+1
-1
@@ -565,7 +565,7 @@ internal sealed class StreamingWorkflowEventComplete : StreamingResponseEvent
|
||||
/// <summary>
|
||||
/// The constant event type identifier for workflow event events.
|
||||
/// </summary>
|
||||
public const string EventType = "response.workflow_event.complete";
|
||||
public const string EventType = "response.workflow_event.completed";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
|
||||
Reference in New Issue
Block a user