fix: enrich tool activity arguments on dedup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-08 12:14:03 +02:00
co-authored by Copilot
parent 0e2f9b8ae5
commit b9e73831e8
6 changed files with 178 additions and 35 deletions
@@ -319,7 +319,8 @@ public class AgentWorkflowTurnRunner : ITurnWorkflowRunner
command, command,
requestInfo, requestInfo,
state.ActiveAgent, state.ActiveAgent,
state.ToolNamesByCallId); state.ToolNamesByCallId,
state.ToolCallHasArgumentsById);
if (activity is null) if (activity is null)
{ {
@@ -23,6 +23,8 @@ internal class TurnExecutionState
public ConcurrentDictionary<string, string> ToolNamesByCallId { get; } = new(StringComparer.Ordinal); public ConcurrentDictionary<string, string> ToolNamesByCallId { get; } = new(StringComparer.Ordinal);
public ConcurrentDictionary<string, bool> ToolCallHasArgumentsById { get; } = new(StringComparer.Ordinal);
public AgentIdentity? ActiveAgent { get; private set; } public AgentIdentity? ActiveAgent { get; private set; }
public List<ChatMessageDto> CompletedMessages { get; private set; } = []; public List<ChatMessageDto> CompletedMessages { get; private set; } = [];
@@ -93,7 +95,7 @@ internal class TurnExecutionState
case ProviderToolExecutionStartEvent toolExecutionStart: case ProviderToolExecutionStartEvent toolExecutionStart:
string toolCallId = toolExecutionStart.ToolCallId; string toolCallId = toolExecutionStart.ToolCallId;
string toolName = toolExecutionStart.ToolName; string toolName = toolExecutionStart.ToolName;
ToolNamesByCallId[toolCallId] = toolName; TrackToolCall(toolCallId, toolName, toolExecutionStart.ToolArguments);
ActiveAgent = agent; ActiveAgent = agent;
AgentActivityEventDto? toolActivity = CreateToolCallingActivity( AgentActivityEventDto? toolActivity = CreateToolCallingActivity(
agent, toolName, toolCallId, toolExecutionStart.ToolArguments); agent, toolName, toolCallId, toolExecutionStart.ToolArguments);
@@ -269,6 +271,15 @@ internal class TurnExecutionState
_lastObservedMessageId = messageId; _lastObservedMessageId = messageId;
} }
private void TrackToolCall(
string toolCallId,
string toolName,
IReadOnlyDictionary<string, object?>? toolArguments)
{
ToolNamesByCallId[toolCallId] = toolName;
ToolCallHasArgumentsById[toolCallId] = toolArguments is { Count: > 0 };
}
private void QueueMessageReclassifiedIfNeeded(string? messageId) private void QueueMessageReclassifiedIfNeeded(string? messageId)
{ {
if (string.IsNullOrWhiteSpace(messageId)) if (string.IsNullOrWhiteSpace(messageId))
@@ -20,7 +20,8 @@ internal static class WorkflowRequestInfoInterpreter
RunTurnCommandDto command, RunTurnCommandDto command,
RequestInfoEvent requestInfo, RequestInfoEvent requestInfo,
AgentIdentity? activeAgent, AgentIdentity? activeAgent,
ConcurrentDictionary<string, string> toolNamesByCallId) ConcurrentDictionary<string, string> toolNamesByCallId,
ConcurrentDictionary<string, bool> toolCallHasArgumentsById)
{ {
RequestInterpretation interpretation = InterpretRequest(command.Workflow, requestInfo); RequestInterpretation interpretation = InterpretRequest(command.Workflow, requestInfo);
return interpretation switch return interpretation switch
@@ -28,7 +29,7 @@ internal static class WorkflowRequestInfoInterpreter
HandoffRequestInterpretation handoff => HandoffRequestInterpretation handoff =>
CreateHandoffActivity(command, handoff.TargetAgent, activeAgent), CreateHandoffActivity(command, handoff.TargetAgent, activeAgent),
ToolRequestInterpretation tool when activeAgent.HasValue => ToolRequestInterpretation tool when activeAgent.HasValue =>
CreateToolCallingActivity(command, activeAgent.Value, tool, toolNamesByCallId), CreateToolCallingActivity(command, activeAgent.Value, tool, toolNamesByCallId, toolCallHasArgumentsById),
_ => null, _ => null,
}; };
} }
@@ -63,14 +64,21 @@ internal static class WorkflowRequestInfoInterpreter
RunTurnCommandDto command, RunTurnCommandDto command,
AgentIdentity activeAgent, AgentIdentity activeAgent,
ToolRequestInterpretation tool, ToolRequestInterpretation tool,
ConcurrentDictionary<string, string> toolNamesByCallId) ConcurrentDictionary<string, string> toolNamesByCallId,
ConcurrentDictionary<string, bool> toolCallHasArgumentsById)
{ {
bool hasToolArguments = tool.ToolArguments is { Count: > 0 };
if (tool.ToolCallId is not null && toolNamesByCallId.ContainsKey(tool.ToolCallId)) if (tool.ToolCallId is not null && toolNamesByCallId.ContainsKey(tool.ToolCallId))
{ {
return null; bool trackedHasArguments = toolCallHasArgumentsById.TryGetValue(tool.ToolCallId, out bool hasTrackedArguments)
&& hasTrackedArguments;
if (trackedHasArguments || !hasToolArguments)
{
return null;
}
} }
TrackToolCallId(toolNamesByCallId, tool.ToolCallId, tool.ToolName); TrackToolCallId(toolNamesByCallId, toolCallHasArgumentsById, tool.ToolCallId, tool.ToolName, hasToolArguments);
return new AgentActivityEventDto return new AgentActivityEventDto
{ {
@@ -88,12 +96,15 @@ internal static class WorkflowRequestInfoInterpreter
private static void TrackToolCallId( private static void TrackToolCallId(
ConcurrentDictionary<string, string> toolNamesByCallId, ConcurrentDictionary<string, string> toolNamesByCallId,
ConcurrentDictionary<string, bool> toolCallHasArgumentsById,
string? toolCallId, string? toolCallId,
string toolName) string toolName,
bool hasToolArguments)
{ {
if (toolCallId is not null) if (toolCallId is not null)
{ {
toolNamesByCallId[toolCallId] = toolName; toolNamesByCallId[toolCallId] = toolName;
toolCallHasArgumentsById[toolCallId] = hasToolArguments;
} }
} }
@@ -78,6 +78,8 @@ public sealed class CopilotTurnExecutionStateTests
Assert.Equal("/src/main.ts", toolActivity.ToolArguments["path"]); Assert.Equal("/src/main.ts", toolActivity.ToolArguments["path"]);
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);
Assert.True(state.ToolCallHasArgumentsById.TryGetValue("tool-call-1", out bool hasArguments));
Assert.True(hasArguments);
} }
[Fact] [Fact]
@@ -93,6 +95,8 @@ public sealed class CopilotTurnExecutionStateTests
AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>()); AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.Null(toolActivity.ToolArguments); Assert.Null(toolActivity.ToolArguments);
Assert.True(state.ToolCallHasArgumentsById.TryGetValue("tool-call-1", out bool hasArguments));
Assert.False(hasArguments);
} }
[Fact] [Fact]
@@ -109,6 +113,8 @@ public sealed class CopilotTurnExecutionStateTests
Assert.Empty(state.DrainPendingEvents().OfType<AgentActivityEventDto>()); Assert.Empty(state.DrainPendingEvents().OfType<AgentActivityEventDto>());
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName)); Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName));
Assert.Equal("handoff_to_specialist", toolName); Assert.Equal("handoff_to_specialist", toolName);
Assert.True(state.ToolCallHasArgumentsById.TryGetValue("tool-call-1", out bool hasArguments));
Assert.False(hasArguments);
} }
[Fact] [Fact]
@@ -231,6 +237,10 @@ public sealed class CopilotTurnExecutionStateTests
Assert.Equal("rg", firstToolName); Assert.Equal("rg", firstToolName);
Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-2", out string? secondToolName)); Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-2", out string? secondToolName));
Assert.Equal("view", secondToolName); Assert.Equal("view", secondToolName);
Assert.True(state.ToolCallHasArgumentsById.TryGetValue("tool-call-1", out bool firstHasArguments));
Assert.False(firstHasArguments);
Assert.True(state.ToolCallHasArgumentsById.TryGetValue("tool-call-2", out bool secondHasArguments));
Assert.False(secondHasArguments);
} }
[Fact] [Fact]
@@ -721,6 +721,65 @@ public sealed class CopilotWorkflowRunnerTests
}); });
} }
[Fact]
public async Task HandleWorkflowEventAsync_EmitsToolActivityEnrichmentWhenRequestInfoAddsMissingArguments()
{
RunTurnCommandDto command = CreateApprovalCommand();
CopilotTurnExecutionState state = new(command);
state.ObserveSessionEvent(
CreateAgent("agent-1", "Primary"),
SessionEvent.FromJson(
"""
{
"type": "tool.execution_start",
"data": {
"toolCallId": "tool-call-1",
"toolName": "view"
},
"id": "f61652d1-120e-4a9f-8f0e-1dbf04fb18da",
"timestamp": "2026-03-27T00:00:00Z"
}
"""));
_ = state.DrainPendingEvents();
RequestInfoEvent requestInfo = CreateRequestInfoEvent(
new FunctionCallContent("tool-call-1", "view", new Dictionary<string, object?>
{
["path"] = @"C:\workspace\README.md",
}));
List<AgentActivityEventDto> activities = [];
MethodInfo handleWorkflowEvent = typeof(CopilotWorkflowRunner).GetMethod(
"HandleWorkflowEventAsync",
BindingFlags.NonPublic | BindingFlags.Static)!;
Task<bool> handleTask = (Task<bool>)handleWorkflowEvent.Invoke(
null,
[
command,
requestInfo,
Array.Empty<ChatMessage>(),
state,
(Func<TurnDeltaEventDto, Task>)(_ => Task.CompletedTask),
(Func<SidecarEventDto, Task>)(sidecarEvent =>
{
activities.Add(Assert.IsType<AgentActivityEventDto>(sidecarEvent));
return Task.CompletedTask;
}),
])!;
bool shouldEndTurn = await handleTask;
Assert.False(shouldEndTurn);
AgentActivityEventDto activity = Assert.Single(activities);
Assert.Equal("tool-calling", activity.ActivityType);
Assert.Equal("view", activity.ToolName);
Assert.Equal("tool-call-1", activity.ToolCallId);
Assert.NotNull(activity.ToolArguments);
Assert.Equal(@"C:\workspace\README.md", activity.ToolArguments["path"]);
Assert.True(state.ToolCallHasArgumentsById.TryGetValue("tool-call-1", out bool hasArguments));
Assert.True(hasArguments);
}
[Fact] [Fact]
public void CreateExecutionEnvironment_UsesLockstepWhenRequested() public void CreateExecutionEnvironment_UsesLockstepWhenRequested()
{ {
@@ -15,7 +15,7 @@ public sealed class WorkflowRequestInfoInterpreterTests
[Fact] [Fact]
public void TryCreateActivityFromRequest_ReturnsToolCallingActivityForFunctionCalls() public void TryCreateActivityFromRequest_ReturnsToolCallingActivityForFunctionCalls()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
new FunctionCallContent("call-1", "view", new Dictionary<string, object?> new FunctionCallContent("call-1", "view", new Dictionary<string, object?>
{ {
@@ -27,7 +27,8 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("tool-calling", activity.ActivityType);
@@ -37,13 +38,14 @@ public sealed class WorkflowRequestInfoInterpreterTests
Assert.NotNull(activity.ToolArguments); Assert.NotNull(activity.ToolArguments);
Assert.Equal(@"C:\workspace\file.txt", activity.ToolArguments["path"]); Assert.Equal(@"C:\workspace\file.txt", activity.ToolArguments["path"]);
Assert.Equal([10, 25], Assert.IsAssignableFrom<IReadOnlyList<object?>>(activity.ToolArguments["viewRange"])); Assert.Equal([10, 25], Assert.IsAssignableFrom<IReadOnlyList<object?>>(activity.ToolArguments["viewRange"]));
Assert.Equal("view", toolNamesByCallId["call-1"]); Assert.Equal("view", tracking.ToolNamesByCallId["call-1"]);
Assert.True(tracking.ToolCallHasArgumentsById["call-1"]);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_MapsMcpToolCalls() public void TryCreateActivityFromRequest_MapsMcpToolCalls()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
CreateMcpToolCall( CreateMcpToolCall(
"call-1", "call-1",
@@ -59,7 +61,8 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("tool-calling", activity.ActivityType);
@@ -67,13 +70,14 @@ public sealed class WorkflowRequestInfoInterpreterTests
Assert.NotNull(activity.ToolArguments); Assert.NotNull(activity.ToolArguments);
Assert.Equal(@"C:\workspace", activity.ToolArguments["path"]); Assert.Equal(@"C:\workspace", activity.ToolArguments["path"]);
Assert.Equal(true, activity.ToolArguments["includeIgnored"]); Assert.Equal(true, activity.ToolArguments["includeIgnored"]);
Assert.Equal("git.status", toolNamesByCallId["call-1"]); Assert.Equal("git.status", tracking.ToolNamesByCallId["call-1"]);
Assert.True(tracking.ToolCallHasArgumentsById["call-1"]);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_MapsCodeInterpreterCallsToSyntheticToolName() public void TryCreateActivityFromRequest_MapsCodeInterpreterCallsToSyntheticToolName()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
CreateCodeInterpreterToolCall("call-1", "print('hello')")); CreateCodeInterpreterToolCall("call-1", "print('hello')"));
@@ -81,7 +85,8 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("tool-calling", activity.ActivityType);
@@ -90,32 +95,35 @@ public sealed class WorkflowRequestInfoInterpreterTests
Assert.Equal( Assert.Equal(
["print('hello')"], ["print('hello')"],
Assert.IsAssignableFrom<IReadOnlyList<object?>>(activity.ToolArguments["inputs"])); Assert.IsAssignableFrom<IReadOnlyList<object?>>(activity.ToolArguments["inputs"]));
Assert.Equal("code interpreter", toolNamesByCallId["call-1"]); Assert.Equal("code interpreter", tracking.ToolNamesByCallId["call-1"]);
Assert.True(tracking.ToolCallHasArgumentsById["call-1"]);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_MapsImageGenerationCallsWithoutTrackingCallId() public void TryCreateActivityFromRequest_MapsImageGenerationCallsWithoutTrackingCallId()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent(CreateImageGenerationToolCall()); RequestInfoEvent requestInfo = CreateRequestInfoEvent(CreateImageGenerationToolCall());
AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest(
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.Equal("tool-calling", activity.ActivityType); Assert.Equal("tool-calling", activity.ActivityType);
Assert.Equal("image generation", activity.ToolName); Assert.Equal("image generation", activity.ToolName);
Assert.Null(activity.ToolArguments); Assert.Null(activity.ToolArguments);
Assert.Empty(toolNamesByCallId); Assert.Empty(tracking.ToolNamesByCallId);
Assert.Empty(tracking.ToolCallHasArgumentsById);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_LeavesToolArgumentsNullWhenFunctionCallHasNoUsableArguments() public void TryCreateActivityFromRequest_LeavesToolArgumentsNullWhenFunctionCallHasNoUsableArguments()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
new FunctionCallContent("call-1", "view", new Dictionary<string, object?> new FunctionCallContent("call-1", "view", new Dictionary<string, object?>
{ {
@@ -127,16 +135,18 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.Null(activity.ToolArguments); Assert.Null(activity.ToolArguments);
Assert.False(tracking.ToolCallHasArgumentsById["call-1"]);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_TruncatesOversizedToolArgumentValues() public void TryCreateActivityFromRequest_TruncatesOversizedToolArgumentValues()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
new FunctionCallContent( new FunctionCallContent(
"call-1", "call-1",
@@ -150,37 +160,71 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.NotNull(activity.ToolArguments); Assert.NotNull(activity.ToolArguments);
Assert.Equal("[truncated]", activity.ToolArguments["command"]); Assert.Equal("[truncated]", activity.ToolArguments["command"]);
Assert.True(tracking.ToolCallHasArgumentsById["call-1"]);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_SkipsDuplicateTrackedToolCallIds() public void TryCreateActivityFromRequest_SkipsDuplicateTrackedToolCallIdsThatAlreadyHaveArguments()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal) var tracking = CreateToolTracking();
{ tracking.ToolNamesByCallId["call-1"] = "view";
["call-1"] = "view", tracking.ToolCallHasArgumentsById["call-1"] = true;
};
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
new FunctionCallContent("call-1", "view", new Dictionary<string, object?>())); new FunctionCallContent("call-1", "view", new Dictionary<string, object?>
{
["path"] = @"C:\workspace\file.txt",
}));
AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest( AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest(
CreateSingleAgentCommand(), CreateSingleAgentCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-1", "Primary"), new AgentIdentity("agent-1", "Primary"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.Null(activity); Assert.Null(activity);
Assert.Equal("view", toolNamesByCallId["call-1"]); Assert.Equal("view", tracking.ToolNamesByCallId["call-1"]);
Assert.True(tracking.ToolCallHasArgumentsById["call-1"]);
}
[Fact]
public void TryCreateActivityFromRequest_EmitsEnrichmentWhenTrackedToolCallWasMissingArguments()
{
var tracking = CreateToolTracking();
tracking.ToolNamesByCallId["call-1"] = "view";
tracking.ToolCallHasArgumentsById["call-1"] = false;
RequestInfoEvent requestInfo = CreateRequestInfoEvent(
new FunctionCallContent("call-1", "view", new Dictionary<string, object?>
{
["path"] = @"C:\workspace\file.txt",
}));
AgentActivityEventDto? activity = WorkflowRequestInfoInterpreter.TryCreateActivityFromRequest(
CreateSingleAgentCommand(),
requestInfo,
new AgentIdentity("agent-1", "Primary"),
tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity);
Assert.Equal("tool-calling", activity.ActivityType);
Assert.Equal("call-1", activity.ToolCallId);
Assert.NotNull(activity.ToolArguments);
Assert.Equal(@"C:\workspace\file.txt", activity.ToolArguments["path"]);
Assert.Equal("view", tracking.ToolNamesByCallId["call-1"]);
Assert.True(tracking.ToolCallHasArgumentsById["call-1"]);
} }
[Fact] [Fact]
public void TryCreateActivityFromRequest_ReturnsHandoffActivityForKnownTargets() public void TryCreateActivityFromRequest_ReturnsHandoffActivityForKnownTargets()
{ {
ConcurrentDictionary<string, string> toolNamesByCallId = new(StringComparer.Ordinal); var tracking = CreateToolTracking();
RequestInfoEvent requestInfo = CreateRequestInfoEvent( RequestInfoEvent requestInfo = CreateRequestInfoEvent(
CreateHandoffTarget("agent-handoff-ux", "UX Specialist")); CreateHandoffTarget("agent-handoff-ux", "UX Specialist"));
@@ -188,7 +232,8 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateHandoffCommand(), CreateHandoffCommand(),
requestInfo, requestInfo,
new AgentIdentity("agent-handoff-triage", "Triage"), new AgentIdentity("agent-handoff-triage", "Triage"),
toolNamesByCallId); tracking.ToolNamesByCallId,
tracking.ToolCallHasArgumentsById);
Assert.NotNull(activity); Assert.NotNull(activity);
Assert.Equal("handoff", activity.ActivityType); Assert.Equal("handoff", activity.ActivityType);
@@ -197,7 +242,8 @@ public sealed class WorkflowRequestInfoInterpreterTests
Assert.Equal("agent-handoff-triage", activity.SourceAgentId); Assert.Equal("agent-handoff-triage", activity.SourceAgentId);
Assert.Equal("Triage", activity.SourceAgentName); Assert.Equal("Triage", activity.SourceAgentName);
Assert.Null(activity.ToolName); Assert.Null(activity.ToolName);
Assert.Empty(toolNamesByCallId); Assert.Empty(tracking.ToolNamesByCallId);
Assert.Empty(tracking.ToolCallHasArgumentsById);
} }
[Fact] [Fact]
@@ -295,6 +341,11 @@ public sealed class WorkflowRequestInfoInterpreterTests
CreateAgent("agent-handoff-ux", "UX Specialist"), CreateAgent("agent-handoff-ux", "UX Specialist"),
]); ]);
private static (
ConcurrentDictionary<string, string> ToolNamesByCallId,
ConcurrentDictionary<string, bool> ToolCallHasArgumentsById) CreateToolTracking()
=> (new(StringComparer.Ordinal), new(StringComparer.Ordinal));
private static RunTurnCommandDto CreateCommand(string orchestrationMode, IReadOnlyList<WorkflowNodeDto> agents) private static RunTurnCommandDto CreateCommand(string orchestrationMode, IReadOnlyList<WorkflowNodeDto> agents)
{ {
return new RunTurnCommandDto return new RunTurnCommandDto