/** * 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 = { 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 { 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)['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 { 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): Promise { 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); }, });