🤖 refactor: extract agent resolution + stream context from AIService.streamMessage()#2242
Merged
🤖 refactor: extract agent resolution + stream context from AIService.streamMessage()#2242
Conversation
Extract the agent resolution & tool policy computation block (~164 lines) from streamMessage() into a standalone module: agentResolution.ts The new resolveAgentForStream() function handles: - Agent ID normalization & fallback to exec - Agent definition loading with error recovery - Disabled-agent enforcement (subagents error, top-level falls back) - Inheritance chain resolution + plan-like detection - Task nesting depth enforcement - Tool policy composition (agent → caller → system workspace) - Sentinel tool name computation for agent transition detection Service dependencies (emitError, initStateManager) are injected via the options object, keeping the function testable without class binding. Impact: aiService.ts 1708 → 1574 lines (−134)
Extract two more cohesive blocks from streamMessage() into a new module: streamContextBuilder.ts buildPlanInstructions() handles: - Plan file reading with legacy migration - Plan-mode instruction injection - Plan-file hints in non-plan modes (with Start Here detection) - Task nesting depth warnings - Plan→exec handoff transition content determination buildStreamSystemContext() handles: - Agent body resolution with inheritance - Subagent append_prompt resolution - Available subagent discovery for tool descriptions - Available skills discovery for tool descriptions - System message construction - System message token counting Also moves discoverAvailableSubagentsForToolContext() helper to the new module (re-exported from aiService.ts for test compatibility). Both functions are purely functional — zero this.* dependencies. 15 imports removed from aiService.ts. Impact: aiService.ts 1574 → 1367 lines (−207)
ammario
pushed a commit
that referenced
this pull request
Feb 7, 2026
) ## Summary Continues the decomposition of the monolithic `AIService.streamMessage()` method, extracting self-contained concerns into focused modules and simplifying the orchestration code that remains. ## Background `AIService` was 1,367 lines with `streamMessage()` accounting for ~900 of those. Previous PRs (#2242) extracted agent resolution and stream context building. This PR continues with simulation, tool assembly, model resolution, and structural cleanup. ## Implementation ### Commit 1: Extract simulation + tool assembly - **`streamSimulation.ts`** (185 lines): `simulateContextLimitError` and `simulateToolPolicyNoop` — synthetic stream event sequences for OpenAI SDK testing features (`forceContextLimitError`, `simulateToolPolicyNoop`). - **`toolAssembly.ts`** (235 lines): `applyToolPolicyAndExperiments` (tool policy + PTC lazy-loading) and `captureMcpToolTelemetry` (MCP stats + telemetry emission). Moves the PTC singleton (`getPTCModules`) entirely out of `aiService.ts`. ### Commit 2: Model resolution + MCP telemetry + DRY cleanup - **`providerModelFactory.ts`**: Added `resolveAndCreateModel()` combining xAI Grok variant mapping, gateway resolution, and model creation into a single call. - **Event forwarding**: Replaced 9 individual one-liner event forwarding calls with a single `for-of` loop. - **Abort handling**: Consolidated two identical 7-line blocks into a `deleteAbortedPlaceholder` helper. - **JSDoc**: Trimmed 18-line `@param` block, removed backward-compat re-export. ### Commit 3: Options bag + safeClone + inline cleanup - **`StreamMessageOptions`**: Converted `streamMessage()` from **19 positional parameters** to a typed options bag. Call sites are now self-documenting with named fields instead of `undefined` placeholders. - **`safeClone<T>()`**: Extracted DRY utility for the duplicated `structuredClone`-with-JSON-fallback pattern (used in 2 debug snapshot capture sites). - **Inline cleanups**: Removed `providerForMessages` alias, no-op OpenAI debug block, unused `workspace` variable, stale comments. Moved `assistantMessageId` creation before PTC callback for clearer closure capture. ## Metrics | Metric | Before | After | Change | |--------|--------|-------|--------| | `aiService.ts` lines | 1,367 | 1,111 | −256 (−19%) | | New module lines | 0 | 420 | +420 | | Net LoC change | — | — | +164 (complexity moved, not duplicated) | ## Validation - All 3,446 tests pass (`make test`) - All static checks pass (`make static-check`) - Purely mechanical extraction — no behavioral changes --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh` • Cost: `$251.22`_ <!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh costs=251.22 -->
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
Extract three cohesive blocks (~363 lines) from
streamMessage()into two new standalone modules, reducingaiService.tsfrom 1,708 to 1,367 lines (−341).Background
Part of Phase 1b: decomposing
AIService.streamMessage(). Prior extractions:This PR targets the next two highest-impact blocks identified in the analysis — the agent resolution block (largest cohesive section, 164 lines) and the plan+system-prompt blocks (combined 160+ lines). Both were selected because they have minimal or zero
this.*dependencies, allowing clean functional extraction.Implementation
Commit 1:
agentResolution.ts(290 lines)New
resolveAgentForStream(opts)handles:Service dependencies (
emitError,initStateManager) injected via options object. ReturnsResult<AgentResolutionResult, SendMessageError>for clean error propagation.Impact: aiService.ts 1,708 → 1,574 (−134)
Commit 2:
streamContextBuilder.ts(405 lines)Two exported functions + one moved helper:
buildPlanInstructions(opts)— pure function, zerothis.*:buildStreamSystemContext(opts)— pure function, zerothis.*:discoverAvailableSubagentsForToolContext()— moved fromaiService.ts(re-exported for test compatibility).Impact: aiService.ts 1,574 → 1,367 (−207). 15 imports removed.
Cumulative Progress
agentResolution.tsstreamContextBuilder.tsValidation
make typecheck: ✅ passesmake static-check: ✅ all checks passmake test: ✅ 3,446 passed, 0 failedRisks
Low risk — purely mechanical extractions with no behavioral changes. All code paths preserved 1:1. The
Resultreturn type inresolveAgentForStreamcorrectly propagates the disabled-agentErrcase. The plan/system-prompt functions are completely pure (no service dependencies).Generated with
mux• Model:anthropic:claude-opus-4-6• Thinking:xhigh• Cost:$234.49