2026 03 12 Vitto Training System Phase1

Documentation for Plans

Vitto-Mediated Training System — Phase 1 Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the training plugin infrastructure on backend-vitto-api and wire ConversationalTeachPlugin as the first plugin, proving the architecture end-to-end (chat + UI both teach agents through Vitto).

Architecture: Plugin system on backend-vitto-api with TrainingPluginRegistry that registers tools and lifecycle hooks. ConversationalTeachPlugin adds a teach_agent tool to Vitto's tool registry. Backend-ai-api adds 2 data convenience endpoints. Frontend McpService reroutes 4 write methods from ai-api to vitto-api.

Tech Stack: NestJS (backend-vitto-api + backend-ai-api), Angular 20+ (frontend), MongoDB, Vitto LLM ProviderFactory

Spec: docs/superpowers/specs/2026-03-12-vitto-mediated-training-system-design.md


File Structure

New Files (backend-vitto-api)

File Responsibility
APIs/backend-vitto-api/src/app/training/training-plugin.interface.ts TrainingPlugin interface, TrainingEndpoint type, TaskOutcome type
APIs/backend-vitto-api/src/app/training/training-plugin-registry.service.ts Plugin registry: register plugins, register tools, dispatch lifecycle notifications
APIs/backend-vitto-api/src/app/training/training-orchestrator.service.ts Orchestrate training operations (train agent, validate examples)
APIs/backend-vitto-api/src/app/training/training.controller.ts REST endpoints: /vitto-api/training/*
APIs/backend-vitto-api/src/app/training/training.module.ts NestJS module wiring
APIs/backend-vitto-api/src/app/training/plugins/conversational-teach.plugin.ts ConversationalTeachPlugin: teach_agent tool + /teach endpoint logic

New Test Files

File Tests
APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.ts Registry: register, notify hooks, tool registration
APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts Teach plugin: classify instruction, persist example, persist knowledge doc
APIs/backend-vitto-api/src/app/training/__tests__/training-controller.spec.ts Controller: teach endpoint, train endpoint, plugins list

Modified Files

File Change
APIs/backend-vitto-api/src/app/engine/engine.module.ts:69 Add TrainingModule to imports
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts:484 Add trainingRegistry.notifyTaskCompleted() hook
APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.ts:838 Add getValidationCandidates() + validateBatch() + submitTrainingExampleViaVitto()
APIs/backend-ai-api/src/app/platform-ai/platform-ai.controller.ts:216 Add validation-candidates + validate-batch endpoints
APIs/backend-ai-api/src/app/platform-ai/services/training-data.service.ts Add getValidationCandidates() query method
libs/libs-ai/ai-agents-training-center/src/lib/services/mcp.service.ts:50-155 Reroute 4 write methods to VITTO_API_URL

Chunk 1: Plugin Infrastructure

Task 1: TrainingPlugin Interface

Files:

// APIs/backend-vitto-api/src/app/training/training-plugin.interface.ts

import { ToolHandler } from '../tools/tool-registry.service';

/**
 * Outcome of a completed Vitto task, passed to training plugin hooks.
 */
export interface TaskOutcome {
  success: boolean;
  durationMs: number;
  totalTokens: number;
  toolsUsed: string[];
  qualityScore: number;
  failureReason?: string;
}

/**
 * REST endpoint definition for a training plugin.
 * Mounted by TrainingController under /vitto-api/training/.
 */
export interface TrainingEndpoint {
  method: 'get' | 'post' | 'put' | 'delete';
  path: string;
  handler: (body: Record<string, unknown>, query: Record<string, string>) => Promise<unknown>;
  description?: string;
}

/**
 * Training Plugin interface — composable backend plugin for agent training.
 * Mirrors the frontend ChatPlugin pattern: each plugin declares tools, hooks, and endpoints.
 */
export interface TrainingPlugin {
  /** Unique plugin identifier */
  id: string;

  /** Human-readable name */
  name: string;

  /** Tools this plugin adds to Vitto's tool registry */
  tools?: ToolHandler[];

  /** Hook: called when a Vitto task completes (success or failure) */
  onTaskCompleted?: (task: unknown, outcome: TaskOutcome) => Promise<void>;

  /** Hook: called when an MCP decision is logged */
  onDecisionLogged?: (decision: unknown) => Promise<void>;

  /** Hook: called when SkillExtractionService extracts a new skill */
  onSkillExtracted?: (skill: unknown) => Promise<void>;

  /** REST endpoints mounted under /vitto-api/training/ */
  endpoints?: TrainingEndpoint[];

  /** Called after plugin registration, before first use */
  onInit?: () => Promise<void>;

