summaryrefslogtreecommitdiff
path: root/src/Agent.Core/Tools/TaskGraphToolHandler.cs
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
commitb07b41cd435991cd254957af79055080c85b346e (patch)
tree27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /src/Agent.Core/Tools/TaskGraphToolHandler.cs
parent2fefd29e86c3b926d0cf4a362251d3f119688612 (diff)
feat: AI Agent Sandbox MCP Server v1を実装
Specifications、Design、ImplementationPlanの優先順位を整理し、単一Task実行モデル、typed option優先、設定可能なresource上限、local Ollamaを必須とするRelease Gateを文書化。 .NET 10/C# 14の5 production project、3 test project、E2E Clientを構築。typed YAML、en/ja locale、fixed/user prompt、stdioおよびloopback限定Streamable HTTP、agent_run/agent_status、Session、Ollama text/tool/Vision、TaskGraphとtoken accountingを実装。 Windows sandboxとしてfixed AppContainer、canonical/reparse検査、能力別ACL、path+SHA-256 executable approval、child-process restriction、Job Object、task journal、atomic file transaction、crash recoveryを実装。WFPのIPv4/IPv6 default denyとexact allow、dynamic session、temporary loopback exemption、管理者/BFE診断を追加。 Security/Recovery test、MCP/Ollama integration test、stdio/HTTP Operational E2E、Release Bundle生成・checksum・manifest・fresh extraction検査を追加。Requirement TraceabilityとPhase 0〜11の進捗、初回セットアップ可能なREADMEを整備。 Debug/Release/publish/objをrepository rootのbuild/へ集約し、旧project-local bin/objへの依存を除去。artifacts/とbuild/をGit管理外に設定し、clean checkoutでもsolution buildからE2E Clientまで生成可能にした。
Diffstat (limited to 'src/Agent.Core/Tools/TaskGraphToolHandler.cs')
-rw-r--r--src/Agent.Core/Tools/TaskGraphToolHandler.cs123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/Agent.Core/Tools/TaskGraphToolHandler.cs b/src/Agent.Core/Tools/TaskGraphToolHandler.cs
new file mode 100644
index 0000000..03daa56
--- /dev/null
+++ b/src/Agent.Core/Tools/TaskGraphToolHandler.cs
@@ -0,0 +1,123 @@
+using System.Collections.Concurrent;
+using System.Text.Json;
+using Agent.Planner;
+
+namespace Agent.Core.Tools;
+
+public interface ITaskUpdateHandler
+{
+ void BeginTask(Guid taskId);
+ ToolExecutionResult Execute(Guid taskId, JsonElement arguments);
+ void EndTask(Guid taskId);
+}
+
+public sealed class TaskGraphToolHandler(int maxTasks = 32) : ITaskUpdateHandler
+{
+ private readonly ConcurrentDictionary<Guid, TaskGraph> _graphs = new();
+
+ public void BeginTask(Guid taskId)
+ {
+ if (!_graphs.TryAdd(taskId, new TaskGraph(maxTasks)))
+ throw new InvalidOperationException($"TaskGraph for '{taskId}' already exists.");
+ }
+
+ public ToolExecutionResult Execute(Guid taskId, JsonElement arguments)
+ {
+ if (!_graphs.TryGetValue(taskId, out TaskGraph? graph))
+ throw new InvalidOperationException("TaskGraph scope is not active.");
+
+ string operation = arguments.GetProperty("operation").GetString()!;
+ switch (operation)
+ {
+ case "add":
+ graph.AddTask(
+ RequiredString(arguments, "description"),
+ Budget(arguments),
+ Estimate(arguments),
+ Ids(arguments, "depends_on"),
+ id: OptionalId(arguments, "task_id"));
+ break;
+ case "add_dependency":
+ Guid taskIdValue = RequiredId(arguments, "task_id");
+ Guid[] dependencies = Ids(arguments, "depends_on");
+ if (dependencies.Length == 0) throw new ToolValidationException("depends_on must contain at least one task id.");
+ foreach (Guid dependency in dependencies) graph.AddDependency(taskIdValue, dependency);
+ break;
+ case "set_state":
+ graph.SetState(RequiredId(arguments, "task_id"), ParseState(RequiredString(arguments, "state")));
+ break;
+ case "set_estimate":
+ graph.SetEstimate(RequiredId(arguments, "task_id"), Budget(arguments), Estimate(arguments));
+ break;
+ default:
+ throw new ToolValidationException($"Unsupported task_update operation '{operation}'.");
+ }
+
+ string output = JsonSerializer.Serialize(new
+ {
+ tasks = graph.Tasks.Select(static task => new
+ {
+ task_id = task.Id,
+ description = task.Description,
+ depends_on = task.Dependencies,
+ state = task.State.ToString().ToLowerInvariant(),
+ expected_tokens = task.ExpectedTokens
+ }),
+ critical_path = graph.CriticalPath.TaskIds,
+ critical_path_tokens = graph.CriticalPath.CumulativeTokenCost,
+ total_token_budget = graph.TotalTokenBudget
+ });
+ return new ToolExecutionResult(true, output, false);
+ }
+
+ public void EndTask(Guid taskId) => _graphs.TryRemove(taskId, out _);
+
+ private static TokenBudgetClass Budget(JsonElement arguments) =>
+ TokenBudgetClassExtensions.Parse(RequiredString(arguments, "budget_class"));
+
+ private static TokenEstimate Estimate(JsonElement arguments) => new(
+ RequiredInt64(arguments, "optimistic_tokens"),
+ RequiredInt64(arguments, "most_likely_tokens"),
+ RequiredInt64(arguments, "pessimistic_tokens"));
+
+ private static string RequiredString(JsonElement arguments, string property) =>
+ arguments.TryGetProperty(property, out JsonElement value) && value.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(value.GetString())
+ ? value.GetString()!
+ : throw new ToolValidationException($"{property} is required for this operation.");
+
+ private static long RequiredInt64(JsonElement arguments, string property) =>
+ arguments.TryGetProperty(property, out JsonElement value) && value.ValueKind == JsonValueKind.Number
+ ? value.GetInt64()
+ : throw new ToolValidationException($"{property} is required for this operation.");
+
+ private static Guid RequiredId(JsonElement arguments, string property) =>
+ OptionalId(arguments, property) ?? throw new ToolValidationException($"{property} must be a UUID for this operation.");
+
+ private static Guid? OptionalId(JsonElement arguments, string property)
+ {
+ if (!arguments.TryGetProperty(property, out JsonElement value) || value.ValueKind == JsonValueKind.Null) return null;
+ return value.ValueKind == JsonValueKind.String && Guid.TryParse(value.GetString(), out Guid id)
+ ? id
+ : throw new ToolValidationException($"{property} must be a UUID.");
+ }
+
+ private static Guid[] Ids(JsonElement arguments, string property)
+ {
+ if (!arguments.TryGetProperty(property, out JsonElement value)) return [];
+ var ids = new List<Guid>();
+ foreach (JsonElement item in value.EnumerateArray())
+ ids.Add(Guid.TryParse(item.GetString(), out Guid id) ? id : throw new ToolValidationException($"{property} must contain only UUIDs."));
+ return [.. ids];
+ }
+
+ private static PlannerTaskState ParseState(string state) => state switch
+ {
+ "queued" => PlannerTaskState.Queued,
+ "planning" => PlannerTaskState.Planning,
+ "running" => PlannerTaskState.Running,
+ "completed" => PlannerTaskState.Completed,
+ "failed" => PlannerTaskState.Failed,
+ "cancelled" => PlannerTaskState.Cancelled,
+ _ => throw new ToolValidationException($"Unknown task state '{state}'.")
+ };
+}