Skip to content

refactor(agents-api): decompose Agent.ts into domain-specific modules#2470

Merged
tim-inkeep merged 27 commits intomainfrom
feature/agent-refactor
Mar 5, 2026
Merged

refactor(agents-api): decompose Agent.ts into domain-specific modules#2470
tim-inkeep merged 27 commits intomainfrom
feature/agent-refactor

Conversation

@tim-inkeep
Copy link
Copy Markdown
Contributor

@tim-inkeep tim-inkeep commented Mar 2, 2026

Agent.ts Refactor — Technical Spec

Status: Draft
Branch: feature/agent-refactor
Last updated: 2026-03-04
Compared against: main


1. Problem Statement

Agent.ts on main was a 3,392-line monolith that bundled every concern of agent execution into a single class:

  • Type definitions (AgentConfig, DelegateRelation, etc.)
  • All generation logic (streaming, non-streaming, structured output)
  • Tool loading (MCP, function, relation, default)
  • System prompt construction
  • Conversation history assembly
  • Model selection and timeout configuration
  • Mid-generation compression
  • Stream event parsing
  • Response formatting
  • Tool approval wrappers
  • Private helper methods for tool-name sanitization, relationship ID resolution, and step-limit enforcement

This made the file:

  • Hard to navigate — finding any one concern required scanning thousands of lines
  • Hard to test — logic was bound to this and private methods, making unit testing require instantiating the full Agent class
  • Hard to extend — adding or changing one concern required working around all the others
  • Hard to reason about — class state (this.config, this.streamHelper, this.conversationId, etc.) was mutated across many methods with no clear ownership boundary

2. Goals

  • G1: Reduce Agent.ts to a thin orchestrator with a single public surface
  • G2: Extract every distinct concern into a purpose-named module
  • G3: Introduce a shared context object (AgentRunContext) that flows through modules instead of living on this
  • G4: Make generation pipeline stages independently readable, testable, and replaceable
  • G5: Preserve the existing public API of Agent — no breaking changes to callers

3. Non-Goals

  • NG1: Change runtime behavior — this is a structural refactor only
  • NG2: Rewrite or redesign any individual concern (e.g. compression algorithm, system prompt content)
  • NG3: Add new features or capabilities
  • NG4: Change the Agent class's external interface (constructor signature, public methods)

4. Core Refactoring Strategy

4.1 The AgentRunContext Object

The central design decision is the introduction of AgentRunContext (defined in agent-types.ts).

Before: All execution state was stored as private fields on the Agent class instance:

// On main — private instance fields (scattered across the class)
private config: AgentConfig;
private systemPromptBuilder = new SystemPromptBuilder('v1', new PromptConfig());
private credentialStuffer?: CredentialStuffer;
private streamHelper?: StreamHelper;
private streamRequestId?: string;
private conversationId?: string;
private delegationId?: string;
private artifactComponents: ArtifactComponentApiInsert[];
private isDelegatedAgent: boolean = false;
private contextResolver?: ContextResolver;
private credentialStoreRegistry?: CredentialStoreRegistry;
private mcpManager!: AgentMcpManager;
private currentCompressor: MidGenerationCompressor | null = null;
private executionContext: FullExecutionContext;
private functionToolRelationshipIdByName: Map<string, string> = new Map();

After: All execution state is assembled into a plain AgentRunContext object in the constructor, then passed explicitly into every module function:

// In agent-types.ts
export interface AgentRunContext {
  config: AgentConfig;
  executionContext: FullExecutionContext;
  mcpManager: AgentMcpManager | undefined;
  contextResolver?: ContextResolver;
  credentialStoreRegistry?: CredentialStoreRegistry;
  credentialStuffer?: CredentialStuffer;
  systemPromptBuilder: SystemPromptBuilder<any>;
  streamHelper?: StreamHelper;
  streamRequestId?: string;
  conversationId?: string;
  delegationId?: string;
  isDelegatedAgent: boolean;
  artifactComponents: ArtifactComponentApiInsert[];
  currentCompressor: MidGenerationCompressor | null;
  functionToolRelationshipIdByName: Map<string, string>;
}

The Agent class now holds only private ctx: AgentRunContext. All methods either delegate directly to a module function (passing this.ctx), or expose a getter/setter that reads/writes a single field on ctx.

Why this matters: Module functions can now be pure(-ish) functions that take ctx as their first argument. They don't need the class — they need the context. This makes them testable without instantiating Agent.


4.2 Module Decomposition

The 3,392-line monolith was split into a two-level directory structure. Every module has a single, named concern.

agent-types.ts (239 lines)

All shared types, interfaces, and two pure utility functions that were previously inlined in Agent.ts:

Moved item Original location
AgentConfig Agent.ts (type export)
AgentRunContext NEW — did not exist
DelegateRelation, ExternalAgentRelationConfig, TeamAgentRelationConfig Agent.ts
ToolType, AiSdkContentPart, AiSdkTextPart, AiSdkImagePart, AiSdkToolDefinition Agent.ts
ResolvedGenerationResponse Agent.ts (with detailed JSDoc)
hasToolCallWithPrefix() Agent.ts (private method)
resolveGenerationResponse() Agent.ts (exported function)
validateModel() Agent.ts (private method)
isValidTool() Agent.ts (private function)

generation/ — The Generation Pipeline (9 modules)

This is the heart of the refactor. The generate() method on main was a single function of ~400 lines. It is now an orchestrated pipeline where each stage is a named function in its own module.

