learning_ai_common_plat/services/mcp-server/src/modules/a2a/goal-coaching-pipeline.ts
saravanakumardb1 0a6950216a feat(mcp-server): A2A batch-5 — GoalCoachingAgent + SkiRunAnalystAgent (peakpulse) + OrgProvisioningAgent (lysnrai) + ProtocolTuningAgent (nomgap) + CalendarImportAgent (chronomind)
goal-coaching-pipeline.ts: peakpulse.goals.coach
  - SessionHistoryAgent -> GoalAnalysisAgent -> GoalReportAgent
  - Computes duration/distance/elevation/speed trends across recent sessions
  - Classifies user as beginner/intermediate/advanced; generates metric-specific goal suggestions

ski-run-analyst-pipeline.ts: peakpulse.ski.analyze
  - SkiSessionCollectorAgent -> RunQualityAnalystAgent -> SkiReportAgent
  - Computes runDensity, verticalPerRun, skiToLiftRatio per session
  - Flags: low_run_density, high_lift_ratio, short_session, vertical_drop vs baseline

org-provisioning-pipeline.ts: lysnrai.orgs.provision
  - OrgInspectorAgent -> ProvisioningActionAgent -> OrgReportAgent
  - Checks API tokens + session activity; classifies as new_org/needs_attention/complete

protocol-tuning-pipeline.ts: nomgap.protocols.tune
  - ProtocolStatsCollectorAgent -> AbandonmentAnalysisAgent -> TuningReportAgent
  - Finds protocols with >40% abandonment; runs extraction for pattern analysis
  - Proposes duration reduction, milestone notifications, or variant creation

calendar-import-pipeline.ts: chronomind.calendar.import
  - EventValidatorAgent -> ConflictDetectorAgent -> ImportReportAgent
  - Validates dtstart/dtend; detects time overlaps vs existing timers
  - Creates alarm timers for conflict-free events; dryRun support

MCP server total: 110 tools
2026-03-05 18:07:59 -08:00

235 lines
8.8 KiB
TypeScript

