refactor(execution): decompose execute_task into named phases (#2314) - #2489
Merged
Merged
Conversation
…2314) Step 1 of the decomposition. `TaskExecutionErrorCode`, `TaskExecutionResult` and `TerminalEnvelope` are the one part of task_execution_service with no collaborators at all — pure dataclasses and an enum, no db, no HTTP, no capacity manager — so they can be imported from anywhere without a cycle. That is what lets the phase modules beside them name the same vocabulary without importing the service they belong to. Re-exported from task_execution_service: ~80 call sites and 83 test files import these names from there, and a decomposition that renames the import surface is not a pure refactor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
…phases (#2314) The 907-line monolith is now a ~350-line orchestrator plus nine phase methods, decomposed IN-PLACE — same module, so the ~140 monkeypatch sites across the unit suites keep patching the module globals the code actually reads (moving the class out is exactly the silent-test-rot hazard the issue's own Technical Notes warn about mid-#1081). - _admission_gate: step 2 + the three fast-fail terminals (CapacityFull / CircuitOpen / EphemeralBudgetExhausted); returns (slot_acquired, denial). Added to the #1804 parity allowlist with the same justification the sibling admission-path entries carry (no dispatch activity exists before step 3). - _start_dispatch_activity, _breaker_fast_fail (hands the transport CircuitState back for the #678 mid-turn re-check), _compose_effective_system_prompt (fallback path preserved verbatim). - _call_agent_with_retries: the #678 reader-race retry and the #792 SUB-003 switch+retry, mutating a shared _AttemptState. - _finalize_sync_response: raise_for_status + #679 cancel cross-validation + #1410 guard + the SUCCESS terminal. - _handle_timeout / _handle_budget_exhausted / _handle_http_error: the three big except bodies, verbatim. _AttemptState replaces the old hoisted retry locals: the exception handlers read what the retries wrote (retry_count, rolled-up failed-attempt cost, the one-shot switch flag, the retry-reset start_time), and creating it BEFORE the try removes the historical NameError hazard the hoisting comment guarded against. The generic Exception / CancelledError handlers, phase 3c, the payload dict, the #1083 202-ACK handoff (which flips async_handoff for the finally) and the finally itself stay inline — each is small and couples to orchestrator-local control flow. Behavior-neutral by construction: every moved block is byte-preserved modulo the state-object renames, checked with an undefined-name AST walk over the module (which caught the one real transcription slip, a log line reading two now-out-of-scope names). Related to #2314 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
4 tasks
obasilakis
approved these changes
Sep 2, 2026
obasilakis
left a comment
Contributor
There was a problem hiding this comment.
/validate-pr: APPROVE
The terminal/CAS contract is preserved by construction, which is the thing that had to be proven for a refactor of this file. Verification run against PR head:
- AST function-level diff, base ↔ head: every pre-existing function is byte-identical. Only
execute_taskchanged (907 → 350 lines) plus 9 private phase methods added.apply_resultand_write_terminal_and_gate— the two owners of the CASwongate,spawn_task_terminal_event(#1578) andclose_execution_activity(#1804) — are untouched. - Flattened, order-sensitive call sequence (orchestrator with phase calls inlined) is identical to base. No reordering, no dropped or added side effect. Counts match exactly:
spawn_task_terminal_event3/3,close_execution_activity3/3,record_outcome3/3,update_execution_status9/9,_write_terminal_and_gate6/6. activity_idis threaded into all 5 exception paths;_breaker_fast_fail(step 3b, post-activity) correctly routes through_write_terminal_and_gatewithactivity_idandagent_name(task_execution_service.py:1257-1263).- The
CancelledErrorhandler keeps itsif won:gate on the close (:1190); thefinallyslot release is still gated onslot_acquired and not async_handoff(:1206), both flags initialised before thetry. _AttemptStatemutation sites map 1:1 to base's retry locals — 10 sites, same order and operators.tests/unit/test_1804_terminal_activity_parity.pyruns green against PR head (6 passed). The new_admission_gateallowlist entry is justified correctly: its three terminals all fire fromcapacity.acquireat step 2, before step 3 opens the dispatch activity.
All 24 checks pass.
Warnings
- #2314 AC#1 ("no resulting file over 800 lines") is explicitly unmet — the file grew 2,202 → 2,397 lines — and the PR uses "Related to #2314" rather than a closing keyword, so the issue stays
status-in-progress. Both are honestly disclosed in the body, and the remaining move is described as mechanical post-#1081. Worth a note on #2314 that #2489 and #2487 are partial delivery, so the P-16 debt slot doesn't read as stalled.
Suggestions
_AttemptStateis constructed before thetry(:984), whereas base assigned the retry locals just above the agent call. That's a small positive behaviour delta in a "pure refactor" — it removes a latentNameErrorif steps 2–4a ever raised intoexcept httpx.HTTPError. Disclosed in the body and no reachable path changes, so no objection.execution_envelope.py:29carries a vestigial@dataclassonclass TaskExecutionErrorCode(str, Enum), moved verbatim from base. A leaf-module carve-out is the cheapest moment to drop it.
Findings produced by /validate-pr (Claude Code).
vybe
pushed a commit
that referenced
this pull request
Sep 3, 2026
Resolves the conflict in services/task_execution_service.py between this branch and #2314 (PR #2489), which decomposed execute_task into named phases. The two changes are semantically disjoint. Every ent#279 scrub site lives in _write_terminal_and_gate and apply_result, both of which #2314 left byte-identical; every other difference this branch had in that file was black reformatting with no semantic content. Resolution therefore takes dev's file whole -- keeping the decomposed execute_task, the nine extracted helpers and the services/execution_envelope re-export -- and re-applies only the scrub import and the four scrub blocks. No reformatting is carried over. One real cross-PR gap surfaced, caught by test_every_free_text_writer_scrubs: #2314 moved three static admission-refusal writes (capacity-full, dispatch-breaker, ephemeral-exhausted) out of execute_task and into the new _admission_gate, which the ent#279 allowlist did not yet name. Those writes are unchanged and still run before any agent call, so no agent text or staged secret can exist on that path; _admission_gate is allowlisted on the same grounds the execute_task entry already recorded, and that entry is narrowed to the single backend-shutdown write it still holds. Verified: 1864 passed, 9 skipped across all 90 unit-test files touching task_execution_service / execute_task / apply_result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTHX5aMsCJf71QhtysewL4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two commits decomposing
task_execution_service.py(#2314), both in-place — the module keeps its name and every module global stays put, because ~140 monkeypatch sites across the unit suites patchservices.task_execution_service.*, and moving the class to a sibling module is precisely the silent-test-rot hazard (patches apply cleanly and test nothing) the issue's own Technical Notes warn against mid-#1081.execution_envelope.py—TaskExecutionErrorCode/TaskExecutionResult/TerminalEnvelopecarved into a leaf module, re-exported from the service so every existing import keeps resolving.execute_taskdecomposed: the 907-line monolith becomes a ~350-line orchestrator (mostly the unchanged preamble + kwarg plumbing) over nine named phase methods:_admission_gate_start_dispatch_activity_breaker_fast_fail_compose_effective_system_prompt_call_agent_with_retries_finalize_sync_response_handle_timeout/_handle_budget_exhausted/_handle_http_error_AttemptStatereplaces the old hoisted retry locals — the exception handlers read what the retries wrote (retry count, rolled-up failed-attempt cost, the #792 one-shot switch flag, the retry-resetstart_timethe timeout handler's elapsed derives from), and creating it before thetryremoves the historical NameError hazard the old hoisting comment guarded against. Phase 3c, the payload dict, the #1083 202-ACK handoff (which flipsasync_handofffor thefinally) and thefinallystay inline: each couples to orchestrator-local control flow._admission_gateis added to the #1804 parity allowlist with the same justification its sibling admission-path entries carry (no dispatch activity exists before step 3 — before this PR those writes sat inline inexecute_task, whose own close calls satisfied the function-level scan).What this deliberately does NOT do
The issue's headline "no module >800 lines" is not met for this file (2,398 lines with the added signatures/docstrings): meeting it requires physically relocating
execute_task/apply_result, which detaches those ~140 patch sites mid-#1081 — the "big-bang" the issue's Technical Notes rule out. The four other oversized modules from #2314's list ship in #2487 as real package splits. The remaining step for this file — moving the phase methods out behind the now-explicit seams once #1081 settles — is now mechanical.Verification
state.renames, checked with an undefined-name AST walk over the module (it caught the one real transcription slip — a log line reading two now-out-of-scope names, repointed at the payload).task_execution_service/execute_task: 1,844 passed, 8 skipped. The single failure istest_2467_turn_integrity's fleet-list arm — the dev-wide hardcoded-timestamp time bomb (fires after 2026-09-02T10:00Z on every branch), fixed separately in fix(tests): defuse the hardcoded-timestamp time bomb in the #2467 fleet-list test #2488.Closes #2314
🤖 Generated with Claude Code
https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux