mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-26 21:03:58 +02:00
fix: enrich tool activity arguments on dedup
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user