/**
* GoalCoachingAgent — A2A pipeline for PeakPulse activity goal recommendations.
*
* Agent roster (3 steps):
* 1. SessionHistoryAgent — fetch recent completed sessions, compute performance trends
* 2. GoalAnalysisAgent — compare trends against overall stats, identify improvement areas
* 3. GoalReportAgent — propose concrete next goals based on trend analysis
*
* MCP tools:
* peakpulse.goals.coach(activityType?, lookback?) — run pipeline
*/
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import type { McpToolRequest } from '../tools/types.js';
import {
peakpulseSessionsList,
peakpulseGetStats,
type PeakSessionDoc,
} from '../../lib/peakpulse-client.js';
// ── Types ──────────────────────────────────────────────────────────────────────
interface PerformanceTrend {
metric: string;
unit: string;
values: number[];
average: number;
recent: number;
delta: number;
trending: 'up' | 'down' | 'flat';
}
interface GoalSuggestion {
metric: string;
currentAverage: number;
suggestedTarget: number;
unit: string;
rationale: string;
}
export interface GoalCoachingReport {
runId: string;
productId: 'peakpulse';
activityType: string | null;
sessionsAnalyzed: number;
trends: PerformanceTrend[];
suggestions: GoalSuggestion[];
overallLevel: 'beginner' | 'intermediate' | 'advanced';
summary: string;
generatedAt: string;
}
// ── Step 1: SessionHistoryAgent ────────────────────────────────────────────────
async function fetchSessionHistory(
activityType: string | null,
lookback: number,
opts: { token?: string; requestId?: string }
): Promise<PeakSessionDoc[]> {
try {
const result = await peakpulseSessionsList(
{ activityType: activityType ?? undefined, status: 'completed', limit: lookback },
opts
);
return result.items.sort(
(a: PeakSessionDoc, b: PeakSessionDoc) =>
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
);
} catch {
return [];
}
}
// ── Step 2: GoalAnalysisAgent ─────────────────────────────────────────────────
function buildTrends(sessions: PeakSessionDoc[]): PerformanceTrend[] {
if (sessions.length < 2) return [];
const metricDefs: Array<{ key: keyof PeakSessionDoc; unit: string; label: string }> = [
{ key: 'durationSeconds', unit: 'min', label: 'duration' },
{ key: 'distanceMeters', unit: 'km', label: 'distance' },
{ key: 'elevationGainMeters', unit: 'm', label: 'elevation_gain' },
{ key: 'averageSpeedMps', unit: 'km/h', label: 'average_speed' },
];
const trends: PerformanceTrend[] = [];
for (const def of metricDefs) {
const rawValues = sessions
.map(s => s[def.key] as number | undefined)
.filter((v): v is number => typeof v === 'number' && v > 0);
if (rawValues.length < 2) continue;
// Convert units
const values =
def.key === 'durationSeconds'
? rawValues.map(v => Math.round(v / 60))
: def.key === 'distanceMeters'
? rawValues.map(v => Math.round(v / 100) / 10)
: def.key === 'averageSpeedMps'
? rawValues.map(v => Math.round(v * 36) / 10)
: rawValues;
const average = values.reduce((s, v) => s + v, 0) / values.length;
const recent = values[values.length - 1]!;
const delta = recent - (values[0] ?? recent);
const trending = delta > average * 0.05 ? 'up' : delta < -average * 0.05 ? 'down' : 'flat';
trends.push({ metric: def.label, unit: def.unit, values, average, recent, delta, trending });
}
return trends;
}
function suggestGoals(
trends: PerformanceTrend[],
stats: Record<string, unknown>
): GoalSuggestion[] {
const suggestions: GoalSuggestion[] = [];
const totalSessions = typeof stats['totalSessions'] === 'number' ? stats['totalSessions'] : 0;
for (const trend of trends) {
let suggestedTarget = trend.average;
let rationale = '';
if (trend.trending === 'up') {
// Already improving — push for 10% more
suggestedTarget = Math.round(trend.recent * 1.1 * 10) / 10;
rationale = `${trend.metric} is trending up (+${trend.delta.toFixed(1)} ${trend.unit}). Push to ${suggestedTarget} ${trend.unit}.`;
} else if (trend.trending === 'flat' && totalSessions > 5) {
// Plateau — target 15% improvement
suggestedTarget = Math.round(trend.average * 1.15 * 10) / 10;
rationale = `${trend.metric} has plateaued at avg ${trend.average.toFixed(1)} ${trend.unit}. Try targeting ${suggestedTarget} ${trend.unit} to break through.`;
} else if (trend.trending === 'down') {
// Declining — target returning to recent average
suggestedTarget = Math.round(trend.average * 10) / 10;
rationale = `${trend.metric} is declining. Focus on recovering to avg ${suggestedTarget} ${trend.unit}.`;
}
if (rationale) {
suggestions.push({
metric: trend.metric,
currentAverage: Math.round(trend.average * 10) / 10,
suggestedTarget,
unit: trend.unit,
rationale,
});
}
}
return suggestions;
}
function classifyLevel(sessions: PeakSessionDoc[]): 'beginner' | 'intermediate' | 'advanced' {
if (sessions.length < 5) return 'beginner';
const avgDuration = sessions.reduce((s, x) => s + (x.durationSeconds ?? 0), 0) / sessions.length;
const avgDistance = sessions.reduce((s, x) => s + (x.distanceMeters ?? 0), 0) / sessions.length;
if (avgDuration > 7200 && avgDistance > 15000) return 'advanced';
if (avgDuration > 3600 || avgDistance > 8000) return 'intermediate';
return 'beginner';
}
// ── Pipeline runner ────────────────────────────────────────────────────────────
async function runGoalCoachingPipeline(
activityType: string | null,
lookback: number,
req: McpToolRequest
): Promise<GoalCoachingReport> {
const runId = randomUUID();
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
req.log.info({ runId, stepId: 'history', activityType, lookback }, 'SessionHistoryAgent start');
const sessions = await fetchSessionHistory(activityType, lookback, opts);
req.log.info({ runId, stepId: 'history', count: sessions.length }, 'SessionHistoryAgent done');
req.log.info({ runId, stepId: 'analyze' }, 'GoalAnalysisAgent start');
const stats = await peakpulseGetStats(opts).catch(() => ({}) as Record<string, unknown>);
const trends = buildTrends(sessions);
const suggestions = suggestGoals(trends, stats as Record<string, unknown>);
const overallLevel = classifyLevel(sessions);
req.log.info(
{ runId, stepId: 'analyze', trendCount: trends.length, suggestionCount: suggestions.length },
'GoalAnalysisAgent done'
);
req.log.info({ runId, stepId: 'report' }, 'GoalReportAgent start');
const summary =
sessions.length < 2
? 'Insufficient session history for goal coaching. Complete at least 2 sessions first.'
: `Analyzed ${sessions.length} ${activityType ?? 'activity'} session(s). Level: ${overallLevel}. ${suggestions.length} goal suggestion(s) generated.`;
req.log.info({ runId, stepId: 'report', summary }, 'GoalReportAgent done');
return {
runId,
productId: 'peakpulse',
activityType,
sessionsAnalyzed: sessions.length,
trends,
suggestions,
overallLevel,
summary,
generatedAt: new Date().toISOString(),
};
}
// ── MCP tool registration ─────────────────────────────────────────────────────
registerTool({
name: 'peakpulse.goals.coach',
description:
'A2A pipeline: analyzes PeakPulse session history to identify performance trends (duration, distance, elevation, speed) and proposes concrete next goals. Classifies user as beginner/intermediate/advanced. Optionally filter by activityType (hiking/skiing/cycling/running). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
activityType: z
.enum(['hiking', 'skiing', 'cycling', 'running'])
.optional()
.describe('Filter sessions by activity type (omit for all types)'),
lookback: z.coerce
.number()
.int()
.min(3)
.max(50)
.default(15)
.describe('Number of recent sessions to analyze (default 15)'),
}),
async execute(args, req) {
return runGoalCoachingPipeline(args.activityType ?? null, args.lookback, req);
},
});