mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-26 12:53:59 +02:00
fix: project copilot tool results into workflows
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -104,6 +105,7 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Channel<AgentResponseUpdate> channel = Channel.CreateUnbounded<AgentResponseUpdate>();
|
Channel<AgentResponseUpdate> channel = Channel.CreateUnbounded<AgentResponseUpdate>();
|
||||||
|
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
using IDisposable subscription = copilotSession.On(evt =>
|
using IDisposable subscription = copilotSession.On(evt =>
|
||||||
{
|
{
|
||||||
@@ -114,9 +116,19 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case AssistantMessageEvent assistantMessage:
|
case AssistantMessageEvent assistantMessage:
|
||||||
|
TrackToolRequestNames(toolNamesByCallId, assistantMessage.Data?.ToolRequests);
|
||||||
channel.Writer.TryWrite(ConvertToAgentResponseUpdate(assistantMessage));
|
channel.Writer.TryWrite(ConvertToAgentResponseUpdate(assistantMessage));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ToolExecutionCompleteEvent toolExecutionComplete:
|
||||||
|
AgentResponseUpdate? toolResultUpdate = ConvertToAgentResponseUpdate(toolExecutionComplete, toolNamesByCallId);
|
||||||
|
if (toolResultUpdate is not null)
|
||||||
|
{
|
||||||
|
channel.Writer.TryWrite(toolResultUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case AssistantUsageEvent usageEvent:
|
case AssistantUsageEvent usageEvent:
|
||||||
channel.Writer.TryWrite(ConvertToAgentResponseUpdate(usageEvent));
|
channel.Writer.TryWrite(ConvertToAgentResponseUpdate(usageEvent));
|
||||||
break;
|
break;
|
||||||
@@ -232,16 +244,6 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only project handoff tool calls as FunctionCallContent for the Agent Framework.
|
|
||||||
// Other tool calls (ask_user, MCP tools, etc.) are resolved by the Copilot SDK
|
|
||||||
// internally and must not be surfaced, because AIAgentHostExecutor tracks every
|
|
||||||
// FunctionCallContent as an outstanding request. An unmatched request prevents
|
|
||||||
// the executor from emitting a TurnToken, which stalls group-chat advancement.
|
|
||||||
if (!IsHandoffToolName(toolRequest.Name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.Add(new FunctionCallContent(
|
contents.Add(new FunctionCallContent(
|
||||||
toolRequest.ToolCallId,
|
toolRequest.ToolCallId,
|
||||||
toolRequest.Name,
|
toolRequest.Name,
|
||||||
@@ -251,6 +253,26 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable
|
|||||||
return contents;
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static FunctionResultContent? TryCreateToolResultContent(
|
||||||
|
ToolExecutionCompleteEvent toolExecutionComplete,
|
||||||
|
string? toolName = null)
|
||||||
|
{
|
||||||
|
// Regular Copilot tools need their result projected back into AF so the function call
|
||||||
|
// remains part of workflow-visible history. Handoff tools are finalized separately by
|
||||||
|
// HandoffAgentExecutor, which already injects its own "Transferred." result.
|
||||||
|
string? toolCallId = toolExecutionComplete.Data?.ToolCallId?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(toolCallId) || IsHandoffToolName(toolName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string result = ResolveToolResultText(toolExecutionComplete.Data);
|
||||||
|
return new FunctionResultContent(toolCallId, result)
|
||||||
|
{
|
||||||
|
RawRepresentation = toolExecutionComplete,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsHandoffToolName(string? name)
|
private static bool IsHandoffToolName(string? name)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrWhiteSpace(name)
|
return !string.IsNullOrWhiteSpace(name)
|
||||||
@@ -441,6 +463,36 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AgentResponseUpdate? ConvertToAgentResponseUpdate(
|
||||||
|
ToolExecutionCompleteEvent toolExecutionComplete,
|
||||||
|
ConcurrentDictionary<string, string> toolNamesByCallId)
|
||||||
|
{
|
||||||
|
string? toolCallId = toolExecutionComplete.Data?.ToolCallId?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(toolCallId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? toolName = null;
|
||||||
|
if (toolNamesByCallId.TryRemove(toolCallId, out string? trackedToolName))
|
||||||
|
{
|
||||||
|
toolName = trackedToolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
FunctionResultContent? toolResult = TryCreateToolResultContent(toolExecutionComplete, toolName);
|
||||||
|
if (toolResult is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AgentResponseUpdate(ChatRole.Tool, [toolResult])
|
||||||
|
{
|
||||||
|
AgentId = Id,
|
||||||
|
MessageId = toolCallId,
|
||||||
|
CreatedAt = toolExecutionComplete.Timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEvent)
|
private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEvent)
|
||||||
{
|
{
|
||||||
AIContent content = new()
|
AIContent content = new()
|
||||||
@@ -455,6 +507,45 @@ internal sealed class AryxCopilotAgent : AIAgent, IAsyncDisposable
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TrackToolRequestNames(
|
||||||
|
ConcurrentDictionary<string, string> toolNamesByCallId,
|
||||||
|
AssistantMessageDataToolRequestsItem[]? toolRequests)
|
||||||
|
{
|
||||||
|
if (toolRequests is not { Length: > 0 })
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (AssistantMessageDataToolRequestsItem toolRequest in toolRequests)
|
||||||
|
{
|
||||||
|
string? toolCallId = toolRequest.ToolCallId?.Trim();
|
||||||
|
string? toolName = toolRequest.Name?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(toolCallId) || string.IsNullOrWhiteSpace(toolName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolNamesByCallId[toolCallId] = toolName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveToolResultText(ToolExecutionCompleteData? toolExecutionCompleteData)
|
||||||
|
{
|
||||||
|
if (toolExecutionCompleteData is null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolExecutionCompleteData.Success)
|
||||||
|
{
|
||||||
|
return toolExecutionCompleteData.Result?.Content
|
||||||
|
?? toolExecutionCompleteData.Result?.DetailedContent
|
||||||
|
?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolExecutionCompleteData.Error?.Message ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<string, object?>? ParseToolArguments(object? arguments)
|
private static Dictionary<string, object?>? ParseToolArguments(object? arguments)
|
||||||
{
|
{
|
||||||
if (arguments is null)
|
if (arguments is null)
|
||||||
|
|||||||
@@ -89,7 +89,16 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
case ToolExecutionStartEvent toolExecutionStart
|
case ToolExecutionStartEvent toolExecutionStart
|
||||||
when !string.IsNullOrWhiteSpace(toolExecutionStart.Data?.ToolCallId)
|
when !string.IsNullOrWhiteSpace(toolExecutionStart.Data?.ToolCallId)
|
||||||
&& !string.IsNullOrWhiteSpace(toolExecutionStart.Data?.ToolName):
|
&& !string.IsNullOrWhiteSpace(toolExecutionStart.Data?.ToolName):
|
||||||
ToolNamesByCallId[toolExecutionStart.Data.ToolCallId.Trim()] = toolExecutionStart.Data.ToolName.Trim();
|
string toolCallId = toolExecutionStart.Data.ToolCallId.Trim();
|
||||||
|
string toolName = toolExecutionStart.Data.ToolName.Trim();
|
||||||
|
ToolNamesByCallId[toolCallId] = toolName;
|
||||||
|
ActiveAgent = agent;
|
||||||
|
AgentActivityEventDto? toolActivity = CreateToolCallingActivity(agent, toolName, toolCallId);
|
||||||
|
if (toolActivity is not null)
|
||||||
|
{
|
||||||
|
_pendingEvents.Enqueue(toolActivity);
|
||||||
|
}
|
||||||
|
|
||||||
QueueMessageReclassifiedIfNeeded(_lastObservedMessageId);
|
QueueMessageReclassifiedIfNeeded(_lastObservedMessageId);
|
||||||
break;
|
break;
|
||||||
case AssistantIntentEvent intentEvent:
|
case AssistantIntentEvent intentEvent:
|
||||||
@@ -278,6 +287,29 @@ internal sealed class CopilotTurnExecutionState
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AgentActivityEventDto? CreateToolCallingActivity(
|
||||||
|
AgentIdentity agent,
|
||||||
|
string toolName,
|
||||||
|
string toolCallId)
|
||||||
|
{
|
||||||
|
if (toolName.StartsWith("handoff_to_", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AgentActivityEventDto
|
||||||
|
{
|
||||||
|
Type = "agent-activity",
|
||||||
|
RequestId = _command.RequestId,
|
||||||
|
SessionId = _command.SessionId,
|
||||||
|
ActivityType = "tool-calling",
|
||||||
|
AgentId = agent.AgentId,
|
||||||
|
AgentName = agent.AgentName,
|
||||||
|
ToolName = toolName,
|
||||||
|
ToolCallId = toolCallId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private MessageReclassifiedEventDto CreateMessageReclassifiedEvent(string messageId)
|
private MessageReclassifiedEventDto CreateMessageReclassifiedEvent(string messageId)
|
||||||
{
|
{
|
||||||
return new MessageReclassifiedEventDto
|
return new MessageReclassifiedEventDto
|
||||||
|
|||||||
@@ -57,12 +57,17 @@ internal static class WorkflowRequestInfoInterpreter
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AgentActivityEventDto CreateToolCallingActivity(
|
private static AgentActivityEventDto? CreateToolCallingActivity(
|
||||||
RunTurnCommandDto command,
|
RunTurnCommandDto command,
|
||||||
AgentIdentity activeAgent,
|
AgentIdentity activeAgent,
|
||||||
ToolRequestInterpretation tool,
|
ToolRequestInterpretation tool,
|
||||||
ConcurrentDictionary<string, string> toolNamesByCallId)
|
ConcurrentDictionary<string, string> toolNamesByCallId)
|
||||||
{
|
{
|
||||||
|
if (tool.ToolCallId is not null && toolNamesByCallId.ContainsKey(tool.ToolCallId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
TrackToolCallId(toolNamesByCallId, tool.ToolCallId, tool.ToolName);
|
TrackToolCallId(toolNamesByCallId, tool.ToolCallId, tool.ToolName);
|
||||||
|
|
||||||
return new AgentActivityEventDto
|
return new AgentActivityEventDto
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ public sealed class CopilotAgentBundleTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ConvertToolRequestsToFunctionCalls_SkipsNonHandoffToolCalls()
|
public void ConvertToolRequestsToFunctionCalls_MapsNonHandoffToolCalls()
|
||||||
{
|
{
|
||||||
AssistantMessageDataToolRequestsItem[] toolRequests =
|
AssistantMessageDataToolRequestsItem[] toolRequests =
|
||||||
{
|
{
|
||||||
@@ -197,9 +197,99 @@ public sealed class CopilotAgentBundleTests
|
|||||||
|
|
||||||
IReadOnlyList<FunctionCallContent> result = AryxCopilotAgent.ConvertToolRequestsToFunctionCalls(toolRequests);
|
IReadOnlyList<FunctionCallContent> result = AryxCopilotAgent.ConvertToolRequestsToFunctionCalls(toolRequests);
|
||||||
|
|
||||||
FunctionCallContent single = Assert.Single(result);
|
Assert.Collection(
|
||||||
Assert.Equal("call-003", single.CallId);
|
result,
|
||||||
Assert.Equal("handoff_to_reviewer", single.Name);
|
functionCall =>
|
||||||
|
{
|
||||||
|
Assert.Equal("call-001", functionCall.CallId);
|
||||||
|
Assert.Equal("ask_user", functionCall.Name);
|
||||||
|
},
|
||||||
|
functionCall =>
|
||||||
|
{
|
||||||
|
Assert.Equal("call-002", functionCall.CallId);
|
||||||
|
Assert.Equal("web_fetch", functionCall.Name);
|
||||||
|
},
|
||||||
|
functionCall =>
|
||||||
|
{
|
||||||
|
Assert.Equal("call-003", functionCall.CallId);
|
||||||
|
Assert.Equal("handoff_to_reviewer", functionCall.Name);
|
||||||
|
},
|
||||||
|
functionCall =>
|
||||||
|
{
|
||||||
|
Assert.Equal("call-004", functionCall.CallId);
|
||||||
|
Assert.Equal("grep", functionCall.Name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryCreateToolResultContent_UsesSdkResultContentForNonHandoffTools()
|
||||||
|
{
|
||||||
|
ToolExecutionCompleteEvent toolExecutionComplete = new()
|
||||||
|
{
|
||||||
|
Data = new ToolExecutionCompleteData
|
||||||
|
{
|
||||||
|
ToolCallId = "call-123",
|
||||||
|
Success = true,
|
||||||
|
Result = new ToolExecutionCompleteDataResult
|
||||||
|
{
|
||||||
|
Content = "Search complete.",
|
||||||
|
DetailedContent = "Search complete with extra context.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
FunctionResultContent? toolResult = AryxCopilotAgent.TryCreateToolResultContent(toolExecutionComplete, "rg");
|
||||||
|
|
||||||
|
Assert.NotNull(toolResult);
|
||||||
|
Assert.Equal("call-123", toolResult.CallId);
|
||||||
|
Assert.Equal("Search complete.", Assert.IsType<string>(toolResult.Result));
|
||||||
|
Assert.Same(toolExecutionComplete, toolResult.RawRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryCreateToolResultContent_UsesSdkErrorMessageForFailedTools()
|
||||||
|
{
|
||||||
|
ToolExecutionCompleteEvent toolExecutionComplete = new()
|
||||||
|
{
|
||||||
|
Data = new ToolExecutionCompleteData
|
||||||
|
{
|
||||||
|
ToolCallId = "call-456",
|
||||||
|
Success = false,
|
||||||
|
Error = new ToolExecutionCompleteDataError
|
||||||
|
{
|
||||||
|
Message = "Permission denied.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
FunctionResultContent? toolResult = AryxCopilotAgent.TryCreateToolResultContent(toolExecutionComplete, "view");
|
||||||
|
|
||||||
|
Assert.NotNull(toolResult);
|
||||||
|
Assert.Equal("call-456", toolResult.CallId);
|
||||||
|
Assert.Equal("Permission denied.", Assert.IsType<string>(toolResult.Result));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryCreateToolResultContent_SkipsHandoffTools()
|
||||||
|
{
|
||||||
|
ToolExecutionCompleteEvent toolExecutionComplete = new()
|
||||||
|
{
|
||||||
|
Data = new ToolExecutionCompleteData
|
||||||
|
{
|
||||||
|
ToolCallId = "call-789",
|
||||||
|
Success = true,
|
||||||
|
Result = new ToolExecutionCompleteDataResult
|
||||||
|
{
|
||||||
|
Content = "Transferred.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
FunctionResultContent? toolResult = AryxCopilotAgent.TryCreateToolResultContent(
|
||||||
|
toolExecutionComplete,
|
||||||
|
"handoff_to_reviewer");
|
||||||
|
|
||||||
|
Assert.Null(toolResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public sealed class CopilotTurnExecutionStateTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObserveSessionEvent_ToolExecutionStart_TracksToolNameByCallId()
|
public void ObserveSessionEvent_ToolExecutionStart_TracksToolNameByCallIdAndQueuesToolActivity()
|
||||||
{
|
{
|
||||||
RunTurnCommandDto command = CreateCommand();
|
RunTurnCommandDto command = CreateCommand();
|
||||||
CopilotTurnExecutionState state = new(command);
|
CopilotTurnExecutionState state = new(command);
|
||||||
@@ -68,22 +68,32 @@ public sealed class CopilotTurnExecutionStateTests
|
|||||||
state.ObserveSessionEvent(
|
state.ObserveSessionEvent(
|
||||||
command.Pattern.Agents[0],
|
command.Pattern.Agents[0],
|
||||||
SessionEvent.FromJson(
|
SessionEvent.FromJson(
|
||||||
"""
|
"""{"type":"tool.execution_start","data":{"toolCallId":"tool-call-1","toolName":"view"},"id":"33333333-3333-3333-3333-333333333333","timestamp":"2026-03-27T00:00:00Z"}"""));
|
||||||
{
|
|
||||||
"type": "tool.execution_start",
|
|
||||||
"data": {
|
|
||||||
"toolCallId": "tool-call-1",
|
|
||||||
"toolName": "view"
|
|
||||||
},
|
|
||||||
"id": "33333333-3333-3333-3333-333333333333",
|
|
||||||
"timestamp": "2026-03-27T00:00:00Z"
|
|
||||||
}
|
|
||||||
"""));
|
|
||||||
|
|
||||||
|
AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
|
||||||
|
Assert.Equal("tool-calling", toolActivity.ActivityType);
|
||||||
|
Assert.Equal("view", toolActivity.ToolName);
|
||||||
|
Assert.Equal("tool-call-1", toolActivity.ToolCallId);
|
||||||
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
|
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
|
||||||
Assert.Equal("view", toolName);
|
Assert.Equal("view", toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObserveSessionEvent_ToolExecutionStart_DoesNotQueueToolActivityForHandoffTools()
|
||||||
|
{
|
||||||
|
RunTurnCommandDto command = CreateCommand();
|
||||||
|
CopilotTurnExecutionState state = new(command);
|
||||||
|
|
||||||
|
state.ObserveSessionEvent(
|
||||||
|
command.Pattern.Agents[0],
|
||||||
|
SessionEvent.FromJson(
|
||||||
|
"""{"type":"tool.execution_start","data":{"toolCallId":"tool-call-1","toolName":"handoff_to_specialist"},"id":"1ce9d1dc-68f1-4df5-9728-f97017233279","timestamp":"2026-03-27T00:00:00Z"}"""));
|
||||||
|
|
||||||
|
Assert.Empty(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
|
||||||
|
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
|
||||||
|
Assert.Equal("handoff_to_specialist", toolName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObserveSessionEvent_AssistantMessageWithToolRequests_QueuesMessageReclassifiedEvent()
|
public void ObserveSessionEvent_AssistantMessageWithToolRequests_QueuesMessageReclassifiedEvent()
|
||||||
{
|
{
|
||||||
@@ -178,6 +188,10 @@ public sealed class CopilotTurnExecutionStateTests
|
|||||||
|
|
||||||
IReadOnlyList<SidecarEventDto> pending = state.DrainPendingEvents();
|
IReadOnlyList<SidecarEventDto> pending = state.DrainPendingEvents();
|
||||||
|
|
||||||
|
AgentActivityEventDto[] toolActivities = [.. pending.OfType<AgentActivityEventDto>().Where(activity => activity.ActivityType == "tool-calling")];
|
||||||
|
Assert.Equal(2, toolActivities.Length);
|
||||||
|
Assert.Contains(toolActivities, activity => activity.ToolCallId == "tool-call-1" && activity.ToolName == "rg");
|
||||||
|
Assert.Contains(toolActivities, activity => activity.ToolCallId == "tool-call-2" && activity.ToolName == "view");
|
||||||
MessageReclassifiedEventDto reclassified = Assert.Single(pending.OfType<MessageReclassifiedEventDto>());
|
MessageReclassifiedEventDto reclassified = Assert.Single(pending.OfType<MessageReclassifiedEventDto>());
|
||||||
Assert.Equal("msg-3", reclassified.MessageId);
|
Assert.Equal("msg-3", reclassified.MessageId);
|
||||||
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? firstToolName));
|
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? firstToolName));
|
||||||
|
|||||||
@@ -86,6 +86,26 @@ public sealed class WorkflowRequestInfoInterpreterTests
|
|||||||
Assert.Empty(toolNamesByCallId);
|
Assert.Empty(toolNamesByCallId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryCreateActivityFromRequest_SkipsDuplicateTrackedToolCallIds()
|
||||||
|
{
|
||||||
|
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["call-1"] = "view",
|
||||||
|
};
|
||||||
|
RequestInfoEvent requestInfo = CreateRequestInfoEvent(
|
||||||
|
new FunctionCallContent("call-1", "view", new Dictionary<string, object?>()));
|
||||||
|
|
||||||
|
AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest(
|
||||||
|
CreateSingleAgentCommand(),
|
||||||
|
requestInfo,
|
||||||
|
new AgentIdentity("agent-1", "Primary"),
|
||||||
|
toolNamesByCallId);
|
||||||
|
|
||||||
|
Assert.Null(activity);
|
||||||
|
Assert.Equal("view", toolNamesByCallId["call-1"]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryCreateActivityFromRequest_ReturnsHandoffActivityForKnownTargets()
|
public void TryCreateActivityFromRequest_ReturnsHandoffActivityForKnownTargets()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user