feat(mcp-server): A2A batch-4 — RoutineQualityAgent (chronomind) + SocialFastCoordinatorAgent (nomgap) + TeamProvisioningAgent (jarvis)
routine-quality-pipeline.ts: chronomind.routines.checkQuality - RoutineInventoryAgent -> QualityCheckAgent -> QualityReportAgent - Flags: duration_overflow, never_completed, empty_steps, no_category - Per-routine issue list + remediation suggestions; configurable maxDurationMinutes social-fast-coordinator-pipeline.ts: nomgap.social.coordinateFast - GroupMemberResolverAgent -> NotificationDispatchAgent -> CoordinationReportAgent - Fires social_invite + stage_transition push to each group member - Sends weekly_digest to session owner; per-member outcome tracking team-provisioning-pipeline.ts: jarvis.teams.provision - NewMemberDetectorAgent -> OnboardingAgent -> ProvisioningReportAgent - Detects invited + recently joined members (configurable sinceHours window) - Recommends starter agents by member role, seeds context memory in each agent - Marks check-in as scheduled after successful memory seed MCP server total: 105 tools
This commit is contained in:
parent
7ed4a105b7
commit
acadc3551e
213
services/mcp-server/src/modules/a2a/routine-quality-pipeline.ts
Normal file
213
services/mcp-server/src/modules/a2a/routine-quality-pipeline.ts
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* RoutineQualityAgent — A2A pipeline for ChronoMind routine health checks.
|
||||
*
|
||||
* Agent roster (3 steps):
|
||||
* 1. RoutineInventoryAgent — list all routines (templates + user routines)
|
||||
* 2. QualityCheckAgent — flag routines exceeding duration limits or with zero completions
|
||||
* 3. QualityReportAgent — assemble flagged routines with remediation suggestions
|
||||
*
|
||||
* MCP tools:
|
||||
* chronomind.routines.checkQuality(maxDurationMinutes?) — 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 { chronomindRoutinesList, type RoutineDoc } from '../../lib/chronomind-client.js';
|
||||
import { config } from '../../lib/config.js';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
type QualityIssue = 'duration_overflow' | 'never_completed' | 'empty_steps' | 'no_category';
|
||||
|
||||
interface RoutineQualityResult {
|
||||
routineId: string;
|
||||
routineName: string;
|
||||
isTemplate: boolean;
|
||||
totalDurationMinutes: number;
|
||||
status: string;
|
||||
stepCount: number;
|
||||
issues: QualityIssue[];
|
||||
suggestions: string[];
|
||||
healthy: boolean;
|
||||
}
|
||||
|
||||
export interface RoutineQualityReport {
|
||||
runId: string;
|
||||
productId: 'chronomind';
|
||||
maxDurationMinutes: number;
|
||||
totalRoutines: number;
|
||||
healthyCount: number;
|
||||
flaggedCount: number;
|
||||
perRoutine: RoutineQualityResult[];
|
||||
topIssues: Record<QualityIssue, number>;
|
||||
summary: string;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
// ── Step 1: RoutineInventoryAgent ─────────────────────────────────────────────
|
||||
|
||||
async function inventoryRoutines(opts: {
|
||||
token?: string;
|
||||
requestId?: string;
|
||||
}): Promise<RoutineDoc[]> {
|
||||
try {
|
||||
const result = await chronomindRoutinesList({ limit: config.QUERY_MAX_LIMIT }, opts);
|
||||
return result.items;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 2: QualityCheckAgent ─────────────────────────────────────────────────
|
||||
|
||||
function checkRoutineQuality(
|
||||
routine: RoutineDoc,
|
||||
maxDurationMinutes: number
|
||||
): RoutineQualityResult {
|
||||
const issues: QualityIssue[] = [];
|
||||
const suggestions: string[] = [];
|
||||
|
||||
const stepCount = Array.isArray(routine.steps) ? routine.steps.length : 0;
|
||||
|
||||
if (routine.totalDurationMinutes > maxDurationMinutes) {
|
||||
issues.push('duration_overflow');
|
||||
suggestions.push(
|
||||
`Duration ${routine.totalDurationMinutes}m exceeds max ${maxDurationMinutes}m. Split into sub-routines or shorten step durations.`
|
||||
);
|
||||
}
|
||||
|
||||
if (routine.status !== 'completed' && !routine.isTemplate && stepCount > 0) {
|
||||
issues.push('never_completed');
|
||||
suggestions.push(
|
||||
'Routine has never reached completed status. Verify step durations and triggers are reachable.'
|
||||
);
|
||||
}
|
||||
|
||||
if (stepCount === 0) {
|
||||
issues.push('empty_steps');
|
||||
suggestions.push('Routine has no steps defined. Add at least one step or delete this routine.');
|
||||
}
|
||||
|
||||
if (!routine.category) {
|
||||
issues.push('no_category');
|
||||
suggestions.push('No category assigned. Categorizing improves searchability and filtering.');
|
||||
}
|
||||
|
||||
return {
|
||||
routineId: routine.id,
|
||||
routineName: routine.name,
|
||||
isTemplate: routine.isTemplate,
|
||||
totalDurationMinutes: routine.totalDurationMinutes,
|
||||
status: routine.status,
|
||||
stepCount,
|
||||
issues,
|
||||
suggestions,
|
||||
healthy: issues.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Step 3: QualityReportAgent ────────────────────────────────────────────────
|
||||
|
||||
function assembleQualityReport(
|
||||
runId: string,
|
||||
maxDurationMinutes: number,
|
||||
results: RoutineQualityResult[]
|
||||
): RoutineQualityReport {
|
||||
const flaggedCount = results.filter(r => !r.healthy).length;
|
||||
const healthyCount = results.length - flaggedCount;
|
||||
|
||||
const topIssues: Record<QualityIssue, number> = {
|
||||
duration_overflow: 0,
|
||||
never_completed: 0,
|
||||
empty_steps: 0,
|
||||
no_category: 0,
|
||||
};
|
||||
for (const result of results) {
|
||||
for (const issue of result.issues) {
|
||||
topIssues[issue]++;
|
||||
}
|
||||
}
|
||||
|
||||
const summary =
|
||||
results.length === 0
|
||||
? 'No routines found.'
|
||||
: flaggedCount === 0
|
||||
? `All ${results.length} routines pass quality checks.`
|
||||
: `${flaggedCount}/${results.length} routines flagged. Top issues: ${Object.entries(
|
||||
topIssues
|
||||
)
|
||||
.filter(([, count]) => count > 0)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([issue, count]) => `${issue} (${count})`)
|
||||
.join(', ')}.`;
|
||||
|
||||
return {
|
||||
runId,
|
||||
productId: 'chronomind',
|
||||
maxDurationMinutes,
|
||||
totalRoutines: results.length,
|
||||
healthyCount,
|
||||
flaggedCount,
|
||||
perRoutine: results,
|
||||
topIssues,
|
||||
summary,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||
|
||||
async function runRoutineQualityPipeline(
|
||||
maxDurationMinutes: number,
|
||||
req: McpToolRequest
|
||||
): Promise<RoutineQualityReport> {
|
||||
const runId = randomUUID();
|
||||
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||
|
||||
req.log.info({ runId, stepId: 'inventory' }, 'RoutineInventoryAgent start');
|
||||
const routines = await inventoryRoutines(opts);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'inventory', count: routines.length },
|
||||
'RoutineInventoryAgent done'
|
||||
);
|
||||
|
||||
req.log.info(
|
||||
{ runId, stepId: 'check', count: routines.length, maxDurationMinutes },
|
||||
'QualityCheckAgent start'
|
||||
);
|
||||
const results = routines.map(r => checkRoutineQuality(r, maxDurationMinutes));
|
||||
req.log.info(
|
||||
{ runId, stepId: 'check', flagged: results.filter(r => !r.healthy).length },
|
||||
'QualityCheckAgent done'
|
||||
);
|
||||
|
||||
req.log.info({ runId, stepId: 'report' }, 'QualityReportAgent start');
|
||||
const report = assembleQualityReport(runId, maxDurationMinutes, results);
|
||||
req.log.info({ runId, stepId: 'report', summary: report.summary }, 'QualityReportAgent done');
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'chronomind.routines.checkQuality',
|
||||
description:
|
||||
'A2A pipeline: scans all ChronoMind routines for quality issues — excessive duration, empty steps, never-completed non-template routines, and missing categories. Returns a per-routine health report with remediation suggestions. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
maxDurationMinutes: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.default(480)
|
||||
.describe(
|
||||
'Maximum allowed routine duration in minutes before flagging (default 480 = 8 hours)'
|
||||
),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return runRoutineQualityPipeline(args.maxDurationMinutes, req);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,217 @@
|
||||
/**
|
||||
* SocialFastCoordinatorAgent — A2A pipeline for NomGap group fast coordination.
|
||||
*
|
||||
* Agent roster (3 steps):
|
||||
* 1. GroupMemberResolverAgent — given a fasting session, identify the group member user IDs
|
||||
* 2. NotificationDispatchAgent — fire social_invite push to all members; fire stage milestone pushes
|
||||
* 3. CoordinationReportAgent — assemble per-member notification results + weekly_digest summary
|
||||
*
|
||||
* MCP tools:
|
||||
* nomgap.social.coordinateFast(sessionId, memberUserIds) — run pipeline for a group fast
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
import { registerTool } from '../tools/registry.js';
|
||||
import type { McpToolRequest } from '../tools/types.js';
|
||||
import {
|
||||
nomgapFastingSessionGet,
|
||||
nomgapPushFire,
|
||||
type FastingSessionDoc,
|
||||
} from '../../lib/nomgap-client.js';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
type PushOutcome = 'fired' | 'failed';
|
||||
|
||||
interface MemberNotification {
|
||||
userId: string;
|
||||
type: string;
|
||||
outcome: PushOutcome;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SocialFastCoordinationReport {
|
||||
runId: string;
|
||||
productId: 'nomgap';
|
||||
sessionId: string;
|
||||
protocolId: string | null;
|
||||
currentStage: string | null;
|
||||
memberCount: number;
|
||||
notificationsFired: number;
|
||||
notificationsFailed: number;
|
||||
perMember: MemberNotification[];
|
||||
weeklyDigestFired: boolean;
|
||||
summary: string;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
// ── Step 1: GroupMemberResolverAgent ──────────────────────────────────────────
|
||||
|
||||
async function resolveSession(
|
||||
sessionId: string,
|
||||
opts: { token?: string; requestId?: string }
|
||||
): Promise<FastingSessionDoc | null> {
|
||||
try {
|
||||
return await nomgapFastingSessionGet(sessionId, opts);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 2: NotificationDispatchAgent ─────────────────────────────────────────
|
||||
|
||||
async function notifyMembers(
|
||||
sessionId: string,
|
||||
session: FastingSessionDoc | null,
|
||||
memberUserIds: string[],
|
||||
opts: { token?: string; requestId?: string }
|
||||
): Promise<MemberNotification[]> {
|
||||
const results: MemberNotification[] = [];
|
||||
|
||||
for (const userId of memberUserIds) {
|
||||
// Fire social_invite to each member
|
||||
try {
|
||||
await nomgapPushFire({ type: 'social_invite', userId }, opts);
|
||||
results.push({ userId, type: 'social_invite', outcome: 'fired' });
|
||||
} catch (err) {
|
||||
results.push({
|
||||
userId,
|
||||
type: 'social_invite',
|
||||
outcome: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
|
||||
// If session has a current stage, fire stage_transition milestone push
|
||||
const stage = (session as unknown as Record<string, unknown>)?.['currentStage'];
|
||||
if (stage && typeof stage === 'string') {
|
||||
try {
|
||||
await nomgapPushFire({ type: 'stage_transition', userId }, opts);
|
||||
results.push({ userId, type: 'stage_transition', outcome: 'fired' });
|
||||
} catch (err) {
|
||||
results.push({
|
||||
userId,
|
||||
type: 'stage_transition',
|
||||
outcome: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── Step 3: CoordinationReportAgent ──────────────────────────────────────────
|
||||
|
||||
async function sendWeeklyDigestAndReport(
|
||||
runId: string,
|
||||
sessionId: string,
|
||||
session: FastingSessionDoc | null,
|
||||
memberUserIds: string[],
|
||||
notifications: MemberNotification[],
|
||||
opts: { token?: string; requestId?: string }
|
||||
): Promise<SocialFastCoordinationReport> {
|
||||
// Send weekly_digest to the session owner as summary
|
||||
let weeklyDigestFired = false;
|
||||
const ownerId = session?.userId;
|
||||
if (ownerId) {
|
||||
try {
|
||||
await nomgapPushFire({ type: 'weekly_digest', userId: ownerId }, opts);
|
||||
weeklyDigestFired = true;
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
const fired = notifications.filter(n => n.outcome === 'fired').length;
|
||||
const failed = notifications.filter(n => n.outcome === 'failed').length;
|
||||
const stage =
|
||||
((session as unknown as Record<string, unknown>)?.['currentStage'] as string | null) ?? null;
|
||||
const protocolId =
|
||||
((session as unknown as Record<string, unknown>)?.['protocolId'] as string | null) ?? null;
|
||||
|
||||
const summary =
|
||||
memberUserIds.length === 0
|
||||
? 'No group members specified — no notifications dispatched.'
|
||||
: `Coordinated group fast session ${sessionId} for ${memberUserIds.length} member(s). ${fired} notification(s) fired, ${failed} failed. Weekly digest to owner: ${weeklyDigestFired ? 'sent' : 'skipped'}.`;
|
||||
|
||||
return {
|
||||
runId,
|
||||
productId: 'nomgap',
|
||||
sessionId,
|
||||
protocolId,
|
||||
currentStage: stage,
|
||||
memberCount: memberUserIds.length,
|
||||
notificationsFired: fired,
|
||||
notificationsFailed: failed,
|
||||
perMember: notifications,
|
||||
weeklyDigestFired,
|
||||
summary,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Pipeline runner ────────────────────────────────────────────────────────────
|
||||
|
||||
async function runSocialFastCoordinatorPipeline(
|
||||
sessionId: string,
|
||||
memberUserIds: string[],
|
||||
req: McpToolRequest
|
||||
): Promise<SocialFastCoordinationReport> {
|
||||
const runId = randomUUID();
|
||||
const opts = { token: req.headers.authorization?.slice(7), requestId: req.id };
|
||||
|
||||
req.log.info({ runId, stepId: 'resolve', sessionId }, 'GroupMemberResolverAgent start');
|
||||
const session = await resolveSession(sessionId, opts);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'resolve', found: session !== null },
|
||||
'GroupMemberResolverAgent done'
|
||||
);
|
||||
|
||||
req.log.info(
|
||||
{ runId, stepId: 'dispatch', memberCount: memberUserIds.length },
|
||||
'NotificationDispatchAgent start'
|
||||
);
|
||||
const notifications = await notifyMembers(sessionId, session, memberUserIds, opts);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'dispatch', fired: notifications.filter(n => n.outcome === 'fired').length },
|
||||
'NotificationDispatchAgent done'
|
||||
);
|
||||
|
||||
req.log.info({ runId, stepId: 'report' }, 'CoordinationReportAgent start');
|
||||
const report = await sendWeeklyDigestAndReport(
|
||||
runId,
|
||||
sessionId,
|
||||
session,
|
||||
memberUserIds,
|
||||
notifications,
|
||||
opts
|
||||
);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'report', summary: report.summary },
|
||||
'CoordinationReportAgent done'
|
||||
);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// ── MCP tool registration ─────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'nomgap.social.coordinateFast',
|
||||
description:
|
||||
'A2A pipeline: coordinates a NomGap group fast by resolving the session, firing social_invite and stage_transition push notifications to all group members, and sending a weekly_digest summary to the session owner. Use when a new group fast is created or when members need to be re-notified. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
sessionId: z.string().min(1).describe('Fasting session ID for the group fast'),
|
||||
memberUserIds: z
|
||||
.array(z.string().min(1))
|
||||
.min(1)
|
||||
.describe('List of user IDs to notify as group fast members'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return runSocialFastCoordinatorPipeline(args.sessionId, args.memberUserIds, req);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// ── 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 ────────────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
|
||||
req.log.info({ runId, stepId: 'detect', teamId, sinceHours }, 'NewMemberDetectorAgent start');
|
||||
const { all, newMembers } = await detectNewMembers(teamId, sinceMs, opts);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'detect', total: all.length, newCount: newMembers.length },
|
||||
'NewMemberDetectorAgent done'
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
req.log.info(
|
||||
{ runId, stepId: 'onboard', seeded: results.filter(r => r.memorySeeded).length },
|
||||
'OnboardingAgent done'
|
||||
);
|
||||
|
||||
req.log.info({ runId, stepId: 'report' }, 'ProvisioningReportAgent start');
|
||||
const report = assembleProvisioningReport(runId, teamId, all.length, results);
|
||||
req.log.info(
|
||||
{ runId, stepId: 'report', summary: report.summary },
|
||||
'ProvisioningReportAgent done'
|
||||
);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
},
|
||||
});
|
||||
@ -51,6 +51,9 @@ import './modules/a2a/brain-overflow-pipeline.js';
|
||||
import './modules/a2a/reflection-synthesis-pipeline.js';
|
||||
import './modules/a2a/keyboard-diagnostics-pipeline.js';
|
||||
import './modules/a2a/nl-parser-eval-pipeline.js';
|
||||
import './modules/a2a/routine-quality-pipeline.js';
|
||||
import './modules/a2a/social-fast-coordinator-pipeline.js';
|
||||
import './modules/a2a/team-provisioning-pipeline.js';
|
||||
import './modules/mindlyst/mindlyst-tools.js';
|
||||
import './modules/lysnrai/lysnrai-tools.js';
|
||||
import './modules/jarvis/jarvis-tools.js';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user