  /** Called on module destruction */
  onDestroy?: () => Promise<void>;
}
git add APIs/backend-vitto-api/src/app/training/training-plugin.interface.ts
git commit -m "[S210.3] feat: add TrainingPlugin interface and types"

Task 2: TrainingPluginRegistry Service

Files:

// APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.ts

import { TrainingPluginRegistry } from '../training-plugin-registry.service';
import { TrainingPlugin, TaskOutcome } from '../training-plugin.interface';

describe('TrainingPluginRegistry', () => {
  let registry: TrainingPluginRegistry;
  const mockToolRegistry = { register: jest.fn() };

  beforeEach(() => {
    registry = new TrainingPluginRegistry(mockToolRegistry as any);
    mockToolRegistry.register.mockClear();
  });

  describe('register', () => {
    it('should store plugin and register its tools', () => {
      const plugin: TrainingPlugin = {
        id: 'test-plugin',
        name: 'Test Plugin',
        tools: [
          {
            name: 'test_tool',
            description: 'A test tool',
            parameters: {},
            layers: ['developer'],
            execute: jest.fn(),
          },
        ],
      };

      registry.register(plugin);

      expect(registry.getPlugin('test-plugin')).toBeDefined();
      expect(registry.getPlugin('test-plugin')!.name).toBe('Test Plugin');
      expect(mockToolRegistry.register).toHaveBeenCalledTimes(1);
      expect(mockToolRegistry.register).toHaveBeenCalledWith(plugin.tools![0]);
    });

    it('should handle plugin with no tools', () => {
      const plugin: TrainingPlugin = { id: 'no-tools', name: 'No Tools' };
      registry.register(plugin);

      expect(registry.getPlugin('no-tools')).toBeDefined();
      expect(mockToolRegistry.register).not.toHaveBeenCalled();
    });
  });

  describe('getAllPlugins', () => {
    it('should return all registered plugins', () => {
      registry.register({ id: 'a', name: 'A' });
      registry.register({ id: 'b', name: 'B' });

      expect(registry.getAllPlugins()).toHaveLength(2);
    });
  });

  describe('notifyTaskCompleted', () => {
    it('should call onTaskCompleted on all plugins that implement it', async () => {
      const handler = jest.fn().mockResolvedValue(undefined);
      registry.register({ id: 'p1', name: 'P1', onTaskCompleted: handler });
      registry.register({ id: 'p2', name: 'P2' }); // No handler

      const outcome: TaskOutcome = {
        success: true, durationMs: 1000, totalTokens: 500,
        toolsUsed: ['read_file'], qualityScore: 0.8,
      };

      await registry.notifyTaskCompleted({ id: 'task-1' } as any, outcome);

      expect(handler).toHaveBeenCalledTimes(1);
      expect(handler).toHaveBeenCalledWith({ id: 'task-1' }, outcome);
    });

    it('should catch and log plugin errors without propagating', async () => {
      const failingHandler = jest.fn().mockRejectedValue(new Error('plugin error'));
      registry.register({ id: 'failing', name: 'Failing', onTaskCompleted: failingHandler });

      // Should not throw
      await expect(
        registry.notifyTaskCompleted({} as any, { success: true } as TaskOutcome)
      ).resolves.not.toThrow();
    });
  });

  describe('notifySkillExtracted', () => {
    it('should call onSkillExtracted on implementing plugins', async () => {
      const handler = jest.fn().mockResolvedValue(undefined);
      registry.register({ id: 'skill-watcher', name: 'SW', onSkillExtracted: handler });

      await registry.notifySkillExtracted({ name: 'some-skill' });
      expect(handler).toHaveBeenCalledWith({ name: 'some-skill' });
    });
  });

  describe('notifyDecisionLogged', () => {
    it('should call onDecisionLogged on implementing plugins', async () => {
      const handler = jest.fn().mockResolvedValue(undefined);
      registry.register({ id: 'decision-watcher', name: 'DW', onDecisionLogged: handler });

      await registry.notifyDecisionLogged({ question: 'which approach?' });
      expect(handler).toHaveBeenCalledWith({ question: 'which approach?' });
    });
  });
});

Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.ts --no-cache Expected: FAIL — Cannot find module ../training-plugin-registry.service

// APIs/backend-vitto-api/src/app/training/training-plugin-registry.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { ToolRegistryService } from '../tools/tool-registry.service';
import { TrainingPlugin, TaskOutcome } from './training-plugin.interface';

@Injectable()
export class TrainingPluginRegistry {
  private readonly logger = new Logger('TrainingPluginRegistry');
  private readonly plugins = new Map<string, TrainingPlugin>();

  constructor(private readonly toolRegistry: ToolRegistryService) {}

  /**
   * Register a training plugin.
   * Tools are registered with Vitto's ToolRegistryService immediately.
   */
  register(plugin: TrainingPlugin): void {
    plugin.tools?.forEach(t => this.toolRegistry.register(t));
    this.plugins.set(plugin.id, plugin);
    this.logger.log(`Registered training plugin: ${plugin.name} (${plugin.id})`);
  }

  getPlugin(id: string): TrainingPlugin | undefined {
    return this.plugins.get(id);
  }

  getAllPlugins(): TrainingPlugin[] {
    return Array.from(this.plugins.values());
  }

  /**
   * Called by VittoEngineService after task completion.
   * Same pattern as SkillExtractionService.extractIfEligible.
   */
  async notifyTaskCompleted(task: unknown, outcome: TaskOutcome): Promise<void> {
    for (const plugin of this.plugins.values()) {
      if (plugin.onTaskCompleted) {
        try {
          await plugin.onTaskCompleted(task, outcome);
        } catch (err) {
          this.logger.warn(`Plugin ${plugin.id} onTaskCompleted failed: ${(err as Error).message}`);
        }
      }
    }
  }

  /**
   * Called by SkillExtractionService after createSkill() succeeds.
   */
  async notifySkillExtracted(skill: unknown): Promise<void> {
    for (const plugin of this.plugins.values()) {
      if (plugin.onSkillExtracted) {
        try {
          await plugin.onSkillExtracted(skill);
        } catch (err) {
          this.logger.warn(`Plugin ${plugin.id} onSkillExtracted failed: ${(err as Error).message}`);
        }
      }
    }
  }

  /**
   * Called by MemoryPersistenceService after learnFromMcpDecision() succeeds.
   */
  async notifyDecisionLogged(decision: unknown): Promise<void> {
    for (const plugin of this.plugins.values()) {
      if (plugin.onDecisionLogged) {
        try {
          await plugin.onDecisionLogged(decision);
        } catch (err) {
          this.logger.warn(`Plugin ${plugin.id} onDecisionLogged failed: ${(err as Error).message}`);
        }
      }
    }
  }
}

Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.ts --no-cache Expected: All 6 tests PASS

git add APIs/backend-vitto-api/src/app/training/training-plugin-registry.service.ts APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.ts
git commit -m "[S210.3] feat: add TrainingPluginRegistry with tool registration and lifecycle hooks"

Task 3: TrainingOrchestratorService

Files:

This service coordinates training operations routed from the controller. Phase 1 only needs teach() and trainAgent().

// APIs/backend-vitto-api/src/app/training/training-orchestrator.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { BackendAiClientService } from '../connectivity/backend-ai-client.service';
import { ProviderFactoryService } from '../llm/provider-factory.service';
import { SubAgentService } from '../engine/sub-agent.service';
import { VittoNarrativeService } from '../engine/vitto-narrative.service';

@Injectable()
export class TrainingOrchestratorService {
  private readonly logger = new Logger('TrainingOrchestrator');

  constructor(
    private readonly aiClient: BackendAiClientService,
    private readonly providerFactory: ProviderFactoryService,
    private readonly subAgentService: SubAgentService,
    private readonly narrative: VittoNarrativeService,
  ) {}

  /**
   * Teach an agent via natural language instruction.
   * Uses LLM to classify and structure the instruction.
   */
  async teach(params: {
    agentId?: string;
    instruction: string;
    type?: 'example' | 'knowledge';
    userId?: string;
  }): Promise<{ targetAgent: string; type: string; summary: string; corpusSize: number }> {
    const { instruction, userId } = params;

    // 1. Determine target agent
    let targetAgentId = params.agentId;
    let targetAgentName = params.agentId || 'unknown';

    if (!targetAgentId) {
      const bestAgent = await this.subAgentService.findBestAgent(instruction);
      if (bestAgent) {
        targetAgentId = bestAgent._id.toString();
        targetAgentName = bestAgent.name;
      } else {
        // Default to first available agent
        targetAgentId = 'general';
        targetAgentName = 'General Agent';
      }
    }

    // 2. Use LLM to classify and structure the instruction
    const provider = this.providerFactory.getDefault();
    const classificationResponse = await provider.sendMessage(
      [
        {
          role: 'user',
          content: `Classify this teaching instruction for an AI agent training system.

Instruction: "${instruction}"

Respond in JSON with exactly these fields:
- type: "example" (if this is a specific input/output pair the agent should learn) or "knowledge" (if this is a general rule, fact, or guideline)
- title: A short title for this instruction (max 80 chars)
- content: The structured content to store
- tags: Array of 1-3 relevant tags

JSON only, no markdown:`,
        },
      ],
      undefined,
      { maxTokens: 500, temperature: 0.1 },
    );

    let classification: { type: string; title: string; content: string; tags: string[] };
    try {
      const text = typeof classificationResponse === 'string'
        ? classificationResponse
        : classificationResponse.content?.[0]?.text || JSON.stringify(classificationResponse);
      classification = JSON.parse(text.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
    } catch {
      // Fallback: treat as knowledge doc
      classification = {
        type: params.type || 'knowledge',
        title: instruction.slice(0, 80),
        content: instruction,
        tags: [],
      };
    }

    const resolvedType = params.type || classification.type;

    // 3. Persist based on type
    let corpusSize = 0;

    if (resolvedType === 'example') {
      // Submit as training example to ai-api
      await this.aiClient.submitTrainingExampleViaVitto({
        agentId: targetAgentId,
        type: 'teach',
        prompt: classification.title,
        code: classification.content,
        quality: 8, // Human-taught examples get high quality score
        source: 'vitto-conversational-teach',
        userId: userId || 'unknown',
      });
    } else {
      // Add as knowledge doc to sub-agent definition
      if (targetAgentId && targetAgentId !== 'general') {
        await this.subAgentService.addKnowledgeDoc(targetAgentId, {
          title: classification.title,
          content: classification.content,
          tags: classification.tags,
        });
      }
    }

    // 4. Emit narrative event
    if (userId) {
      this.narrative.emitConscience(
        'conscience.training.taught' as any,
        userId,
        'context',
        `Taught ${targetAgentName}: "${classification.title}"`,
        { agentId: targetAgentId, type: resolvedType },
      );
    }

    this.logger.log(`Taught ${targetAgentName} (${resolvedType}): ${classification.title}`);

    return {
      targetAgent: targetAgentName,
      type: resolvedType,
      summary: classification.title,
      corpusSize,
    };
  }

  /**
   * Trigger training validation for an agent.
   * Fetches unvalidated examples from ai-api, uses LLM to validate quality,
   * then bulk-updates validation status.
   */
  async trainAgent(agentId: string, userId?: string): Promise<{
    success: boolean;
    validatedCount: number;
    totalCount: number;
    avgQuality: number;
  }> {
    // 1. Emit start narrative
    if (userId) {
      this.narrative.emitConscience(
        'conscience.training.started' as any,
        userId,
        'context',
        `Starting training validation for ${agentId}...`,
        { agentId },
      );
    }

    // 2. Fetch unvalidated candidates from ai-api
    const candidates = await this.aiClient.getValidationCandidates(agentId);

    if (candidates.length === 0) {
      return { success: true, validatedCount: 0, totalCount: 0, avgQuality: 0 };
    }

    // 3. LLM validates each example
    const provider = this.providerFactory.getDefault();
    const validatedIds: string[] = [];
    const rejectedIds: string[] = [];
    let totalQuality = 0;

    for (const example of candidates) {
      const response = await provider.sendMessage(
        [
          {
            role: 'user',
            content: `Rate this training example for a GoGrab platform AI agent on a scale of 1-10.

Prompt: ${example.prompt}
Expected Output: ${example.code}

Criteria: correctness, completeness, follows platform conventions, code quality.
Respond with just the number (1-10):`,
          },
        ],
        undefined,
        { maxTokens: 10, temperature: 0 },
      );

      const text = typeof response === 'string'
        ? response
        : response.content?.[0]?.text || '5';
      const score = parseInt(text.trim(), 10) || 5;
      totalQuality += score;

      if (score >= 7) {
        validatedIds.push(example.id);
      } else {
        rejectedIds.push(example.id);
      }
    }

    // 4. Bulk update on ai-api
    if (validatedIds.length > 0) {
      await this.aiClient.validateBatch(validatedIds, true);
    }
    if (rejectedIds.length > 0) {
      await this.aiClient.validateBatch(rejectedIds, false);
    }

    const avgQuality = totalQuality / candidates.length;

    // 5. Emit completion narrative
    if (userId) {
      this.narrative.emitConscience(
        'conscience.training.completed' as any,
        userId,
        'context',
        `Training complete for ${agentId}: ${validatedIds.length}/${candidates.length} validated (avg quality: ${avgQuality.toFixed(1)})`,
        { agentId, validatedCount: validatedIds.length, totalCount: candidates.length },
      );
    }

    this.logger.log(`Training ${agentId}: ${validatedIds.length}/${candidates.length} validated`);

    return {
      success: true,
      validatedCount: validatedIds.length,
      totalCount: candidates.length,
      avgQuality,
    };
  }
}
git add APIs/backend-vitto-api/src/app/training/training-orchestrator.service.ts
git commit -m "[S210.3] feat: add TrainingOrchestratorService with teach + trainAgent"

Task 4: ConversationalTeachPlugin

Files:

// APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts

import { createConversationalTeachPlugin } from '../plugins/conversational-teach.plugin';

describe('ConversationalTeachPlugin', () => {
  const mockOrchestrator = {
    teach: jest.fn().mockResolvedValue({
      targetAgent: 'angular-agent',
      type: 'knowledge',
      summary: 'Always use OnPush',
      corpusSize: 5,
    }),
  };

  it('should have correct id and name', () => {
    const plugin = createConversationalTeachPlugin(mockOrchestrator as any);
    expect(plugin.id).toBe('conversational-teach');
    expect(plugin.name).toBe('Conversational Teaching');
  });

  it('should register teach_agent tool', () => {
    const plugin = createConversationalTeachPlugin(mockOrchestrator as any);
    expect(plugin.tools).toHaveLength(1);
    expect(plugin.tools![0].name).toBe('teach_agent');
    expect(plugin.tools![0].layers).toContain('developer');
    expect(plugin.tools![0].layers).toContain('platform_builder');
  });

  it('should execute teach_agent tool and return structured result', async () => {
    const plugin = createConversationalTeachPlugin(mockOrchestrator as any);
    const tool = plugin.tools![0];

    const result = await tool.execute(
      { instruction: 'Always use OnPush change detection', agentId: 'angular-agent' },
      { userId: 'user-1' },
    );

    expect(mockOrchestrator.teach).toHaveBeenCalledWith({
      agentId: 'angular-agent',
      instruction: 'Always use OnPush change detection',
      userId: 'user-1',
    });

    const parsed = JSON.parse(result);
    expect(parsed.targetAgent).toBe('angular-agent');
    expect(parsed.type).toBe('knowledge');
  });

  it('should register teach endpoint', () => {
    const plugin = createConversationalTeachPlugin(mockOrchestrator as any);
    expect(plugin.endpoints).toHaveLength(1);
    expect(plugin.endpoints![0].method).toBe('post');
    expect(plugin.endpoints![0].path).toBe('teach');
  });
});

Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts --no-cache Expected: FAIL — Cannot find module

// APIs/backend-vitto-api/src/app/training/plugins/conversational-teach.plugin.ts

import { TrainingPlugin } from '../training-plugin.interface';
import { TrainingOrchestratorService } from '../training-orchestrator.service';

/**
 * ConversationalTeachPlugin — Teach agents via natural language.
 *
 * Registers the `teach_agent` tool so Vitto can parse instructions
 * from chat into structured training examples or knowledge docs.
 * Also exposes a REST endpoint for the Training Center UI.
 */
export function createConversationalTeachPlugin(
  orchestrator: TrainingOrchestratorService,
): TrainingPlugin {
  return {
    id: 'conversational-teach',
    name: 'Conversational Teaching',

    tools: [
      {
        name: 'teach_agent',
        description:
          'Teach an AI agent a new skill, rule, or pattern. ' +
          'Parses the instruction into a structured training example or knowledge document ' +
          'and adds it to the target agent\'s training corpus.',
        parameters: {
          type: 'object',
          properties: {
            instruction: {
              type: 'string',
              description: 'The teaching instruction in natural language',
            },
            agentId: {
              type: 'string',
              description:
                'Target agent ID (e.g., "angular-agent"). If omitted, the best matching agent is auto-selected.',
            },
          },
          required: ['instruction'],
        },
        layers: ['developer', 'platform_builder'],
        execute: async (input, context) => {
          const result = await orchestrator.teach({
            agentId: input.agentId as string | undefined,
            instruction: input.instruction as string,
            userId: context?.userId,
          });
          return JSON.stringify(result);
        },
      },
    ],

    endpoints: [
      {
        method: 'post',
        path: 'teach',
        description: 'Submit a teaching instruction to train an agent',
        handler: async (body) => {
          return orchestrator.teach({
            agentId: body.agentId as string | undefined,
            instruction: body.instruction as string,
            type: body.type as 'example' | 'knowledge' | undefined,
            userId: body.userId as string | undefined,
          });
        },
      },
    ],
  };
}

Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts --no-cache Expected: All 4 tests PASS

git add APIs/backend-vitto-api/src/app/training/plugins/conversational-teach.plugin.ts APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts
git commit -m "[S210.3] feat: add ConversationalTeachPlugin with teach_agent tool"

Task 5: TrainingController + TrainingModule

Files:

// APIs/backend-vitto-api/src/app/training/training.controller.ts

import { Controller, Get, Post, Param, Body, Logger } from '@nestjs/common';
import { TrainingPluginRegistry } from './training-plugin-registry.service';
import { TrainingOrchestratorService } from './training-orchestrator.service';

@Controller('training')
export class TrainingController {
  private readonly logger = new Logger('TrainingController');

  constructor(
    private readonly registry: TrainingPluginRegistry,
    private readonly orchestrator: TrainingOrchestratorService,
  ) {}

  /**
   * POST /vitto-api/training/teach
   * Teach an agent via natural language instruction.
   */
  @Post('teach')
  async teach(@Body() body: { agentId?: string; instruction: string; type?: string; userId?: string }) {
    return this.orchestrator.teach({
      agentId: body.agentId,
      instruction: body.instruction,
      type: body.type as 'example' | 'knowledge' | undefined,
      userId: body.userId,
    });
  }

  /**
   * POST /vitto-api/training/train/:agentId
   * Trigger training validation for an agent.
   */
  @Post('train/:agentId')
  async trainAgent(@Param('agentId') agentId: string, @Body() body: { userId?: string }) {
    return this.orchestrator.trainAgent(agentId, body.userId);
  }

  /**
   * GET /vitto-api/training/plugins
   * List all registered training plugins.
   */
  @Get('plugins')
  getPlugins() {
    return {
      plugins: this.registry.getAllPlugins().map(p => ({
        id: p.id,
        name: p.name,
        toolCount: p.tools?.length || 0,
        endpointCount: p.endpoints?.length || 0,
        hasTaskHook: !!p.onTaskCompleted,
        hasDecisionHook: !!p.onDecisionLogged,
        hasSkillHook: !!p.onSkillExtracted,
      })),
    };
  }
}
// APIs/backend-vitto-api/src/app/training/training.module.ts

import { Module, OnModuleInit, Logger } from '@nestjs/common';
import { TrainingPluginRegistry } from './training-plugin-registry.service';
import { TrainingOrchestratorService } from './training-orchestrator.service';
import { TrainingController } from './training.controller';
import { ToolsModule } from '../tools/tools.module';
import { ConnectivityModule } from '../connectivity/connectivity.module';
import { LLMModule } from '../llm/llm.module';
import { MindModule } from '../mind/mind.module';

// Plugins
import { createConversationalTeachPlugin } from './plugins/conversational-teach.plugin';

@Module({
  imports: [ToolsModule, ConnectivityModule, LLMModule, MindModule],
  controllers: [TrainingController],
  providers: [TrainingPluginRegistry, TrainingOrchestratorService],
  exports: [TrainingPluginRegistry],
})
export class TrainingModule implements OnModuleInit {
  private readonly logger = new Logger('TrainingModule');

  constructor(
    private readonly registry: TrainingPluginRegistry,
    private readonly orchestrator: TrainingOrchestratorService,
  ) {}

  async onModuleInit() {
    // Register core plugins
    this.registry.register(createConversationalTeachPlugin(this.orchestrator));

    this.logger.log(`Training system initialized: ${this.registry.getAllPlugins().length} plugin(s) registered`);
  }
}
git add APIs/backend-vitto-api/src/app/training/training.controller.ts APIs/backend-vitto-api/src/app/training/training.module.ts
git commit -m "[S210.3] feat: add TrainingController + TrainingModule with plugin registration"

Task 6: Wire TrainingModule into EngineModule + VittoEngineService Hook

Files:

In APIs/backend-vitto-api/src/app/engine/engine.module.ts, add TrainingModule to the imports array at line 69:

// Before (line 69):
MindModule, ConnectivityModule, LLMModule, ToolsModule, StrategyModule, PromptModule, ProactiveModule, SecurityModule, UsersModule,

// After:
MindModule, ConnectivityModule, LLMModule, ToolsModule, StrategyModule, PromptModule, ProactiveModule, SecurityModule, UsersModule, TrainingModule,

Add the import statement at the top:

import { TrainingModule } from '../training/training.module';

In APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts, add to the constructor:

import { TrainingPluginRegistry } from '../training/training-plugin-registry.service';

// In constructor:
private readonly trainingRegistry: TrainingPluginRegistry,

After the existing this.skillExtraction.extractIfEligible(task).catch(() => {}) line, add:

      // S210.3: Notify training plugins of task completion (non-blocking)
      const taskOutcome = {
        success: isSuccess,
        durationMs: task.cost.durationMs,
        totalTokens: task.cost.totalTokens || 0,
        toolsUsed: task.phases.flatMap((p: any) => (p.toolCalls || []).map((tc: any) => tc.toolName)).filter(Boolean),
        qualityScore: isSuccess ? 0.8 : 0.3,
        failureReason: isSuccess ? undefined : this.getTaskFailureReason(task),
      };
      this.trainingRegistry.notifyTaskCompleted(task, taskOutcome).catch(() => {});

Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.app.json 2>&1 | head -20 Expected: No errors from training-related files

git add APIs/backend-vitto-api/src/app/engine/engine.module.ts APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts
git commit -m "[S210.3] feat: wire TrainingModule into EngineModule + task completion hook"

Chunk 2: Backend-AI-API Data Endpoints + Frontend Routing

Task 7: Add ai-api Convenience Endpoints

Files:

In APIs/backend-ai-api/src/app/platform-ai/services/training-data.service.ts, add after the existing getExamplesForAgent() method:

  /**
   * S210.3: Get unvalidated training examples for Vitto to evaluate.
   * Returns examples with quality >= 5 that haven't been validated yet.
   */
  async getValidationCandidates(agentId: string, limit = 20): Promise<any[]> {
    const collection = this.agentsConnection.collection('training_examples');
    return collection
      .find({
        agentId,
        $or: [{ validated: false }, { validated: { $exists: false } }],
        'quality': { $gte: 5 },
      })
      .sort({ createdAt: -1 })
      .limit(limit)
      .toArray();
  }

In APIs/backend-ai-api/src/app/platform-ai/platform-ai.controller.ts, add after the getTrainingExamples() method (after line ~216):

  /**
   * S210.3: Get unvalidated examples for Vitto training orchestrator.
   */
  @Public()
  @Get('training/validation-candidates/:agentId')
  async getValidationCandidates(
    @Param('agentId') agentId: string,
    @Query('limit') limit = '20',
  ) {
    const candidates = await this.trainingDataService.getValidationCandidates(
      agentId,
      parseInt(limit),
    );
    return { candidates };
  }

  /**
   * S210.3: Bulk validate/reject training examples.
   * Called by Vitto's TrainingOrchestratorService after LLM evaluation.
   */
  @Public()
  @Post('training/validate-batch')
  async validateBatch(
    @Body() dto: { exampleIds: string[]; isValid: boolean },
  ) {
    const collection = this.agentsConnection.collection('training_examples');
    const result = await collection.updateMany(
      { id: { $in: dto.exampleIds } },
      {
        $set: {
          validated: dto.isValid,
          'metadata.validated': dto.isValid,
          'metadata.validatedAt': new Date(),
          'metadata.validatedBy': 'vitto-training-orchestrator',
        },
      },
    );
    return {
      success: true,
      matchedCount: result.matchedCount,
      modifiedCount: result.modifiedCount,
    };
  }

Note: The controller needs @InjectConnection(AI_CONNECTION_NAMES.AGENTS) for the validate-batch endpoint. Check if the controller already has it injected. If not, inject via the TrainingDataService instead — add a validateBatch() method there and call it from the controller.

git add APIs/backend-ai-api/src/app/platform-ai/platform-ai.controller.ts APIs/backend-ai-api/src/app/platform-ai/services/training-data.service.ts
git commit -m "[S210.3] feat: add validation-candidates and validate-batch endpoints to ai-api"

Task 8: Add BackendAiClientService Methods

Files:

  // ── S210.3: Training Orchestration ──

  /**
   * Fetch unvalidated training examples for Vitto to evaluate.
   */
  async getValidationCandidates(agentId: string, limit = 20): Promise<any[]> {
    try {
      const { data } = await firstValueFrom(
        this.http.get<{ candidates: any[] }>(
          `${this.baseUrl}/platform/training/validation-candidates/${encodeURIComponent(agentId)}?limit=${limit}`,
          { headers: this.headers, timeout: 5000 },
        ),
      );
      return data?.candidates || [];
    } catch {
      return [];
    }
  }

  /**
   * Bulk validate/reject training examples after LLM evaluation.
   */
  async validateBatch(exampleIds: string[], isValid: boolean): Promise<void> {
    try {
      await firstValueFrom(
        this.http.post(
          `${this.baseUrl}/platform/training/validate-batch`,
          { exampleIds, isValid },
          { headers: this.headers, timeout: 10000 },
        ),
      );
    } catch (err) {
      this.logger.warn(`validateBatch failed: ${(err as Error).message}`);
    }
  }

  /**
   * Submit a training example with Vitto-specific metadata.
   * Wraps the existing submitExample endpoint with source tracking.
   */
  async submitTrainingExampleViaVitto(params: {
    agentId: string;
    type: string;
    prompt: string;
    code: string;
    quality: number;
    source: string;
    userId: string;
  }): Promise<void> {
    try {
      await firstValueFrom(
        this.http.post(
          `${this.baseUrl}/platform/training/examples`,
          {
            ...params,
            metadata: { source: params.source, submittedVia: 'vitto-training' },
          },
          { headers: this.headers, timeout: 10000 },
        ),
      );
    } catch (err) {
      this.logger.warn(`submitTrainingExampleViaVitto failed: ${(err as Error).message}`);
    }
  }
git add APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.ts
git commit -m "[S210.3] feat: add training methods to BackendAiClientService"

Task 9: Reroute Frontend McpService Write Methods

Files:

Change submitTrainingExample() (lines ~103-113):

  // Before:
  return this.http.post(`${this.aiApiUrl}/platform/training/examples`, params)

  // After:
  return this.http.post(`${this.vittoApiUrl}/training/teach`, {
    agentId: params.agentId,
    instruction: `Training example — Prompt: ${params.prompt}\nExpected: ${params.code}`,
    type: 'example',
  })

Change trainAgent() (lines ~50-54):

  // Before:
  return this.http.post(`${this.aiApiUrl}/platform/agents/${agentId}/train`, {})

  // After:
  return this.http.post(`${this.vittoApiUrl}/training/train/${agentId}`, {})

Change testAgent() (lines ~59-67):

  // Before:
  return this.http.post(`${this.aiApiUrl}/platform/agents/test`, params)

  // After — Phase 1 stub: route to Vitto, placeholder until AgentEvaluationPlugin (Phase 3):
  return this.http.post(`${this.vittoApiUrl}/training/evaluate`, params)

Change extractPlatformPatterns() (lines ~151-155):

  // Before:
  return this.http.post(`${this.aiApiUrl}/platform/patterns/extract`, {})

  // After — Phase 1 stub: route to Vitto, placeholder until PatternExtractionPlugin (Phase 4):
  return this.http.post(`${this.vittoApiUrl}/training/patterns/extract`, {})

Note: The evaluate and patterns/extract endpoints don't exist on Vitto yet (Phase 3 and 4). Add temporary stubs in TrainingController that return { status: 'not-yet-implemented', message: 'Coming in Phase 3/4' } to avoid 404 errors.

In APIs/backend-vitto-api/src/app/training/training.controller.ts, add:

  /** Stub: Agent evaluation (Phase 3) */
  @Post('evaluate')
  async evaluateAgent(@Body() body: any) {
    return { status: 'not-yet-implemented', message: 'Agent evaluation coming in Phase 3' };
  }

  /** Stub: Pattern extraction (Phase 4) */
  @Post('patterns/extract')
  async extractPatterns(@Body() body: any) {
    return { status: 'not-yet-implemented', message: 'Pattern extraction coming in Phase 4' };
  }

Run: npx tsc --noEmit --project libs/libs-ai/ai-agents-training-center/tsconfig.lib.json 2>&1 | head -10 Expected: No new errors

git add libs/libs-ai/ai-agents-training-center/src/lib/services/mcp.service.ts APIs/backend-vitto-api/src/app/training/training.controller.ts
git commit -m "[S210.3] feat: reroute Training Center write operations through Vitto"

Task 10: End-to-End Verification

Kill and restart the Vitto backend to pick up the new TrainingModule:

# In the terminal running backend-vitto-api, Ctrl+C then:
yarn serve:backend-vitto

Watch for: TrainingModule: Training system initialized: 1 plugin(s) registered

curl -s http://localhost:3003/vitto-api/training/plugins | jq

Expected:

{
  "plugins": [
    {
      "id": "conversational-teach",
      "name": "Conversational Teaching",
      "toolCount": 1,
      "endpointCount": 1,
      "hasTaskHook": false,
      "hasDecisionHook": false,
      "hasSkillHook": false
    }
  ]
}
curl -s -X POST http://localhost:3003/vitto-api/training/teach \
  -H 'Content-Type: application/json' \
  -d '{"instruction": "Angular agents should always use OnPush change detection strategy", "agentId": "angular-agent"}' | jq

Expected: Response with targetAgent, type, summary fields (may fail gracefully if angular-agent definition doesn't exist — that's ok for verification).

curl -s "http://localhost:3001/ai-api/platform/training/validation-candidates/module-generator?limit=5" | jq

Expected: { "candidates": [...] } (may be empty array — that's correct)

git add -A
git commit -m "[S210.3] chore: Phase 1 complete — Vitto training plugin system operational"

Summary: What Phase 1 Delivers

After completing all 10 tasks:

  1. TrainingPlugin interface — composable, extensible plugin contract
  2. TrainingPluginRegistry — registers tools + dispatches lifecycle hooks
  3. ConversationalTeachPlugin — teach_agent tool registered in Vitto's tool system
  4. TrainingController — REST endpoints at /vitto-api/training/*
  5. TrainingOrchestratorService — teach() and trainAgent() operations
  6. VittoEngineService hook — task completion notifies training plugins
  7. Backend-ai-api convenience endpoints — validation candidates + batch validate
  8. Frontend routing — Training Center writes go through Vitto, reads stay on ai-api

What's NOT in Phase 1: SelfLearningPlugin (Phase 2), AgentEvaluationPlugin (Phase 3), PatternExtractionPlugin (Phase 4), rate limiting, duplicate detection, regression guard.