Skip to main content

ADR-018: Atomic Tools Architecture

Status: Accepted
Date: 2025-12-16
Deciders: Development Team
Related: ADR-003 (Memory-Centric Architecture), ADR-005 (Testing Strategy), Issue #311 (Test Infrastructure Improvements)

Contextโ€‹

The current tool architecture creates significant testing and performance challenges:

Problems with Current Architectureโ€‹

  1. Complex Orchestrator Classes: Tools depend on orchestrator classes (ResearchOrchestrator, KnowledgeGraphManager) that create deep dependency chains
  2. Test Complexity: Deep dependency trees require extensive ESM mocking with jest.unstable_mockModule(), resulting in:
    • 50+ lines of mock setup per test file
    • 300-400 line test files
    • 37+ timeout failures in CI
    • 850+ second test suite execution time
  3. Token Overhead: Orchestrators execute sequentially, adding 2-8 seconds per call and 5,000-6,000 tokens per session
  4. Architecture Misalignment: Orchestrators conflict with CE-MCP directive-based architecture (ADR-014)

Test Suite Metrics (Before)โ€‹

MetricCurrent StateImpact
Test suite time850+ secondsDeveloper productivity loss
Timeout failures37+ testsCI reliability issues
ESM mock setup50+ lines/testHigh maintenance burden
Test file size300-400 linesReduced readability
Mock chainsDeep dependency treesBrittle tests

Decisionโ€‹

We will adopt an Atomic Tools Architecture with Dependency Injection pattern:

Core Principlesโ€‹

  1. Atomic Tools: Each tool is self-contained with minimal external dependencies
  2. Dependency Injection: External dependencies are injected as parameters with sensible defaults
  3. Resource-Based State: Complex state managers (like KnowledgeGraphManager) converted to MCP Resources
  4. Direct Execution: Tools call utilities directly instead of through orchestrator layers

Architecture Patternโ€‹

Old Pattern (Orchestrator-based):

// Tool with deep dependencies
import { ResearchOrchestrator } from '../utils/research-orchestrator.js';
import { KnowledgeGraphManager } from '../utils/knowledge-graph-manager.js';

export async function myTool(args: ToolArgs) {
const orchestrator = new ResearchOrchestrator(args.projectPath, 'docs/adrs');
const result = await orchestrator.answerResearchQuestion(args.query);
// ... complex logic with multiple orchestrator calls
}

// Test requires complex mocking
beforeAll(async () => {
await setupESMMocks({
'../../src/utils/research-orchestrator.js': {
ResearchOrchestrator: mockClass,
},
'../../src/utils/tree-sitter-analyzer.js': {
TreeSitterAnalyzer: mockClass,
},
'../../src/utils/file-system.js': {
analyzeProjectStructure: mock,
findRelatedCode: mock,
},
'../../src/utils/adr-discovery.js': {
discoverAdrsInDirectory: mock,
},
// ... 50+ lines of mocks
});
const module = await import('../../src/tools/my-tool.js');
});

New Pattern (Atomic with DI):

// Tool with dependency injection
import { findFiles, readFile } from '../utils/file-system.js';

interface ToolDependencies {
fs?: {
findFiles?: typeof findFiles;
readFile?: typeof readFile;
};
}

export async function myTool(args: ToolArgs, deps: ToolDependencies = {}) {
// Use injected dependencies or real implementations
const findFilesImpl = deps.fs?.findFiles ?? findFiles;
const readFileImpl = deps.fs?.readFile ?? readFile;

const files = await findFilesImpl(args.projectPath, '*.ts');
const content = await readFileImpl(files[0]);
// ... direct logic without orchestrator

return formatMCPResponse(result);
}

// Test with simple DI
test('myTool finds files', async () => {
const mockFs = {
findFiles: jest.fn().mockResolvedValue(['file1.ts']),
readFile: jest.fn().mockResolvedValue('content'),
};

const result = await myTool({ projectPath: '/test', query: 'auth' }, { fs: mockFs });

expect(result.content[0].text).toContain('file1.ts');
});

Resource Pattern for Stateโ€‹

Old Pattern (Stateful Manager):

const kgManager = new KnowledgeGraphManager();
const snapshot = await kgManager.loadKnowledgeGraph();
const results = await kgManager.queryKnowledgeGraph('what ADRs exist?');

New Pattern (Resource + Simple Tool):

// Read graph - zero token cost via MCP Resource
const graph = await readResource('knowledge://graph');
const adrs = graph.nodes.filter(n => n.type === 'adr');

// Modify graph - use CRUD tool
await callTool('update_knowledge', {
operation: 'add_entity',
entity: 'adr-019',
entityType: 'adr',
metadata: { title: 'New Decision' },
});

Target State (After Implementation)โ€‹

MetricBeforeAfterImprovement
Test suite time850s<60s93% faster
Timeout failures37+~0Eliminated
ESM mock setup50+ lines5-10 lines80-90% less
Test file size300-400 lines50-100 lines70-80% smaller
Mock chainsDeep treesFlat DISimplified

Implementation Strategyโ€‹

Phase 1: Foundationโ€‹

  1. โœ… Document atomic tools pattern (this ADR)
  2. Create atomic tool template with DI examples
  3. Update test documentation
  4. Mark ResearchOrchestrator as deprecated