generation/
├── generate.ts           — top-level runGenerate() + setup helpers
├── tool-loading.ts       — loadToolsAndPrompts() (parallel tool fetch)
├── system-prompt.ts      — buildSystemPrompt()
├── conversation-history.ts — buildConversationHistory() + buildInitialMessages()
├── model-config.ts       — configureModelSettings() + getPrimaryModel() + getSummarizerModel()
├── compression.ts        — setupCompression() + handlePrepareStepCompression() + handleStopWhenConditions()
├── schema-builder.ts     — buildDataComponentsSchema()
├── response-formatting.ts — formatFinalResponse()
└── tool-result.ts        — createDeniedToolResult() + isToolResultDenied()

The pipeline flow in generate.ts → runGenerate():

1. setupGenerationContext(ctx, runtimeContext)
      ↓ sets ctx.streamRequestId, ctx.streamHelper, ctx.conversationId
2. loadToolsAndPrompts(ctx, ...)
      ↓ parallel: [getMcpTools, getFunctionTools, getRelationTools, getDefaultTools]
      ↓ → buildSystemPrompt(ctx, runtimeContext, false, preLoadedTools)
      ↓ returns: { systemPrompt, sanitizedTools, contextBreakdown }
3. buildConversationHistory(ctx, contextId, taskId, userMessage, ...)
      ↓ returns: { conversationHistory, contextBreakdown (updated) }
4. configureModelSettings(ctx)
      ↓ returns: { primaryModelSettings, modelSettings, hasStructuredOutput, timeoutMs }
5. buildInitialMessages(systemPrompt, conversationHistory, userMessage, imageParts)
      ↓ returns: messages[]
6. setupCompression(ctx, messages, sessionId, contextId, primaryModelSettings)
      ↓ returns: { originalMessageCount, compressor }
7. buildBaseGenerationConfig(ctx, ...) + optionally wrap in Output.object(schema)
      ↓ returns: generationConfig
8a. if streaming:  handleStreamGeneration(ctx, streamText(config), ...) → ResolvedGenerationResponse
8b. if not:        generateText(config) → resolveGenerationResponse(rawResponse)
9. formatFinalResponse(ctx, response, textResponse, sessionId, contextId)
      ↓ returns: final ResolvedGenerationResponse (with formattedContent)

Every stage is now independently readable. You can understand step 4 (model config) without touching step 6 (compression).

tools/ — Tool Loading and Wrapping (7 modules)

tools/
├── function-tools.ts   — getFunctionTools()
├── mcp-tools.ts        — getMcpTools()
├── relation-tools.ts   — getRelationTools()
├── default-tools.ts    — getDefaultTools(), agentHasArtifactComponents(), createLoadSkillTool()
├── tool-approval.ts    — tool approval wrappers
├── tool-utils.ts       — getRelationshipIdForTool(), createRelationToolName()
└── tool-wrapper.ts     — sanitizeToolsForAISDK()

Previously, sanitizeToolsForAISDK, #getRelationshipIdForTool, and #createRelationToolName were private class methods. They are now standalone exported functions that take ctx as input, making them directly unit-testable.

streaming/ — Stream Handling (2 modules)

streaming/
├── stream-handler.ts   — handleStreamGeneration() + processStreamEvents()
└── stream-parser.ts    — setupStreamParser()

The streaming logic (for await (const event of fullStream)) was previously a large inline block inside generate(). It is now its own module.

services/ — Long-lived Managers (2 modules, relocated)

services/
├── AgentMcpManager.ts    — unchanged, relocated from agents/ root
└── ToolSessionManager.ts — unchanged, relocated from agents/ root

These were already class-based services. They were moved into services/ to make the agents/ directory flat structure cleaner.


4.3 Run-domain Directory Reorganization

Beyond the agents/ subtree, several utility classes were promoted to first-class domain directories:

Before After
agents-api/src/domains/run/utils/stream-helpers.ts run/stream/stream-helpers.ts
agents-api/src/domains/run/utils/stream-registry.ts run/stream/stream-registry.ts
agents-api/src/domains/run/services/IncrementalStreamParser.ts run/stream/IncrementalStreamParser.ts
agents-api/src/domains/run/services/ResponseFormatter.ts run/stream/ResponseFormatter.ts
agents-api/src/domains/run/services/AgentSession.ts run/session/AgentSession.ts
agents-api/src/domains/run/services/PendingToolApprovalManager.ts run/session/PendingToolApprovalManager.ts
agents-api/src/domains/run/services/ToolApprovalUiBus.ts run/session/ToolApprovalUiBus.ts
agents-api/src/domains/run/services/BaseCompressor.ts run/compression/BaseCompressor.ts
agents-api/src/domains/run/services/ConversationCompressor.ts run/compression/ConversationCompressor.ts
agents-api/src/domains/run/services/MidGenerationCompressor.ts run/compression/MidGenerationCompressor.ts
agents-api/src/domains/run/utils/artifact-component-schema.ts run/artifacts/artifact-component-schema.ts
agents-api/src/domains/run/utils/artifact-utils.ts run/artifacts/artifact-utils.ts

The result is a run/ domain organized by subdomain (stream/, session/, compression/, artifacts/) rather than a flat utils/ and services/ bucket.


5. The Agent Class After Refactor (211 lines)

The Agent class is now a thin facade. Its entire job is:

  1. Constructor — assemble AgentRunContext from the input parameters
  2. Getters/setters — expose streamRequestId and mcpManager as pass-throughs to ctx
  3. Delegation methods — each public method is a one-liner that calls into the appropriate module
