diff options
| -rw-r--r-- | src/Agent.Core/Runtime/PlannerService.cs | 16 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/PromptAndRuntimeTests.cs | 4 |
2 files changed, 17 insertions, 3 deletions
diff --git a/src/Agent.Core/Runtime/PlannerService.cs b/src/Agent.Core/Runtime/PlannerService.cs index b5ac80e..2a006f7 100644 --- a/src/Agent.Core/Runtime/PlannerService.cs +++ b/src/Agent.Core/Runtime/PlannerService.cs @@ -229,25 +229,34 @@ public sealed class PlannerService(IOllamaClient ollama, AgentRuntimeSettings se Action<string, IReadOnlyList<OllamaMessage>, OllamaResult?, Exception?>? observeAttempt, Func<PlanResult, TaskGraph> buildGraph, CancellationToken cancellationToken) { Exception? firstFailure = null; + IReadOnlyList<OllamaMessage> attemptMessages = messages; for (int attempt = 0; attempt < 2; attempt++) { OllamaResult? result = null; try { bool? thinkOverride = attempt == 0 ? null : false; - result = await _ollama.ChatStructuredAsync(messages, temperature, PlanSchema.Element, thinkOverride, cancellationToken).ConfigureAwait(false); + result = await _ollama.ChatStructuredAsync(attemptMessages, temperature, PlanSchema.Element, thinkOverride, cancellationToken).ConfigureAwait(false); PlanResult? plan = ParsePlan(result.Content); if (plan is null || plan.Tasks.Count == 0) throw new PlannerValidationException("Ollama returned an empty or invalid structured plan."); TaskGraph graph = buildGraph(plan); - observeAttempt?.Invoke(phase, messages, result, null); + observeAttempt?.Invoke(phase, attemptMessages, result, null); return graph; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException)) { - observeAttempt?.Invoke(phase, messages, result, ex); + observeAttempt?.Invoke(phase, attemptMessages, result, ex); firstFailure ??= ex; + if (attempt == 0) + { + attemptMessages = messages.Concat([ + new OllamaMessage("system", + "The previous response was not a valid TaskGraph JSON object. Do not call or execute any Tool while planning. " + + "Return only one JSON object that matches the supplied schema, beginning with {\"tasks\":[ and containing no Markdown or commentary.") + ]).ToArray(); + } } } throw new PlannerValidationException($"Structured {phase} failed after think=false retry: {firstFailure?.Message}"); @@ -294,6 +303,7 @@ public sealed class PlannerService(IOllamaClient ollama, AgentRuntimeSettings se private static string BuildPlanningInstruction() => """ You are a Task Planner. Decompose the user request into a TaskGraph of 1-32 tasks. + Planning is read-only: do not call or execute Tools. Return only one JSON object matching the supplied schema, with no Markdown or commentary. Each task must have: - description: a clear, self-contained sub-task description - dependencies: array of task indices (0-based) that must complete before this task diff --git a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs index 3ef5bde..fef5340 100644 --- a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs +++ b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs @@ -284,6 +284,8 @@ public sealed class PromptAndRuntimeTests Assert.Equal(AgentTaskState.Completed, completed!.State); Assert.Equal([null, false], ollama.ThinkOverrides); Assert.All(ollama.Temperatures, value => Assert.Equal(0.7, value)); + Assert.Contains(ollama.PlanningMessages[1], message => + message.Role == "system" && message.Content.Contains("Do not call or execute any Tool", StringComparison.Ordinal)); } [Fact] @@ -603,6 +605,7 @@ public sealed class PromptAndRuntimeTests { public List<bool?> ThinkOverrides { get; } = []; public List<double?> Temperatures { get; } = []; + public List<IReadOnlyList<OllamaMessage>> PlanningMessages { get; } = []; public int RunCalls { get; private set; } public Task<OllamaResult> ChatAsync(IReadOnlyList<OllamaMessage> messages, double? temperatureOverride, CancellationToken cancellationToken) => Task.FromResult(new OllamaResult("done", 1, 1)); @@ -610,6 +613,7 @@ public sealed class PromptAndRuntimeTests { ThinkOverrides.Add(thinkOverride); Temperatures.Add(temperatureOverride); + PlanningMessages.Add(messages); string content = ThinkOverrides.Count == 2 && succeedOnRetry ? """{"tasks":[{"description":"analyze","dependencies":[],"budget_class":"4K","optimistic_tokens":10,"most_likely_tokens":20,"pessimistic_tokens":30,"requires_external_effect":false}]}""" : "not-json"; |
