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:
Create:
APIs/backend-vitto-api/src/app/training/training-plugin.interface.tsStep 1: Create the interface file
// 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>;
}
- Step 2: Commit
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:
Create:
APIs/backend-vitto-api/src/app/training/training-plugin-registry.service.tsCreate:
APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.tsStep 1: Write the failing test
// 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?' });
});
});
});
- Step 2: Run test to verify it fails
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
- Step 3: Implement TrainingPluginRegistry
// 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}`);
}
}
}
}
}
- Step 4: Run test to verify it passes
Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/training-plugin-registry.spec.ts --no-cache
Expected: All 6 tests PASS
- Step 5: Commit
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:
- Create:
APIs/backend-vitto-api/src/app/training/training-orchestrator.service.ts
This service coordinates training operations routed from the controller. Phase 1 only needs teach() and trainAgent().
- Step 1: Create the orchestrator service
// 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,
};
}
}
- Step 2: Commit
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:
Create:
APIs/backend-vitto-api/src/app/training/plugins/conversational-teach.plugin.tsCreate:
APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.tsStep 1: Write the failing test
// 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');
});
});
- Step 2: Run test to verify it fails
Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts --no-cache
Expected: FAIL — Cannot find module
- Step 3: Implement ConversationalTeachPlugin
// 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,
});
},
},
],
};
}
- Step 4: Run test to verify it passes
Run: npx jest APIs/backend-vitto-api/src/app/training/__tests__/conversational-teach.spec.ts --no-cache
Expected: All 4 tests PASS
- Step 5: Commit
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:
Create:
APIs/backend-vitto-api/src/app/training/training.controller.tsCreate:
APIs/backend-vitto-api/src/app/training/training.module.tsStep 1: Create the controller
// 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,
})),
};
}
}
- Step 2: Create the module
// 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`);
}
}
- Step 3: Commit
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:
Modify:
APIs/backend-vitto-api/src/app/engine/engine.module.ts:69Modify:
APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts:484Step 1: Add TrainingModule import to EngineModule
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';
- Step 2: Inject TrainingPluginRegistry into VittoEngineService
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,
- Step 3: Add training hook after skill extraction (line ~484)
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(() => {});
- Step 4: Verify backend-vitto-api compiles
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.app.json 2>&1 | head -20
Expected: No errors from training-related files
- Step 5: Commit
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:
Modify:
APIs/backend-ai-api/src/app/platform-ai/platform-ai.controller.tsModify:
APIs/backend-ai-api/src/app/platform-ai/services/training-data.service.tsStep 1: Add
getValidationCandidates()to TrainingDataService
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();
}
- Step 2: Add controller endpoints
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.
- Step 3: Commit
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:
Modify:
APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.tsStep 1: Add 3 new methods before the closing brace (before line ~839)
// ── 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}`);
}
}
- Step 2: Commit
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:
Modify:
libs/libs-ai/ai-agents-training-center/src/lib/services/mcp.service.tsStep 1: Reroute 4 write methods from
aiApiUrltovittoApiUrl
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.
- Step 2: Add stub endpoints to TrainingController
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' };
}
- Step 3: Verify frontend compiles
Run: npx tsc --noEmit --project libs/libs-ai/ai-agents-training-center/tsconfig.lib.json 2>&1 | head -10
Expected: No new errors
- Step 4: Commit
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
- Step 1: Restart backend-vitto-api
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
- Step 2: Verify plugin list endpoint
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
}
]
}
- Step 3: Verify teach endpoint
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).
- Step 4: Verify ai-api validation-candidates endpoint
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)
- Step 5: Final commit with all verification passing
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:
- TrainingPlugin interface — composable, extensible plugin contract
- TrainingPluginRegistry — registers tools + dispatches lifecycle hooks
- ConversationalTeachPlugin —
teach_agenttool registered in Vitto's tool system - TrainingController — REST endpoints at
/vitto-api/training/* - TrainingOrchestratorService —
teach()andtrainAgent()operations - VittoEngineService hook — task completion notifies training plugins
- Backend-ai-api convenience endpoints — validation candidates + batch validate
- 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.