/** * ProgressAnalystAgent — A2A pipeline for JarvisJr coaching skill progress analysis. * * Agent roster (3 steps): * 1. SessionMetricsCollectorAgent — per agent: list recent sessions, extract skillMetrics timeseries * 2. PlateauDetectorAgent — identify metrics that have not improved across last N sessions * 3. ProgressReportAgent — assemble per-agent recommendations (difficulty change / supplementary agent) * * MCP tools: * jarvis.progress.analyze(lookbackSessions?, plateauThreshold?) — run pipeline across all agents */ import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { registerTool } from '../tools/registry.js'; import type { McpToolRequest } from '../tools/types.js'; import { jarvisAgentsList, jarvisSessionsList, type JarvisAgentDoc, type JarvisSessionDoc, } from '../../lib/jarvis-client.js'; // ── Types ────────────────────────────────────────────────────────────────────── interface SkillTrend { skill: string; scores: number[]; latest: number; earliest: number; delta: number; isPlateaued: boolean; } interface AgentProgressAnalysis { agentId: string; agentName: string; sessionsAnalyzed: number; skills: SkillTrend[]; plateauedSkills: string[]; recommendation: AgentRecommendation; } type AgentRecommendation = | { action: 'none'; reason: string } | { action: 'increase_difficulty'; currentLevel: string; reason: string } | { action: 'supplementary_agent'; suggestedRole: string; reason: string } | { action: 'insufficient_data'; reason: string }; export interface ProgressAnalystReport { runId: string; productId: 'jarvisjr'; lookbackSessions: number; plateauThreshold: number; agentsAnalyzed: number; agentsWithPlateau: number; agentsProgressing: number; perAgent: AgentProgressAnalysis[]; summary: string; generatedAt: string; } // ── Step 1: SessionMetricsCollectorAgent ────────────────────────────────────── async function collectAgentMetrics( agent: JarvisAgentDoc, lookbackSessions: number, opts: { token?: string; requestId?: string } ): Promise<{ agent: JarvisAgentDoc; sessions: JarvisSessionDoc[] }> { try { const result = await jarvisSessionsList({ agentId: agent.id, limit: lookbackSessions }, opts); return { agent, sessions: result.sessions }; } catch { return { agent, sessions: [] }; } } // ── Step 2: PlateauDetectorAgent ────────────────────────────────────────────── function detectPlateaus(sessions: JarvisSessionDoc[], plateauThreshold: number): SkillTrend[] { const completed = sessions .filter(s => s.status === 'completed' && s.skillMetrics) .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); if (completed.length < 2) return []; const skillMap = new Map(); for (const session of completed) { for (const [skill, score] of Object.entries(session.skillMetrics!)) { const arr = skillMap.get(skill) ?? []; arr.push(score); skillMap.set(skill, arr); } } const trends: SkillTrend[] = []; for (const [skill, scores] of skillMap.entries()) { if (scores.length < 2) continue; const earliest = scores[0]!; const latest = scores[scores.length - 1]!; const delta = latest - earliest; const isPlateaued = Math.abs(delta) < plateauThreshold; trends.push({ skill, scores, latest, earliest, delta, isPlateaued }); } return trends; } function buildRecommendation( agent: JarvisAgentDoc, trends: SkillTrend[], sessionCount: number ): AgentRecommendation { if (sessionCount < 3) { return { action: 'insufficient_data', reason: `Only ${sessionCount} completed sessions — need at least 3 to detect plateaus.`, }; } const plateaued = trends.filter(t => t.isPlateaued); const progressing = trends.filter(t => !t.isPlateaued && t.delta > 0); if (plateaued.length === 0) { return { action: 'none', reason: `${progressing.length} skill(s) improving. No plateau detected.`, }; } const avgLatest = plateaued.length > 0 ? plateaued.reduce((s, t) => s + t.latest, 0) / plateaued.length : 0; const currentLevel = ((agent as unknown as Record)['difficultyLevel'] as string) ?? 'intermediate'; if (avgLatest >= 0.75 && currentLevel !== 'advanced') { return { action: 'increase_difficulty', currentLevel, reason: `${plateaued.map(t => t.skill).join(', ')} plateaued at high scores (avg ${(avgLatest * 100).toFixed(0)}%). User may have outgrown current difficulty.`, }; } return { action: 'supplementary_agent', suggestedRole: 'specialist', reason: `${plateaued.map(t => t.skill).join(', ')} stagnant despite ${sessionCount} sessions. A specialist agent targeting these skills may break the plateau.`, }; } // ── Step 3: ProgressReportAgent ─────────────────────────────────────────────── function assembleProgressReport( runId: string, lookbackSessions: number, plateauThreshold: number, analyses: AgentProgressAnalysis[] ): ProgressAnalystReport { const agentsWithPlateau = analyses.filter(a => a.plateauedSkills.length > 0).length; const agentsProgressing = analyses.filter( a => a.plateauedSkills.length === 0 && a.recommendation.action !== 'insufficient_data' ).length; const summary = analyses.length === 0 ? 'No agents found to analyze.' : `${agentsWithPlateau}/${analyses.length} agents show skill plateaus. ${agentsProgressing} progressing normally.`; return { runId, productId: 'jarvisjr', lookbackSessions, plateauThreshold, agentsAnalyzed: analyses.length, agentsWithPlateau, agentsProgressing, perAgent: analyses, summary, generatedAt: new Date().toISOString(), }; } // ── Pipeline runner ──────────────────────────────────────────────────────────── async function runProgressAnalystPipeline( lookbackSessions: number, plateauThreshold: number, req: McpToolRequest ): Promise { const runId = randomUUID(); const opts = { token: req.headers.authorization?.slice(7), requestId: req.id }; req.log.info( { runId, stepId: 'collect', lookbackSessions }, 'SessionMetricsCollectorAgent start' ); const agentsResult = await jarvisAgentsList({ limit: 50 }, opts).catch(() => ({ agents: [] as JarvisAgentDoc[], total: 0, })); const collected = await Promise.all( agentsResult.agents.map(agent => collectAgentMetrics(agent, lookbackSessions, opts)) ); req.log.info( { runId, stepId: 'collect', agentCount: collected.length }, 'SessionMetricsCollectorAgent done' ); req.log.info( { runId, stepId: 'plateau', agentCount: collected.length }, 'PlateauDetectorAgent start' ); const analyses: AgentProgressAnalysis[] = collected.map(({ agent, sessions }) => { const trends = detectPlateaus(sessions, plateauThreshold); const plateauedSkills = trends.filter(t => t.isPlateaued).map(t => t.skill); const recommendation = buildRecommendation( agent, trends, sessions.filter(s => s.status === 'completed').length ); return { agentId: agent.id, agentName: agent.name, sessionsAnalyzed: sessions.length, skills: trends, plateauedSkills, recommendation, }; }); req.log.info( { runId, stepId: 'plateau', plateauCount: analyses.filter(a => a.plateauedSkills.length > 0).length, }, 'PlateauDetectorAgent done' ); req.log.info({ runId, stepId: 'report' }, 'ProgressReportAgent start'); const report = assembleProgressReport(runId, lookbackSessions, plateauThreshold, analyses); req.log.info({ runId, stepId: 'report', summary: report.summary }, 'ProgressReportAgent done'); return report; } // ── MCP tool registration ───────────────────────────────────────────────────── registerTool({ name: 'jarvis.progress.analyze', description: 'A2A pipeline: analyzes skill progression across all JarvisJr coaching agents. Collects skillMetrics from recent sessions, detects stagnation/plateaus, and recommends difficulty increases or supplementary agents. Returns a per-agent report with skill trend deltas and actionable recommendations. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ lookbackSessions: z.coerce .number() .int() .min(3) .max(50) .default(10) .describe('Number of most recent sessions to analyze per agent (default 10)'), plateauThreshold: z.coerce .number() .min(0) .max(0.5) .default(0.05) .describe('Max skill score delta to classify as plateaued (default 0.05 = 5% change)'), }), async execute(args, req) { return runProgressAnalystPipeline(args.lookbackSessions, args.plateauThreshold, req); }, });