using System.Globalization; using System.Runtime.CompilerServices; using System.Net.ServerSentEvents; using System.Text; using System.Text.Json; using DebugTools.MccMcpWebPlayground.Contracts; using DebugTools.MccMcpWebPlayground.Infrastructure.Mcp; using DebugTools.MccMcpWebPlayground.Infrastructure.OpenRouter; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; namespace DebugTools.MccMcpWebPlayground.Harness; public interface IMccAgentRunService { IAsyncEnumerable> StreamAsync(ChatStreamRequest request, HttpContext httpContext, CancellationToken cancellationToken); } public sealed class MccAgentRunService : IMccAgentRunService { private readonly MccMcpSessionFactory sessionFactory; private readonly MccGuidanceSource guidanceSource; private readonly MccPromptComposer promptComposer; private readonly MccContextCompressor contextCompressor; private readonly MccFinalizer finalizer; private readonly OpenRouterChatClient openRouterChatClient; private readonly MccWebHarnessOptions options; public MccAgentRunService( MccMcpSessionFactory sessionFactory, MccGuidanceSource guidanceSource, MccPromptComposer promptComposer, MccContextCompressor contextCompressor, MccFinalizer finalizer, OpenRouterChatClient openRouterChatClient, IOptions options) { this.sessionFactory = sessionFactory; this.guidanceSource = guidanceSource; this.promptComposer = promptComposer; this.contextCompressor = contextCompressor; this.finalizer = finalizer; this.openRouterChatClient = openRouterChatClient; this.options = options.Value; } public async IAsyncEnumerable> StreamAsync( ChatStreamRequest request, HttpContext httpContext, [EnumeratorCancellation] CancellationToken cancellationToken) { using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, httpContext.RequestAborted); CancellationToken linkedToken = linkedCts.Token; string runId = Guid.NewGuid().ToString("n"); long sequence = 0; string? model = options.ResolveModel(); if (string.IsNullOrWhiteSpace(model)) { yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("configuration_error", "OPENROUTER_MODEL or MccWebHarness:Model must be configured.")); yield break; } if (!options.HasApiKeyConfigured()) { yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("configuration_error", "OPENROUTER_API_KEY is not set.")); yield break; } List baseConversationMessages = NormalizeConversation(request.Messages); string userRequest = ExtractUserRequest(request.Messages); if (string.IsNullOrWhiteSpace(userRequest)) { yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("invalid_request", "No user message was provided.")); yield break; } await using McpClient client = await sessionFactory.CreateAsync(linkedToken); MccGuidanceBundle guidance = await guidanceSource.LoadAsync(client, linkedToken); MccRunState runState = new() { RunId = runId, UserRequest = userRequest, BaseConversationMessages = baseConversationMessages, ConfiguredModel = model, Guidance = guidance }; yield return CreateEvent(runId, ref sequence, "run_started", new MccRunStartedData(model, options.ResolveMcpEndpoint(), runState.StartedAtUtc)); yield return CreateEvent(runId, ref sequence, "guidance_loaded", new MccGuidanceLoadedData( guidance.SourceToolName, guidance.CanonicalPromptName, guidance.GuidanceVersion, guidance.CapabilityStatus)); IList tools = await client.ListToolsAsync(cancellationToken: linkedToken); MccToolCatalog catalog = MccToolPolicy.BuildCatalog(tools, options, finalizer.BuildSubmitToolSchema()); while (!linkedToken.IsCancellationRequested) { runState.TurnCount++; contextCompressor.CompactIfNeeded(runState); yield return CreateEvent(runId, ref sequence, "state_summary", BuildStateSummary(runState, options)); if (runState.IsSoftFinish(options, DateTimeOffset.UtcNow)) { yield return CreateEvent(runId, ref sequence, "budget", BuildBudgetData(runState)); } if (runState.IsHardStop(options, DateTimeOffset.UtcNow)) break; MccModelTurn? turn = null; Exception? providerException = null; try { turn = await openRouterChatClient.CreateTurnAsync( promptComposer.Compose(runState), catalog.ModelVisibleTools, options, linkedToken); } catch (Exception ex) { providerException = ex; } if (providerException is not null || turn is null) { yield return CreateEvent(runId, ref sequence, "error", new MccErrorData("provider_error", "OpenRouter request failed.", providerException?.Message)); yield return CreateEvent(runId, ref sequence, "final", finalizer.BuildHardStopResult(runState, options)); yield break; } runState.RoutedModel = turn.ModelId; runState.RoutedProvider = turn.RoutedProvider; if (turn.ToolCalls.Count == 0) { runState.DirectAnswerAttempts++; string content = string.IsNullOrWhiteSpace(turn.AssistantContent) ? "(empty assistant turn)" : turn.AssistantContent.Trim(); runState.ToolConversationMessages.Add(new Dictionary { ["role"] = "assistant", ["content"] = content }); if (runState.DirectAnswerAttempts >= 4) { yield return CreateEvent(runId, ref sequence, "error", new MccErrorData( "model_protocol_error", "The model kept returning plain assistant text instead of using tools or mcc_submit_final.", content)); yield return CreateEvent(runId, ref sequence, "final", finalizer.BuildHardStopResult(runState, options)); yield break; } runState.ToolConversationMessages.Add(new Dictionary { ["role"] = "user", ["content"] = "The previous plain assistant text was not accepted by this harness. On your next turn, you must either call the relevant MCC tools or call mcc_submit_final. Do not answer with plain assistant text again." }); continue; } Dictionary assistantMessage = new() { ["role"] = "assistant", ["content"] = turn.AssistantContent, ["tool_calls"] = turn.ToolCalls.Select(call => new Dictionary { ["id"] = call.CallId, ["type"] = "function", ["function"] = new Dictionary { ["name"] = call.Name, ["arguments"] = call.ArgumentsJson } }).ToArray() }; runState.ToolConversationMessages.Add(assistantMessage); foreach (MccModelToolCall toolCall in turn.ToolCalls) { MccToolProfile profile = MccToolPolicy.GetProfile(toolCall.Name); yield return CreateEvent(runId, ref sequence, "tool_called", new MccToolCalledData( toolCall.CallId, toolCall.Name, toolCall.ArgumentsJson, profile.Risk == MccToolRisk.EscapeHatch, profile.Risk == MccToolRisk.Sensitive)); if (toolCall.Name.Equals("mcc_submit_final", StringComparison.OrdinalIgnoreCase)) { MccFinalizationValidation validation = finalizer.Validate(runState, toolCall.ArgumentsJson); if (validation.Accepted) { yield return CreateEvent(runId, ref sequence, "final", validation.Payload!); yield break; } string localResultText = JsonSerializer.Serialize(new { success = false, errorCode = "invalid_final_submission", message = validation.ErrorText }); runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, localResultText)); yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData( toolCall.CallId, toolCall.Name, IsError: true, Success: false, ErrorCode: "invalid_final_submission", Summary: validation.ErrorText ?? "Invalid final submission.", RawText: localResultText, EvidenceId: string.Empty)); continue; } if (MccToolPolicy.RequiresExplicitUserIntent(toolCall.Name) && !MccToolPolicy.HasExplicitUserIntent(runState.UserRequest, toolCall.Name)) { string localResultText = JsonSerializer.Serialize(new { success = false, errorCode = "explicit_user_intent_required", message = $"Tool '{toolCall.Name}' requires explicit user intent." }); runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, localResultText)); yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData( toolCall.CallId, toolCall.Name, IsError: true, Success: false, ErrorCode: "explicit_user_intent_required", Summary: $"Tool '{toolCall.Name}' requires explicit user intent.", RawText: localResultText, EvidenceId: string.Empty)); continue; } if (!catalog.ToolsByName.TryGetValue(toolCall.Name, out MccToolCatalogEntry? entry)) { string unknownToolText = JsonSerializer.Serialize(new { success = false, errorCode = "unknown_tool", message = $"Unknown tool '{toolCall.Name}'." }); runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, unknownToolText)); yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData( toolCall.CallId, toolCall.Name, IsError: true, Success: false, ErrorCode: "unknown_tool", Summary: $"Unknown tool '{toolCall.Name}'.", RawText: unknownToolText, EvidenceId: string.Empty)); continue; } CallToolResult? result = null; Exception? toolException = null; try { Dictionary arguments = MccJsonArguments.Parse(toolCall.ArgumentsJson); result = await client.CallToolAsync(toolCall.Name, arguments, cancellationToken: linkedToken); } catch (Exception ex) { toolException = ex; } if (toolException is not null || result is null) { string failedText = JsonSerializer.Serialize(new { success = false, errorCode = "tool_call_failed", message = toolException?.Message }); runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, failedText)); yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData( toolCall.CallId, toolCall.Name, IsError: true, Success: false, ErrorCode: "tool_call_failed", Summary: toolException?.Message ?? "Tool call failed.", RawText: failedText, EvidenceId: string.Empty)); continue; } runState.ToolCallCount++; MccNormalizedToolResult normalized = MccMcpJson.Normalize(result); MccEvidenceRecord evidence = CreateEvidence(runState, toolCall.Name, normalized); runState.Evidence.Add(evidence); runState.ToolExecutions.Add(new MccToolExecutionRecord { CallId = toolCall.CallId, ToolName = toolCall.Name, ArgumentsJson = toolCall.ArgumentsJson, Evidence = evidence }); runState.ToolConversationMessages.Add(BuildToolMessage(toolCall.CallId, normalized.Text)); foreach (MccVerificationObligation obligation in CreateObligations(runState, evidence, toolCall.ArgumentsJson)) { runState.VerificationObligations.Add(obligation); yield return CreateEvent(runId, ref sequence, "verification_required", new MccVerificationEventData( obligation.Id, obligation.ToolName, obligation.Kind, obligation.Description)); if (obligation.Cleared) { yield return CreateEvent(runId, ref sequence, "verification_cleared", new MccVerificationEventData( obligation.Id, obligation.ToolName, obligation.Kind, obligation.Description)); } } foreach (MccVerificationObligation cleared in TryClearObligationsFromEvidence(runState, evidence)) { yield return CreateEvent(runId, ref sequence, "verification_cleared", new MccVerificationEventData( cleared.Id, cleared.ToolName, cleared.Kind, cleared.Description)); } yield return CreateEvent(runId, ref sequence, "tool_result", new MccToolResultData( toolCall.CallId, toolCall.Name, evidence.IsError, evidence.Success, evidence.ErrorCode, evidence.Summary, evidence.RawText, evidence.Id)); } } yield return CreateEvent(runId, ref sequence, "final", finalizer.BuildHardStopResult(runState, options)); } private static List NormalizeConversation(List? incoming) { List messages = []; if (incoming is null) return messages; foreach (ChatMessage message in incoming) { if (string.IsNullOrWhiteSpace(message.Role) || string.IsNullOrWhiteSpace(message.Content)) continue; string role = message.Role.Trim().ToLowerInvariant(); if (role is not ("user" or "assistant" or "system")) continue; messages.Add(new Dictionary { ["role"] = role, ["content"] = message.Content.Trim() }); } return messages; } private static string ExtractUserRequest(List? incoming) { return incoming? .LastOrDefault(message => string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(message.Content)) ?.Content ?.Trim() ?? string.Empty; } private static Dictionary BuildToolMessage(string callId, string content) { return new Dictionary { ["role"] = "tool", ["tool_call_id"] = callId, ["content"] = content }; } private static MccStateSummaryData BuildStateSummary(MccRunState runState, MccWebHarnessOptions options) { return new MccStateSummaryData( TurnCount: runState.TurnCount, ToolCallCount: runState.ToolCallCount, SoftFinish: runState.IsSoftFinish(options, DateTimeOffset.UtcNow), DirectAnswerAttempts: runState.DirectAnswerAttempts, OpenVerification: runState.OpenObligations .Select(obligation => new MccVerificationObligationView(obligation.Id, obligation.ToolName, obligation.Kind, obligation.Description)) .ToArray(), RecentEvidence: runState.Evidence .TakeLast(6) .Select(evidence => new MccEvidenceView(evidence.Id, evidence.ToolName, evidence.Summary, evidence.IsError)) .ToArray(), CompactionSummary: runState.CompactionSummary); } private MccBudgetData BuildBudgetData(MccRunState runState) { return new MccBudgetData( TurnCount: runState.TurnCount, MaxTurns: options.MaxTurns, ToolCallCount: runState.ToolCallCount, MaxToolCalls: options.MaxToolCalls, ElapsedSeconds: (DateTimeOffset.UtcNow - runState.StartedAtUtc).TotalSeconds, MaxWallClockSeconds: options.MaxWallClockSeconds); } private static MccEvidenceRecord CreateEvidence(MccRunState runState, string toolName, MccNormalizedToolResult result) { string summary = SummarizeEvidence(toolName, result); return new MccEvidenceRecord { Id = runState.NextEvidenceId(), ToolName = toolName, Summary = summary, RawText = result.Text, IsError = result.IsError, Success = result.Success, ErrorCode = result.ErrorCode, Root = result.Root, Data = result.Data }; } private static string SummarizeEvidence(string toolName, MccNormalizedToolResult result) { if (result.Data is JsonElement data) { if ((toolName.Equals("mcc_move_to", StringComparison.OrdinalIgnoreCase) || toolName.Equals("mcc_move_to_player", StringComparison.OrdinalIgnoreCase)) && TryReadBool(data, "arrived", out bool arrived)) { return arrived ? $"movement verified; arrived={arrived}" : $"movement not yet verified; arrived={arrived}"; } if (toolName.Equals("mcc_dig_block", StringComparison.OrdinalIgnoreCase)) { bool destroyed = TryReadBool(data, "destroyed", out bool destroyedValue) && destroyedValue; bool changed = TryReadBool(data, "changed", out bool changedValue) && changedValue; return $"dig result changed={changed} destroyed={destroyed}"; } if (toolName.Equals("mcc_items_pickup", StringComparison.OrdinalIgnoreCase)) { int successful = TryReadInt(data, "successfulPickups", out int successfulValue) ? successfulValue : 0; int collected = TryReadInt(data, "collectedCount", out int collectedValue) ? collectedValue : 0; return $"pickup result successfulPickups={successful} collectedCount={collected}"; } if (toolName.Equals("mcc_container_open_at", StringComparison.OrdinalIgnoreCase) && TryReadBool(data, "opened", out bool opened)) { return $"container open result opened={opened}"; } if (toolName is "mcc_container_deposit_item" or "mcc_container_withdraw_item" or "mcc_inventory_drop_item") { int moved = TryReadInt(data, "movedCount", out int movedValue) ? movedValue : TryReadInt(data, "droppedCount", out int droppedValue) ? droppedValue : 0; return $"{toolName} movedCount={moved}"; } } string prefix = result.IsError ? "error" : "ok"; return $"{prefix}: {Truncate(result.Text.Replace('\n', ' '), 180)}"; } private List CreateObligations(MccRunState runState, MccEvidenceRecord evidence, string argumentsJson) { List obligations = []; JsonElement metadata = ParseArgumentsToJson(argumentsJson); if (evidence.ToolName.Equals("mcc_move_to", StringComparison.OrdinalIgnoreCase)) { MccVerificationObligation obligation = new() { Id = runState.NextObligationId(), ToolName = evidence.ToolName, Kind = "movement", Description = "Verify final player location for the requested move target.", SourceEvidenceId = evidence.Id, Metadata = BuildMoveMetadata(evidence, metadata), Cleared = IsMovementVerified(evidence) }; obligations.Add(obligation); return obligations; } if (evidence.ToolName.Equals("mcc_move_to_player", StringComparison.OrdinalIgnoreCase)) { MccVerificationObligation obligation = new() { Id = runState.NextObligationId(), ToolName = evidence.ToolName, Kind = "movement", Description = "Verify final proximity to the requested player target.", SourceEvidenceId = evidence.Id, Metadata = BuildMoveToPlayerMetadata(evidence, metadata), Cleared = IsMovementVerified(evidence) }; obligations.Add(obligation); return obligations; } if (evidence.ToolName.Equals("mcc_container_open_at", StringComparison.OrdinalIgnoreCase)) { obligations.Add(new MccVerificationObligation { Id = runState.NextObligationId(), ToolName = evidence.ToolName, Kind = "container", Description = "Verify that the target container is open and active.", SourceEvidenceId = evidence.Id, Metadata = null, Cleared = IsContainerOpenVerified(evidence) }); return obligations; } if (evidence.ToolName is "mcc_container_deposit_item" or "mcc_container_withdraw_item" or "mcc_inventory_drop_item") { obligations.Add(new MccVerificationObligation { Id = runState.NextObligationId(), ToolName = evidence.ToolName, Kind = "inventory", Description = "Verify the requested inventory delta.", SourceEvidenceId = evidence.Id, Metadata = evidence.Data, Cleared = IsInventoryVerified(evidence) }); return obligations; } if (evidence.ToolName.Equals("mcc_items_pickup", StringComparison.OrdinalIgnoreCase)) { obligations.Add(new MccVerificationObligation { Id = runState.NextObligationId(), ToolName = evidence.ToolName, Kind = "pickup", Description = "Verify that the requested dropped items were picked up.", SourceEvidenceId = evidence.Id, Metadata = evidence.Data, Cleared = IsPickupVerified(evidence) }); return obligations; } if (evidence.ToolName.Equals("mcc_dig_block", StringComparison.OrdinalIgnoreCase)) { obligations.Add(new MccVerificationObligation { Id = runState.NextObligationId(), ToolName = evidence.ToolName, Kind = "block_change", Description = "Verify that the target block changed state after digging.", SourceEvidenceId = evidence.Id, Metadata = evidence.Data, Cleared = IsDigVerified(evidence) }); } return obligations; } private List TryClearObligationsFromEvidence(MccRunState runState, MccEvidenceRecord evidence) { List cleared = []; foreach (MccVerificationObligation obligation in runState.OpenObligations) { if (obligation.Cleared) continue; if (obligation.Kind == "movement" && TryClearMovementObligation(obligation, evidence)) { obligation.Cleared = true; obligation.ClearedByEvidenceId = evidence.Id; cleared.Add(obligation); continue; } if (obligation.Kind == "block_change" && TryClearDigObligation(obligation, evidence)) { obligation.Cleared = true; obligation.ClearedByEvidenceId = evidence.Id; cleared.Add(obligation); } } return cleared; } private static bool TryClearMovementObligation(MccVerificationObligation obligation, MccEvidenceRecord evidence) { if (evidence.ToolName.Equals("mcc_player_state", StringComparison.OrdinalIgnoreCase) && evidence.Data is JsonElement data && data.TryGetProperty("location", out JsonElement location) && obligation.Metadata is JsonElement metadata) { if (obligation.ToolName.Equals("mcc_move_to", StringComparison.OrdinalIgnoreCase) && metadata.TryGetProperty("x", out JsonElement targetX) && metadata.TryGetProperty("y", out JsonElement targetY) && metadata.TryGetProperty("z", out JsonElement targetZ)) { double tolerance = metadata.TryGetProperty("tolerance", out JsonElement toleranceElement) && toleranceElement.TryGetDouble(out double tol) ? tol : 1.5; return TryReadDouble(location, "x", out double x) && TryReadDouble(location, "y", out double y) && TryReadDouble(location, "z", out double z) && Distance(x, y, z, targetX.GetDouble(), targetY.GetDouble(), targetZ.GetDouble()) <= tolerance; } } if (evidence.ToolName.Equals("mcc_player_locate", StringComparison.OrdinalIgnoreCase) && obligation.ToolName.Equals("mcc_move_to_player", StringComparison.OrdinalIgnoreCase) && evidence.Data is JsonElement playerData && obligation.Metadata is JsonElement playerMetadata) { string? expectedName = playerMetadata.TryGetProperty("playerName", out JsonElement nameElement) ? nameElement.GetString() : null; string? matchedName = playerData.TryGetProperty("matchedName", out JsonElement matchedNameElement) ? matchedNameElement.GetString() : null; if (!string.IsNullOrWhiteSpace(expectedName) && !string.Equals(expectedName, matchedName, StringComparison.OrdinalIgnoreCase)) return false; if (TryReadDouble(playerData, "distance", out double distance)) { double tolerance = playerMetadata.TryGetProperty("tolerance", out JsonElement toleranceElement) && toleranceElement.TryGetDouble(out double tol) ? tol : 2.0; return distance <= tolerance; } } return false; } private static bool TryClearDigObligation(MccVerificationObligation obligation, MccEvidenceRecord evidence) { if (!evidence.ToolName.Equals("mcc_world_block_at", StringComparison.OrdinalIgnoreCase) || evidence.Data is not JsonElement data || obligation.Metadata is not JsonElement metadata) { return false; } if (!metadata.TryGetProperty("target", out JsonElement target) || !TryReadDouble(target, "x", out double x) || !TryReadDouble(target, "y", out double y) || !TryReadDouble(target, "z", out double z)) { return false; } return TryReadInt(data, "x", out int blockX) && TryReadInt(data, "y", out int blockY) && TryReadInt(data, "z", out int blockZ) && Math.Abs(blockX - x) < 0.5 && Math.Abs(blockY - y) < 0.5 && Math.Abs(blockZ - z) < 0.5 && data.TryGetProperty("block", out JsonElement block) && block.TryGetProperty("material", out JsonElement material) && !string.Equals(material.GetString(), "Air", StringComparison.OrdinalIgnoreCase); } private static bool IsMovementVerified(MccEvidenceRecord evidence) { if (evidence.Data is not JsonElement data) return false; if (TryReadBool(data, "arrived", out bool arrived) && arrived) return true; if (TryReadDouble(data, "finalDistance", out double finalDistance)) { double tolerance = TryReadDouble(data, "tolerance", out double tol) ? tol : 1.5; return finalDistance <= tolerance; } return false; } private static bool IsContainerOpenVerified(MccEvidenceRecord evidence) { return evidence.Data is JsonElement data && TryReadBool(data, "opened", out bool opened) && opened; } private static bool IsInventoryVerified(MccEvidenceRecord evidence) { if (evidence.Data is not JsonElement data) return false; if (TryReadInt(data, "requestedCount", out int requestedCount) && TryReadInt(data, "movedCount", out int movedCount)) { return movedCount == requestedCount; } if (TryReadInt(data, "requestedCount", out requestedCount) && TryReadInt(data, "droppedCount", out int droppedCount)) { return droppedCount == requestedCount; } return evidence.Success; } private static bool IsPickupVerified(MccEvidenceRecord evidence) { if (evidence.Data is not JsonElement data) return false; return (TryReadInt(data, "successfulPickups", out int successfulPickups) && successfulPickups > 0) || (TryReadInt(data, "collectedCount", out int collectedCount) && collectedCount > 0); } private static bool IsDigVerified(MccEvidenceRecord evidence) { if (evidence.Data is not JsonElement data) return false; return (TryReadBool(data, "destroyed", out bool destroyed) && destroyed) || (TryReadBool(data, "changed", out bool changed) && changed); } private static JsonElement? BuildMoveMetadata(MccEvidenceRecord evidence, JsonElement arguments) { if (evidence.Data is not JsonElement data) return null; double x = TryReadDoubleFromArguments(arguments, "x", out double targetX) ? targetX : data.TryGetProperty("target", out JsonElement target) && TryReadDouble(target, "x", out double fromDataX) ? fromDataX : 0; double y = TryReadDoubleFromArguments(arguments, "y", out double targetY) ? targetY : data.TryGetProperty("target", out target) && TryReadDouble(target, "y", out double fromDataY) ? fromDataY : 0; double z = TryReadDoubleFromArguments(arguments, "z", out double targetZ) ? targetZ : data.TryGetProperty("target", out target) && TryReadDouble(target, "z", out double fromDataZ) ? fromDataZ : 0; double tolerance = TryReadDouble(data, "tolerance", out double tol) ? tol : 1.5; return JsonSerializer.SerializeToElement(new { x, y, z, tolerance }); } private static JsonElement? BuildMoveToPlayerMetadata(MccEvidenceRecord evidence, JsonElement arguments) { string? playerName = arguments.TryGetProperty("playerName", out JsonElement property) ? property.GetString() : null; double tolerance = evidence.Data is JsonElement data && TryReadDouble(data, "tolerance", out double tol) ? tol : 2.0; return JsonSerializer.SerializeToElement(new { playerName, tolerance }); } private static JsonElement ParseArgumentsToJson(string argumentsJson) { try { using JsonDocument document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson); return document.RootElement.Clone(); } catch { using JsonDocument document = JsonDocument.Parse("{}"); return document.RootElement.Clone(); } } private static bool TryReadBool(JsonElement element, string propertyName, out bool value) { value = false; return element.TryGetProperty(propertyName, out JsonElement property) && property.ValueKind is JsonValueKind.True or JsonValueKind.False && ((value = property.GetBoolean()) || !value || true); } private static bool TryReadInt(JsonElement element, string propertyName, out int value) { value = 0; return element.TryGetProperty(propertyName, out JsonElement property) && property.TryGetInt32(out value); } private static bool TryReadDouble(JsonElement element, string propertyName, out double value) { value = 0; return element.TryGetProperty(propertyName, out JsonElement property) && property.TryGetDouble(out value); } private static bool TryReadDoubleFromArguments(JsonElement element, string propertyName, out double value) { value = 0; if (!element.TryGetProperty(propertyName, out JsonElement property)) return false; return property.ValueKind == JsonValueKind.Number ? property.TryGetDouble(out value) : property.ValueKind == JsonValueKind.String && double.TryParse(property.GetString(), out value); } private static double Distance(double x1, double y1, double z1, double x2, double y2, double z2) { double dx = x1 - x2; double dy = y1 - y2; double dz = z1 - z2; return Math.Sqrt(dx * dx + dy * dy + dz * dz); } private static string Truncate(string text, int maxLength) { return string.IsNullOrEmpty(text) || text.Length <= maxLength ? text : text[..maxLength] + "..."; } private static SseItem CreateEvent(string runId, ref long sequence, string kind, T data) { sequence++; return new SseItem( new MccStreamEnvelope(runId, sequence, kind, data!), kind) { EventId = sequence.ToString(CultureInfo.InvariantCulture) }; } }