ADR-029: Adaptive Decomposition — Investigate, Per-File, then Cross-File¶
Status: Accepted
Date: 2026-06-13
Related: ADR-025 (SDK Task counterpart),
ADR-001 (native DAG engine)
Context¶
OrchestratorAgent (agentic_v2/agents/orchestrator.py) decomposed every task
in a single shot: one _call_model call produced a complete JSON plan that
was parsed into SubTasks and executed. On the no-backend baseline that call
returned a frozen constant — a fixed generate → review pair — regardless
of the task. Two problems followed:
- Non-adaptive. The plan was fixed before any investigation. The orchestrator never looked at what the task actually touched before deciding how to break it up, so it could not, for example, spin up one analysis pass per file actually discovered.
- Capability scoring was inert on the no-key path. Because the no-backend
stub always emitted the same two capabilities (
code_generation,code_review),_assign_agents'sCapabilitySet.score_matchhad nothing task-dependent to route on — it scored the same two subtasks every time.
The exam pattern this models is adaptive decomposition: a coordinator runs an initial investigation step, generates new subtasks from the intermediate findings (a local pass per file discovered), then runs a separate cross-file integration pass over those per-file outputs. ARP modeled the shape of multi-agent orchestration but never derived the decomposition from findings.
Decision¶
Make decomposition adaptive in three phases, and replace the frozen stub with a task-derived plan so capability-scored selection becomes operational.
- Investigate —
_investigate(task)runs an initial step that returns concreteInvestigationFindings(thefilesthe task touches +observations). With a backend it sendsINVESTIGATION_SYSTEM_PROMPT; with none (or a placeholder backend that returns prose) it derives findings deterministically from file-like tokens in the task text. - Per-file —
_subtasks_from_findingsemits one local analysis subtask per discovered file (per_file_{i}, requiringSTATIC_ANALYSIS, no dependencies). This is the findings-driven branch: the subtask set is a function of what investigation found, not a constant. - Cross-file — a single, distinct
cross_file_integrationsubtask is emitted thatdepends_onevery per-file pass, so it runs strictly after all local passes complete and reconciles them.
decompose_adaptive(task) returns an immutable AdaptiveDecomposition and
registers the generated plan into self._subtasks / self._fallback_chains
(clearing any prior plan), so the existing _assign_agents and _execute_plan
machinery runs over the findings-derived plan unchanged. run_adaptive(task)
ties the phases to assignment + bounded-parallel execution end-to-end.
The hardcoded _call_model stub is gone: the no-backend path now calls
_intent_decomposition(task_text), which maps task keywords to the capability a
matching subtask should require (a "review" task → a code_review subtask, a
"test" task → a test_generation subtask, chained in declaration order). The
backed path additionally falls back to _intent_decomposition when the model
returns content with no extractable JSON (the placeholder-backend case), so a
plan is always produced and capability scoring always has task-dependent
subtasks to route.
Single-shot vs. adaptive decomposition¶
| Dimension | Single-shot (before) | Adaptive (this ADR) |
|---|---|---|
| When the plan is fixed | Up front, before any inspection | After an investigation step observes the task |
| Subtask set | Constant (generate/review) on the no-key path |
A function of discovered files (one pass each) |
| Cross-file reasoning | None — independent subtasks only | A distinct integration pass gated on all per-file passes |
| Capability scoring | Inert on no-key path (same 2 caps always) | Operational — task-derived caps drive score_match |
| No-backend determinism | Frozen constant | Task-text-derived plan (still deterministic) |
Takeaway. Adaptive decomposition is the right default when the correct
breakdown is not knowable until the task is inspected — investigation finds the
files, the per-file tier fans out over exactly those, and a separate cross-file
pass integrates. The single-shot path remains available (decompose_task /
run) for tasks whose plan is genuinely known in advance; it is now
task-derived rather than a constant.
Consequences¶
- Positive: The orchestrator now derives subtasks from intermediate findings and runs a distinct cross-file integration pass — the named adaptive pattern, end-to-end and tested.
- Positive: Capability-scored selection is operational on the no-key
baseline because subtasks now carry task-dependent capabilities; the frozen
generate/reviewconstant is removed. - Positive: All new types (
InvestigationFindings,AdaptiveDecomposition) are frozen/immutable and JSON-serializable, fitting the repo's immutable-data and wire-format conventions. - Neutral: The no-backend investigation uses a conservative path/extension regex to find files. It is a deterministic stand-in for an LLM investigation, not a real file-system scan; tasks that imply files without naming them yield an empty per-file tier (and therefore no cross-file pass).
- Negative: With a real backend the investigation adds one extra model round trip before the per-file plan exists. This is the cost of adaptivity and is bounded to a single low-temperature call.