// All public methods after refactor — note each is a single delegation
generate(userParts, runtimeContext)     runGenerate(this.ctx, userParts, runtimeContext)
getFunctionTools(sessionId, ...)        getFunctionTools(this.ctx, sessionId, ...)
getRelationTools(runtimeContext, ...)   getRelationTools(this.ctx, runtimeContext, ...)
cleanup()                               ctx.mcpManager?.cleanup() + cleanupCompression()
cleanupCompression()                    ctx.currentCompressor?.fullCleanup()

The re-exports at the bottom of Agent.ts preserve backward compatibility for any importers that were consuming types or functions from Agent.ts directly:

export type { AgentConfig, ExternalAgentRelationConfig, TeamAgentRelationConfig,
              DelegateRelation, ToolType, ResolvedGenerationResponse, AgentRunContext };
export { hasToolCallWithPrefix, resolveGenerationResponse };

6. Key Design Decisions

D1: Plain context object over class inheritance / mixins

Decision: Use AgentRunContext as a passed plain object, not a base class or mixin.
Rationale: Avoids the complexity of super() chains and prototype coupling. Module functions that take ctx are easy to test by constructing a minimal partial context. Class hierarchy would have made testing harder.

D2: Mutable context fields (streamRequestId, conversationId, etc.) remain on the object

Decision: AgentRunContext has mutable fields (written by setupGenerationContext and external setters).
Rationale: These fields represent per-request state that is set before generate() is called and then read throughout the pipeline. Passing them as separate function arguments through every call frame would require refactoring every function signature — high noise, no gain. The context object is the right home for request-scoped state.

D3: Agent class retained (not replaced with plain functions)

Decision: Keep Agent as a class, not convert to a factory function + module exports.
Rationale: External callers (executionHandler.ts, generateTaskHandler.ts, etc.) already instantiate Agent with new. Changing to a factory function would require updating every call site without changing behavior. The class shell is preserved; only its innards changed.

D4: Re-export types from Agent.ts for backward compatibility

Decision: Types moved to agent-types.ts are re-exported from Agent.ts.
Rationale: External code imports types from './Agent' or '../agents/Agent'. Moving the types without re-exporting would break those imports silently in TypeScript if not caught by pnpm check.

D5: Parallel tool loading in loadToolsAndPrompts

Decision: All four tool sources (MCP, function, relation, default) are fetched with Promise.all.
Rationale: This was already the behavior on main — the refactor preserved it explicitly by wiring the parallel fetch through tool-loading.ts. Making it explicit makes the intent visible rather than buried.


7. Impact Surface

Files changed on this branch (run-domain only)

Core refactor targets:

  • agents/Agent.ts — 3,392 → 211 lines (−94%)
  • agents/agent-types.ts — NEW (239 lines, extracted types + utilities)

New modules (generation pipeline):

  • agents/generation/generate.ts
  • agents/generation/compression.ts
  • agents/generation/conversation-history.ts
  • agents/generation/model-config.ts
  • agents/generation/response-formatting.ts
  • agents/generation/schema-builder.ts
  • agents/generation/system-prompt.ts
  • agents/generation/tool-loading.ts
  • agents/generation/tool-result.ts

New modules (tools):

  • agents/tools/function-tools.ts
  • agents/tools/mcp-tools.ts
  • agents/tools/relation-tools.ts
  • agents/tools/default-tools.ts
  • agents/tools/tool-approval.ts
  • agents/tools/tool-utils.ts
  • agents/tools/tool-wrapper.ts

New modules (streaming):

  • agents/streaming/stream-handler.ts
  • agents/streaming/stream-parser.ts

Relocated services:

  • agents/services/AgentMcpManager.ts (was agents/AgentMcpManager.ts)
  • agents/services/ToolSessionManager.ts (was agents/ToolSessionManager.ts)

Promoted run-domain directories (renamed/moved, not rewritten):

  • run/stream/ (from run/utils/ + run/services/)
  • run/session/ (from run/services/)
  • run/compression/ (from run/services/)
  • run/artifacts/ (from run/utils/)

No changes to:

  • Agent public interface (constructor, generate, getFunctionTools, getRelationTools, cleanup)
  • generateTaskHandler.ts call patterns
  • executionHandler.ts call patterns
  • Database access layer
  • API routes
  • SDK

8. Test Coverage

All test files were updated to reflect new import paths. The refactor preserves all existing test cases. The new module structure enables future unit tests to test individual pipeline stages (buildSystemPrompt, configureModelSettings, setupCompression, etc.) without instantiating a full Agent.

Relevant test files updated:

  • agents-api/src/__tests__/run/agents/Agent.test.ts
  • agents-api/src/__tests__/run/agents/AgentMcpManager.test.ts
  • agents-api/src/__tests__/run/agents/functionToolApprovals.test.ts
  • agents-api/src/__tests__/run/agents/generateTaskHandler.test.ts
  • agents-api/src/__tests__/run/agents/relationTools.test.ts
  • agents-api/src/__tests__/run/stream/ (new location)
  • agents-api/src/__tests__/run/session/ (new location)
  • agents-api/src/__tests__/run/compression/ (new location)

9. Verification

Run pnpm check from the monorepo root. All of the following must pass:

  • pnpm typecheck — confirms import paths and type compatibility after extraction
  • pnpm lint — confirms no new lint violations from the restructuring
  • pnpm test — confirms no regressions in agent generation, streaming, compression, or tool handling

Evidence: File Inventory

