From ab69aaa72903b0ac5a35c4823ee97a629b3984fb Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Tue, 11 Aug 2026 22:23:27 +0900 Subject: Enforce planner task effect scope --- doc/Design.md | 2 +- src/Agent.Core/Runtime/AgentRuntime.cs | 19 +++++++++++++++++-- tests/Agent.Core.Tests/PromptAndRuntimeTests.cs | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/doc/Design.md b/doc/Design.md index ac496bb..6897064 100644 --- a/doc/Design.md +++ b/doc/Design.md @@ -358,7 +358,7 @@ Task RequiresExternalEffect: bool RequiredEffectTools: set ``` -`RequiresExternalEffect=true`のTaskは、Tool catalogに存在し副作用を持つTool名を`RequiredEffectTools`へ1件以上持つ。Planner structured outputの`required_effect_tools`から構築し、Runtimeは成功した副作用Toolがこの集合に含まれる場合だけ期待副作用の証跡として扱う。例えば`process_run`が必要なTask中の`file_edit`成功は完了根拠にしない。`RequiresExternalEffect=false`では集合を空にする。 +`RequiresExternalEffect=true`のTaskは、Tool catalogに存在し副作用を持つTool名を`RequiredEffectTools`へ1件以上持つ。Planner structured outputの`required_effect_tools`から構築し、Runtimeは成功した副作用Toolがこの集合に含まれる場合だけ期待副作用の証跡として扱う。例えば`process_run`が必要なTask中の`file_edit`成功は完了根拠にしない。current Taskの集合に含まれない副作用ToolはBackendへdispatchせず、Task scope外のTool resultとしてLLMへ返してqueued Taskの先取りを決定的に防ぐ。`RequiresExternalEffect=false`では集合を空にする。 ### 8.2 PERT計算 ```text ExpectedTokens = (O + 4*M + P) / 6.0 diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs index 333d557..9c051e9 100644 --- a/src/Agent.Core/Runtime/AgentRuntime.cs +++ b/src/Agent.Core/Runtime/AgentRuntime.cs @@ -266,7 +266,14 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag foreach (OllamaToolCall call in calls) { ToolExecutionResult toolResult; - try + 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); } @@ -283,7 +290,7 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag // 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) unresolvedToolFailures[call.Function.Name] = toolResult.FailureFingerprint ?? toolResult.Output; + 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)); @@ -330,6 +337,14 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag 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. diff --git a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs index d72e02a..c13f03c 100644 --- a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs +++ b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs @@ -142,7 +142,7 @@ public sealed class PromptAndRuntimeTests Assert.True(registry.TryGet(task.TaskId, null, out TaskSnapshot? completed)); Assert.Equal(AgentTaskState.Completed, completed!.State); - Assert.Equal(["file_edit", "process_run"], backend.Calls.Select(static call => call.Name)); + Assert.Equal(["process_run"], backend.Calls.Select(static call => call.Name)); } [Fact] -- cgit v1.2.3