using AxCopilot.Models; using AxCopilot.Services; namespace AxCopilot.Services.Agent; /// /// AX Agent execution-prep engine. /// Inspired by the `claw-code` split between input preparation and session execution, /// so the UI layer stops owning message assembly and final assistant commit logic. /// public sealed class AxAgentExecutionEngine { public sealed record PreparedTurn(List Messages); public sealed record ExecutionMode(bool UseAgentLoop, bool UseStreamingTransport, string? TaskSystemPrompt); public IReadOnlyList BuildPromptStack( string? conversationSystem, string? slashSystem, string? taskSystem = null) { var prompts = new List(); if (!string.IsNullOrWhiteSpace(conversationSystem)) prompts.Add(conversationSystem.Trim()); if (!string.IsNullOrWhiteSpace(slashSystem)) prompts.Add(slashSystem.Trim()); if (!string.IsNullOrWhiteSpace(taskSystem)) prompts.Add(taskSystem.Trim()); return prompts; } public ExecutionMode ResolveExecutionMode( string runTab, bool streamingEnabled, string resolvedService, string? coworkSystemPrompt, string? codeSystemPrompt) { if (string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)) return new ExecutionMode(true, false, coworkSystemPrompt); if (string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase)) return new ExecutionMode(true, false, codeSystemPrompt); return new ExecutionMode(false, false, null); } public PreparedTurn PrepareTurn( ChatConversation conversation, IEnumerable systemPrompts, string? fileContext = null, IReadOnlyList? images = null) { var outbound = conversation.Messages .Select(CloneMessage) .ToList(); if (!string.IsNullOrWhiteSpace(fileContext)) { var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase)); if (lastUserIndex >= 0) outbound[lastUserIndex].Content = (outbound[lastUserIndex].Content ?? string.Empty) + fileContext; } if (images is { Count: > 0 }) { var lastUserIndex = outbound.FindLastIndex(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase)); if (lastUserIndex >= 0) outbound[lastUserIndex].Images = images.Select(CloneImage).ToList(); } var promptList = systemPrompts .Where(prompt => !string.IsNullOrWhiteSpace(prompt)) .Select(prompt => prompt!.Trim()) .ToList(); for (var i = promptList.Count - 1; i >= 0; i--) outbound.Insert(0, new ChatMessage { Role = "system", Content = promptList[i] }); return new PreparedTurn(outbound); } public ChatMessage CommitAssistantMessage( ChatSessionStateService? session, ChatConversation conversation, string tab, string content, ChatStorageService? storage = null) { var assistant = new ChatMessage { Role = "assistant", Content = content, }; if (session != null) { session.AppendMessage(tab, assistant, storage); return assistant; } conversation.Messages.Add(assistant); conversation.UpdatedAt = DateTime.Now; return assistant; } private static ChatMessage CloneMessage(ChatMessage source) { return new ChatMessage { Role = source.Role, Content = source.Content, Timestamp = source.Timestamp, MetaRunId = source.MetaRunId, AttachedFiles = source.AttachedFiles?.ToList(), Images = source.Images?.Select(CloneImage).ToList(), }; } private static ImageAttachment CloneImage(ImageAttachment source) { return new ImageAttachment { Base64 = source.Base64, MimeType = source.MimeType, FileName = source.FileName, }; } }