diff options
| author | Claude Sonnet 5 <noreply@anthropic.com> | 2026-07-03 23:22:42 +0000 |
|---|---|---|
| committer | Claude Sonnet 5 <noreply@anthropic.com> | 2026-07-03 23:22:42 +0000 |
| commit | 997cd8b56bc086a02b9c7c006dd62b07b9fcd2f3 (patch) | |
| tree | 94351bb0aebeb92cb4c0f63f2e0a2636e06c4f1e /internal/executor/agentmcp.go | |
| parent | 787b7fb1aed92c2b701724a7741576053b93cccb (diff) | |
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'internal/executor/agentmcp.go')
| -rw-r--r-- | internal/executor/agentmcp.go | 2 |
1 files changed, 2 insertions, 0 deletions
diff --git a/internal/executor/agentmcp.go b/internal/executor/agentmcp.go index 4368031..2177fbd 100644 --- a/internal/executor/agentmcp.go +++ b/internal/executor/agentmcp.go @@ -66,6 +66,7 @@ type spawnSubtaskInput struct { Instructions string `json:"instructions" jsonschema:"complete instructions for the subtask agent"` Model string `json:"model,omitempty" jsonschema:"optional model override, e.g. sonnet or opus"` MaxBudgetUSD float64 `json:"max_budget_usd,omitempty" jsonschema:"optional budget cap in USD"` + Role string `json:"role,omitempty" jsonschema:"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"` } type recordProgressInput struct { @@ -118,6 +119,7 @@ func newAgentServer(ch AgentChannel) *mcp.Server { Instructions: in.Instructions, Model: in.Model, MaxBudgetUSD: in.MaxBudgetUSD, + Role: in.Role, }) if err != nil { return nil, nil, err |
