Skip to content

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

Open
tim-inkeep wants to merge 22 commits intomainfrom
feature/agent-refactor
Open

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

Conversation

@tim-inkeep
Copy link
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

vercel bot commented Mar 2, 2026

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

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
agents-api Ignored Ignored Preview Mar 3, 2026 7:48pm
agents-docs Ignored Ignored Preview Mar 3, 2026 7:48pm
agents-manage-ui Ignored Ignored Preview Mar 3, 2026 7:48pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Mar 2, 2026

🦋 Changeset detected

Latest commit: 5886d9a

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

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
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
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
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
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
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
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
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
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
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
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
Contributor Author

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

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 3, 2026

Addressed both review feedback items from @tim-inkeep:

  1. Removed any types from buildBaseGenerationConfig — replaced with Record<string, unknown>, unknown[], ToolSet, and MidGenerationCompressor | null. Return type changed from object to Record<string, unknown>. Callback parameter types updated from any[] to unknown[].

  2. Reused resolveGenerationResponse in stream-handler.ts — replaced manual Promise.all resolution of steps/text/finishReason/output with a call to the shared resolveGenerationResponse helper. Updated return type to ResolvedGenerationResponse. Removed part: any annotation (TypeScript infers the type from getCollectedParts()).

Both threads resolved. Typecheck passes.

Pullfrog  | View workflow run | Using Claude Code | Triggered by Pullfrogpullfrog.com𝕏

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 3, 2026

activity timeout: no output for 300s

Pullfrog  | Rerun failed job ➔View workflow run | Triggered by Pullfrogpullfrog.com𝕏

@vercel vercel bot temporarily deployed to Preview – agents-docs March 3, 2026 18:42 Inactive
@vercel vercel bot temporarily deployed to Preview – agents-docs March 3, 2026 18:43 Inactive
messages: any[],
sanitizedTools: any,
compressor: any,
modelSettings: Record<string, unknown>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@pullfrog, do we have to use unknown here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@pullfrog do we have to use unknown here?

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 3, 2026

Error

agent completed without reporting progress

Pullfrog  | Rerun failed job ➔View workflow run | Triggered by Pullfrogpullfrog.com𝕏

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 3, 2026

Leaping into action...

Pullfrog  | View workflow run | Using Claude Code | Triggered by Pullfrogpullfrog.com𝕏

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 3, 2026

Decomposes the monolithic Agent.ts (~3400 lines) into focused, domain-specific modules under agents/ and reorganizes shared run/ utilities into co-located subdirectories.

Key changes:

  • Agent.ts decomposition — extracted generation logic (generate.ts, system-prompt.ts, model-config.ts, conversation-history.ts, compression.ts, tool-result.ts, response-formatting.ts, schema-builder.ts, tool-loading.ts), streaming (stream-handler.ts, stream-parser.ts), and tool management (function-tools.ts, mcp-tools.ts, tool-wrapper.ts, default-tools.ts, tool-approval.ts, relation-tools.ts, tool-utils.ts) into new subdirectories under agents/
  • Shared types — new agent-types.ts consolidates type definitions previously scattered through Agent.ts
  • Service relocationAgentMcpManager.ts and ToolSessionManager.ts moved into agents/services/
  • run/ directory reorganization — flat services/ and utils/ files relocated into domain directories: artifacts/, compression/, session/, stream/
  • Import updates — all 16 test files and ~10 consumer modules updated to reflect new paths
  • Template bumpcreate-agents-template dependency versions updated

Pullfrog  | View workflow run | Using Claude Code | Triggered by Pullfrogpullfrog.com𝕏

@github-actions github-actions bot deleted a comment from claude bot Mar 3, 2026
Copy link
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.

Low urgency — clean decomposition, but a few correctness issues to address before merge.

The refactoring is architecturally sound: the module graph is acyclic, the Agent.ts barrel re-exports everything consumers need, and AgentRunContext correctly carries all fields. Two issues need fixes: (1) a parameter ordering bug in system-prompt.ts's fallback tool-loading path, and (2) duplicate hasAgentArtifactComponents logic with divergent error-handling. The rest is minor type-safety improvements and a pre-existing test mock path bug worth fixing while these paths are being touched.

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

