using System.Text.Json;
using Agent.Core.Ollama;
using Agent.Core.Localization;
using Agent.Core.Prompts;
using Agent.Core.Sessions;
using Agent.Core.Skills;
using Agent.Core.Tasks;
using Agent.Core.Tools;
using Agent.Planner;
namespace Agent.Core.Runtime;
public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, AgentRuntimeSettings settings, InternalToolDispatcher? toolDispatcher = null, IAgentRuntimeObserver? observer = null, PlannerService? plannerService = null, LocaleCatalog? locale = null)
{
private readonly InternalToolDispatcher _tools = toolDispatcher ?? new(
new DenyByDefaultPolicy(InternalToolCatalog.All.Select(static item => item.Name).ToHashSet(StringComparer.Ordinal)),
new FakeInternalToolBackend());
private readonly IAgentRuntimeObserver _observer = observer ?? NullAgentRuntimeObserver.Instance;
private readonly PlannerService _planner = plannerService ?? new(ollama, settings);
///
/// Plan → Run lifecycle for a single MCP Task.
/// 1. Planning: generate Initial TaskGraph via structured output.
/// 2. Running: execute ready Tasks (dependencies satisfied) in critical-path / creation order.
/// Sandbox Task Resource is applied at RunAsync time, not PlanAsync time.
///
public async Task ExecuteAsync(Guid taskId, CancellationToken cancellationToken)
{
if (!registry.TryGet(taskId, null, out TaskSnapshot? task) || task is null) return;
if (!registry.Sessions.TryGet(task.SessionId, out AgentSession? session) || session is null)
{
registry.Transition(taskId, AgentTaskState.Failed, result: Message("runtime.session_not_found", "Session not found."));
return;
}
try
{
var counters = new ExecutionCounters();
// Vision capability check before any Ollama call.
if (task.Images.Count > 0 && await ollama.SupportsVisionAsync(cancellationToken).ConfigureAwait(false) is false)
throw new InvalidOperationException(settings.VisionUnsupportedMessage);
// Phase 1: Planning — generate TaskGraph via structured output.
TaskGraph graph;
try
{
graph = await _planner.PlanAsync(taskId, task.SessionId, session, task.Request, task.Temperature, task.Images,
(phase, messages, result, error) => RecordLlmAttempt(task, counters, phase, messages, result, error), cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch (Exception ex)
{
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, "planning_failed", ex.Message);
registry.Transition(taskId, AgentTaskState.Failed, result: Message("runtime.planning_failed", $"Planning failed: {ex.Message}", ex.Message));
return;
}
// Phase 2: Running — execute ready Tasks sequentially.
registry.Transition(taskId, AgentTaskState.Running, "running");
await RunAsync(taskId, task, session, graph, counters, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
string? cleanupFailure = await EndTaskSafelyAsync(taskId, false, CancellationToken.None).ConfigureAwait(false);
if (cleanupFailure is null)
{
registry.Transition(taskId, AgentTaskState.Cancelled, result: Message("runtime.cancelled", "Cancelled."));
}
else
{
registry.UpdateDiagnostics(taskId, null, null, "cleanup_failed", cleanupFailure);
registry.Transition(taskId, AgentTaskState.Failed, result: cleanupFailure);
}
}
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
string? cleanupFailure = await EndTaskSafelyAsync(taskId, false, CancellationToken.None).ConfigureAwait(false);
registry.TryGet(taskId, null, out TaskSnapshot? failedSnapshot);
string baseResult = failedSnapshot?.LastErrorCode is not null and not "context_summary_failed"
? failedSnapshot.LastError ?? ex.Message
: ex.Message;
string result = cleanupFailure is null ? baseResult : $"{baseResult} {cleanupFailure}";
string errorCode = cleanupFailure is not null ? "cleanup_failed" : failedSnapshot?.LastErrorCode ?? "execution_failed";
string errorMessage = cleanupFailure is not null ? result : failedSnapshot?.LastError ?? result;
registry.UpdateDiagnostics(taskId, null, null, errorCode, errorMessage);
registry.Transition(taskId, AgentTaskState.Failed, result: result);
}
}
///
/// Executes ready Tasks (dependencies satisfied) in critical-path then creation order.
/// Each PlannerTask is executed as a single LLM chat-with-tools iteration loop.
/// Sandbox Task Resource (BeginTask/EndTask) wraps the entire RunAsync phase.
///
private async Task RunAsync(Guid taskId, TaskSnapshot task, AgentSession session, TaskGraph graph, ExecutionCounters counters, CancellationToken cancellationToken)
{
// Apply Sandbox Task Resource at RunAsync time.
await _tools.BeginTaskAsync(taskId, task.SessionId, cancellationToken).ConfigureAwait(false);
var finalContents = new List();
const int maxReplans = 3;
var denyFingerprints = new HashSet(StringComparer.Ordinal);
try
{
UpdateTaskProgress(taskId, graph, null, counters);
// Execute tasks in ready order: dependencies satisfied → critical path → creation order.
while (true)
{
PlannerTask? next = SelectNextReadyTask(graph);
if (next is null)
{
if (graph.Tasks.All(static item => item.State == PlannerTaskState.Completed)) break;
throw new InvalidOperationException("TaskGraph has incomplete Tasks but no executable Task.");
}
graph.SetState(next.Id, PlannerTaskState.Running);
UpdateTaskProgress(taskId, graph, next.Description, counters);
try
{
string content = await ExecutePlannerTaskAsync(taskId, task, session, graph, next, counters, cancellationToken).ConfigureAwait(false);
graph.SetState(next.Id, PlannerTaskState.Completed);
finalContents.Add(content);
UpdateTaskProgress(taskId, graph, null, counters);
}
catch (PolicyDeniedException ex)
{
graph.SetState(next.Id, PlannerTaskState.Failed);
string localizedDenial = LocalizePolicyDenial(ex);
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, ex.ErrorCode, localizedDenial);
if (!denyFingerprints.Add(ex.Fingerprint))
{
string repeated = Message("runtime.policy_repeated", $"The same sandbox policy denial occurred again: {ex.Message}");
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, "policy.repeated", repeated);
throw new InvalidOperationException(repeated);
}
if (counters.Replans >= maxReplans)
{
string exhausted = Message("runtime.policy_replan_exhausted", $"Sandbox policy denial remained unresolved after {maxReplans} replans: {ex.Message}", maxReplans.ToString(System.Globalization.CultureInfo.InvariantCulture));
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, "policy.replan_exhausted", exhausted);
throw new InvalidOperationException(exhausted);
}
string previousSignature = GraphSignature(graph);
counters.Replans++;
registry.Transition(taskId, AgentTaskState.Planning, "replan");
graph = await _planner.ReplanAsync(taskId, task.SessionId, session, task.Request, task.Temperature, ex.Message,
(phase, messages, result, error) => RecordLlmAttempt(task, counters, phase, messages, result, error), cancellationToken).ConfigureAwait(false);
if (string.Equals(previousSignature, GraphSignature(graph), StringComparison.Ordinal))
{
string unchanged = Message("runtime.replan_unchanged", "Replan did not change the incomplete TaskGraph.");
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, "planning.replan_unchanged", unchanged);
throw new InvalidOperationException(unchanged);
}
if (!registry.Transition(taskId, AgentTaskState.Running, "replan"))
throw new InvalidOperationException("Task state could not return to running after replan.");
UpdateTaskProgress(taskId, graph, null, counters);
continue;
}
}
if (finalContents.Count == 0) throw new InvalidOperationException("TaskGraph completed without producing a result.");
string finalContent = string.Join("\n\n", finalContents);
string? cleanupFailure = await EndTaskSafelyAsync(taskId, true, CancellationToken.None).ConfigureAwait(false);
if (cleanupFailure is not null) throw new InvalidOperationException(cleanupFailure);
session.Append(new ConversationMessage("user", task.Request));
session.Append(new ConversationMessage("assistant", finalContent));
registry.Transition(taskId, AgentTaskState.Completed, result: finalContent);
}
catch
{
// Re-throw without double-cleanup; ExecuteAsync's catch handles EndTaskSafelyAsync.
throw;
}
}
///
/// Selects the next ready Task: dependencies satisfied, not yet completed/failed.
/// Priority: critical path first, then creation order.
///
private static PlannerTask? SelectNextReadyTask(TaskGraph graph)
{
var completedIds = graph.Tasks
.Where(static t => t.State == PlannerTaskState.Completed)
.Select(static t => t.Id)
.ToHashSet();
// Critical path task IDs in order.
var criticalPath = new HashSet(graph.CriticalPath.TaskIds);
// Ready = not completed/failed/running and all dependencies completed.
var ready = graph.Tasks
.Where(t => t.State is not (PlannerTaskState.Completed or PlannerTaskState.Failed or PlannerTaskState.Running))
.Where(t => t.Dependencies.All(depId => completedIds.Contains(depId)))
.ToList();
if (ready.Count == 0) return null;
// Prefer critical path tasks, then creation order (Tasks list is in insertion order).
return ready.FirstOrDefault(t => criticalPath.Contains(t.Id)) ?? ready[0];
}
///
/// Executes a single PlannerTask: LLM chat-with-tools iteration loop.
/// Uses PlannerTask.RequiresExternalEffect (not keyword heuristic) for ToolUseGuard.
///
private async Task ExecutePlannerTaskAsync(Guid taskId, TaskSnapshot task, AgentSession session, TaskGraph graph, PlannerTask plannerTask,
ExecutionCounters counters, CancellationToken cancellationToken)
{
// Progressive Disclosure Stage 2: LLM Skill Selection.
IReadOnlyList selectedSkills = await SelectSkillsAsync(task, counters, cancellationToken).ConfigureAwait(false);
var messages = PromptComposer.Compose(new PromptContext(
FixedPrompt.Load(),
settings.UserSystemPrompt,
settings.Locale,
settings.Skills,
selectedSkills,
settings.SandboxCapabilitySummary,
BuildTaskGraphSummary(graph, plannerTask),
session.Summary ?? string.Empty,
session.ExecutionResults,
session.Conversation,
task.Request,
plannerTask.Description,
plannerTask.RequiresExternalEffect,
task.Images)).ToList();
var guard = new ToolUseGuard(settings.SameFailureLimit);
bool requiresExternalEffect = plannerTask.RequiresExternalEffect;
bool externalEffectEvidence = false;
var unresolvedToolFailures = new Dictionary(StringComparer.Ordinal);
string? finalContent = null;
while (counters.Iterations < settings.MaxIterations)
{
int iteration = counters.Iterations++;
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans);
var callContext = new AgentLlmCallContext(Guid.NewGuid(), task.SessionId, taskId, plannerTask.Id,
"running", iteration, counters.Replans, settings.Model);
_observer.LlmRequest(callContext, messages);
OllamaResult result;
try
{
result = await ollama.ChatWithToolsAsync(messages, task.Temperature, InternalToolCatalog.All, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
_observer.LlmFailure(callContext, "ollama_call_failed", ex.Message);
throw;
}
_observer.LlmResponse(callContext, result);
registry.AddUsage(taskId, result.InputTokens, result.OutputTokens);
// Record usage in TaskGraph for CPM tracking.
graph.AddUsage(plannerTask.Id, result.InputTokens, result.OutputTokens);
// Context Summary: compact old conversation/results when context utilization exceeds threshold.
// Usage = latest prompt_eval_count / num_ctx (NOT cumulative InputTokens).
await TryContextSummaryAsync(taskId, task, session, result.PromptEvalCount, counters, cancellationToken).ConfigureAwait(false);
IReadOnlyList calls = result.ToolCalls ?? [];
if (calls.Count > 0)
{
messages.Add(new OllamaMessage("assistant", result.Content, ToolCalls: calls));
string? failureFingerprint = null;
foreach (OllamaToolCall call in calls)
{
ToolExecutionResult toolResult;
bool outOfScopeEffect = IsOutOfScopeEffectCall(call, plannerTask.RequiredEffectTools);
if (outOfScopeEffect)
{
toolResult = new(false,
$"The '{call.Function.Name}' external effect belongs to another Planner Task. Perform only the current Task; required effect Tool(s): {string.Join(", ", plannerTask.RequiredEffectTools.Order(StringComparer.Ordinal))}.",
false, $"task_scope:{plannerTask.Id}:{call.Function.Name}");
}
else try
{
toolResult = await _tools.DispatchAsync(taskId, call.Function.Name, call.Function.Arguments, cancellationToken).ConfigureAwait(false);
}
catch (UnauthorizedAccessException ex)
{
toolResult = new(false, ex.Message, false, $"{call.Function.Name}:{ex.GetType().Name}:{ex.Message}", RetryAllowed: false);
}
catch (Exception ex) when (ex is ToolValidationException or ArgumentException or PlannerValidationException or FileNotFoundException)
{
toolResult = new(false, ex.Message, false, $"{call.Function.Name}:{ex.GetType().Name}:{ex.Message}");
}
externalEffectEvidence |= toolResult.Success && toolResult.ExternalEffect && plannerTask.RequiredEffectTools.Contains(call.Function.Name);
// task_update is a metadata tool for graph bookkeeping; its failures should not block completion.
bool isMetadataTool = call.Function.Name == "task_update";
if (toolResult.Success) unresolvedToolFailures.Remove(call.Function.Name);
else if (!isMetadataTool && !outOfScopeEffect) unresolvedToolFailures[call.Function.Name] = toolResult.FailureFingerprint ?? toolResult.Output;
// Record ExecutionResult in session for context summary.
session.AppendExecutionResult(new ExecutionResult(taskId, call.Function.Name, toolResult.Success, toolResult.Output));
_observer.ToolResult(callContext, call.Function.Name, toolResult);
if (!toolResult.Success && !toolResult.RetryAllowed && !isMetadataTool)
throw new PolicyDeniedException(toolResult.FailureFingerprint ?? $"policy:{call.Function.Name}:{toolResult.Output}", toolResult.Output,
toolResult.ErrorCode ?? "policy_denied", toolResult.ErrorCategory, toolResult.TargetPath,
toolResult.RequiredPermission, toolResult.Resolution);
failureFingerprint = toolResult.FailureFingerprint ?? failureFingerprint;
messages.Add(new OllamaMessage("tool", toolResult.Output, ToolName: call.Function.Name));
}
ToolUseGuardResult callGuard = guard.Evaluate(requiresExternalEffect, true, externalEffectEvidence, failureFingerprint);
if (callGuard.Decision == ToolUseGuardDecision.Fail) throw new InvalidOperationException(callGuard.Observation);
if (callGuard.Decision == ToolUseGuardDecision.Retry)
messages.Add(new OllamaMessage("system", callGuard.Observation + " Required effect Tool(s) for the current Task: " +
string.Join(", ", plannerTask.RequiredEffectTools.Order(StringComparer.Ordinal)) + "."));
continue;
}
if (unresolvedToolFailures.Count > 0)
{
string unresolvedFingerprint = string.Join(";", unresolvedToolFailures.OrderBy(static item => item.Key, StringComparer.Ordinal)
.Select(static item => $"{item.Key}={item.Value}"));
ToolUseGuardResult unresolvedGuard = guard.Evaluate(true, false, false, unresolvedFingerprint);
if (unresolvedGuard.Decision == ToolUseGuardDecision.Fail) throw new InvalidOperationException(unresolvedGuard.Observation);
messages.Add(new OllamaMessage("system", "Completion is not allowed while tool failures remain unresolved. Retry or correct: " +
string.Join("; ", unresolvedToolFailures.Select(static item => $"{item.Key}={item.Value}"))));
continue;
}
ToolUseGuardResult completion = guard.Evaluate(requiresExternalEffect, false, externalEffectEvidence);
if (completion.Decision == ToolUseGuardDecision.Retry)
{
messages.Add(new OllamaMessage("system", completion.Observation));
continue;
}
if (completion.Decision == ToolUseGuardDecision.Fail) throw new InvalidOperationException(completion.Observation);
finalContent = result.Content;
break;
}
if (finalContent is null) throw new InvalidOperationException($"Agent exceeded the Task iteration limit of {settings.MaxIterations}.");
return finalContent;
}
private static bool IsOutOfScopeEffectCall(OllamaToolCall call, IReadOnlySet requiredEffectTools)
{
bool createsExternalEffect = call.Function.Name is "file_edit" or "process_run" ||
call.Function.Name == "dir" && call.Function.Arguments.TryGetProperty("operation", out JsonElement operation) &&
operation.GetString() is "mkdir" or "rmdir";
return createsExternalEffect && !requiredEffectTools.Contains(call.Function.Name);
}
///
/// Builds a TaskGraph summary for the prompt context (Design §9.2 item 6).
/// Shows the current task position within the overall graph.
///
private static string BuildTaskGraphSummary(TaskGraph graph, PlannerTask currentTask)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("Current TaskGraph:");
for (int i = 0; i < graph.Tasks.Count; i++)
{
PlannerTask t = graph.Tasks[i];
string marker = t.Id == currentTask.Id ? "→ " : " ";
sb.AppendLine($"{marker}[{t.State}] {t.Description}");
}
sb.AppendLine($"Critical path tokens: {graph.CriticalPath.CumulativeTokenCost:F0}");
return sb.ToString();
}
///
/// Context Summary: compacts old Conversation/ExecutionResults when context utilization exceeds threshold.
/// Usage = latest prompt_eval_count / num_ctx (Design §9.3). NOT cumulative InputTokens.
/// Preserves: user constraints, incomplete tasks, completed task results, failure causes, security denials.
///
private async Task TryContextSummaryAsync(Guid taskId, TaskSnapshot task, AgentSession session, long promptEvalCount,
ExecutionCounters counters, CancellationToken cancellationToken)
{
if (settings.NumCtx <= 0) return;
double utilization = (double)promptEvalCount / settings.NumCtx;
if (utilization < settings.ContextSummaryThreshold) return;
string source = JsonSerializer.Serialize(new
{
previous_summary = session.Summary,
conversation = session.Conversation,
execution_results = session.ExecutionResults
});
var messages = new List
{
new("system", "Summarize context without chain-of-thought. Preserve user constraints, incomplete Tasks, important completed results, changed files, verification results, failure reasons, retry counts, and security denials. Return JSON with a non-empty 'summary' string."),
new("user", source)
};
using JsonDocument schema = JsonDocument.Parse("""{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}""");
AgentLlmCallContext? callContext = null;
try
{
if (counters.Iterations >= settings.MaxIterations) return;
int iteration = counters.Iterations++;
callContext = new AgentLlmCallContext(Guid.NewGuid(), task.SessionId, taskId, null,
"context_summary", iteration, counters.Replans, settings.Model);
_observer.LlmRequest(callContext, messages);
OllamaResult result = await ollama.ChatStructuredAsync(messages, task.Temperature, schema.RootElement.Clone(), false, cancellationToken).ConfigureAwait(false);
_observer.LlmResponse(callContext, result);
registry.AddUsage(taskId, result.InputTokens, result.OutputTokens);
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans);
using JsonDocument response = JsonDocument.Parse(result.Content);
string? summary = response.RootElement.GetProperty("summary").GetString();
if (string.IsNullOrWhiteSpace(summary)) return;
session.SetSummary(summary);
session.CompactConversation(settings.ConversationKeepLastN);
session.CompactExecutionResults(settings.ExecutionResultsKeepLastN);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
if (callContext is not null) _observer.LlmFailure(callContext, "context_summary_failed", ex.Message);
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, "context_summary_failed", ex.Message);
}
}
private async Task> SelectSkillsAsync(TaskSnapshot task, ExecutionCounters counters, CancellationToken cancellationToken)
{
if (settings.Skills.Count == 0) return [];
string catalog = string.Join("\n", settings.Skills.Select(static s => $"- {s.Name}: {s.Description}"));
var selectionMessages = new List
{
new("system", $"Select skills relevant to the user request. Available skills:\n{catalog}\n\nReturn a JSON object with a 'selected_skills' array containing skill names. Select only relevant skills. If none are relevant, return an empty array."),
new("user", task.Request)
};
string schema = """{"type":"object","properties":{"selected_skills":{"type":"array","items":{"type":"string"}}},"required":["selected_skills"]}""";
using JsonDocument formatDoc = JsonDocument.Parse(schema);
OllamaResult result;
AgentLlmCallContext? callContext = null;
try
{
if (counters.Iterations >= settings.MaxIterations) return [];
int iteration = counters.Iterations++;
callContext = new AgentLlmCallContext(Guid.NewGuid(), task.SessionId, task.TaskId, null,
"skill_selection", iteration, counters.Replans, settings.Model);
_observer.LlmRequest(callContext, selectionMessages);
result = await ollama.ChatStructuredAsync(selectionMessages, task.Temperature, formatDoc.RootElement.Clone(), cancellationToken).ConfigureAwait(false);
_observer.LlmResponse(callContext, result);
registry.AddUsage(task.TaskId, result.InputTokens, result.OutputTokens);
registry.UpdateDiagnostics(task.TaskId, counters.Iterations, counters.Replans);
}
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
if (callContext is not null) _observer.LlmFailure(callContext, "skill_selection_failed", ex.Message);
return [];
}
return ParseSelectedSkills(result.Content);
}
private void RecordLlmAttempt(TaskSnapshot task, ExecutionCounters counters, string phase,
IReadOnlyList messages, OllamaResult? result, Exception? error)
{
if (counters.Iterations >= settings.MaxIterations)
throw new InvalidOperationException($"Agent exceeded the Task iteration limit of {settings.MaxIterations} during {phase}.");
counters.Iterations++;
int iteration = counters.Iterations - 1;
var callContext = new AgentLlmCallContext(Guid.NewGuid(), task.SessionId, task.TaskId, null,
phase, iteration, counters.Replans, settings.Model);
_observer.LlmRequest(callContext, messages);
if (result is not null)
{
_observer.LlmResponse(callContext, result);
registry.AddUsage(task.TaskId, result.InputTokens, result.OutputTokens);
}
if (error is not null) _observer.LlmFailure(callContext, "ollama_call_failed", error.Message);
registry.UpdateDiagnostics(task.TaskId, counters.Iterations, counters.Replans);
}
private void UpdateTaskProgress(Guid taskId, TaskGraph graph, string? currentTask, ExecutionCounters counters)
{
int completed = graph.Tasks.Count(static item => item.State == PlannerTaskState.Completed);
registry.UpdateProgress(taskId, completed, graph.Count, currentTask, graph.CriticalPath.CumulativeTokenCost);
registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans);
}
private static string GraphSignature(TaskGraph graph)
{
IReadOnlyDictionary descriptions = graph.Tasks.ToDictionary(static item => item.Id, static item => item.Description);
return string.Join("\n", graph.Tasks
.Where(static item => item.State != PlannerTaskState.Completed)
.Select(item => $"{item.Description}|{item.RequiresExternalEffect}|{string.Join(',', item.Dependencies.Select(id => descriptions.GetValueOrDefault(id, id.ToString())).Order(StringComparer.Ordinal))}"));
}
private sealed class ExecutionCounters
{
public int Iterations { get; set; }
public int Replans { get; set; }
}
private string Message(string key, string fallback, string? detail = null)
{
if (locale is null) return fallback;
return detail is null ? locale.Get(key) : locale.Format(key, new Dictionary { ["message"] = detail });
}
private sealed class PolicyDeniedException(string fingerprint, string message, string errorCode, string? errorCategory,
string? targetPath, string? requiredPermission, string? resolution) : Exception(message)
{
public string Fingerprint { get; } = fingerprint;
public string ErrorCode { get; } = errorCode;
public string? ErrorCategory { get; } = errorCategory;
public string? TargetPath { get; } = targetPath;
public string? RequiredPermission { get; } = requiredPermission;
public string? Resolution { get; } = resolution;
}
private string LocalizePolicyDenial(PolicyDeniedException error)
{
if (locale is null) return error.Message;
return locale.Format("runtime.policy_denied", new Dictionary
{
["code"] = error.ErrorCode,
["path"] = error.TargetPath ?? "-",
["permission"] = error.RequiredPermission ?? "-",
["resolution"] = error.Resolution ?? "-"
});
}
private IReadOnlyList ParseSelectedSkills(string content)
{
try
{
using JsonDocument doc = JsonDocument.Parse(content);
if (!doc.RootElement.TryGetProperty("selected_skills", out JsonElement skillsElement)) return [];
var selectedNames = new HashSet(StringComparer.Ordinal);
foreach (JsonElement item in skillsElement.EnumerateArray())
{
string? name = item.GetString();
if (!string.IsNullOrWhiteSpace(name)) selectedNames.Add(name);
}
var valid = new List();
foreach (AgentSkill skill in settings.Skills)
{
if (selectedNames.Contains(skill.Name))
{
valid.Add(skill);
if (valid.Count >= 10) break;
}
}
return valid;
}
catch
{
return [];
}
}
private async Task EndTaskSafelyAsync(Guid taskId, bool success, CancellationToken cancellationToken)
{
try
{
await _tools.EndTaskAsync(taskId, success, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
return $"Task resource cleanup failed: {ex.Message}";
}
}
}