Skip to content

Development Workflow

This document outlines the development workflow and processes for the Robota project.

Code Quality Process

Pragmatic Lint Strategy

  • Iterative Improvement: Fix lint issues progressively, not all at once
  • Context-aware Decisions: Some lint rules may be overridden with good reason
  • Team Consistency: Maintain consistent code style across the project
  • Tool-assisted: Use automated tools where possible, manual review where needed

Lint Workflow Best Practices

  1. Development Phase:

    • Fix critical errors immediately (syntax, type errors)
    • Address warnings that affect functionality
    • Defer style-only warnings to review phase
    • Do not suppress any/unknown warnings in shipped source via eslint-disable (prohibited by policy). Prefer SSOT types and type guards.
  2. Review Phase:

    • Run pnpm run lint:fix to auto-fix issues
    • Address remaining warnings with context consideration
    • Document any intentional rule overrides
  3. Pre-commit Phase:

    • Ensure no critical errors remain
    • All auto-fixable issues should be resolved
    • Document any remaining warnings in PR description

Available Lint Commands

bash
# Check lint issues across all packages
pnpm run lint

# Fix auto-fixable lint issues across all packages
pnpm run lint:fix

# Package-specific linting
pnpm --filter @robota-sdk/agent-core run lint:fix
pnpm --filter @robota-sdk/agent-core run lint:fix
pnpm --filter @robota-sdk/agent-provider-openai run lint:fix

# Examples and apps
pnpm --filter @robota-sdk/agent-core run lint:fix

Lint Rule Categories

Critical Issues (Fix Immediately)

  • Syntax Errors: Break compilation/runtime
  • Type Errors: Cause runtime failures
  • Security Issues: Potential vulnerabilities
  • Logic Errors: Incorrect program behavior

Important Issues (Address in Review)

  • Unused Variables: May indicate incomplete code
  • Missing Error Handling: Potential runtime issues
  • Performance Issues: Inefficient patterns
  • Accessibility Issues: User experience impacts

Style Issues (Batch Fix)

  • Import Ordering: Can be auto-fixed
  • Formatting: Should be handled by prettier
  • Naming Conventions: Consistency improvements
  • Comment Style: Documentation improvements

Compatibility Code Management

Pragmatic Compatibility Handling

  • Change with Purpose: Make breaking changes only when there's a clear, validated alternative
  • Migration Support: Provide migration documentation
  • Compatibility Windows: Maintain compatibility where it is structurally sound and explicitly supported
  • User Communication: Clear communication about changes and timelines

Refactoring Strategy

  • Incremental Changes: Large refactors should be broken into smaller, reviewable changes
  • Backward Compatibility: Maintain API compatibility during refactoring
  • Feature Flags: Use feature flags for major changes
  • Testing: Comprehensive testing during refactoring

Technical Debt Management

  • Documentation: Document known technical debt and its impact
  • Prioritization: Address technical debt based on user impact and development velocity
  • Boy Scout Rule: Leave code better than you found it
  • Regular Reviews: Periodic technical debt assessment and planning

IDE Configuration

VSCode Recommendations

json
// VSCode settings.json example
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.organizeImports": true
  },
  "editor.formatOnSave": true,
  "typescript.preferences.importModuleSpecifier": "relative"
}

Acceptable Code Overrides

Temporary Lint Overrides

typescript
// ✅ Acceptable (tests only): any/unknown may be used for mocks
// NOTE: In shipped source (src/), disabling any/unknown lint rules is prohibited.