Comment on lines +310 to +313
const rawSchema = toolDef.inputSchema as Record<string, unknown>;
let inputSchema: ReturnType<typeof z.fromJSONSchema> | ReturnType<typeof jsonSchema>;
try {
inputSchema = override.schema
? z.fromJSONSchema(override.schema)
: (toolDef as any).inputSchema;
inputSchema = override.schema ? z.fromJSONSchema(override.schema) : jsonSchema(rawSchema);
Copy link
Contributor

Choose a reason for hiding this comment

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

toolDef.inputSchema is typed as unknown on AiSdkToolDefinition, but cast to Record<string, unknown> without a guard. If inputSchema is undefined (which the type allows), jsonSchema(undefined as any) will throw.

Add a fallback:

const rawSchema = (toolDef.inputSchema ?? {}) as Record<string, unknown>;

Also worth verifying: previously the non-override path used (toolDef as any).inputSchema raw. Now jsonSchema(rawSchema) produces an AI SDK wrapper that disables argument validation by default (unlike Zod schemas). If MCP tools previously relied on Zod validation from the raw schema, this is a behavioral change. If inputSchema is always a plain JSON Schema object here (likely), this is fine.


// Mock stream helpers
vi.mock('../../domains/run/utils/stream-helpers.js', () => ({
vi.mock('../../domains/run/stream/stream-helpers.js', () => ({
Copy link
Contributor

Choose a reason for hiding this comment

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

Pre-existing bug carried forward: this test is at __tests__/run/routes/webhooks.test.ts, so ../../domains/run/stream/stream-helpers.js resolves to __tests__/domains/run/stream/... which doesn't exist. Should be ../../../domains/run/stream/stream-helpers.js (three ../ levels), matching the pattern in chat.test.ts line 73. Since this line is being touched anyway, worth fixing now.

Suggested change
vi.mock('../../domains/run/stream/stream-helpers.js', () => ({
vi.mock('../../../domains/run/stream/stream-helpers.js', () => ({

@github-actions github-actions bot deleted a comment from claude bot Mar 3, 2026
Copy link
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

(5) Total Issues | Risk: Low

This is a well-executed structural refactoring that successfully decomposes a monolithic 3400-line Agent.ts into focused, domain-specific modules. The module graph is acyclic, the facade pattern is correctly implemented, and all test mock paths have been properly updated. The issues identified are minor correctness and consistency improvements — no blocking concerns.

🟠⚠️ Major (2) 🟠⚠️

🟠 1) tool-utils.ts:32-39 Missing transfer tool type handling

Issue: getRelationshipIdForTool handles 'mcp', 'tool', and 'delegation' types but has no case for 'transfer', despite ToolType including 'transfer' (agent-types.ts:194) and relation-tools.ts:39 passing 'transfer' to wrapToolWithStreaming.

Why: Transfer tool relationship IDs will always return undefined, causing incomplete telemetry tracking and session recording for transfer tool calls. This is a behavioral gap, not just missing code.

Fix: Add a case for toolType === 'transfer' that looks up the relation in ctx.config.transferRelations using the same pattern as delegation.

Refs:

🟠 2) stream-handler.ts:67-73 Error handler loses context when error is not an Error instance

Issue: The error event handler extracts event.error?.error?.message but if that path doesn't exist, it throws new Error(errorMessage) where errorMessage is undefined, producing unhelpful "Error: undefined" messages.

Why: AI SDK errors can have different shapes. This code path may produce generic errors that are impossible to debug in production, especially for edge cases like network failures or malformed responses.

Fix: Add fallback error message extraction: errorObj?.error?.message ?? errorObj?.message ?? String(event.error) ?? 'Stream encountered an unknown error'

Refs:

Inline Comments:

  • 🟠 Major: tool-utils.ts:39 Missing transfer tool type handling
  • 🟠 Major: stream-handler.ts:73 Error handler loses error context

🟡 Minor (3) 🟡

🟡 1) AgentMcpManager.ts:20 Import AgentConfig from agent-types.ts for consistency

Issue: AgentConfig is imported from ../Agent while AiSdkToolDefinition is imported from ../agent-types. Since AgentConfig is defined in agent-types.ts and merely re-exported by Agent.ts, this creates inconsistent import patterns within the same file.

Why: All other files in tools/ and generation/ subdirectories consistently import types from ../agent-types. Aligning this improves consistency and clarifies the canonical type source.

Fix: Change to import type { AgentConfig, AiSdkToolDefinition } from '../agent-types';

Refs:

🟡 2) default-tools.ts:125-126 Redundant null check on createLoadSkillTool

Issue: The if (loadSkillTool) check can never fail — createLoadSkillTool(ctx) always returns a Tool<...> object via the tool() factory.

Why: This is dead code that adds unnecessary complexity and may mislead future maintainers into thinking the function can return null.

Fix: Remove the inner conditional and call wrapToolWithStreaming directly.

Refs:

🟡 3) relation-tools.ts:23 Use ToolSet type for return type consistency

Issue: getRelationTools returns Record<string, any> instead of ToolSet type used by the other tool-loading functions (getMcpTools, getFunctionTools, getDefaultTools).

Why: Inconsistent return types make it harder to compose tools and lose type information at call sites.

Fix: Change return type to ToolSet from the ai package.

Refs:

Inline Comments:

  • 🟡 Minor: AgentMcpManager.ts:20 Import consistency
  • 🟡 Minor: default-tools.ts:135 Redundant null check
  • 🟡 Minor: relation-tools.ts:23 Use ToolSet type

💭 Consider (1) 💭

💭 1) agent-types.ts:196 Replace any with unknown in type guard

Issue: The isValidTool type guard uses any for the input parameter, allowing callers to pass arbitrary values without compile-time checks.
Why: Using unknown would force explicit narrowing at call sites, catching potential type mismatches earlier.
Fix: Change parameter type from any to unknown.

Inline Comments:

  • 💭 Consider: agent-types.ts:196 Use unknown instead of any

🕐 Pending Recommendations (3)

  • 🟠 tool-loading.ts Duplicate tool loading per generation — tools loaded in loadToolsAndPrompts are re-fetched inside buildSystemPrompt (prior review by pullfrog)
  • 🟠 system-prompt.ts Parameter ordering bug in fallback tool-loading path (prior review by pullfrog)
  • 🟡 default-tools.ts + system-prompt.ts Duplicate hasAgentArtifactComponents logic with divergent error handling (prior review by pullfrog)

💡 APPROVE WITH SUGGESTIONS

Summary: This is a solid refactoring that successfully decomposes a monolithic file into well-organized modules. The architecture is sound — clean module boundaries, proper facade pattern, correct dependency direction, and all test paths properly updated. The 2 Major issues (missing transfer tool handling, error context loss) are worth addressing, but they're localized fixes that don't require rethinking the decomposition. The Minor issues and prior pending recommendations are polish items. Ship it after addressing the Majors. 🚀

Discarded (8)
Location Issue Reason Discarded
compression.ts:11-17 any types in setupCompression Pre-existing pattern from original Agent.ts; out of scope for this refactor
agent-types.ts:44-51 any in hasToolCallWithPrefix Pre-existing code moved without modification
agent-types.ts:70-72 any in ResolvedGenerationResponse Pre-existing type definition
model-config.ts:85-90 any return in configureModelSettings Pre-existing pattern; would require broader typing changes
function-tools.ts:89 any in execute context Addressed in Consider section via isValidTool suggestion
mcp-tools.ts:81 any in execute context Same as above
tool-wrapper.ts:77 any in wrapped execute Same as above
agents/streaming/ directory Two streaming directories Valid architectural choice; agents/streaming handles agent-specific orchestration, run/stream provides reusable primitives
Reviewers (6)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 4 2 0 0 2 0 0
pr-review-architecture 6 0 0 0 1 0 5
pr-review-types 9 1 1 0 2 0 5
pr-review-precision 5 0 0 0 1 3 1
pr-review-tests 0 0 0 0 0 0 0
pr-review-consistency 6 0 0 0 0 0 6
Total 30 3 1 0 6 3 17

Note: pr-review-tests confirmed all mock paths are correctly updated — no findings needed.

);

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

Choose a reason for hiding this comment

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

🟠 MAJOR Missing transfer tool type handling

Issue: getRelationshipIdForTool handles 'mcp', 'tool', and 'delegation' types but has no case for 'transfer', despite ToolType including 'transfer' and relation-tools.ts:39 passing 'transfer' to wrapToolWithStreaming.

Why: Transfer tool relationship IDs will always return undefined, causing incomplete telemetry tracking and session recording for transfer tool calls.

Fix:

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

Refs:

}
const errorMessage = (event.error as { error?: { message?: string } })?.error?.message;
throw new Error(errorMessage);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 MAJOR Error handler loses context when error is not an Error instance

Issue: The error case extracts event.error?.error?.message but if that path doesn't exist, it throws new Error(undefined) which provides no debugging information.

Why: AI SDK errors can have different shapes. This code path may produce generic "Error: undefined" messages that are impossible to debug in production.

Fix:

Suggested change
}
case 'error': {
if (event.error instanceof Error) {
throw event.error;
}
const errorObj = event.error as { error?: { message?: string }; message?: string } | undefined;
const errorMessage = errorObj?.error?.message ?? errorObj?.message ?? String(event.error);
throw new Error(errorMessage || 'Stream encountered an unknown error');
}

Refs:

Copy link
Contributor

Choose a reason for hiding this comment

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

@tim-inkeep worth addressing

Copy link
Contributor

Choose a reason for hiding this comment

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

@claude can you fix?

import { getLogger } from '../../../../logger';
import { agentSessionManager } from '../../session/AgentSession';
import { setSpanWithError, tracer } from '../../utils/tracer';
import type { AgentConfig } from '../Agent';
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Minor Import AgentConfig from agent-types.ts for consistency

Issue: AgentConfig is imported from ../Agent while AiSdkToolDefinition is imported from ../agent-types. Since AgentConfig is defined in agent-types.ts and merely re-exported by Agent.ts, this creates inconsistent import patterns.

Why: All other files in tools/ and generation/ subdirectories consistently import types from ../agent-types. This improves consistency and clarifies the canonical type source.

Fix:

Suggested change
import type { AgentConfig } from '../Agent';
import type { AgentConfig, AiSdkToolDefinition } from '../agent-types';

Refs:

Copy link
Contributor

Choose a reason for hiding this comment

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

@tim-inkeep decent point

'tool'
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Minor Redundant null check on createLoadSkillTool return value

Issue: The if (loadSkillTool) check at line 126 can never fail — createLoadSkillTool(ctx) always returns a Tool<...> object via the tool() factory (see lines 80-110). The function signature declares a return type of Tool<...>, not Tool<...> | null.

Why: This is dead code that adds unnecessary complexity. The check may have been carried over from a previous implementation or added defensively without verifying the function's contract.

Fix:

Suggested change
}
const hasOnDemandSkills = ctx.config.skills?.some((skill) => !skill.alwaysLoaded);
if (hasOnDemandSkills) {
defaultTools.load_skill = wrapToolWithStreaming(
ctx,
'load_skill',
createLoadSkillTool(ctx),
streamRequestId,
'tool'
);
}

Refs:

};
},
sessionId?: string
): Record<string, any> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Minor Use ToolSet type for consistency

Issue: getRelationTools returns Record<string, any> instead of ToolSet type used by the other tool-loading functions (getMcpTools, getFunctionTools, getDefaultTools).

Why: Inconsistent return types make it harder to compose tools and lose type information at call sites.

Fix: Change return type to ToolSet:

import type { ToolSet } from 'ai';

export function getRelationTools(
  ctx: AgentRunContext,
  runtimeContext?: { ... },
  sessionId?: string
): ToolSet {

Refs:


export type ToolType = 'transfer' | 'delegation' | 'mcp' | 'tool';

export function isValidTool(tool: any): tool is {
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Consider Replace any with unknown in type guard

Issue: The isValidTool type guard uses any for the input parameter. Using unknown would provide better type safety by forcing explicit narrowing.

Why: With any, callers can pass arbitrary values without compile-time checks. unknown would catch type mismatches at call sites.

Fix: Change signature to:

export function isValidTool(tool: unknown): tool is {
  description: string;
  inputSchema: Record<string, unknown>;
  execute: (args: unknown, context?: unknown) => Promise<unknown>;
}

Refs:

@github-actions github-actions bot deleted a comment from claude bot Mar 3, 2026
@mike-inkeep
Copy link
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

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