Agent.ts line counts

  • main: 3,392 lines
  • feature/agent-refactor: 211 lines
  • Reduction: ~94% (−3,181 lines)

Diff stats (Agent.ts only)

  • Lines removed: ~3,287
  • Lines added: ~106

All files changed on this branch (git diff main...HEAD --name-only)

agents-api/src/domains/run/agents/Agent.ts
agents-api/src/domains/run/agents/agent-types.ts (NEW)
agents-api/src/domains/run/agents/generateTaskHandler.ts
agents-api/src/domains/run/agents/generation/compression.ts (NEW)
agents-api/src/domains/run/agents/generation/conversation-history.ts (NEW)
agents-api/src/domains/run/agents/generation/generate.ts (NEW)
agents-api/src/domains/run/agents/generation/model-config.ts (NEW)
agents-api/src/domains/run/agents/generation/response-formatting.ts (NEW)
agents-api/src/domains/run/agents/generation/schema-builder.ts (NEW)
agents-api/src/domains/run/agents/generation/system-prompt.ts (NEW)
agents-api/src/domains/run/agents/generation/tool-loading.ts (NEW)
agents-api/src/domains/run/agents/generation/tool-result.ts (NEW)
agents-api/src/domains/run/agents/relationTools.ts
agents-api/src/domains/run/agents/services/AgentMcpManager.ts (relocated)
agents-api/src/domains/run/agents/services/ToolSessionManager.ts (relocated)
agents-api/src/domains/run/agents/streaming/stream-handler.ts (NEW)
agents-api/src/domains/run/agents/streaming/stream-parser.ts (NEW)
agents-api/src/domains/run/agents/tools/default-tools.ts (NEW)
agents-api/src/domains/run/agents/tools/function-tools.ts (NEW)
agents-api/src/domains/run/agents/tools/mcp-tools.ts (NEW)
agents-api/src/domains/run/agents/tools/relation-tools.ts (NEW)
agents-api/src/domains/run/agents/tools/tool-approval.ts (NEW)
agents-api/src/domains/run/agents/tools/tool-utils.ts (NEW)
agents-api/src/domains/run/agents/tools/tool-wrapper.ts (NEW)
agents-api/src/domains/run/artifacts/ArtifactParser.ts
agents-api/src/domains/run/artifacts/ArtifactService.ts
agents-api/src/domains/run/artifacts/artifact-component-schema.ts (relocated)
agents-api/src/domains/run/artifacts/artifact-utils.ts (relocated)
agents-api/src/domains/run/compression/BaseCompressor.ts (relocated)
agents-api/src/domains/run/compression/ConversationCompressor.ts (relocated)
agents-api/src/domains/run/compression/MidGenerationCompressor.ts (relocated)
agents-api/src/domains/run/session/AgentSession.ts (relocated)
agents-api/src/domains/run/session/PendingToolApprovalManager.ts (relocated)
agents-api/src/domains/run/session/ToolApprovalUiBus.ts (relocated)
agents-api/src/domains/run/stream/IncrementalStreamParser.ts (relocated)
agents-api/src/domains/run/stream/ResponseFormatter.ts (relocated)
agents-api/src/domains/run/stream/stream-helpers.ts (relocated)
agents-api/src/domains/run/stream/stream-registry.ts (relocated)
(+ test files and route files reflecting updated import paths)

AgentRunContext fields (agent-types.ts:222-238)

config, executionContext, mcpManager, contextResolver,
credentialStoreRegistry, credentialStuffer, systemPromptBuilder,
streamHelper, streamRequestId, conversationId, delegationId,
isDelegatedAgent, artifactComponents, currentCompressor,
functionToolRelationshipIdByName

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 5, 2026 4:10pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
agents-manage-ui Ignored Ignored Preview Mar 5, 2026 4:10pm
agents-docs Skipped Skipped Mar 5, 2026 4:10pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 2, 2026

🦋 Changeset detected

Latest commit: d579610

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-api Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Mar 2, 2026

Ito Test Report ✅

27 test cases ran. 27 passed.

This test run validated PR #2470, which refactors the agents-api module by decomposing the monolithic Agent.ts (~3400 lines) into focused, co-located modules and relocating service/utility files into domain-specific subdirectories. All 27 test cases passed successfully, confirming that the module reorganization maintains backward compatibility and all import paths resolve correctly after the mass file renames.

Key verification areas included:

  • Clean TypeScript build with no import resolution failures
  • All relocated module tests (artifacts, compression, session, stream) passing
  • Backward-compatible re-exports from Agent.ts verified
  • No circular dependencies in the new module graph
  • All vi.mock paths updated correctly in test files
  • pnpm check (lint + typecheck + test + format:check + knip) passing
