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/reflection-synthesis-pipeline.js';
|
||||||
import './modules/a2a/keyboard-diagnostics-pipeline.js';
|
import './modules/a2a/keyboard-diagnostics-pipeline.js';
|
||||||
import './modules/a2a/nl-parser-eval-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/mindlyst/mindlyst-tools.js';
|
||||||
import './modules/lysnrai/lysnrai-tools.js';
|
import './modules/lysnrai/lysnrai-tools.js';
|
||||||
import './modules/jarvis/jarvis-tools.js';
|
import './modules/jarvis/jarvis-tools.js';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user