summaryrefslogtreecommitdiff
path: root/src/Agent.Core
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.Core')
-rw-r--r--src/Agent.Core/Runtime/AgentRuntime.cs50
-rw-r--r--src/Agent.Core/Runtime/PlannerService.cs1
-rw-r--r--src/Agent.Core/Tools/InternalToolDispatcher.cs3
-rw-r--r--src/Agent.Core/Tools/ToolPolicy.cs7
4 files changed, 52 insertions, 9 deletions
diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs
index 8cf1ddb..bc9fc5e 100644
--- a/src/Agent.Core/Runtime/AgentRuntime.cs
+++ b/src/Agent.Core/Runtime/AgentRuntime.cs
@@ -75,8 +75,11 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
{
string? cleanupFailure = await EndTaskSafelyAsync(taskId, false, CancellationToken.None).ConfigureAwait(false);
- string result = cleanupFailure is null ? ex.Message : $"{ex.Message} {cleanupFailure}";
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);
@@ -122,11 +125,20 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
catch (PolicyDeniedException ex)
{
graph.SetState(next.Id, PlannerTaskState.Failed);
- registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, "policy_denied", ex.Message);
+ string localizedDenial = LocalizePolicyDenial(ex);
+ registry.UpdateDiagnostics(taskId, counters.Iterations, counters.Replans, ex.ErrorCode, localizedDenial);
if (!denyFingerprints.Add(ex.Fingerprint))
- throw new InvalidOperationException($"The same sandbox policy denial occurred again: {ex.Message}");
+ {
+ 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)
- throw new InvalidOperationException($"Sandbox policy denial remained unresolved after {maxReplans} replans: {ex.Message}");
+ {
+ 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++;
@@ -134,7 +146,11 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
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))
- throw new InvalidOperationException("Replan did not change the incomplete TaskGraph.");
+ {
+ 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);
@@ -271,7 +287,9 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
_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);
+ 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));
}
@@ -454,9 +472,27 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
return detail is null ? locale.Get(key) : locale.Format(key, new Dictionary<string, object?> { ["message"] = detail });
}
- private sealed class PolicyDeniedException(string fingerprint, string message) : Exception(message)
+ 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<string, object?>
+ {
+ ["code"] = error.ErrorCode,
+ ["path"] = error.TargetPath ?? "-",
+ ["permission"] = error.RequiredPermission ?? "-",
+ ["resolution"] = error.Resolution ?? "-"
+ });
}
private IReadOnlyList<AgentSkill> ParseSelectedSkills(string content)
diff --git a/src/Agent.Core/Runtime/PlannerService.cs b/src/Agent.Core/Runtime/PlannerService.cs
index 99569e4..7d8c3e1 100644
--- a/src/Agent.Core/Runtime/PlannerService.cs
+++ b/src/Agent.Core/Runtime/PlannerService.cs
@@ -288,6 +288,7 @@ public sealed class PlannerService(IOllamaClient ollama, AgentRuntimeSettings se
For pure analysis or informational tasks, requires_external_effect should be false.
For tasks that modify files, run processes, or produce verifiable side effects, requires_external_effect should be true.
+ Read-only operations (file_read, file_search, and dir list) do not create an external effect and must use requires_external_effect=false.
A single task is valid for simple requests.
""";
diff --git a/src/Agent.Core/Tools/InternalToolDispatcher.cs b/src/Agent.Core/Tools/InternalToolDispatcher.cs
index 0ae078c..c88b92c 100644
--- a/src/Agent.Core/Tools/InternalToolDispatcher.cs
+++ b/src/Agent.Core/Tools/InternalToolDispatcher.cs
@@ -11,7 +11,8 @@ public sealed class InternalToolDispatcher(IToolPolicyEngine policy, IInternalTo
{
InternalToolValidator.Validate(toolName, arguments);
ToolPolicyDecision decision = policy.Evaluate(toolName, arguments);
- if (!decision.Allowed) return new(false, decision.Reason, false, $"policy:{toolName}:{decision.Reason}", RetryAllowed: false);
+ if (!decision.Allowed) return new(false, decision.Reason, false, $"policy:{toolName}:{decision.Reason}", RetryAllowed: false,
+ ErrorCode: "policy.tool_denied", ErrorCategory: "policy", Resolution: "Review the enabled Internal Tool policy.");
return await backend.ExecuteAsync(taskId, toolName, arguments, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/src/Agent.Core/Tools/ToolPolicy.cs b/src/Agent.Core/Tools/ToolPolicy.cs
index cefca8d..66f63ab 100644
--- a/src/Agent.Core/Tools/ToolPolicy.cs
+++ b/src/Agent.Core/Tools/ToolPolicy.cs
@@ -25,7 +25,12 @@ public sealed record ToolExecutionResult(
string Output,
bool ExternalEffect,
string? FailureFingerprint = null,
- bool RetryAllowed = true);
+ bool RetryAllowed = true,
+ string? ErrorCode = null,
+ string? ErrorCategory = null,
+ string? TargetPath = null,
+ string? RequiredPermission = null,
+ string? Resolution = null);
public sealed class DenyByDefaultPolicy(IReadOnlySet<string>? allowedTools = null) : IToolPolicyEngine
{