Skip to content

Latest commit

 

History

History
378 lines (299 loc) · 10.2 KB

File metadata and controls

378 lines (299 loc) · 10.2 KB

Adapter Development Guide

This guide explains how to create custom adapters for agentrace to support additional AI agents.

Overview

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.

AgentAdapter Interface

All adapters must implement the following interface:

export interface AgentAdapter {
  name: string;
  detect(): boolean;
  findSessions(): string[];
  parseSession(sessionPath: string): ParsedSession | null;
}

Properties and Methods

name: string

Unique identifier for the adapter (e.g., "claude-code", "codex", "openclaw").

detect(): boolean

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

findSessions(): string[]

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

parseSession(sessionPath: string): ParsedSession | null

Parses a session file and converts it to agentrace format. Returns:

  • ParsedSession object on success
  • null if the file cannot be parsed or doesn't exist

Creating a New Adapter

1. Basic Structure

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...
}

2. Session Conversion

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';
  }
}

3. Event Conversion

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';
  }
}

4. File Change Tracking

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';
  }
}

5. Utility Methods

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
}

Agent-Specific Implementation Notes

Claude Code

  • 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

Codex

  • Location: ~/.codex/sessions/
  • Format: Single JSON files per session
  • Key fields: turns, tool_calls, usage
  • Special handling: Conversation turns, function calls

OpenClaw

  • Location: Various (configurable)
  • Format: JSON log files
  • Key fields: logs, entries, session_start
  • Special handling: Multiple log entry types

Custom Agents

For new agents, consider:

  1. Session identification: How to uniquely identify sessions
  2. Temporal ordering: How to sort events chronologically
  3. Tool/function mapping: How to map agent actions to agentrace event types
  4. Cost calculation: If token usage and model info is available
  5. File tracking: If the agent modifies files

Testing Your Adapter

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
  });
});

Registration

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
];

Best Practices

Error Handling

  • Always handle missing files gracefully
  • Log warnings for unparseable sessions but continue processing
  • Never throw exceptions that would crash the entire discovery process

Performance

  • Sort sessions by modification time for faster recent session access
  • Limit large directory scans
  • Cache expensive operations when possible

Data Quality

  • Validate required fields before creating sessions
  • Provide sensible defaults for missing data
  • Ensure timestamps are in ISO 8601 format

Security

  • Be cautious when reading arbitrary files
  • Validate file paths to prevent directory traversal
  • Don't expose sensitive data in session content

Common Patterns

JSONL Processing

const lines = content.trim().split('\n');
const messages = lines.map(line => JSON.parse(line));

Timestamp Parsing

const timestamp = data.timestamp || new Date().toISOString();
const isoTimestamp = new Date(timestamp).toISOString();

Tool Call Extraction

const toolCalls = data.tool_use?.map(tool => ({
  name: tool.name,
  parameters: tool.input,
  timestamp: data.timestamp
})) || [];

Contributing

When contributing a new adapter:

  1. Follow the existing code style
  2. Include comprehensive tests
  3. Update documentation
  4. Provide example session files (anonymized)
  5. Test with real agent installations

Support

Need help developing an adapter?