using System; using System.Threading; using System.Threading.Tasks; using Codex.Dal; using Codex.Dal.Enums; using FluentValidation; using Microsoft.EntityFrameworkCore; using OpenHarbor.CQRS.Abstractions; namespace Codex.CQRS.Commands; /// /// Completes an agent execution with results and metrics /// public record CompleteAgentExecutionCommand { /// Execution ID to complete public Guid ExecutionId { get; init; } /// Agent's output/response public string Output { get; init; } = string.Empty; /// Execution status (Completed, Failed, Cancelled) public ExecutionStatus Status { get; init; } /// Input tokens consumed public int? InputTokens { get; init; } /// Output tokens generated public int? OutputTokens { get; init; } /// Estimated cost in USD public decimal? EstimatedCost { get; init; } /// Tool calls made (JSON array) public string? ToolCalls { get; init; } /// Tool call results (JSON array) public string? ToolCallResults { get; init; } /// Error message if failed public string? ErrorMessage { get; init; } } public class CompleteAgentExecutionCommandHandler : ICommandHandler { private readonly CodexDbContext _dbContext; public CompleteAgentExecutionCommandHandler(CodexDbContext dbContext) { _dbContext = dbContext; } public async Task HandleAsync(CompleteAgentExecutionCommand command, CancellationToken cancellationToken = default) { var execution = await _dbContext.AgentExecutions .FirstOrDefaultAsync(e => e.Id == command.ExecutionId, cancellationToken); if (execution == null) { throw new InvalidOperationException($"Execution with ID {command.ExecutionId} not found"); } if (execution.Status != ExecutionStatus.Running) { throw new InvalidOperationException($"Execution {command.ExecutionId} is not in Running state (current: {execution.Status})"); } var completedAt = DateTime.UtcNow; var executionTimeMs = (long)(completedAt - execution.StartedAt).TotalMilliseconds; execution.Output = command.Output; execution.Status = command.Status; execution.CompletedAt = completedAt; execution.ExecutionTimeMs = executionTimeMs; execution.InputTokens = command.InputTokens; execution.OutputTokens = command.OutputTokens; execution.TotalTokens = (command.InputTokens ?? 0) + (command.OutputTokens ?? 0); execution.EstimatedCost = command.EstimatedCost; execution.ToolCalls = command.ToolCalls; execution.ToolCallResults = command.ToolCallResults; execution.ErrorMessage = command.ErrorMessage; await _dbContext.SaveChangesAsync(cancellationToken); } } public class CompleteAgentExecutionCommandValidator : AbstractValidator { public CompleteAgentExecutionCommandValidator() { RuleFor(x => x.ExecutionId) .NotEmpty().WithMessage("Execution ID is required"); RuleFor(x => x.Status) .Must(s => s == ExecutionStatus.Completed || s == ExecutionStatus.Failed || s == ExecutionStatus.Cancelled) .WithMessage("Status must be Completed, Failed, or Cancelled"); RuleFor(x => x.ErrorMessage) .NotEmpty() .When(x => x.Status == ExecutionStatus.Failed) .WithMessage("Error message is required when status is Failed"); RuleFor(x => x.InputTokens) .GreaterThanOrEqualTo(0) .When(x => x.InputTokens.HasValue) .WithMessage("Input tokens must be >= 0"); RuleFor(x => x.OutputTokens) .GreaterThanOrEqualTo(0) .When(x => x.OutputTokens.HasValue) .WithMessage("Output tokens must be >= 0"); RuleFor(x => x.EstimatedCost) .GreaterThanOrEqualTo(0) .When(x => x.EstimatedCost.HasValue) .WithMessage("Estimated cost must be >= 0"); } }