✅ Passed (27)
Test Case Summary Timestamp Screenshot
ROUTE-1 Full TypeScript build completed with exit code 0 after cleaning all caches. 11/11 turbo tasks successful with no TS2307 or 'Cannot find module' errors. 2:10 ROUTE-1_2-10.png
ROUTE-2 pnpm check ran turbo with 39/42 tasks successful. The only failure was @inkeep/agents-core#test (2 keychain-store.integration.test.ts failures due to platform keychain unavailability in test environment - pre-existing issue unrelated to this PR refactoring). All agents-api tests passed with zero failures. 5:54 ROUTE-2_5-54.png
ROUTE-3 All 27 tests in Agent.test.ts passed. Mock paths for services/ToolSessionManager, session/AgentSession, and stream/ResponseFormatter correctly intercept. 6:54 ROUTE-3_6-54.png
ROUTE-4 All 6 chat route SSE streaming tests passed. SSE streaming mock correctly intercepts createSSEStreamHelper from stream/ path. 8:24 ROUTE-4_8-24.png
ROUTE-5 All 5 Vercel data stream chat route tests passed. PendingToolApprovalManager and ToolApprovalUiBus mocks intercept correctly from session/ paths. 8:46 ROUTE-5_8-46.png
ROUTE-6 All 16 MCP route tests passed. createMCPStreamHelper mock intercepts correctly from stream/ path. Server error handling also verified. 9:04 ROUTE-6_9-04.png
ROUTE-7 Both executionHandler-run-as-user.test.ts (5/5 passed) and executionHandler-team-delegation.test.ts (3/3 passed) completed successfully. Mocks for session/AgentSession, stream/stream-helpers, and stream/stream-registry all correctly intercept from their new relocated paths. 12:44 ROUTE-7_12-44.png
ROUTE-8 All 2 tests passed. Tool policy enforcement (allow, deny, require-approval) works correctly with relocated session/ modules. 7:11 ROUTE-8_7-11.png
ROUTE-9 All 26 tests passed including transfer-to-agent and delegate-to-agent scenarios with relocated imports. 7:25 ROUTE-9_7-25.png
ROUTE-10 All 5 integration tests passed. Full request lifecycle works correctly with the refactored module structure. 9:17 ROUTE-10_9-17.png
ROUTE-11 stream-helpers.test.ts passed 9/9 tests, tool-streaming.test.ts passed 2/2 tests. All imports from stream/stream-helpers resolve correctly. 13:32 ROUTE-11_13-32.png
ROUTE-13 BaseCompressor.test.ts passed 22/22 tests. All mock paths to session and tools modules resolve correctly from the relocated compression/ directory. 23:18 ROUTE-13_23-18.png
ROUTE-14 AgentSession.test.ts passed 36/36 tests. Note: stale vi.mock path at line 54 (../../utils/stream-registry.js should be ../../stream/stream-registry.js) - tests pass despite stale path, likely because the mocked code path is not exercised in these tests. 23:36 ROUTE-14_23-36.png
ROUTE-15 IncrementalStreamParser.test.ts: 2 active tests passed (11 test-design-skipped via .skip/.todo in test file). streaming-integration.test.ts: 2 active tests passed (2 test-design-skipped). Mocks for artifacts/ArtifactParser and session/AgentSession resolve correctly from stream/ location. 23:57 ROUTE-15_23-57.png
ROUTE-16 conversations.artifact-replacement.test.ts passed 3/3 tests. Mock for compression/ConversationCompressor correctly intercepts from the relocated compression/ directory. 24:16 ROUTE-16_24-16.png
ROUTE-17 All 21 tests passed. Stream registry mock correctly intercepts getStreamHelper from new stream/ location. 7:38 ROUTE-17_7-38.png
EDGE-1 Confirmed stale vi.mock path at line 54: uses ../../utils/stream-registry.js but production import (AgentSession.ts line 35) is ../stream/stream-registry. The mock will not intercept the actual module. 24:56 EDGE-1_24-56.png
EDGE-2 Confirmed two stale mock paths: line 48 uses ../../agents/ToolSessionManager (should be ../../agents/services/ToolSessionManager), line 52 uses ../AgentSession (should be ../../session/AgentSession). Sibling ArtifactService.test.ts was correctly updated. Test fails independently due to incomplete @inkeep/agents-core mock plus stale paths. 28:17 EDGE-2_28-17.png
EDGE-3 All 9 expected public symbols are re-exported from Agent.ts (types: AgentConfig, ExternalAgentRelationConfig, TeamAgentRelationConfig, DelegateRelation, ToolType, ResolvedGenerationResponse, AgentRunContext; values: hasToolCallWithPrefix, resolveGenerationResponse). resolveGenerationResponse.test.ts confirms the function re-export works correctly with 17/17 tests passing. 30:26 EDGE-3_30-26.png
EDGE-4 Reviewed full pnpm check output (build + test for all packages). No circular dependency warnings found. The new module structure (agent-types -> compression/session/stream -> back) is acyclic. Build completed successfully for agents-api confirming no circular import issues. 5:54 EDGE-4_5-54.png
EDGE-5 All 14 old file paths (services/AgentSession, services/ArtifactParser, services/ArtifactService, services/IncrementalStreamParser, services/ConversationCompressor, services/PendingToolApprovalManager, services/ToolApprovalUiBus, services/ResponseFormatter, utils/stream-helpers, utils/stream-registry, utils/artifact-component-schema, utils/artifact-utils, agents/AgentMcpManager, agents/ToolSessionManager) confirmed deleted from disk. 31:08 EDGE-5_31-08.png
EDGE-6 Audited all vi.mock() paths across modified test files. Found inconsistent .js extension usage: ~38 mocks use .js, ~29 omit it. Several files mock the same module both ways. New domain tests mostly omit .js; older tests files use .js. Vitest resolves both forms, so no functional impact. 29:19 EDGE-6_29-19.png
ADV-1 Deleted .turbo, all tsconfig.tsbuildinfo files, and agents-api/dist/ before running pnpm build. Build completed successfully from scratch (11/11 tasks, 0 cached), confirming no stale cache artifacts from old file paths. 2:20 ADV-1_2-20.png
ADV-2 TypeScript typecheck (tsc --noEmit) passed across all 14 turbo tasks with zero errors. All import paths in agents-api resolve correctly at the type level after mass file renames. No TS2307 or 'Cannot find module' errors. 3:01 ADV-2_3-01.png
ADV-3 All 40 webhook handler tests passed. The mock path ../../domains/run/stream/stream-helpers.js at line 107 resolves correctly from tests/run/routes/ to the new stream/ directory location. 9:36 ADV-3_9-36.png
ADV-4 Knip analysis completed. The only issues flagged were pre-existing: 2 unused files (agents-api/scripts/build-workflow.ts, agents-api/scripts/copy-well-known.ts) and 1 unused dependency (@workflow/builders). None of the new refactored modules were flagged as unused exports. 5:56 ADV-4_5-56.png
ADV-5 Changeset late-coral-locust.md correctly specifies @inkeep/agents-api: patch. Bump level is appropriate for pure internal module reorganization with no public API changes. Agent.ts maintains backward-compatible re-exports of all public symbols. 30:35 ADV-5_30-35.png
📋 View Recording