// NOT acceptable: Lazy typing
function processData(data: any): any { // Should define proper types

Package-Specific Guidelines

@robota-sdk/agent-core Package

  • Must not depend on deprecated packages
  • Should be the primary entry point for new users
  • Implement all functionality independently
  • Maintain compatibility with existing provider packages

Provider Packages (@robota-sdk/agent-provider-openai, anthropic, google)

  • Should remain lightweight and focused
  • Implement only provider-specific logic
  • Maintain backward compatibility
  • Support both core and agents packages

Deprecation Strategy

  • Gradual Migration: Provide clear migration paths for deprecated features
  • Timing: Maintain deprecated features for at least 2 major versions
  • Documentation: Clear deprecation warnings with replacement suggestions
  • Breaking Changes: Only in major version releases

Plugin vs Module Development Guidelines

Module vs Plugin Decision Tree

When developing a new component, use these questions to determine whether it should be a Module or Plugin:

Primary Filter: Optional Extension vs Essential Component

❓ "Can Robota perform basic text conversation without this feature?"
   ✅ Yes → Module or Plugin candidate (optional extension)
   ❌ No → Internal core class (not Module/Plugin)

Secondary Filter: LLM Capabilities vs External Extensions

❓ "Is this something LLMs cannot do natively?"
   ✅ Yes → Module candidate (e.g., file processing, DB integration, speech processing)
   ❌ No → "Does it observe/enhance existing behavior?"
           ✅ Yes → Plugin (e.g., logging, monitoring, notifications)
           ❌ No → Internal class (things LLMs already do well)

Important: Cases Where Module/Plugin is NOT Possible

❓ "Would removing this cause errors or break main functionality?"
   ✅ Yes → Internal core class (AI Provider, Tool Execution, Message Processing, etc.)
   ❌ No → Can be implemented as Module or Plugin

❓ "Is this something LLMs already do well?"
   ✅ Yes → Internal class or unnecessary (conversation, reasoning, text understanding, etc.)
   ❌ No → Module candidate

Module Development Guidelines

1. Design Principles

typescript
// ✅ Good Module design (things LLMs cannot do)
export class VectorSearchModule extends BaseModule {
  // Clear responsibility: vector search (LLMs cannot do directly)
  async addDocument(id: string, text: string, metadata: any): Promise<void>;
  async search(query: string, topK: number): Promise<SearchResult[]>;
  async embed(text: string): Promise<number[]>;

  // Clear capability specification
  getCapabilities(): ModuleCapabilities {
    return {
      vectorDimensions: [512, 1024, 1536],
      similarityMethods: ['cosine', 'euclidean'],
      indexTypes: ['flat', 'ivf', 'hnsw'],
    };
  }
}

// ❌ Bad Module design (things LLMs already do well)
export class ReasoningModule extends BaseModule {
  // LLMs already reason well - unnecessary Module
  async analyze(text: string): Promise<Analysis>;
  async infer(facts: string[]): Promise<string>;
  async explain(conclusion: string): Promise<string>;
}

// ❌ Essential component as Module
export class AIProviderModule extends BaseModule {
  // Without this, conversation is impossible - should be internal class
  async generateResponse(messages: Message[]): Promise<string>;
}

2. Dependency Management

typescript
// ✅ Clear dependency definition (actual needed modules)
export class DatabaseModule extends BaseModule {
  readonly dependencies = ['transport']; // Network connection needed

  validateDependencies(): boolean {
    return this.dependencies.every((dep) => ModuleTypeRegistry.getType(dep) !== undefined);
  }

  // ✅ EventEmitter-based dependency resolution (no direct references)
  async initialize(config: DatabaseConfig): Promise<void> {
    // Emit transport module request event
    this.emitEvent('transport.request', {
      requestor: this.name,
      operation: 'initialize',
      config: config.transportConfig,
    });

    // Subscribe to transport response event
    this.eventEmitter?.on('transport.ready', (event) => {
      if (event.data.requestor === this.name) {
        this.establishConnection(config);
      }
    });
  }
}

// ❌ Circular dependency - this design is prohibited
export class ModuleA extends BaseModule {
  readonly dependencies = ['module-b']; // If ModuleB also depends on ModuleA, circular dependency
}

// ✅ Correct dependency design - solved with Event-Driven approach
export class ModuleA extends BaseModule {
  readonly dependencies = []; // No direct dependencies

  async processData(data: any): Promise<any> {
    // Emit event when ModuleB functionality is needed
    this.emitEvent('moduleB.request', {
      requestor: this.name,
      operation: 'process',
      data,
    });

    // Receive result via event
    return new Promise((resolve) => {
      this.eventEmitter?.once('moduleB.response', (event) => {
        if (event.data.requestor === this.name) {
          resolve(event.data.result);
        }
      });
    });
  }
}

3. Interface Design

typescript
// ✅ Standard interface implementation (actual needed functionality)
export interface FileProcessingModule {
  processImage(buffer: Buffer): Promise<string>;
  processPDF(buffer: Buffer): Promise<string>;
  processAudio(buffer: Buffer): Promise<string>;
  extractMetadata(buffer: Buffer, type: string): Promise<any>;
}

export class LocalFileProcessingModule extends BaseModule implements FileProcessingModule {
  // Standard interface implementation
  async processImage(buffer: Buffer): Promise<string> {
    /* OCR processing */
  }
  async processPDF(buffer: Buffer): Promise<string> {
    /* PDF parsing */
  }
  async processAudio(buffer: Buffer): Promise<string> {
    /* Speech conversion */
  }
  async extractMetadata(buffer: Buffer, type: string): Promise<any> {
    /* Metadata extraction */
  }

  // Extended functionality
  async batchProcess(files: Buffer[]): Promise<string[]> {
    /* Batch processing */
  }
}

Plugin Development Guidelines

1. Performance Considerations

typescript
// ✅ Efficient Plugin design
export class PerformancePlugin extends BasePlugin {
  private metrics = new Map<string, number>();

  async beforeRun(input: string): Promise<void> {
    // Only perform lightweight measurements
    this.metrics.set('startTime', performance.now());
  }

  async afterRun(input: string, output: string): Promise<void> {
    // Process asynchronously to avoid affecting main flow
    setImmediate(() => {
      this.recordMetrics(input, output);
    });
  }
}

// ❌ Plugin that affects performance
export class BadPlugin extends BasePlugin {
  async beforeRun(input: string): Promise<void> {
    // Heavy operations that delay main flow
    await this.heavyAnalysis(input);
    await this.syncToDatabase();
  }
}

2. Error Handling

typescript
// ✅ Robust error handling
export class LoggingPlugin extends BasePlugin {
  async afterRun(input: string, output: string): Promise<void> {
    try {
      await this.logConversation(input, output);
    } catch (error) {
      // Handle plugin errors silently without affecting main flow
      this.handleError('Failed to log conversation', error);
    }
  }

  private handleError(message: string, error: any): void {
    // Handle internally only, don't propagate to external
    console.error(`${this.name}: ${message}`, error);
    this.incrementErrorCount();
  }
}

3. State Management

typescript
// ✅ Independent state management
export class UsagePlugin extends BasePlugin {
  private usage = {
    totalRequests: 0,
    totalTokens: 0,
    totalCost: 0,
  };

  // State is isolated and independent of other plugins
  getStats(): UsageStats {
    return { ...this.usage }; // Return copy
  }

  async afterRun(input: string, output: string): Promise<void> {
    this.usage.totalRequests++;
    this.usage.totalTokens += this.countTokens(input, output);
    this.usage.totalCost += this.calculateCost(this.usage.totalTokens);
  }
}

Practical Application Scenarios

Requirement: Document search-based answer generation

Analysis Process:

  1. Optional extension?: ✅ (general conversation possible without it)
  2. Something LLMs cannot do?: ✅ (real-time document search not directly possible for LLMs)
  3. Conclusion: Module

Implementation:

typescript
export class RAGModule extends BaseModule<RAGConfig> {
  readonly name = 'rag-search';
  readonly version = '1.0.0';
  readonly dependencies = ['vector-search', 'storage'];

  getModuleType(): ModuleTypeDescriptor {
    return {
      type: 'rag-search',
      category: ModuleCategory.CAPABILITY,
      layer: ModuleLayer.APPLICATION,
      dependencies: this.dependencies,
      capabilities: ['document-search', 'context-retrieval', 'rag-generation'],
    };
  }

  async addDocument(id: string, content: string, metadata?: any): Promise<void> {
    // Event-Driven: emit event to vector search module
    this.emitEvent('vector.addDocument', {
      requestor: this.name,
      id,
      content,
      metadata,
    });
  }

  async searchRelevant(query: string, topK: number = 5): Promise<string[]> {
    // Event-Driven: emit search request event
    this.emitEvent('vector.search', {
      requestor: this.name,
      query,
      topK,
    });

    // Receive search results via event
    return new Promise((resolve) => {
      this.eventEmitter?.once('vector.searchResponse', (event) => {
        if (event.data.requestor === this.name) {
          resolve(event.data.results.map((r: any) => r.content));
        }
      });
    });
  }

  async generateAnswer(query: string, context: string[]): Promise<string> {
    // RAG logic: support answer generation based on retrieved documents
    const contextText = context.join('\n\n');
    return `Based on the following context:\n${contextText}\n\nAnswer: [LLM will generate based on this context]`;
  }
}

Scenario 2: Conversation Quality Assessment

Requirement: Automatically evaluate and score AI response quality

Analysis Process:

  1. Optional extension?: ✅ (conversation works normally without it)
  2. Something LLMs cannot do?: ❌ (observes/evaluates existing behavior)
  3. Conclusion: Plugin

Implementation:

typescript
export class QualityAssessmentPlugin extends BasePlugin<QualityOptions, QualityStats> {
  readonly name = 'quality-assessment';
  readonly version = '1.0.0';
  readonly category = PluginCategory.MONITORING;
  readonly priority = PluginPriority.NORMAL;

  private qualityScores: number[] = [];

  async afterRun(input: string, output: string): Promise<void> {
    // Execute quality assessment asynchronously (no impact on main flow)
    setImmediate(async () => {
      const score = await this.assessQuality(input, output);
      this.qualityScores.push(score);

      if (score < 0.5) {
        await this.flagLowQuality(input, output, score);
      }
    });
  }

  getStats(): QualityStats {
    return {
      averageScore: this.qualityScores.reduce((a, b) => a + b, 0) / this.qualityScores.length,
      totalAssessments: this.qualityScores.length,
      lowQualityCount: this.qualityScores.filter((s) => s < 0.5).length,
    };
  }

  private async assessQuality(input: string, output: string): Promise<number> {
    // Response quality assessment logic (observation functionality)
    return Math.random(); // Example
  }
}

Scenario 3: File Processing System

Requirement: Process PDF, image, and audio files to convert to text

Analysis Process:

  1. Optional extension?: ✅ (text conversation possible without it)
  2. Something LLMs cannot do?: ✅ (file parsing not directly possible for LLMs)
  3. Conclusion: Module

Implementation:

typescript
export class FileProcessingModule extends BaseModule<FileProcessingConfig> {
  readonly name = 'file-processing';
  readonly version = '1.0.0';
  readonly dependencies = ['storage'];

  getModuleType(): ModuleTypeDescriptor {
    return {
      type: 'file-processing',
      category: ModuleCategory.CAPABILITY,
      layer: ModuleLayer.APPLICATION,
      dependencies: this.dependencies,
      capabilities: ['pdf-parsing', 'image-ocr', 'audio-transcription'],
    };
  }

  async processFile(buffer: Buffer, type: string): Promise<string> {
    switch (type) {
      case 'pdf':
        return await this.processPDF(buffer);
      case 'image':
        return await this.processImage(buffer);
      case 'audio':
        return await this.processAudio(buffer);
      default:
        throw new Error(`Unsupported file type: ${type}`);
    }
  }

  private async processPDF(buffer: Buffer): Promise<string> {
    // PDF parsing logic (file processing LLMs cannot do)
    return 'Extracted text from PDF';
  }

  private async processImage(buffer: Buffer): Promise<string> {
    // OCR processing logic (image text extraction LLMs cannot do)
    return 'Extracted text from image';
  }

  private async processAudio(buffer: Buffer): Promise<string> {
    // Speech recognition logic (audio conversion LLMs cannot do)
    return 'Transcribed text from audio';
  }
}

Best Practices

Module Development

  1. Clear Responsibility: Focus on capabilities LLMs cannot provide
  2. Event-Driven Communication: Use EventEmitter for loose coupling
  3. Dependency Declaration: Clearly specify module dependencies
  4. Capability Definition: Explicitly define what the module provides
  5. Optional Extension: Ensure agent works without the module

Plugin Development

  1. Performance Awareness: Minimize impact on main execution flow
  2. Error Isolation: Handle errors without affecting agent operation
  3. Independent State: Maintain isolated state from other components
  4. Asynchronous Processing: Use async operations for heavy tasks
  5. Graceful Degradation: Function properly even when other plugins fail

General Guidelines

  1. Type Safety: Use TypeScript interfaces and generics
  2. Event-Driven Architecture: Prefer events over direct coupling
  3. Modular Design: Keep components focused and independent
  4. Documentation: Clearly document capabilities and dependencies
  5. Testing: Write comprehensive tests for both modules and plugins

This development workflow ensures that the Plugin vs Module architecture maintains clarity, performance, and extensibility while providing a solid foundation for building sophisticated AI agents.

Released under the MIT License.