summaryrefslogtreecommitdiff
path: root/src/Agent.Core/Runtime/AgentRuntime.cs
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-12 00:02:23 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-12 00:02:23 +0900
commitce550a127b7dc972651bd15b9f09979a2c38688f (patch)
treef4e09d0f64f74a585e015e6f548c5589fd2442ba /src/Agent.Core/Runtime/AgentRuntime.cs
parent55db81082f9165a3965f860cd618bac4242c56b1 (diff)
feat: harden Python workflow and structured planning
Replace the uv-specific skill with a portable Python skill and rewrite the default user system prompt as a concise, structured Markdown execution policy. Normalize a single fenced structured-JSON response, retry invalid skill selection with think disabled, validate selected skill output, and make coding plans keep editing and verification in one task. Document the file_edit patch payload, expose that contract in the tool catalog, and convert malformed patch input into a retryable tool failure instead of terminating the agent task. Update configuration and runtime tests for the Python skill, exact tool catalog, structured-output retry, and implementation-and-verification planning guidance. Verified with 82 Agent.Core tests and an agent-mcp Python scheduling scenario with 49 passing unittest cases.
Diffstat (limited to 'src/Agent.Core/Runtime/AgentRuntime.cs')
-rw-r--r--src/Agent.Core/Runtime/AgentRuntime.cs61
1 files changed, 39 insertions, 22 deletions
diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs
index 4aaf646..31a4414 100644
--- a/src/Agent.Core/Runtime/AgentRuntime.cs
+++ b/src/Agent.Core/Runtime/AgentRuntime.cs
@@ -426,34 +426,46 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
string catalog = string.Join("\n", settings.Skills.Select(static s => $"- {s.Name}: {s.Description}"));
var selectionMessages = new List<OllamaMessage>
{
- 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)
+ new("system", $"You are a skill classifier, not a task executor. Do not solve the request, inspect files, write code, or call Tools. Available skills:\n{catalog}\n\nReturn only one JSON object with a 'selected_skills' array containing exact skill names. Select only relevant skills. If none are relevant, return an empty array."),
+ new("user", $"Classify skills for this request only:\n{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
+ IReadOnlyList<OllamaMessage> attemptMessages = selectionMessages;
+ for (int attempt = 0; attempt < 2; attempt++)
{
if (counters.Iterations >= settings.MaxIterations) return [];
int iteration = counters.Iterations++;
- callContext = new AgentLlmCallContext(Guid.NewGuid(), task.SessionId, task.TaskId, null,
+ var 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 [];
+ _observer.LlmRequest(callContext, attemptMessages);
+ OllamaResult? result = null;
+ try
+ {
+ bool? thinkOverride = attempt == 0 ? null : false;
+ result = await ollama.ChatStructuredAsync(attemptMessages, task.Temperature, formatDoc.RootElement.Clone(), thinkOverride, cancellationToken).ConfigureAwait(false);
+ _observer.LlmResponse(callContext, result);
+ registry.AddUsage(task.TaskId, result.InputTokens, result.OutputTokens);
+ registry.UpdateDiagnostics(task.TaskId, counters.Iterations, counters.Replans);
+ if (TryParseSelectedSkills(result.Content, out IReadOnlyList<AgentSkill> selected)) return selected;
+ throw new PlannerValidationException("Ollama returned an invalid skill selection object.");
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
+ catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException))
+ {
+ _observer.LlmFailure(callContext, "skill_selection_failed", ex.Message);
+ if (attempt == 0)
+ {
+ attemptMessages = selectionMessages.Concat([
+ new OllamaMessage("system", "The previous response was invalid. Return only {\"selected_skills\":[\"name\"]} or {\"selected_skills\":[]}, with no Markdown, code, Tool call, or commentary.")
+ ]).ToArray();
+ }
+ }
}
- return ParseSelectedSkills(result.Content);
+ return [];
}
private void RecordLlmAttempt(TaskSnapshot task, ExecutionCounters counters, string phase,
@@ -525,15 +537,19 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
});
}
- private IReadOnlyList<AgentSkill> ParseSelectedSkills(string content)
+ private bool TryParseSelectedSkills(string content, out IReadOnlyList<AgentSkill> selected)
{
+ selected = [];
try
{
- using JsonDocument doc = JsonDocument.Parse(content);
- if (!doc.RootElement.TryGetProperty("selected_skills", out JsonElement skillsElement)) return [];
+ using JsonDocument doc = JsonDocument.Parse(StructuredJson.Unwrap(content));
+ if (doc.RootElement.ValueKind != JsonValueKind.Object ||
+ !doc.RootElement.TryGetProperty("selected_skills", out JsonElement skillsElement) ||
+ skillsElement.ValueKind != JsonValueKind.Array) return false;
var selectedNames = new HashSet<string>(StringComparer.Ordinal);
foreach (JsonElement item in skillsElement.EnumerateArray())
{
+ if (item.ValueKind != JsonValueKind.String) return false;
string? name = item.GetString();
if (!string.IsNullOrWhiteSpace(name)) selectedNames.Add(name);
}
@@ -546,11 +562,12 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag
if (valid.Count >= 10) break;
}
}
- return valid;
+ selected = valid;
+ return true;
}
catch
{
- return [];
+ return false;
}
}