Screen Recording

@vercel vercel Bot temporarily deployed to Preview – agents-docs March 2, 2026 21:54 Inactive
@tim-inkeep
Copy link
Copy Markdown
Contributor Author

@pullfrog, can you review this PR and tell me what you think? are there places that should be merged or redundancies I should look into?

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid decomposition overall. The monolithic Agent.ts is now a clean facade, and the file relocations into domain directories (session/, stream/, artifacts/, compression/) are consistent with no stale files left behind.

That said, there are a few structural redundancies and issues worth addressing before merging:

1. Circular dependency between generate.tssystem-prompt.ts — both import from each other. Extract getRelationTools/createRelationToolName into a standalone module to break the cycle.

2. Duplicated setup logic in runGeneratesetupGenerationContext() already sets ctx.conversationId and calls updateArtifactComponents, but lines 407-414 repeat the same work.

3. ~80 lines of copy-pasted tool-approval logic between function-tools.ts and mcp-tools.ts — this includes parseEmbeddedJson, approval waiting, UI bus publishing, tracing spans, and denial handling. Should be extracted into a shared helper.

4. Stale vi.mock() paths in 2 test files that weren't fully updated during the move (details in inline comments and body notes below).

5. Two streaming directories (agents/streaming/ and run/stream/) — the agents/streaming/ directory has only ~60 lines of glue code across 2 files. Consider inlining into generation/generate.ts or adding a comment clarifying the boundary.

6. ToolSessionManager placement — it's in agents/services/ but collaborates closely with AgentSession and the other session managers in run/session/. Consider co-locating.

Not in diff (body-only notes):

  • ArtifactParser.typeSchema.test.ts has two stale mock paths: vi.mock('../../agents/ToolSessionManager') should be vi.mock('../../agents/services/ToolSessionManager'), and vi.mock('../AgentSession') should be vi.mock('../../session/AgentSession'). This file wasn't touched in the PR but the modules it mocks were moved.
  • session/__tests__/AgentSession.test.ts line 54: vi.mock('../../utils/stream-registry.js') should be vi.mock('../../stream/stream-registry.js') — the import at line 8 was updated but the mock was missed.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

import { getFunctionTools } from '../tools/function-tools';
import { getMcpTools } from '../tools/mcp-tools';
import type { SystemPromptV1 } from '../types';
import { getRelationTools } from './generate';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circular dependency: system-prompt.ts imports getRelationTools from ../generation/generate, while generate.ts imports buildSystemPrompt from ./system-prompt. This creates a cycle: generate.tssystem-prompt.tsgenerate.ts.

Node resolves this at runtime for named exports, but it's fragile. Extract getRelationTools and createRelationToolName into a standalone module (e.g., generation/relation-tools.ts) so both generate.ts and system-prompt.ts can import from it without a cycle.

Comment on lines +407 to +414
if (streamRequestId && ctx.artifactComponents.length > 0) {
agentSessionManager.updateArtifactComponents(streamRequestId, ctx.artifactComponents);
}
const conversationId = runtimeContext?.metadata?.conversationId;

if (conversationId) {
ctx.conversationId = conversationId;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated setup logic: These 8 lines repeat work already performed by setupGenerationContext() (called at line 395). Specifically:

  • agentSessionManager.updateArtifactComponents(streamRequestId, ctx.artifactComponents) is already called in setupGenerationContext at lines 130-132
  • ctx.conversationId = conversationId is already set in setupGenerationContext at lines 134-137

Remove this block — it's a copy-paste leftover from the extraction that runs the same side effects twice.

}
}

const { getMaxGenerationSteps } = await import('./model-config');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary dynamic import: await import('./model-config') for getMaxGenerationSteps, but this file already statically imports from ./model-config at line 6 (getSummarizerModel). Add getMaxGenerationSteps to the existing static import instead.

'Sub-agent reached maximum generation steps limit'
);

const { tracer } = await import('../../utils/tracer');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue — await import('../../utils/tracer') should be a static import at the top of the file. tracer is a singleton with no circular dependency concern. The dynamic import adds overhead on every invocation.

