This guide explains how to create custom adapters for agentrace to support additional AI agents.
Adapters are responsible for discovering, parsing, and converting agent-specific session files into agentrace's universal format. Each adapter implements the AgentAdapter interface and handles the unique data formats and storage patterns of different AI agents.
All adapters must implement the following interface:
export interface AgentAdapter {
name: string;
detect(): boolean;
findSessions(): string[];
parseSession(sessionPath: string): ParsedSession | null;
}Unique identifier for the adapter (e.g., "claude-code", "codex", "openclaw").
Returns true if the agent is installed and available on the system. This method should check for:
- Agent installation directories
- Configuration files
- Session storage locations
Returns an array of file paths to session files. Should:
- Scan known session directories
- Return paths sorted by modification time (newest first)
- Handle missing directories gracefully
Parses a session file and converts it to agentrace format. Returns:
ParsedSessionobject on successnullif the file cannot be parsed or doesn't exist
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { v4 as uuidv4 } from 'uuid';
import { AgentAdapter, ParsedSession } from './types.js';
import { Session, Event, FileChange } from '../db/schema.js';
export class MyAgentAdapter implements AgentAdapter {
name = 'my-agent';
detect(): boolean {
// Check if agent is installed
const agentDir = join(homedir(), '.my-agent');
return existsSync(agentDir);
}
findSessions(): string[] {
const sessionsDir = join(homedir(), '.my-agent', 'sessions');
if (!existsSync(sessionsDir)) return [];
return readdirSync(sessionsDir)
.filter(f => f.endsWith('.json'))
.map(f => join(sessionsDir, f))
.sort((a, b) => {
try {
const statsA = require('fs').statSync(a);
const statsB = require('fs').statSync(b);
return statsB.mtime.getTime() - statsA.mtime.getTime();
} catch {
return 0;
}
});
}
parseSession(sessionPath: string): ParsedSession | null {
try {
// Your parsing logic here
const content = readFileSync(sessionPath, 'utf-8');
const data = JSON.parse(content);
// Convert to agentrace format
const session = this.convertToSession(data, sessionPath);
const events = this.convertToEvents(data, session.id);
const fileChanges = this.convertToFileChanges(data, session.id);
return { session, events, fileChanges };
} catch (error) {
console.warn(`Failed to parse session ${sessionPath}:`, error);
return null;
}
}
// Helper methods...
}Convert agent-specific session metadata to agentrace format:
private convertToSession(data: any, sessionPath: string): Session {
return {
id: this.generateSessionId(sessionPath),
agent: this.name,
started_at: data.startTime || new Date().toISOString(),
ended_at: data.endTime || null,
status: this.mapStatus(data.status),
prompt: data.initialPrompt || '',
working_dir: data.workingDirectory || '',
total_tokens: data.totalTokens || 0,
input_tokens: data.inputTokens || 0,
output_tokens: data.outputTokens || 0,
estimated_cost_usd: this.estimateCost(data),
model: data.model || 'unknown',
metadata: JSON.stringify({ session_path: sessionPath })
};
}
private mapStatus(agentStatus: string): Session['status'] {
switch (agentStatus) {
case 'success': return 'completed';
case 'error': return 'failed';
case 'running': return 'running';
default: return 'abandoned';
}
}Convert agent activities to agentrace events:
private convertToEvents(data: any, sessionId: string): Event[] {
const events: Event[] = [];
data.activities?.forEach((activity: any) => {
events.push({
session_id: sessionId,
timestamp: activity.timestamp,
type: this.mapEventType(activity.type),
name: activity.name || activity.action,
content: JSON.stringify(activity.data || {}),
duration_ms: activity.duration,
tokens: activity.tokenUsage,
metadata: JSON.stringify(activity.metadata || {})
});
});
return events;
}
private mapEventType(agentType: string): Event['type'] {
switch (agentType) {
case 'function_call': return 'tool_call';
case 'function_result': return 'tool_result';
case 'user_input': return 'message';
case 'ai_response': return 'message';
case 'error': return 'error';
case 'file_edit': return 'file_change';
case 'shell_command': return 'command';
default: return 'message';
}
}Track code modifications:
private convertToFileChanges(data: any, sessionId: string): FileChange[] {
const changes: FileChange[] = [];
data.fileOperations?.forEach((op: any) => {
changes.push({
session_id: sessionId,
file_path: op.filePath,
change_type: this.mapChangeType(op.operation),
diff: op.diff || '',
lines_added: op.linesAdded || 0,
lines_removed: op.linesRemoved || 0
});
});
return changes;
}
private mapChangeType(operation: string): FileChange['change_type'] {
switch (operation) {
case 'create': return 'create';
case 'update': return 'modify';
case 'delete': return 'delete';
default: return 'modify';
}
}private generateSessionId(sessionPath: string): string {
try {
const stats = require('fs').statSync(sessionPath);
const hash = require('crypto').createHash('md5')
.update(sessionPath + stats.size + stats.mtime.getTime())
.digest('hex');
return hash.substring(0, 8);
} catch {
return uuidv4().substring(0, 8);
}
}
private estimateCost(data: any): number {
// Implement cost calculation based on model and token usage
const model = data.model || 'unknown';
const tokens = data.totalTokens || 0;
// Your cost calculation logic
return tokens * 0.00001; // Example rate
}- Location:
~/.claude/projects/*/sessions/ - Format: JSONL files with one JSON object per line
- Key fields:
type,content,tool_use,usage - Special handling: Tool use blocks, conversation threading
- Location:
~/.codex/sessions/ - Format: Single JSON files per session
- Key fields:
turns,tool_calls,usage - Special handling: Conversation turns, function calls
- Location: Various (configurable)
- Format: JSON log files
- Key fields:
logs,entries,session_start - Special handling: Multiple log entry types
For new agents, consider:
- Session identification: How to uniquely identify sessions
- Temporal ordering: How to sort events chronologically
- Tool/function mapping: How to map agent actions to agentrace event types
- Cost calculation: If token usage and model info is available
- File tracking: If the agent modifies files
Create tests for your adapter:
import { describe, it, expect } from 'vitest';
import { MyAgentAdapter } from '../src/adapters/my-agent.js';
describe('MyAgentAdapter', () => {
let adapter: MyAgentAdapter;
beforeEach(() => {
adapter = new MyAgentAdapter();
});
it('should detect agent installation', () => {
const result = adapter.detect();
expect(typeof result).toBe('boolean');
});
it('should find session files', () => {
const sessions = adapter.findSessions();
expect(Array.isArray(sessions)).toBe(true);
});
it('should parse valid sessions', () => {
// Test with mock data
const mockPath = '/path/to/mock/session.json';
const result = adapter.parseSession(mockPath);
// Add assertions based on your mock data
});
});Add your adapter to the main adapter index:
// src/adapters/index.ts
import { MyAgentAdapter } from './my-agent.js';
export const AVAILABLE_ADAPTERS: AgentAdapter[] = [
new ClaudeCodeAdapter(),
new CodexAdapter(),
new OpenClawAdapter(),
new CursorAdapter(),
new MyAgentAdapter(), // Add your adapter here
];- Always handle missing files gracefully
- Log warnings for unparseable sessions but continue processing
- Never throw exceptions that would crash the entire discovery process
- Sort sessions by modification time for faster recent session access
- Limit large directory scans
- Cache expensive operations when possible
- Validate required fields before creating sessions
- Provide sensible defaults for missing data
- Ensure timestamps are in ISO 8601 format
- Be cautious when reading arbitrary files
- Validate file paths to prevent directory traversal
- Don't expose sensitive data in session content
const lines = content.trim().split('\n');
const messages = lines.map(line => JSON.parse(line));const timestamp = data.timestamp || new Date().toISOString();
const isoTimestamp = new Date(timestamp).toISOString();const toolCalls = data.tool_use?.map(tool => ({
name: tool.name,
parameters: tool.input,
timestamp: data.timestamp
})) || [];When contributing a new adapter:
- Follow the existing code style
- Include comprehensive tests
- Update documentation
- Provide example session files (anonymized)
- Test with real agent installations
Need help developing an adapter?
- Open an issue
- Start a discussion
- Check existing adapter implementations for reference