2026 03 18 Jarvis P1 Sub Vitto Engine
Documentation for Plans
Jarvis Phase 1: Sub-Vitto Engine — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Enable Vitto to deploy parallel sub-Vittos via a deploy_agents tool, running them through the same AgentLoopService.run() engine with scoped context, tier-based tool filtering, and prefixed SSE events.
Architecture: Refactor SubAgentService.executeTask() to delegate to AgentLoopService.run() instead of its own while loop (~160 lines removed). New SubVittoOrchestratorService handles parallel dispatch via Promise.allSettled() with multi-round synthesis. New orchestration.plugin.ts registers the deploy_agents tool. Frontend gets a SubVittoOrchestrationRenderer for the chat panel.
Tech Stack: NestJS (backend-vitto-api), Angular 20+ (libs-ai/vitto-chat), RxJS Subject (SSE), Mongoose (MongoDB)
Spec: docs/superpowers/specs/2026-03-18-vitto-jarvis-comprehensive-evolution-design.md (Section 1 + Section 8 chat renderer)
File Structure
New Files
| File | Responsibility |
|---|---|
APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.ts |
Parallel dispatch, multi-round synthesis, parent budget tracking |
APIs/backend-vitto-api/src/app/engine/sub-vitto-sse-proxy.ts |
Wraps parent SSE subject, re-emits events with subvitto.* prefix + subTaskId |
APIs/backend-vitto-api/src/app/tools/plugins/orchestration.plugin.ts |
deploy_agents tool handler |
APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.ts |
Unit tests for orchestrator |
APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-sse-proxy.spec.ts |
Unit tests for SSE proxy |
APIs/backend-vitto-api/src/app/tools/plugins/__tests__/orchestration.plugin.spec.ts |
Unit tests for deploy_agents tool |
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component.ts |
Chat renderer component |
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component.html |
Chat renderer template |
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component.scss |
Chat renderer styles |
Modified Files
| File | Change |
|---|---|
APIs/backend-vitto-api/src/app/engine/sub-agent.service.ts |
Replace executeTask() while loop with AgentLoopService.run() call |
APIs/backend-vitto-api/src/app/tools/plugins/plugins.index.ts |
Register orchestrationPlugin |
APIs/backend-vitto-api/src/app/tools/plugins/tool-plugin.types.ts |
Add orchestrator to ToolPluginContext |
APIs/backend-vitto-api/src/app/engine/engine.module.ts |
Register SubVittoOrchestratorService, add to CHANNEL_ADAPTERS or exports |
APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts |
Pass orchestrator to plugin context |
APIs/backend-vitto-api/src/app/types/vitto.types.ts |
Add SubVittoEventType SSE types |
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/tool-renderer-host/tool-renderer-host.component.ts |
Register orchestration renderer |
Task 1: SubVitto SSE Proxy
Why first: Both the orchestrator and the SubAgentService refactoring depend on this. It's a pure utility with no dependencies — ideal starting point.
Files:
Create:
APIs/backend-vitto-api/src/app/engine/sub-vitto-sse-proxy.tsTest:
APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-sse-proxy.spec.tsStep 1: Write the failing test
// sub-vitto-sse-proxy.spec.ts
import { Subject } from 'rxjs';
import { SubVittoSSEProxy } from '../sub-vitto-sse-proxy';
interface SSEEvent {
type: string;
taskId: string;
data: Record<string, unknown>;
timestamp: string;
}
describe('SubVittoSSEProxy', () => {
let parentSubject: Subject<SSEEvent>;
let proxy: SubVittoSSEProxy;
const subTaskId = 'sub-task-123';
const parentTaskId = 'parent-task-456';
beforeEach(() => {
parentSubject = new Subject<SSEEvent>();
proxy = new SubVittoSSEProxy(parentSubject, subTaskId, parentTaskId);
});
it('should re-emit agent.iteration as subvitto.progress', () => {
const events: SSEEvent[] = [];
parentSubject.subscribe(e => events.push(e));
proxy.subject.next({
type: 'agent.iteration',
taskId: subTaskId,
data: { iteration: 1, maxIterations: 10 },
timestamp: new Date().toISOString(),
});
expect(events).toHaveLength(1);
expect(events[0].type).toBe('subvitto.progress');
expect(events[0].data['subTaskId']).toBe(subTaskId);
expect(events[0].data['parentTaskId']).toBe(parentTaskId);
expect(events[0].data['iteration']).toBe(1);
});
it('should re-emit tool.called with subvitto prefix', () => {
const events: SSEEvent[] = [];
parentSubject.subscribe(e => events.push(e));
proxy.subject.next({
type: 'tool.called',
taskId: subTaskId,
data: { tool: 'recall_memory', success: true },
timestamp: new Date().toISOString(),
});
expect(events).toHaveLength(1);
expect(events[0].type).toBe('subvitto.tool');
expect(events[0].data['subTaskId']).toBe(subTaskId);
});
it('should pass through task.stuck as subvitto.stuck', () => {
const events: SSEEvent[] = [];
parentSubject.subscribe(e => events.push(e));
proxy.subject.next({
type: 'task.stuck',
taskId: subTaskId,
data: { reason: 'loop detected' },
timestamp: new Date().toISOString(),
});
expect(events).toHaveLength(1);
expect(events[0].type).toBe('subvitto.stuck');
});
it('should unsubscribe on dispose', () => {
proxy.dispose();
const events: SSEEvent[] = [];
parentSubject.subscribe(e => events.push(e));
proxy.subject.next({
type: 'agent.iteration',
taskId: subTaskId,
data: {},
timestamp: new Date().toISOString(),
});
expect(events).toHaveLength(0);
});
});
- Step 2: Run test to verify it fails
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-sse-proxy.spec.ts --no-cache
Expected: FAIL — Cannot find module '../sub-vitto-sse-proxy'
- Step 3: Implement SubVittoSSEProxy
// sub-vitto-sse-proxy.ts
import { Subject, Subscription } from 'rxjs';
interface SSEEvent {
type: string;
taskId: string;
data: Record<string, unknown>;
timestamp: string;
}
/**
* Wraps a parent SSE subject, intercepts child events, and re-emits them
* with subvitto.* type prefix + subTaskId/parentTaskId metadata.
*
* This prevents sub-Vitto events (agent.iteration, tool.called, etc.)
* from polluting the main event stream.
*/
export class SubVittoSSEProxy {
readonly subject = new Subject<SSEEvent>();
private subscription: Subscription;
private static readonly TYPE_MAP: Record<string, string> = {
'agent.iteration': 'subvitto.progress',
'agent.text': 'subvitto.text',
'tool.starting': 'subvitto.tool_starting',
'tool.called': 'subvitto.tool',
'task.stuck': 'subvitto.stuck',
'phase.started': 'subvitto.phase_started',
'phase.completed': 'subvitto.phase_completed',
};
constructor(
private readonly parentSubject: Subject<SSEEvent>,
private readonly subTaskId: string,
private readonly parentTaskId: string,
) {
this.subscription = this.subject.subscribe(event => {
const mappedType = SubVittoSSEProxy.TYPE_MAP[event.type] || `subvitto.${event.type}`;
this.parentSubject.next({
type: mappedType,
taskId: this.parentTaskId,
data: {
...event.data,
subTaskId: this.subTaskId,
parentTaskId: this.parentTaskId,
},
timestamp: event.timestamp,
});
});
}
dispose(): void {
this.subscription.unsubscribe();
this.subject.complete();
}
}
- Step 4: Run test to verify it passes
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-sse-proxy.spec.ts --no-cache
Expected: PASS — all 4 tests green
- Step 5: Commit
git add APIs/backend-vitto-api/src/app/engine/sub-vitto-sse-proxy.ts APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-sse-proxy.spec.ts
git commit -m "[S234.2] feat: add SubVittoSSEProxy for scoped sub-agent event streaming"
Task 2: SSE Type Definitions
Why now: The orchestrator and renderer both need these types. Define them once in the shared types file.
Files:
Modify:
APIs/backend-vitto-api/src/app/types/vitto.types.tsStep 1: Add SubVitto SSE event types
Add after the existing OrchestrationEventType definition (around line 84):
// --- S234: Sub-Vitto Orchestration Events ---
export type SubVittoEventType =
| 'subvitto.spawned'
| 'subvitto.progress'
| 'subvitto.tool'
| 'subvitto.tool_starting'
| 'subvitto.text'
| 'subvitto.stuck'
| 'subvitto.completed'
| 'subvitto.failed'
| 'orchestrator.round';
export type SubVittoTier = 'in-process' | 'vm' | 'browser';
export interface SubVittoTask {
subTaskId: string;
description: string;
tier: SubVittoTier;
status: 'pending' | 'running' | 'completed' | 'failed';
result?: string;
error?: string;
iterations?: number;
inputTokens?: number;
outputTokens?: number;
durationMs?: number;
}
export interface SubVittoOrchestratorResult {
round: number;
tasks: SubVittoTask[];
synthesis: string;
totalInputTokens: number;
totalOutputTokens: number;
totalDurationMs: number;
}
- Step 2: Verify types compile
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -20
Expected: No new errors
- Step 3: Commit
git add APIs/backend-vitto-api/src/app/types/vitto.types.ts
git commit -m "[S234.2] feat: add SubVitto SSE event types and orchestration interfaces"
Task 3: SubVittoOrchestratorService
Why now: This is the core engine. It depends on Task 1 (SSE proxy) and Task 2 (types). Everything else depends on it.
Files:
Create:
APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.tsTest:
APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.tsStep 1: Write the failing test — single-round parallel dispatch
// sub-vitto-orchestrator.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { SubVittoOrchestratorService } from '../sub-vitto-orchestrator.service';
import { AgentLoopService } from '../agent-loop.service';
import { VittoNarrativeService } from '../vitto-narrative.service';
import { ConfigService } from '@nestjs/config';
import { Subject } from 'rxjs';
describe('SubVittoOrchestratorService', () => {
let service: SubVittoOrchestratorService;
let agentLoop: jest.Mocked<AgentLoopService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SubVittoOrchestratorService,
{
provide: AgentLoopService,
useValue: {
run: jest.fn().mockResolvedValue({
output: 'Task completed successfully',
toolCalls: [],
iterations: 3,
inputTokens: 500,
outputTokens: 200,
stopReason: 'end_turn',
completedViaTaskComplete: true,
}),
},
},
{
provide: VittoNarrativeService,
useValue: {
emitConscience: jest.fn(),
emitIntrospection: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string, def: unknown) => def),
},
},
],
}).compile();
service = module.get(SubVittoOrchestratorService);
agentLoop = module.get(AgentLoopService);
});
it('should dispatch multiple tasks in parallel', async () => {
const result = await service.orchestrate({
parentTaskId: 'parent-1',
userId: 'user-1',
tasks: [
{ description: 'Search for security patterns', tier: 'in-process' },
{ description: 'Analyze code structure', tier: 'in-process' },
],
sseSubject: new Subject(),
});
expect(agentLoop.run).toHaveBeenCalledTimes(2);
expect(result.tasks).toHaveLength(2);
expect(result.tasks[0].status).toBe('completed');
expect(result.tasks[1].status).toBe('completed');
expect(result.totalInputTokens).toBe(1000); // 500 * 2
});
it('should handle partial failures gracefully', async () => {
agentLoop.run
.mockResolvedValueOnce({
output: 'Success', toolCalls: [], iterations: 2,
inputTokens: 300, outputTokens: 100, stopReason: 'end_turn',
completedViaTaskComplete: true,
})
.mockRejectedValueOnce(new Error('LLM timeout'));
const result = await service.orchestrate({
parentTaskId: 'parent-2',
userId: 'user-2',
tasks: [
{ description: 'Task A', tier: 'in-process' },
{ description: 'Task B', tier: 'in-process' },
],
sseSubject: new Subject(),
});
expect(result.tasks[0].status).toBe('completed');
expect(result.tasks[1].status).toBe('failed');
expect(result.tasks[1].error).toContain('LLM timeout');
});
it('should respect max parallel limit', async () => {
const callOrder: number[] = [];
let callCount = 0;
agentLoop.run.mockImplementation(async () => {
const myIndex = callCount++;
callOrder.push(myIndex);
await new Promise(r => setTimeout(r, 10));
return {
output: `Result ${myIndex}`, toolCalls: [], iterations: 1,
inputTokens: 100, outputTokens: 50, stopReason: 'end_turn',
completedViaTaskComplete: true,
};
});
const tasks = Array.from({ length: 7 }, (_, i) => ({
description: `Task ${i}`, tier: 'in-process' as const,
}));
const result = await service.orchestrate({
parentTaskId: 'parent-3',
userId: 'user-3',
tasks,
sseSubject: new Subject(),
maxParallel: 3,
});
expect(result.tasks).toHaveLength(7);
// All should complete even with concurrency limit
expect(result.tasks.filter(t => t.status === 'completed')).toHaveLength(7);
});
it('should block recursive orchestration (depth > 1)', async () => {
const result = await service.orchestrate({
parentTaskId: 'parent-4',
userId: 'user-4',
tasks: [{ description: 'Recursive task', tier: 'in-process' }],
sseSubject: new Subject(),
depth: 2,
});
expect(agentLoop.run).not.toHaveBeenCalled();
expect(result.tasks[0].status).toBe('failed');
expect(result.tasks[0].error).toContain('depth');
});
});
- Step 2: Run test to verify it fails
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.ts --no-cache
Expected: FAIL — Cannot find module '../sub-vitto-orchestrator.service'
- Step 3: Implement SubVittoOrchestratorService
// sub-vitto-orchestrator.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Subject } from 'rxjs';
import { AgentLoopService, AgentLoopOptions } from './agent-loop.service';
import { VittoNarrativeService } from './vitto-narrative.service';
import { SubVittoSSEProxy } from './sub-vitto-sse-proxy';
import {
SubVittoTask,
SubVittoTier,
SubVittoOrchestratorResult,
} from '../types/vitto.types';
/** Tool names allowed per tier (in-process only for P1; VM/browser deferred) */
const TIER_TOOL_FILTERS: Record<SubVittoTier, string[]> = {
'in-process': [
'recall_memory', 'search_decisions', 'query_docs',
'web_search', 'fetch_webpage', 'http_request',
'task_complete',
],
// VM and browser tiers will be enabled in Phase 2
'vm': [
'recall_memory', 'search_decisions', 'query_docs',
'web_search', 'fetch_webpage', 'http_request',
'run_command', 'write_file', 'read_file', 'search_code',
'task_complete',
],
'browser': [
'recall_memory', 'search_decisions', 'query_docs',
'web_search', 'fetch_webpage', 'http_request',
'browser_navigate', 'browser_interact', 'browser_screenshot',
'task_complete',
],
};
export interface OrchestrateOptions {
parentTaskId: string;
userId: string;
tasks: Array<{ description: string; tier: SubVittoTier }>;
sseSubject: Subject<unknown>;
maxParallel?: number;
maxIterationsPerTask?: number;
maxTokensPerTask?: number;
/** Recursion depth — blocked if > 1 */
depth?: number;
}
interface SSEEvent {
type: string;
taskId: string;
data: Record<string, unknown>;
timestamp: string;
}
const MAX_DEPTH = 1;
@Injectable()
export class SubVittoOrchestratorService {
private readonly logger = new Logger('SubVittoOrchestrator');
private readonly maxParallel: number;
private readonly defaultIterations: number;
private readonly defaultMaxTokens: number;
constructor(
private readonly agentLoop: AgentLoopService,
private readonly narrative: VittoNarrativeService,
private readonly config: ConfigService,
) {
this.maxParallel = this.config.get<number>('VITTO_SUBVITTO_MAX_PARALLEL', 5);
this.defaultIterations = this.config.get<number>('VITTO_SUBVITTO_DEFAULT_ITERATIONS', 10);
this.defaultMaxTokens = this.config.get<number>('VITTO_SUBVITTO_DEFAULT_MAX_TOKENS', 100000);
}
async orchestrate(options: OrchestrateOptions): Promise<SubVittoOrchestratorResult> {
const startTime = Date.now();
const depth = options.depth || 1;
const maxParallel = options.maxParallel || this.maxParallel;
const sseSubject = options.sseSubject as Subject<SSEEvent>;
// Block recursive orchestration
if (depth > MAX_DEPTH) {
this.logger.warn(`Blocked recursive orchestration at depth ${depth} for task ${options.parentTaskId}`);
return {
round: 0,
tasks: options.tasks.map((t, i) => ({
subTaskId: `${options.parentTaskId}_sub_${i}`,
description: t.description,
tier: t.tier,
status: 'failed' as const,
error: `Recursive orchestration blocked: max depth is ${MAX_DEPTH}`,
})),
synthesis: 'Orchestration blocked: recursive sub-Vitto deployment is not allowed.',
totalInputTokens: 0,
totalOutputTokens: 0,
totalDurationMs: 0,
};
}
// Only in-process tier supported in P1
const validTasks = options.tasks.map((t, i) => {
if (t.tier !== 'in-process') {
this.logger.warn(`Tier '${t.tier}' not yet supported, falling back to in-process for: ${t.description}`);
}
return {
...t,
tier: 'in-process' as SubVittoTier,
subTaskId: `${options.parentTaskId}_sub_${i}_${Date.now().toString(36)}`,
};
});
// Emit spawned events
for (const task of validTasks) {
sseSubject.next({
type: 'subvitto.spawned',
taskId: options.parentTaskId,
data: {
subTaskId: task.subTaskId,
tier: task.tier,
description: task.description,
},
timestamp: new Date().toISOString(),
});
}
// Execute with concurrency limit
const results = await this.executeWithConcurrency(
validTasks, options, sseSubject, maxParallel,
);
const totalDurationMs = Date.now() - startTime;
const orchestratorResult: SubVittoOrchestratorResult = {
round: 1,
tasks: results,
synthesis: this.buildSynthesis(results),
totalInputTokens: results.reduce((sum, t) => sum + (t.inputTokens || 0), 0),
totalOutputTokens: results.reduce((sum, t) => sum + (t.outputTokens || 0), 0),
totalDurationMs,
};
// Emit round complete
sseSubject.next({
type: 'orchestrator.round',
taskId: options.parentTaskId,
data: {
roundNumber: 1,
subTaskIds: results.map(t => t.subTaskId),
completedCount: results.filter(t => t.status === 'completed').length,
failedCount: results.filter(t => t.status === 'failed').length,
totalDurationMs,
},
timestamp: new Date().toISOString(),
});
return orchestratorResult;
}
private async executeWithConcurrency(
tasks: Array<{ subTaskId: string; description: string; tier: SubVittoTier }>,
options: OrchestrateOptions,
sseSubject: Subject<SSEEvent>,
maxParallel: number,
): Promise<SubVittoTask[]> {
const results: SubVittoTask[] = [];
const chunks = this.chunk(tasks, maxParallel);
for (const chunk of chunks) {
const chunkResults = await Promise.allSettled(
chunk.map(task => this.executeSubTask(task, options, sseSubject)),
);
for (let i = 0; i < chunkResults.length; i++) {
const settled = chunkResults[i];
const task = chunk[i];
if (settled.status === 'fulfilled') {
results.push(settled.value);
} else {
results.push({
subTaskId: task.subTaskId,
description: task.description,
tier: task.tier,
status: 'failed',
error: settled.reason?.message || String(settled.reason),
});
sseSubject.next({
type: 'subvitto.failed',
taskId: options.parentTaskId,
data: { subTaskId: task.subTaskId, error: settled.reason?.message },
timestamp: new Date().toISOString(),
});
}
}
}
return results;
}
private async executeSubTask(
task: { subTaskId: string; description: string; tier: SubVittoTier },
options: OrchestrateOptions,
parentSSE: Subject<SSEEvent>,
): Promise<SubVittoTask> {
const startTime = Date.now();
const proxy = new SubVittoSSEProxy(parentSSE, task.subTaskId, options.parentTaskId);
try {
const loopOptions: AgentLoopOptions = {
systemPrompt: this.buildSubVittoSystemPrompt(task.description),
userMessage: task.description,
layer: 'developer',
taskId: task.subTaskId,
toolFilter: TIER_TOOL_FILTERS[task.tier],
maxIterations: options.maxIterationsPerTask || this.defaultIterations,
maxTokens: options.maxTokensPerTask || this.defaultMaxTokens,
userId: options.userId,
temperature: 0.3,
};
const result = await this.agentLoop.run(loopOptions, proxy.subject as Subject<unknown>);
const subTask: SubVittoTask = {
subTaskId: task.subTaskId,
description: task.description,
tier: task.tier,
status: 'completed',
result: result.output,
iterations: result.iterations,
inputTokens: result.inputTokens,
outputTokens: result.outputTokens,
durationMs: Date.now() - startTime,
};
parentSSE.next({
type: 'subvitto.completed',
taskId: options.parentTaskId,
data: {
subTaskId: task.subTaskId,
result: result.output.substring(0, 500),
iterations: result.iterations,
durationMs: subTask.durationMs,
},
timestamp: new Date().toISOString(),
});
return subTask;
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
return {
subTaskId: task.subTaskId,
description: task.description,
tier: task.tier,
status: 'failed',
error,
durationMs: Date.now() - startTime,
};
} finally {
proxy.dispose();
}
}
private buildSubVittoSystemPrompt(taskDescription: string): string {
return [
'You are a sub-Vitto — a focused specialist deployed by the main Vitto agent.',
'Your job is to complete ONE specific task thoroughly and report results.',
'',
'Guidelines:',
'- Stay focused on the assigned task. Do not expand scope.',
'- Use available tools to gather information needed.',
'- Call task_complete when done, with a clear summary of findings.',
'- Be concise but thorough in your analysis.',
'',
`Task: ${taskDescription}`,
].join('\n');
}
private buildSynthesis(tasks: SubVittoTask[]): string {
const completed = tasks.filter(t => t.status === 'completed');
const failed = tasks.filter(t => t.status === 'failed');
const parts: string[] = [];
if (completed.length > 0) {
parts.push(`${completed.length} task(s) completed successfully.`);
for (const t of completed) {
parts.push(`- ${t.description}: ${(t.result || '').substring(0, 200)}`);
}
}
if (failed.length > 0) {
parts.push(`${failed.length} task(s) failed.`);
for (const t of failed) {
parts.push(`- ${t.description}: ${t.error}`);
}
}
return parts.join('\n');
}
private chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
}
- Step 4: Run test to verify it passes
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.ts --no-cache
Expected: PASS — all 4 tests green
- Step 5: Commit
git add APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.ts APIs/backend-vitto-api/src/app/engine/__tests__/sub-vitto-orchestrator.spec.ts
git commit -m "[S234.2] feat: add SubVittoOrchestratorService with parallel dispatch and budget control"
Task 4: Orchestration Tool Plugin (deploy_agents)
Files:
Create:
APIs/backend-vitto-api/src/app/tools/plugins/orchestration.plugin.tsTest:
APIs/backend-vitto-api/src/app/tools/plugins/__tests__/orchestration.plugin.spec.tsStep 1: Write the failing test
// orchestration.plugin.spec.ts
import { orchestrationPlugin } from '../orchestration.plugin';
import { ToolPluginContext } from '../tool-plugin.types';
describe('orchestrationPlugin', () => {
it('should return a deploy_agents tool handler', () => {
const mockOrchestrator = { orchestrate: jest.fn() };
const ctx = { orchestrator: mockOrchestrator } as unknown as ToolPluginContext;
const handlers = orchestrationPlugin(ctx);
expect(handlers).toHaveLength(1);
expect(handlers[0].name).toBe('deploy_agents');
expect(handlers[0].layers).toContain('developer');
expect(handlers[0].layers).toContain('platform_builder');
});
it('should return error when orchestrator not available', async () => {
const ctx = { orchestrator: undefined } as unknown as ToolPluginContext;
const handlers = orchestrationPlugin(ctx);
const result = await handlers[0].execute({ tasks: [] });
expect(result).toContain('not available');
});
it('should call orchestrator.orchestrate with correct params', async () => {
const mockOrchestrator = {
orchestrate: jest.fn().mockResolvedValue({
round: 1,
tasks: [{ subTaskId: 'sub-1', status: 'completed', result: 'Done' }],
synthesis: '1 task completed.',
totalInputTokens: 500,
totalOutputTokens: 200,
totalDurationMs: 1500,
}),
};
const ctx = { orchestrator: mockOrchestrator } as unknown as ToolPluginContext;
const handlers = orchestrationPlugin(ctx);
const result = await handlers[0].execute(
{
tasks: [
{ description: 'Analyze auth module', tier: 'in-process' },
],
},
{ userId: 'user-1', taskId: 'task-1', sseSubject: {} },
);
expect(mockOrchestrator.orchestrate).toHaveBeenCalledTimes(1);
const callArgs = mockOrchestrator.orchestrate.mock.calls[0][0];
expect(callArgs.parentTaskId).toBe('task-1');
expect(callArgs.userId).toBe('user-1');
expect(callArgs.tasks).toHaveLength(1);
const parsed = JSON.parse(result);
expect(parsed.synthesis).toBe('1 task completed.');
});
});
- Step 2: Run test to verify it fails
Run: npx jest APIs/backend-vitto-api/src/app/tools/plugins/__tests__/orchestration.plugin.spec.ts --no-cache
Expected: FAIL — Cannot find module '../orchestration.plugin'
- Step 3: Implement orchestration.plugin.ts
// orchestration.plugin.ts
/**
* Orchestration Plugin — deploy_agents tool
* S234: Enables Vitto to spawn parallel sub-Vittos for complex tasks.
*/
import { ToolHandler, ToolPluginContext, ToolExecutionContext } from './tool-plugin.types';
import { SubVittoTier } from '../../types/vitto.types';
import { Subject } from 'rxjs';
export function orchestrationPlugin(ctx: ToolPluginContext): ToolHandler[] {
return [
{
name: 'deploy_agents',
description: [
'Deploy multiple sub-Vitto agents to work on subtasks in parallel.',
'Use this when a task can be broken into independent pieces that benefit from parallel execution.',
'Each sub-agent runs with its own context and reports results back.',
'Available tiers: "in-process" (search, analyze, recall — instant startup).',
'Call this tool when you need to: search multiple topics simultaneously,',
'analyze different aspects of a problem in parallel, or gather diverse context.',
].join(' '),
parameters: {
type: 'object',
properties: {
tasks: {
type: 'array',
items: {
type: 'object',
properties: {
description: {
type: 'string',
description: 'Clear description of what this sub-agent should accomplish',
},
tier: {
type: 'string',
enum: ['in-process'],
description: 'Execution tier. Currently only "in-process" is available.',
},
},
required: ['description'],
},
description: 'Array of sub-tasks to execute in parallel (max 5)',
},
},
required: ['tasks'],
},
layers: ['developer', 'platform_builder'],
execute: async (
input: Record<string, unknown>,
context?: ToolExecutionContext,
): Promise<string> => {
if (!ctx.orchestrator) {
return JSON.stringify({ error: 'Sub-Vitto orchestrator not available' });
}
const rawTasks = input.tasks as Array<{ description: string; tier?: string }>;
if (!rawTasks?.length) {
return JSON.stringify({ error: 'No tasks provided' });
}
// Cap at 5 tasks
const tasks = rawTasks.slice(0, 5).map(t => ({
description: t.description,
tier: (t.tier || 'in-process') as SubVittoTier,
}));
try {
const result = await ctx.orchestrator.orchestrate({
parentTaskId: context?.taskId || `orch_${Date.now().toString(36)}`,
userId: context?.userId || 'system',
tasks,
sseSubject: (context?.sseSubject || new Subject()) as Subject<unknown>,
});
return JSON.stringify({
round: result.round,
tasks: result.tasks.map((t: Record<string, unknown>) => ({
subTaskId: t.subTaskId,
description: t.description,
status: t.status,
result: typeof t.result === 'string' ? t.result.substring(0, 1000) : t.result,
error: t.error,
iterations: t.iterations,
durationMs: t.durationMs,
})),
synthesis: result.synthesis,
totalDurationMs: result.totalDurationMs,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return JSON.stringify({ error: `Orchestration failed: ${message}` });
}
},
},
];
}
- Step 4: Run test to verify it passes
Run: npx jest APIs/backend-vitto-api/src/app/tools/plugins/__tests__/orchestration.plugin.spec.ts --no-cache
Expected: PASS — all 3 tests green
- Step 5: Commit
git add APIs/backend-vitto-api/src/app/tools/plugins/orchestration.plugin.ts APIs/backend-vitto-api/src/app/tools/plugins/__tests__/orchestration.plugin.spec.ts
git commit -m "[S234.2] feat: add deploy_agents tool plugin for sub-Vitto orchestration"
Task 5: Wire Orchestrator into Plugin System
Files:
Modify:
APIs/backend-vitto-api/src/app/tools/plugins/tool-plugin.types.ts(~line 76)Modify:
APIs/backend-vitto-api/src/app/tools/plugins/plugins.index.tsModify:
APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts(plugin context builder)Modify:
APIs/backend-vitto-api/src/app/engine/engine.module.tsStep 1: Add
orchestratorto ToolPluginContext
In tool-plugin.types.ts, add to ToolPluginContext interface (after the whistleOrchestrator field, around line 105):
/** S234: Sub-Vitto orchestrator — optional, for deploy_agents tool */
orchestrator?: {
orchestrate: (options: {
parentTaskId: string;
userId: string;
tasks: Array<{ description: string; tier: string }>;
sseSubject: import('rxjs').Subject<unknown>;
maxParallel?: number;
maxIterationsPerTask?: number;
maxTokensPerTask?: number;
depth?: number;
}) => Promise<{
round: number;
tasks: Array<Record<string, unknown>>;
synthesis: string;
totalInputTokens: number;
totalOutputTokens: number;
totalDurationMs: number;
}>;
};
- Step 2: Register orchestrationPlugin in plugins.index.ts
Add import and registration:
import { orchestrationPlugin } from './orchestration.plugin';
Add orchestrationPlugin to the array returned by getAllToolPlugins():
export function getAllToolPlugins(): ToolPluginFn[] {
return [
// ... existing plugins ...
whistlePlugin,
orchestrationPlugin,
];
}
- Step 3: Register SubVittoOrchestratorService in engine.module.ts
Add import:
import { SubVittoOrchestratorService } from './sub-vitto-orchestrator.service';
Add to providers array (after WhistleOrchestratorService):
// S234: Sub-Vitto parallel orchestration
SubVittoOrchestratorService,
Add to exports array:
SubVittoOrchestratorService,
- Step 4: Wire orchestrator into tool-registry.service.ts plugin context
Find the buildPluginContext() or equivalent method in tool-registry.service.ts that builds ToolPluginContext. Add orchestrator injection.
First, read tool-registry.service.ts to find the exact location (~lines 299-326 per exploration).
Add SubVittoOrchestratorService to constructor injection and pass it in the context:
// In constructor:
@Optional() private readonly subVittoOrchestrator?: SubVittoOrchestratorService,
// In the plugin context builder:
orchestrator: this.subVittoOrchestrator ? {
orchestrate: (opts) => this.subVittoOrchestrator.orchestrate(opts as any),
} : undefined,
- Step 5: Verify compilation
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -30
Expected: No new errors (warnings OK)
- Step 6: Commit
git add APIs/backend-vitto-api/src/app/tools/plugins/tool-plugin.types.ts \
APIs/backend-vitto-api/src/app/tools/plugins/plugins.index.ts \
APIs/backend-vitto-api/src/app/engine/engine.module.ts \
APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts
git commit -m "[S234.2] feat: wire SubVittoOrchestratorService into plugin system and module"
Task 6: Refactor SubAgentService.executeTask() to use AgentLoopService
Why now: With the orchestrator wired, refactor the duplicated while loop in SubAgentService to use the shared engine. This is the spec's core refactoring.
Files:
Modify:
APIs/backend-vitto-api/src/app/engine/sub-agent.service.tsStep 1: Read current executeTask() fully
Read lines 460-690 of sub-agent.service.ts to understand all post-loop logic (validation, performance tracking, etc.) that must be preserved.
- Step 2: Write the refactored executeTask()
Replace the while loop (lines 550-614) with a call to AgentLoopService.run(). Preserve:
- Learning context enrichment (lines 498-514) — keep as-is
- System prompt building (line 505) — keep as-is
- MATCH phase enrichment (lines 508-514) — keep as-is
- Cost limits calculation (lines 540-545) — map to AgentLoopOptions.maxIterations
- VALIDATE phase (lines 627-629) — keep as-is
- Performance tracking (lines 631+) — keep as-is
The refactored method body (replacing lines 516-614):
// Use AgentLoopService.run() instead of manual while loop
// This gives sub-agents: stuck detection, parallel tool calls, checkpoints, SSE
const loopOptions: AgentLoopOptions = {
systemPrompt,
userMessage: userPrompt,
layer: 'developer',
taskId: parentTaskId,
provider: def.preferredProvider as ProviderName | undefined,
model: def.preferredModel || undefined,
maxTokens: options?.maxTokens || def.maxTokens || 4096,
temperature: 0.3,
toolFilter: SubAgentService.SUB_AGENT_TOOL_NAMES,
maxIterations: Math.min(
options?.maxIterations || limits.maxIterations,
def.maxIterations || 15,
),
userId: undefined, // Sub-agents don't have user-scoped tools
};
let finalResult = '';
let taskSuccess = false;
let iterations = 0;
let inputTokens = 0;
let outputTokens = 0;
try {
const result = await this.agentLoop.run(loopOptions);
finalResult = result.output;
taskSuccess = result.completedViaTaskComplete || !!result.output;
iterations = result.iterations;
inputTokens = result.inputTokens;
outputTokens = result.outputTokens;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger.error(`[${agent.agentId}] AgentLoop error: ${message}`);
finalResult = `[ERROR: ${message}]`;
taskSuccess = false;
}
Also add a static SUB_AGENT_TOOL_NAMES field (string array of allowed tool names) alongside the existing SUB_AGENT_TOOLS definitions array:
static readonly SUB_AGENT_TOOL_NAMES = [
'recall_memory', 'search_decisions', 'query_docs', 'task_complete',
];
And inject AgentLoopService in the constructor.
- Step 3: Add AgentLoopService to SubAgentService constructor
Find the constructor and add:
@Optional() private readonly agentLoop?: AgentLoopService,
Add the import at the top:
import { AgentLoopService, AgentLoopOptions } from './agent-loop.service';
- Step 4: Run existing sub-agent tests
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/ --testPathPattern="sub-agent|delegation" --no-cache 2>&1 | tail -20
Expected: Existing tests still pass (some may need mock updates for AgentLoopService)
- Step 5: Verify compilation
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -30
Expected: No errors
- Step 6: Commit
git add APIs/backend-vitto-api/src/app/engine/sub-agent.service.ts
git commit -m "[S234.2] refactor: SubAgentService.executeTask() uses AgentLoopService.run() instead of manual while loop"
Task 7: Frontend — SubVittoOrchestrationRenderer
Files:
Create:
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component.tsCreate:
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component.htmlCreate:
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component.scssModify:
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/tool-renderer-host/tool-renderer-host.component.tsStep 1: Create the renderer component
// sub-vitto-orchestration-renderer.component.ts
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
import { ToolCallInfo } from '../../../../types/chat.types';
interface SubVittoTaskDisplay {
subTaskId: string;
description: string;
status: 'pending' | 'running' | 'completed' | 'failed';
result?: string;
error?: string;
iterations?: number;
durationMs?: number;
}
@Component({
selector: 'lib-sub-vitto-orchestration-renderer',
standalone: true,
templateUrl: './sub-vitto-orchestration-renderer.component.html',
styleUrls: ['./sub-vitto-orchestration-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SubVittoOrchestrationRendererComponent {
readonly toolCall = input.required<ToolCallInfo>();
readonly parsedOutput = computed(() => {
try {
const raw = this.toolCall().output;
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
});
readonly tasks = computed<SubVittoTaskDisplay[]>(() => {
const output = this.parsedOutput();
return output?.tasks || [];
});
readonly synthesis = computed(() => {
const output = this.parsedOutput();
return output?.synthesis || '';
});
readonly totalDuration = computed(() => {
const output = this.parsedOutput();
const ms = output?.totalDurationMs || 0;
return ms > 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
});
readonly isRunning = computed(() => {
return !this.toolCall().output;
});
getStatusIcon(status: string): string {
switch (status) {
case 'completed': return '✓';
case 'failed': return '✗';
case 'running': return '⏳';
default: return '○';
}
}
}
- Step 2: Create the template
<!-- sub-vitto-orchestration-renderer.component.html -->
<div class="sub-vitto-orchestration">
<div class="header">
<span class="title">
@if (isRunning()) {
Deploying {{ tasks().length || '...' }} agents
} @else {
Deployed {{ tasks().length }} agents
}
</span>
@if (!isRunning()) {
<span class="duration">{{ totalDuration() }}</span>
}
</div>
<div class="task-list">
@for (task of tasks(); track task.subTaskId) {
<div class="task-row" [class]="'status-' + task.status">
<span class="status-icon">{{ getStatusIcon(task.status) }}</span>
<span class="description">{{ task.description }}</span>
@if (task.iterations) {
<span class="meta">{{ task.iterations }} iter</span>
}
@if (task.durationMs) {
<span class="meta">{{ task.durationMs > 1000 ? (task.durationMs / 1000).toFixed(1) + 's' : task.durationMs + 'ms' }}</span>
}
</div>
@if (task.error) {
<div class="task-error">{{ task.error }}</div>
}
}
</div>
@if (synthesis() && !isRunning()) {
<div class="synthesis">
<span class="synthesis-label">Synthesis</span>
<span class="synthesis-text">{{ synthesis() }}</span>
</div>
}
@if (isRunning()) {
<div class="running-indicator">Running...</div>
}
</div>
- Step 3: Create the styles
// sub-vitto-orchestration-renderer.component.scss
:host {
display: block;
}
.sub-vitto-orchestration {
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
border-radius: 8px;
padding: 12px;
font-size: 13px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.title {
font-weight: 600;
}
.duration {
opacity: 0.6;
font-size: 12px;
}
}
.task-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.task-row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
&.status-completed {
.status-icon { color: var(--success-color, #4caf50); }
}
&.status-failed {
.status-icon { color: var(--error-color, #f44336); }
}
&.status-running {
opacity: 0.8;
}
.description {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
opacity: 0.5;
font-size: 11px;
white-space: nowrap;
}
}
.task-error {
margin-left: 24px;
padding: 4px 8px;
font-size: 12px;
color: var(--error-color, #f44336);
opacity: 0.8;
}
.synthesis {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
font-size: 12px;
.synthesis-label {
font-weight: 600;
margin-right: 6px;
}
.synthesis-text {
opacity: 0.8;
}
}
.running-indicator {
margin-top: 8px;
opacity: 0.6;
font-style: italic;
}
- Step 4: Register in tool-renderer-host.component.ts
Add import:
import { SubVittoOrchestrationRendererComponent } from '../renderers/sub-vitto-orchestration-renderer/sub-vitto-orchestration-renderer.component';
Add to imports array:
SubVittoOrchestrationRendererComponent,
Add to the rendererType computed (before the return 'default'):
if (name === 'deploy_agents') return 'sub-vitto-orchestration';
- Step 5: Update the template (tool-renderer-host.component.html)
Read the template file and add the new case to the ngSwitch:
@case ('sub-vitto-orchestration') {
<lib-sub-vitto-orchestration-renderer [toolCall]="toolCall()" />
}
- Step 6: Verify frontend compilation
Run: npx nx build libs-ai-vitto-chat --skip-nx-cache 2>&1 | tail -20
Expected: Build succeeds (or at least no new errors from our changes)
- Step 7: Commit
git add libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/renderers/sub-vitto-orchestration-renderer/ \
libs/libs-ai/vitto-chat/src/lib/components/tool-renderers/tool-renderer-host/tool-renderer-host.component.ts
git commit -m "[S234.2] feat: add SubVittoOrchestrationRenderer for parallel agent execution display"
Task 8: Integration Smoke Test
Files:
Modify:
APIs/backend-vitto-api/src/app/engine/__tests__/orchestration.integration.spec.ts(if exists, otherwise create)Step 1: Write integration test verifying the full pipeline
// Test that deploy_agents tool is registered and callable
import { Test, TestingModule } from '@nestjs/testing';
import { ToolRegistryService } from '../../tools/tool-registry.service';
describe('Sub-Vitto Integration', () => {
it('deploy_agents tool should be registered', async () => {
// This test may need the full module setup — if too heavy,
// verify by checking getAllToolPlugins() includes orchestrationPlugin
const { getAllToolPlugins } = await import('../../tools/plugins/plugins.index');
const plugins = getAllToolPlugins();
// orchestrationPlugin should be in the list
expect(plugins.length).toBeGreaterThanOrEqual(18); // 17 existing + 1 new
});
});
- Step 2: Run all engine tests to verify no regressions
Run: npx jest APIs/backend-vitto-api/src/app/engine/__tests__/ --no-cache 2>&1 | tail -30
Expected: All existing tests pass, no regressions
- Step 3: Run full backend-vitto-api compilation check
Run: npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | tail -20
Expected: Clean compilation
- Step 4: Commit
git add APIs/backend-vitto-api/src/app/engine/__tests__/
git commit -m "[S234.2] test: integration smoke test for Sub-Vitto engine pipeline"
Post-Implementation Notes
What This Plan Does NOT Cover (Deferred to Phase 2+)
- VM tier execution —
SandboxedToolProxy, warm pool (spec Section 2) - Browser tier execution — Reuse ComputerUseSupervisorService
- Multi-round orchestration — Round 2+ based on Round 1 results (spec mentions it, but single-round is the MVP)
- Parent-level budget aggregation — Flagged in critical analysis. Add
parentTaskBudgetin Phase 2. - OAuth infrastructure — Spec Section 4, separate plan
- Expert sub-agent UX — Chat-based creation, marketplace (spec Section 3)
- Vitto's Soul — Personality drift, visual evolution (spec Section 6)
Verification Checklist
-
deploy_agentstool appears in Vitto's tool list when queried - SSE events with
subvitto.*prefix appear in the event stream during parallel execution - Sub-Vitto tasks use
AgentLoopService.run()(stuck detection, cost guards work) - Frontend renders collapsible card group for deploy_agents tool calls
- Recursive orchestration (depth > 1) is blocked
- Partial failures don't crash the orchestrator (Promise.allSettled)