stt-fallback-monitor-pipeline.ts: lysnrai.stt.monitorFallback - STTEventScannerAgent -> DiagnosticsLaunchAgent -> FallbackReportAgent - Queries stt_completed telemetry, splits azure vs whisper events, computes offline rate - Opens diagnostics session if rate exceeds threshold (default 20%); 6h default window progress-analyst-pipeline.ts: jarvis.progress.analyze - SessionMetricsCollectorAgent -> PlateauDetectorAgent -> ProgressReportAgent - Collects skillMetrics timeseries per agent from recent sessions - Detects plateau (delta < threshold), recommends increase_difficulty or supplementary_agent brain-overflow-pipeline.ts: mindlyst.brains.checkOverflow - BrainInventoryAgent -> OverflowDetectorAgent -> OverflowReportAgent - Flags brains above item count threshold with no acted-on items in N days - Per-brain suggestion: archive / reassign / review MCP server total: 99 tools
273 lines
9.3 KiB
TypeScript
273 lines
9.3 KiB
TypeScript
/**
|
|
* 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<string, number[]>();
|
|
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<string, unknown>)['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<ProgressAnalystReport> {
|
|
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);
|
|
},
|
|
});
|