410 lines
13 KiB
TypeScript
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);
|
|
},
|
|
});
|