learning_ai_common_plat/services/mcp-server/src/modules/a2a/team-provisioning-pipeline.ts

410 lines
13 KiB
TypeScript

/**
* TeamProvisioningAgent — A2A pipeline for JarvisJr enterprise team onboarding.
*
* Agent roster (3 steps):
* 1. NewMemberDetectorAgent — list team members with 'invited' or recently joined status
* 2. OnboardingAgent — per new member: recommend starter agents, seed initial memory, schedule check-in
* 3. ProvisioningReportAgent — assemble per-member onboarding action report
*
* MCP tools:
* jarvis.teams.provision(teamId, sinceHours?) — run pipeline for newly joined/invited members
*/
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import type { McpToolRequest } from '../tools/types.js';
import {
jarvisTeamsListMembers,
jarvisAgentsList,
jarvisMemoryCreate,
type JarvisTeamMemberDoc,
type JarvisAgentDoc,
} from '../../lib/jarvis-client.js';
import {
trackRunCompleted,
trackRunFailed,
trackRunStarted,
trackStepCompleted,
trackStepFailed,
trackStepStarted,
} from './run-tracker.js';
import { createSupportCaseForRun } from './governance.js';
// ── Types ──────────────────────────────────────────────────────────────────────
interface MemberOnboardingResult {
userId: string;
teamId: string;
memberRole: string;
status: string;
recommendedAgents: string[];
memorySeeded: boolean;
checkInScheduled: boolean;
errors: string[];
}
export interface TeamProvisioningReport {
runId: string;
productId: 'jarvisjr';
teamId: string;
totalMembers: number;
newMembersProcessed: number;
fullyOnboarded: number;
partiallyOnboarded: number;
failed: number;
perMember: MemberOnboardingResult[];
summary: string;
generatedAt: string;
}
// ── Step 1: NewMemberDetectorAgent ─────────────────────────────────────────────
function isNewMember(member: JarvisTeamMemberDoc, sinceMs: number): boolean {
if (member.status === 'invited') return true;
if (member.status === 'active' && member.joinedAt) {
const joinedAt = new Date(member.joinedAt).getTime();
return Date.now() - joinedAt < sinceMs;
}
return false;
}
async function detectNewMembers(
teamId: string,
sinceMs: number,
opts: { token?: string; requestId?: string }
): Promise<{ all: JarvisTeamMemberDoc[]; newMembers: JarvisTeamMemberDoc[] }> {
try {
const result = await jarvisTeamsListMembers(teamId, opts);
const newMembers = result.members.filter(m => isNewMember(m, sinceMs));
return { all: result.members, newMembers };
} catch {
return { all: [], newMembers: [] };
}
}
// ── Step 2: OnboardingAgent ────────────────────────────────────────────────────
const STARTER_AGENT_ROLES: Record<string, string[]> = {
owner: ['executive_coach', 'goal_setter', 'accountability_partner'],
manager: ['leadership_coach', 'team_motivator'],
member: ['productivity_coach', 'skill_builder'],
};
async function onboardMember(
member: JarvisTeamMemberDoc,
availableAgents: JarvisAgentDoc[],
opts: { token?: string; requestId?: string }
): Promise<MemberOnboardingResult> {
const errors: string[] = [];
// Recommend agents matching the member's role-based preferences
const preferredRoles = STARTER_AGENT_ROLES[member.role] ?? STARTER_AGENT_ROLES['member']!;
const recommendedAgents = availableAgents
.filter(a => {
const role = (
((a as unknown as Record<string, unknown>)['role'] as string) ?? ''
).toLowerCase();
return preferredRoles.some(r => role.includes(r.replace('_', '')));
})
.slice(0, 3)
.map(a => a.id);
// If no matching agents found, just recommend the first available agents
const finalRecommended =
recommendedAgents.length > 0 ? recommendedAgents : availableAgents.slice(0, 2).map(a => a.id);
// Seed initial context memory for each recommended agent
let memorySeeded = false;
for (const agentId of finalRecommended) {
try {
await jarvisMemoryCreate(
agentId,
{
sessionId: `team-provision-${member.teamId}`,
type: 'context',
content: `New team member: userId=${member.userId}, role=${member.role}, team=${member.teamId}. Joined: ${member.joinedAt}.`,
importance: 0.6,
tags: ['team-onboarding', member.role],
},
opts
);
memorySeeded = true;
} catch (err) {
errors.push(
`Memory seed for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`
);
}
}
// Check-in scheduling is indicated (actual scheduling requires a background job integration)
// Here we mark it as scheduled if memory seeding succeeded
const checkInScheduled = memorySeeded;
return {
userId: member.userId,
teamId: member.teamId,
memberRole: member.role,
status: member.status,
recommendedAgents: finalRecommended,
memorySeeded,
checkInScheduled,
errors,
};
}
// ── Step 3: ProvisioningReportAgent ───────────────────────────────────────────
function assembleProvisioningReport(
runId: string,
teamId: string,
totalMembers: number,
results: MemberOnboardingResult[]
): TeamProvisioningReport {
const fullyOnboarded = results.filter(
r => r.memorySeeded && r.checkInScheduled && r.errors.length === 0
).length;
const failed = results.filter(r => !r.memorySeeded && r.errors.length > 0).length;
const partiallyOnboarded = results.length - fullyOnboarded - failed;
const summary =
results.length === 0
? `No new/invited members found in the specified window for team ${teamId}.`
: `Processed ${results.length} new member(s) for team ${teamId}. ${fullyOnboarded} fully onboarded, ${partiallyOnboarded} partial, ${failed} failed.`;
return {
runId,
productId: 'jarvisjr',
teamId,
totalMembers,
newMembersProcessed: results.length,
fullyOnboarded,
partiallyOnboarded,
failed,
perMember: results,
summary,
generatedAt: new Date().toISOString(),
};
}
// ── Pipeline runner ────────────────────────────────────────────────────────────
export async function runTeamProvisioningPipeline(
teamId: string,
sinceHours: number,
req: McpToolRequest
): Promise<TeamProvisioningReport> {
const runId = randomUUID();
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
const sinceMs = sinceHours * 60 * 60 * 1000;
let currentStep: { stepName: 'detect' | 'onboard' | 'report'; order: number } | undefined;
await safeTrack(() =>
trackRunStarted({
runId,
productId: 'jarvisjr',
name: 'team-provisioning',
requestId: req.id,
token: opts.token,
input: { teamId, sinceHours },
})
);
try {
currentStep = { stepName: 'detect', order: 1 };
const detectStep = currentStep;
await safeTrack(() =>
trackStepStarted({
runId,
productId: 'jarvisjr',
stepName: detectStep.stepName,
order: detectStep.order,
token: opts.token,
requestId: req.id,
input: { teamId, sinceHours },
})
);
req.log.info({ runId, stepId: 'detect', teamId, sinceHours }, 'NewMemberDetectorAgent start');
const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts);
await safeTrack(() =>
trackStepCompleted({
runId,
productId: 'jarvisjr',
stepName: detectStep.stepName,
order: detectStep.order,
token: opts.token,
requestId: req.id,
output: { totalMembers: all.length, newMembers: newMembers.length },
})
);
req.log.info(
{ runId, stepId: 'detect', total: all.length, newCount: newMembers.length },
'NewMemberDetectorAgent done'
);
currentStep = { stepName: 'onboard', order: 2 };
const onboardStep = currentStep;
await safeTrack(() =>
trackStepStarted({
runId,
productId: 'jarvisjr',
stepName: onboardStep.stepName,
order: onboardStep.order,
token: opts.token,
requestId: req.id,
input: { newMembers: newMembers.length },
})
);
req.log.info(
{ runId, stepId: 'onboard', newCount: newMembers.length },
'OnboardingAgent start'
);
const agentsResult = await jarvisAgentsList({ limit: 20 }, opts).catch(() => ({
agents: [] as JarvisAgentDoc[],
total: 0,
}));
const results: MemberOnboardingResult[] = [];
for (const member of newMembers) {
const result = await onboardMember(member, agentsResult.agents, opts);
results.push(result);
}
await safeTrack(() =>
trackStepCompleted({
runId,
productId: 'jarvisjr',
stepName: onboardStep.stepName,
order: onboardStep.order,
token: opts.token,
requestId: req.id,
output: {
seeded: results.filter(r => r.memorySeeded).length,
failed: results.filter(r => r.errors.length > 0).length,
},
})
);
req.log.info(
{ runId, stepId: 'onboard', seeded: results.filter(r => r.memorySeeded).length },
'OnboardingAgent done'
);
currentStep = { stepName: 'report', order: 3 };
const reportStep = currentStep;
await safeTrack(() =>
trackStepStarted({
runId,
productId: 'jarvisjr',
stepName: reportStep.stepName,
order: reportStep.order,
token: opts.token,
requestId: req.id,
})
);
req.log.info({ runId, stepId: 'report' }, 'ProvisioningReportAgent start');
const report = assembleProvisioningReport(runId, teamId, all.length, results);
await safeTrack(() =>
trackStepCompleted({
runId,
productId: 'jarvisjr',
stepName: reportStep.stepName,
order: reportStep.order,
token: opts.token,
requestId: req.id,
output: {
fullyOnboarded: report.fullyOnboarded,
partiallyOnboarded: report.partiallyOnboarded,
failed: report.failed,
},
})
);
await safeTrack(() =>
trackRunCompleted({
runId,
productId: 'jarvisjr',
name: 'team-provisioning',
requestId: req.id,
token: opts.token,
output: {
fullyOnboarded: report.fullyOnboarded,
partiallyOnboarded: report.partiallyOnboarded,
failed: report.failed,
},
})
);
if (report.failed > 0 || report.partiallyOnboarded > 0) {
await createSupportCaseForRun(
{
productId: 'jarvisjr',
runId,
title: `Team provisioning follow-up for ${teamId}`,
description: report.summary,
priority: report.failed > 0 ? 'high' : 'medium',
tags: ['a2a', 'team-provisioning', 'onboarding'],
},
opts
);
}
req.log.info(
{ runId, stepId: 'report', summary: report.summary },
'ProvisioningReportAgent done'
);
return report;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (currentStep) {
const failedStep = currentStep;
await safeTrack(() =>
trackStepFailed({
runId,
productId: 'jarvisjr',
stepName: failedStep.stepName,
order: failedStep.order,
token: opts.token,
requestId: req.id,
error: message,
})
);
}
await safeTrack(() =>
trackRunFailed({
runId,
productId: 'jarvisjr',
name: 'team-provisioning',
requestId: req.id,
token: opts.token,
error: message,
})
);
throw error;
}
}
async function safeTrack(fn: () => Promise<void>): Promise<void> {
try {
await fn();
} catch {
// Tracking must never fail the pipeline itself.
}
}
// ── MCP tool registration ─────────────────────────────────────────────────────
registerTool({
name: 'jarvis.teams.provision',
description:
'A2A pipeline: detects new/invited JarvisJr enterprise team members, recommends starter coaching agents based on their role, seeds initial context memories in each agent, and marks check-in as scheduled. Returns a per-member onboarding action report. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
teamId: z.string().min(1).describe('Enterprise team ID to provision'),
sinceHours: z.coerce
.number()
.int()
.min(1)
.default(48)
.describe('Look back this many hours for newly joined members (default 48h)'),
}),
async execute(args, req) {
return runTeamProvisioningPipeline(args.teamId, args.sinceHours, req);
},
});