Skip to content

🤖 refactor: split turn preparation into a TurnRequestBuilder pipeline - #4014

Open
ibetitsmike wants to merge 20 commits into
mainfrom
mike/arch-turn-request-builder
Open

🤖 refactor: split turn preparation into a TurnRequestBuilder pipeline#4014
ibetitsmike wants to merge 20 commits into
mainfrom
mike/arch-turn-request-builder

Conversation

@ibetitsmike

Copy link
Copy Markdown
Contributor

Summary

Splits turn preparation from turn execution. All request preparation that lived inside AIService.streamMessage (a 2,925-line method) moves into a new TurnRequestBuilder whose pipeline produces a self-contained TurnExecutionOptions; StreamManager (the turn engine) now owns the execution lifecycle end to end, including pre-start abort tracking and the mock stream lifecycle. AIService's pass-through methods and 12 DI setters are deleted.

Net LOC vs merge-base: production -285 (+3,391 / -3,676), tests -1,210 (+859 / -2,069), total -1,495.

Background

aiService.ts was 4,370 lines with one god-method interleaving runtime init, model resolution, memory context, prompt assembly, tool policy/wrapping, history placeholder append, a ~543-line refusal-fallback closure that re-implemented per-model preparation, and thinking-override closures. Around it sat verbatim pass-throughs to StreamManager and 12 DI setters feeding closure state. Changing tool wrapping meant navigating ~3,000 lines of mutable closures, and agentSession.testHarness.ts faked the whole class via as unknown as AIService. Builds on #3999 (typed TurnExecutionOptions seam).

Implementation

  • turnRequestBuilder.ts (new): preparation as one pipeline (context -> tools -> provider request -> fallback plan). The primary request and the refusal-fallback prepare() now share one per-model preparation path (prepareModelSeed + prepareModelRequest); the old fallback closure was near-duplicated preparation and is gone.
  • Engine owns lifecycle: StreamManager absorbs pendingStreamStarts (pre-start aborts) and the mock stream lifecycle, so a turn is stoppable/queryable through the engine for its entire life. AIService's getStreamInfo/getStreamState/replayStream/waitForInit/debug pass-throughs are deleted and callers migrated; stopStream/isStreaming remain as 1-line delegations because ~45 taskService/workspaceService callsites still consume them through AIService (migrating those is mechanical follow-up churn this PR avoids).
  • DI setters deleted: the 12 single-purpose setters become a TurnRequestBuilderBindings object populated at wiring time. ProviderModelFactory reads OAuth services from the live bindings at use time, preserving the old eager-setter semantics for createModel callers outside a turn (branch summaries, refinement).
  • Narrow session seam: AgentSession depends on AgentSessionAIService + an asserted stream-lifecycle seam instead of the AIService class; the CLI entrypoints (xum run, xum workflow) now pass streamManager explicitly. The test harness implements the narrow interface honestly (Err results, shared createStreamLifecycleMocks()), and prep-focused tests moved to direct TurnRequestBuilder tests with far less mocking.
  • Prompt assembly (buildPlanInstructions/buildStreamSystemContext) is untouched and called as an opaque stage, so a future context assembler can slot in as the builder's first stage.

Irreducible additions, per item: turnRequestBuilder.ts (+2,899) is the moved pipeline offset by aiService.ts (-3,741); streamManager.ts (+~125) is the absorbed pre-start/mock lifecycle ownership; turnRequestBuilder.test.ts (+417) replaces ~2,000 lines of heavier aiService tests; the createStreamLifecycleMocks() spreads across agentSession/workspaceService tests satisfy the new constructor invariant that a session always has stream-lifecycle access.

Validation

  • Behavior-preserving contract checked by remote dogfood UAT on the pushed SHA (streaming, reload persistence, plan mode, tool calls + sub-agent spawn, mid-turn and next-turn thinking overrides, mid-stream and startup interrupts, model routing incl. one-shot /<model>, usage/costs). One finding: an empty assistant placeholder row persists in chat.jsonl when an interrupt lands after stream registration but before the first delta. Verified pre-existing on main: the unchanged cleanup comment documents that deletion only covers aborts before registration, and main already filters such rows from provider requests and hides them in the UI. Left as a follow-up candidate rather than a behavior change here.
  • workspaceService.test.ts has one failure that reproduces identically on pristine main (bash monitor wake-store reconciliation); tests/ui/config/modelOneshot.test.ts fails on this host on pristine main too (happy-dom/Lottie environment quirk). Everything else green locally including make static-check.

Risks

Highest-risk areas are the moved ordering invariants: pre-start abort registration before any await, providers-config snapshot pinning before thinking/tools identity resolution, placeholder append before startStream with delete-on-abort, and the shared mutable refs (advisorTranscriptRef, toolSearchRuntime.state, activeTurnThinkingOverride). These are exercised by the migrated unit suites and the UAT interrupt/override scenarios. The OAuth live-binding read and the CLI streamManager wiring are the two spots where behavior intentionally diverged from the intermediate refactor state (both were regressions caught before this PR).


Generated with xum • Model: anthropic:claude-fable-5 • Thinking: xhigh • Cost: $85.95

Xum acted on Mike's behalf.

Generated with xum • Model: openai:gpt-5.6-sol • Thinking: high • Cost: .41

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high costs=3.41 -->
Generated with xum • Model: openai:gpt-5.6-sol • Thinking: high • Cost: .41

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high costs=3.41 -->
Generated with xum • Model: openai:gpt-5.6-sol • Thinking: high • Cost: .41

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high costs=3.41 -->
_Generated with `xum` • Model: `openai:gpt-5.6-sol` • Thinking: `high`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high -->
---

_Generated with �0xum�0 • Model: �0openai:gpt-5.6-sol�0 • Thinking: �0high�0 • Cost: �0�40.00�0_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high costs=0.00 -->
The builder only copied codexOauthService/coderOauthService into
ProviderModelFactory at build() time, so AIService.createModel callers
(branch summaries, refinement, status generation) hit 'Codex OAuth
service not initialized' before any turn ran. The factory now reads the
shared bindings object at use time. Also restores the slow-startup
diagnostic on the caller's error path by writing logSlowStreamStartup
back into startupState, and fixes lint in the migrated tests.
AgentSession now asserts stream-lifecycle access on its engine seam, and
WorkspaceService reads delegated-turn stream info from the engine. Share
one createStreamLifecycleMocks() harness helper across the hand-rolled
AIService mocks (file-specific overrides still win), make the harness
report failures as Err results like the real implementations, and give
the archive-hooks suite an engine stub for the stream exemption.
…tion

Delete the 37 single-line getters/forwarders TurnRequestBuilder grew
during the extraction (direct this.dependencies access instead), reuse
the prepared request's first-step rebuild closure rather than re-wrapping
it, share markProviderMetadataCostsIncluded from streamManager, and
collapse AgentSession.isAiStreaming to the asserted engine reference.
Pass streamManager into the CLI AgentSession constructions ('xum run'
and 'xum workflow' would fail the new lifecycle assert without it, since
AIService no longer carries getStreamInfo/replayStream). Unexport ten
module-private types, fix assert messages and comments still naming
AIService as the request builder, and drop narrative comments.
@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: ecf7ee0758

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: dc0a4908c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 4053219246

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 4053219246

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant