# 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)
| Metric | Current State | Impact |
|--------|--------------|--------|
| Test suite time | 850+ seconds | Developer productivity loss |
| Timeout failures | 37+ tests | CI reliability issues |
| ESM mock setup | 50+ lines/test | High maintenance burden |
| Test file size | 300-400 lines | Reduced readability |
| Mock chains | Deep dependency trees | Brittle 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)**:
```typescript
// 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)**:
```typescript
// 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)**:
```typescript
const kgManager = new KnowledgeGraphManager();
const snapshot = await kgManager.loadKnowledgeGraph();
const results = await kgManager.queryKnowledgeGraph("what ADRs exist?");
```
**New Pattern (Resource + Simple Tool)**:
```typescript
// 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)
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Test suite time | 850s | <60s | 93% faster |
| Timeout failures | 37+ | ~0 | Eliminated |
| ESM mock setup | 50+ lines | 5-10 lines | 80-90% less |
| Test file size | 300-400 lines | 50-100 lines | 70-80% smaller |
| Mock chains | Deep trees | Flat DI | Simplified |
## 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:
```typescript
// ✅ 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
```typescript
// ✅ 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