diff --git a/sidecar/.vs/Aryx.AgentHost.slnx/DesignTimeBuild/.dtbcache.v2 b/sidecar/.vs/Aryx.AgentHost.slnx/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..0a64be5 Binary files /dev/null and b/sidecar/.vs/Aryx.AgentHost.slnx/DesignTimeBuild/.dtbcache.v2 differ diff --git a/sidecar/.vs/Aryx.AgentHost.slnx/FileContentIndex/c67ca173-dd43-42f7-b189-aa5b0dbe371b.vsidx b/sidecar/.vs/Aryx.AgentHost.slnx/FileContentIndex/c67ca173-dd43-42f7-b189-aa5b0dbe371b.vsidx new file mode 100644 index 0000000..252774b Binary files /dev/null and b/sidecar/.vs/Aryx.AgentHost.slnx/FileContentIndex/c67ca173-dd43-42f7-b189-aa5b0dbe371b.vsidx differ diff --git a/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs b/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs index 00a9835..11b05b5 100644 --- a/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs +++ b/sidecar/src/Aryx.AgentHost/Services/CopilotTurnExecutionState.cs @@ -98,7 +98,8 @@ internal sealed class CopilotTurnExecutionState string toolName = toolExecutionStart.Data.ToolName.Trim(); ToolNamesByCallId[toolCallId] = toolName; ActiveAgent = agent; - AgentActivityEventDto? toolActivity = CreateToolCallingActivity(agent, toolName, toolCallId); + AgentActivityEventDto? toolActivity = CreateToolCallingActivity( + agent, toolName, toolCallId, toolExecutionStart.Data.Arguments); if (toolActivity is not null) { _pendingEvents.Enqueue(toolActivity); @@ -295,7 +296,8 @@ internal sealed class CopilotTurnExecutionState private AgentActivityEventDto? CreateToolCallingActivity( AgentIdentity agent, string toolName, - string toolCallId) + string toolCallId, + object? rawArguments = null) { if (toolName.StartsWith("handoff_to_", StringComparison.Ordinal)) { @@ -312,6 +314,7 @@ internal sealed class CopilotTurnExecutionState AgentName = agent.AgentName, ToolName = toolName, ToolCallId = toolCallId, + ToolArguments = WorkflowRequestInfoInterpreter.NormalizeRawToolArguments(rawArguments), }; } diff --git a/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs b/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs index 2e99906..ec97f9f 100644 --- a/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs +++ b/sidecar/src/Aryx.AgentHost/Services/WorkflowRequestInfoInterpreter.cs @@ -205,6 +205,26 @@ internal static class WorkflowRequestInfoInterpreter return false; } + public static IReadOnlyDictionary? NormalizeRawToolArguments(object? rawArguments) + { + return rawArguments switch + { + null => null, + JsonElement { ValueKind: JsonValueKind.Object } element => NormalizeToolArgumentObject(element), + IEnumerable> dictionary => NormalizeToolArguments(dictionary), + _ => NormalizeRawToolArgumentsViaJson(rawArguments), + }; + } + + private static IReadOnlyDictionary? NormalizeRawToolArgumentsViaJson(object value) + { + string json = JsonSerializer.Serialize(value, value.GetType(), JsonOptions); + using JsonDocument document = JsonDocument.Parse(json); + return document.RootElement.ValueKind == JsonValueKind.Object + ? NormalizeToolArgumentObject(document.RootElement) + : null; + } + private static IReadOnlyDictionary? NormalizeToolArguments( IEnumerable>? arguments) { diff --git a/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs index d982068..b96ecc0 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/CopilotTurnExecutionStateTests.cs @@ -68,16 +68,33 @@ public sealed class CopilotTurnExecutionStateTests state.ObserveSessionEvent( command.Workflow.GetAgentNodes()[0], 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","arguments":{"path":"/src/main.ts","view_range":[10,20]}},"id":"33333333-3333-3333-3333-333333333333","timestamp":"2026-03-27T00:00:00Z"}""")); AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType()); Assert.Equal("tool-calling", toolActivity.ActivityType); Assert.Equal("view", toolActivity.ToolName); Assert.Equal("tool-call-1", toolActivity.ToolCallId); + Assert.NotNull(toolActivity.ToolArguments); + Assert.Equal("/src/main.ts", toolActivity.ToolArguments["path"]); Assert.True(state.ToolNamesByCallId.TryGetValue("tool-call-1", out string? toolName)); Assert.Equal("view", toolName); } + [Fact] + public void ObserveSessionEvent_ToolExecutionStart_WithoutArguments_SetsToolArgumentsToNull() + { + RunTurnCommandDto command = CreateCommand(); + CopilotTurnExecutionState state = new(command); + + state.ObserveSessionEvent( + command.Workflow.GetAgentNodes()[0], + 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"}""")); + + AgentActivityEventDto toolActivity = Assert.Single(state.DrainPendingEvents().OfType()); + Assert.Null(toolActivity.ToolArguments); + } + [Fact] public void ObserveSessionEvent_ToolExecutionStart_DoesNotQueueToolActivityForHandoffTools() { diff --git a/sidecar/tests/Aryx.AgentHost.Tests/WorkflowRequestInfoInterpreterTests.cs b/sidecar/tests/Aryx.AgentHost.Tests/WorkflowRequestInfoInterpreterTests.cs index 1fd0080..c0c46c7 100644 --- a/sidecar/tests/Aryx.AgentHost.Tests/WorkflowRequestInfoInterpreterTests.cs +++ b/sidecar/tests/Aryx.AgentHost.Tests/WorkflowRequestInfoInterpreterTests.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Runtime.CompilerServices; +using System.Text.Json; using Aryx.AgentHost.Contracts; using Aryx.AgentHost.Services; using Microsoft.Agents.AI; @@ -255,6 +256,35 @@ public sealed class WorkflowRequestInfoInterpreterTests Assert.False(requiresBoundary); } + [Fact] + public void NormalizeRawToolArguments_JsonElement_ExtractsArguments() + { + using JsonDocument doc = JsonDocument.Parse("""{"path":"/src/main.ts","view_range":[10,20]}"""); + JsonElement element = doc.RootElement.Clone(); + + IReadOnlyDictionary? result = WorkflowRequestInfoInterpreter.NormalizeRawToolArguments(element); + + Assert.NotNull(result); + Assert.Equal("/src/main.ts", result["path"]); + IReadOnlyList viewRange = Assert.IsAssignableFrom>(result["view_range"]); + Assert.Equal(2, viewRange.Count); + } + + [Fact] + public void NormalizeRawToolArguments_Null_ReturnsNull() + { + Assert.Null(WorkflowRequestInfoInterpreter.NormalizeRawToolArguments(null)); + } + + [Fact] + public void NormalizeRawToolArguments_EmptyObject_ReturnsNull() + { + using JsonDocument doc = JsonDocument.Parse("{}"); + JsonElement element = doc.RootElement.Clone(); + + Assert.Null(WorkflowRequestInfoInterpreter.NormalizeRawToolArguments(element)); + } + private static RunTurnCommandDto CreateSingleAgentCommand() => CreateCommand("single", [CreateAgent("agent-1", "Primary")]);