Phase 2: High-Priority Tool Migrationโ€‹

Convert tools with heaviest test burden:

  1. review-existing-adrs-tool.ts - Currently uses ResearchOrchestrator
  2. adr-suggestion-tool.ts - Currently uses ResearchOrchestrator
  3. environment-analysis-tool.ts - Currently uses ResearchOrchestrator
  4. Remaining tools as needed

Phase 3: Test Infrastructure Cleanupโ€‹

  1. Simplify esm-mock-helper.ts (reduce to basic DI support)
  2. Remove orchestrator mock factories
  3. Update test setup documentation

Phase 4: Validationโ€‹

  1. Full test suite passes in <60 seconds
  2. Zero timeout failures in CI
  3. Test coverage maintained or improved (โ‰ฅ85%)
  4. CI reliability >99%

Phase 5: Deprecation (Future)โ€‹

  • v3.0.0: Mark orchestrators as deprecated
  • v4.0.0: Remove orchestrator classes entirely

Migration Guideโ€‹

For Tool Developersโ€‹

When creating new tools:

// โœ… DO: Use dependency injection
export async function newTool(
args: ToolArgs,
deps: {
fs?: typeof import('../utils/file-system.js');
ai?: ReturnType<typeof getAIExecutor>;
} = {}
) {
const fs = deps.fs ?? (await import('../utils/file-system.js'));
const ai = deps.ai ?? getAIExecutor();
// ... implementation
}

// โŒ DON'T: Create orchestrator instances
export async function oldTool(args: ToolArgs) {
const orchestrator = new ResearchOrchestrator(args.projectPath);
const result = await orchestrator.answerResearchQuestion(args.query);
}

For Test Writersโ€‹

// โœ… DO: Simple DI mocking
test('tool processes files', async () => {
const mockFs = {
readFile: jest.fn().mockResolvedValue('content'),
};

const result = await myTool({ projectPath: '/test' }, { fs: mockFs });

expect(mockFs.readFile).toHaveBeenCalledWith('/test/file.ts');
});

// โŒ DON'T: Complex ESM module mocking
beforeAll(async () => {
await setupESMMocks({
'../../src/utils/research-orchestrator.js': MockFactories.createResearchOrchestrator(),
'../../src/utils/tree-sitter-analyzer.js': MockFactories.createTreeSitterAnalyzer(),
// ... 50 more lines
});
});

For Existing Toolsโ€‹

Tools can be migrated incrementally:

  1. Add optional deps parameter with defaults
  2. Replace direct instantiation with injected dependencies
  3. Update tests to use DI instead of ESM mocks
  4. Remove orchestrator dependencies

Example PR: [See Phase 2 implementations]

Consequencesโ€‹

Positiveโ€‹

  1. Dramatic Test Speed Improvement: 850s โ†’ <60s (93% faster)
  2. Eliminated Timeout Failures: 37+ failures โ†’ ~0
  3. Simplified Test Code: 50+ lines mock setup โ†’ 5-10 lines
  4. Improved Maintainability: Smaller, clearer test files (50-100 lines vs 300-400)
  5. Better Tool Isolation: Each tool is independently testable
  6. Zero Token Overhead: No sequential orchestrator calls
  7. Alignment with CE-MCP: Direct execution matches directive-based architecture
  8. Easy Onboarding: New developers can understand and test tools quickly

Negativeโ€‹

  1. Migration Effort: Requires updating existing tools and tests
  2. Breaking Changes: Tools with orchestrator dependencies need refactoring
  3. Coordination Required: Multiple tools need to be migrated systematically

Neutralโ€‹

  1. Internal Orchestrators: Can still be used internally for complex workflows
  2. Backward Compatibility: Old tools continue working during migration
  3. Gradual Rollout: Can be implemented incrementally

Validationโ€‹

Success Criteriaโ€‹

  1. โœ… Full test suite completes in <60 seconds
  2. โœ… Zero timeout failures in CI pipeline
  3. โœ… New tools can be tested in <20 lines of setup
  4. โœ… Test coverage maintained at โ‰ฅ85%
  5. โœ… CI pipeline reliability >99%

Monitoringโ€‹

Track metrics after each phase:

  • Test suite execution time
  • Number of timeout failures
  • Average test file size
  • Average mock setup lines per test
  • Test coverage percentage
  • CI success rate

Review Pointsโ€‹

  • After Phase 2: Review first 3 tool migrations
  • After Phase 3: Validate test infrastructure improvements
  • After Phase 4: Final validation against success criteria

Referencesโ€‹

  • Issue #311: Test Infrastructure Improvements (parent EPIC)
  • Issues #334-337: Current test failures
  • ADR-003: Memory-Centric Architecture
  • ADR-005: Testing and Quality Assurance Strategy
  • ADR-014: CE-MCP Architecture
  • docs/knowledge-graph-resource-tool.md: Resource pattern example

Notesโ€‹

  • This ADR supersedes the implicit orchestrator-based architecture
  • Existing tools can continue using orchestrators during migration period
  • The knowledge graph has already been converted to resource pattern (see docs/knowledge-graph-resource-tool.md)
  • ResearchOrchestrator marked as deprecated in v2.x, removed in v4.0.0