From 997cd8b56bc086a02b9c7c006dd62b07b9fcd2f3 Mon Sep 17 00:00:00 2001 From: Claude Sonnet 5 Date: Fri, 3 Jul 2026 23:22:42 +0000 Subject: feat(executor): add DAG auto-cascade-fail + role-typed subtask spawning (Phase 6) Two prerequisites for safe parallel evaluator fan-out (later phase): 1. Auto-cascade-fail: previously, a failed task's dependents just sat PENDING/QUEUED forever (or until something eventually tried to dispatch them and discovered the dependency was dead). Pool.cascadeFail now fires right after a task lands in a terminal failure state (FAILED/TIMED_OUT/ CANCELLED/BUDGET_EXCEEDED, from handleRunResult, the budget-gate reject path, and the checkDepsReady dependency-failure path), recursively cancelling every not-yet-run dependent (PENDING/QUEUED only -- RUNNING and terminal states are left alone) with a message referencing the upstream failure. A visited-set guards recursion, which turned out to be load-bearing rather than defense-in-depth: task creation does not prevent dependency cycles anywhere in this codebase. Correction to an earlier assumption: internal/executor's waitForDependencies is dead code, never called. The live mechanism is checkDepsReady, invoked synchronously in execute() with a self-requeue via time.AfterFunc. Added a freshness re-check (GetTask, bail if no longer QUEUED) at the top of that block so a task cascade-cancelled while sitting in the requeue loop stops silently instead of hitting an invalid CANCELLED->CANCELLED transition or, worse, still getting dispatched. 2. storeChannel.SpawnSubtask hardcoded Agent.Type: "claude" on every spawned child regardless of what role it should play -- a hard blocker for a Planner/Builder task spawning role-typed evaluator subtasks. SubtaskSpec (internal/agentchannel) gains a Role field; when set, the child task gets Agent.Role instead of a hardcoded Type, so Phase 5's role-resolution picks provider/model from that role's escalation ladder. spec.Role == "" (every existing caller) preserves today's exact behavior byte-for-byte -- proven by an explicit regression test, not just new-feature coverage. Threaded the new `role` parameter through both spawn_subtask transports: the native tool-use loop (internal/agentloop/tools.go) and the MCP tool exposed to ContainerRunner-driven claude/gemini agents (internal/executor/agentmcp.go). go build/vet/test -race -count=1 all pass, full suite (20 packages). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs --- internal/agentloop/tools.go | 3 +++ 1 file changed, 3 insertions(+) (limited to 'internal/agentloop') diff --git a/internal/agentloop/tools.go b/internal/agentloop/tools.go index fcb17a2..fc003cb 100644 --- a/internal/agentloop/tools.go +++ b/internal/agentloop/tools.go @@ -52,6 +52,7 @@ func agentToolSpecs() []provider.ToolSpec { "instructions": strProp("complete instructions for the subtask agent"), "model": strProp("optional model override"), "max_budget_usd": map[string]any{"type": "number", "description": "optional budget cap in USD"}, + "role": strProp("optional role name to dispatch the subtask through instead of a fixed model (e.g. an evaluator role); when set, model is ignored and the role's escalation ladder picks the provider/model"), }, "required": []string{"name", "instructions"}, }, @@ -153,6 +154,7 @@ func (l *Loop) dispatchTool(ctx context.Context, name, argsJSON string) (result Instructions string `json:"instructions"` Model string `json:"model"` MaxBudgetUSD float64 `json:"max_budget_usd"` + Role string `json:"role"` } _ = json.Unmarshal([]byte(argsJSON), &a) id, ssErr := l.Channel.SpawnSubtask(ctx, agentchannel.SubtaskSpec{ @@ -160,6 +162,7 @@ func (l *Loop) dispatchTool(ctx context.Context, name, argsJSON string) (result Instructions: a.Instructions, Model: a.Model, MaxBudgetUSD: a.MaxBudgetUSD, + Role: a.Role, }) if ssErr != nil { return "", false, ssErr -- cgit v1.2.3