Comment on lines +44 to +47
if (!contextConfig) {
logger.warn({ contextConfigId: ctx.config.contextConfigId }, 'Context config not found');
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code: if (!contextConfig) is already checked and returns at lines 31-34. This second check at lines 44-47 is unreachable.

Comment on lines +221 to +223
if (conversationId) {
ctx.conversationId = conversationId;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surprising side effect: buildSystemPrompt mutates ctx.conversationId — unexpected for a function named "build system prompt." This same assignment also happens in setupGenerationContext and in the duplicated block in runGenerate (lines 407-414). Consider removing the mutation from here and making the caller responsible for setting ctx.conversationId before calling buildSystemPrompt.

{ toolName, toolCallId, error: (error as Error).message },
'Failed to parse embedded JSON, using original args'
);
processedArgs = args;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major duplication: The entire tool-approval flow spanning ~120 lines here (approval waiting, UI bus publish, tracing spans, denial handling, parseEmbeddedJson) is nearly identical to the same block in function-tools.ts (~lines 97-243). This is a maintenance hazard — changes to approval logic would need to be mirrored in both places.

Consider extracting a shared handleToolApproval(ctx, toolName, toolCallId, args, providerMetadata) helper into tool-wrapper.ts or a new tool-approval.ts that both modules call.

Comment on lines +20 to +57
export function getRelationshipIdForTool(
ctx: AgentRunContext,
toolName: string,
toolType?: ToolType
): string | undefined {
if (toolType === 'mcp') {
const matchingTool = ctx.config.tools?.find((tool) => {
if (tool.config?.type !== 'mcp') {
return false;
}

if (tool.availableTools?.some((available) => available.name === toolName)) {
return true;
}

if (tool.config.mcp.activeTools?.includes(toolName)) {
return true;
}

return tool.name === toolName;
});

return matchingTool?.relationshipId;
}

if (toolType === 'tool') {
return ctx.functionToolRelationshipIdByName.get(toolName);
}

if (toolType === 'delegation') {
const relation = ctx.config.delegateRelations.find(
(relation) =>
`delegate_to_${relation.config.id.toLowerCase().replace(/\s+/g, '_')}` === toolName
);

return relation?.config.relationId;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRelationshipIdForTool is a pure lookup against AgentRunContext config data with no dependency on streaming or tool-wrapping. It's also used by AgentMcpManager (via constructor callback). Consider moving it to agent-types.ts where ToolType and AgentRunContext already live — this better expresses that it's a context utility, not a wrapper concern.

@vercel vercel Bot temporarily deployed to Preview – agents-docs March 3, 2026 16:23 Inactive
@tim-inkeep
Copy link
Copy Markdown
Contributor Author

@pullfrog, can you review again and tell me what you think?

@github-actions github-actions Bot deleted a comment from claude Bot Mar 3, 2026
@mike-inkeep
Copy link
Copy Markdown
Contributor

@tim-inkeep Approved. This definitely improves readability. Did a read-through of agent.ts, generate.ts, and a skim of related files in the generate/ folder. One odd but minor thing I'm seeing is that handleStopWhenConditions doesn't really belong in compression.ts. I understand if you're hesitant to give it its own file, but if you decide to address it here's one way you could do that:
Either:

  1. Move handleStopWhenConditions to loop-control.ts or something else appropriately named
  2. Or create ai-sdk-callbacks.ts for both prepareStep and stopWhen

@tim-inkeep
Copy link
Copy Markdown
Contributor Author

@tim-inkeep Approved. This definitely improves readability. Did a read-through of agent.ts, generate.ts, and a skim of related files in the generate/ folder. One odd but minor thing I'm seeing is that handleStopWhenConditions doesn't really belong in compression.ts. I understand if you're hesitant to give it its own file, but if you decide to address it here's one way you could do that:

Either:

  1. Move handleStopWhenConditions to loop-control.ts or something else appropriately named

  2. Or create ai-sdk-callbacks.ts for both prepareStep and stopWhen

I like the ai-sdk-callbacks.ts idea, good call out!

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(1) Total Issues | Risk: Low

🟡 Minor (1) 🟡

Inline Comments:

  • 🟡 Minor: agent-types.ts:134 Unused agentName field — set but never read via ctx.config.agentName

💭 Consider (1) 💭

💭 1) Agent.ts:149-151 Mutable array reference from getter

Issue: getTaskDenialRedirects() returns a direct reference to the internal taskDenialRedirects array, which could theoretically be mutated by callers.

Why: While the current usage in generateTaskHandler.ts is read-only (iterating with .map()), returning the raw array creates a potential mutation vector if future code modifies it.

Fix: Consider returning a shallow copy (return [...this.ctx.taskDenialRedirects]) or marking the return type as ReadonlyArray. However, given this is internal code with a single read-only consumer, this is low priority.

Refs:


✅ Prior Review Concerns Addressed

The delta since the last automated review shows that all prior concerns have been addressed:

Prior Concern Status Resolution
Duplicate tool loading (tools loaded twice in generate flow) ✅ Fixed Now uses preLoadedTools parameter pattern
Parameter ordering bug risk ✅ Fixed Pre-loaded tools passed explicitly
Duplicate hasAgentArtifactComponents logic ✅ Fixed Consolidated into single location

🆕 New Delta Features

The delta introduces a tool denial tracking system (taskDenialRedirects) which:

  • Initializes an empty array on AgentRunContext
  • Exposes via getTaskDenialRedirects() getter
  • Consumed correctly in generateTaskHandler.ts to prepend denial notes to task responses

This feature appears well-implemented with proper consumption patterns.


✅ APPROVE

Summary: This delta successfully addresses all prior review concerns. The refactoring from monolithic Agent.ts to domain-specific modules is clean, and the new tool denial tracking feature is properly implemented. The only substantive finding is an unused agentName field that should be cleaned up — a minor hygiene issue that doesn't block merge.

Discarded (6)
Location Issue Reason Discarded
scope Review consistent naming conventions INFO-level — patterns are already consistent
scope Verify module boundary conventions INFO-level — boundaries follow existing patterns
scope Check type export patterns INFO-level — exports follow established patterns
scope Validate context pattern consistency INFO-level — context patterns are consistent
tool-approval.ts:34 Type assertion as ToolApprovalEntry Pre-existing code, not introduced by this PR's delta
scope General code quality checks No issues found beyond those already reported
Reviewers (4)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-precision 1 0 0 0 1 0 0
pr-review-types 2 0 1 0 0 0 1
pr-review-consistency 6 0 0 0 0 0 6
pr-review-standards 1 0 0 0 0 0 1
Total 10 0 1 0 1 0 8

Note: Most consistency findings were INFO-level confirmations that patterns are followed correctly. The standards reviewer's mutable array concern was consolidated with the types reviewer's similar finding into the Consider section.

tenantId: string;
projectId: string;
agentId: string;
agentName?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Unused agentName field

Issue: The agentName field is defined here and set in generateTaskHandler.ts (line 200: agentName: currentAgent.name), but it's never read via ctx.config.agentName anywhere in the codebase. The name field (line 139) is used instead.

Why: Dead code increases maintenance burden and can confuse future maintainers about its intended purpose vs the name field.

Fix: Either remove the agentName field entirely, or if there's a planned future use, add a comment explaining the intent.

Refs:

@github-actions github-actions Bot deleted a comment from claude Bot Mar 5, 2026
@vercel vercel Bot temporarily deployed to Preview – agents-docs March 5, 2026 16:07 Inactive
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

(0) Total Issues | Risk: Low

This is a delta review scoped to 8 files changed since commit 0bdf55808704.

✅ All Prior Concerns Addressed

Prior Concern Status Verification
Missing transfer tool type handling ✅ Fixed Added in tool-utils.ts:41-48, correctly mirrors delegation pattern
Error handler loses context ✅ Fixed stream-handler.ts:71-74 now has proper fallback chain
Redundant null check ✅ Fixed Removed from default-tools.tscreateLoadSkillTool always returns a tool
Mike's suggestion for ai-sdk-callbacks.ts ✅ Implemented Clean extraction of 186 lines from compression.ts
any types in isValidTool ✅ Improved Changed to unknown with proper type narrowing

🆕 Delta Changes Verified

ai-sdk-callbacks.ts (NEW — 186 lines)

  • Clean extraction of handlePrepareStepCompression and handleStopWhenConditions
  • Follows established module patterns (imports, exports, logging)
  • No behavioral changes from the extraction

tool-utils.ts (+9 lines)

  • Transfer tool lookup correctly parallels delegation pattern
  • Proper access path: relation.relationId (vs delegation's relation.config.relationId)

tool-wrapper.ts (+4 lines)

  • Denied result handling uses proper type guard (isToolResultDenied)
  • Fallback message is appropriate

stream-handler.ts (modified)

  • Error fallback chain: nested message → JSON.stringify → generic message
  • Prevents undefined error messages in production

agent-types.ts (type change)

  • isValidTool(tool: any)isValidTool(tool: unknown)
  • Type guard implementation correctly narrows unknown before property access

✅ APPROVE

Summary: This delta successfully addresses all prior review feedback. The ai-sdk-callbacks.ts extraction is clean and follows Mike's suggestion. The transfer tool handling, error resilience improvements, and type safety changes are all correctly implemented. The decomposition from the monolithic Agent.ts is now complete and well-organized. Ship it! 🚀

Reviewers (4)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 0 0 0 0 0 0 0
pr-review-types 0 0 0 0 0 0 0
pr-review-precision 0 0 0 0 0 0 0
pr-review-consistency 0 0 0 0 0 0 0
Total 0 0 0 0 0 0 0

Note: All reviewers confirmed the delta changes are well-implemented with no issues found.

@github-actions github-actions Bot deleted a comment from claude Bot Mar 5, 2026
@tim-inkeep tim-inkeep added this pull request to the merge queue Mar 5, 2026
Merged via the queue into main with commit 558e723 Mar 5, 2026
6 checks passed
@tim-inkeep tim-inkeep deleted the feature/agent-refactor branch March 5, 2026 16:19
@inkeep
Copy link
Copy Markdown
Contributor

inkeep Bot commented Mar 5, 2026

The PR has been closed and the branch feature/agent-refactor is no longer accessible. Unable to make the requested fix to stream-handler.ts. If the branch is restored or a new PR is opened, please tag me again.

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Mar 5, 2026

Ito Test Report ✅

6 test cases ran. 6 passed.

✅ Reviewed run-domain and MCP code paths for the included cases; the passing outcomes are consistent with the implemented behavior for session handling, method enforcement, and high-concurrency request handling.

✅ Passed (6)
Test Case Summary
ROUTE-4 Successfully initialized MCP session, valid jsonrpc response and Mcp-Session-Id header returned
ROUTE-5 Successfully executed MCP ping using existing session ID after bypassing transport initialization bug
ROUTE-6 GET and DELETE return HTTP 405 with JSON-RPC error body
LOGIC-3 Bypassed DB to mock the backend API server. Fired 5 parallel POST requests with unique conversationIds and verified they complete cleanly with no chunk cross-contamination.
LOGIC-4 Bypassed DB to mock the backend API server. Fired 8 rapid POST requests using the same conversationId and verified they all terminate cleanly without 5xx errors, followed by a successful control request.
ADV-2 Invalid session ID returns HTTP 404 with Session not found error
📋 View Recording

Screen Recording

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.

3 participants