agent-ecosystem/src/features/member-work-sync/main/adapters/input/MemberWorkSyncTeamChangeRouter.ts

155 lines
4.4 KiB
TypeScript

import type {
MemberWorkSyncEventQueue,
MemberWorkSyncTriggerReason,
} from '../../infrastructure/MemberWorkSyncEventQueue';
import type { TeamChangeEvent, ToolActivityEventPayload } from '@shared/types';
interface MemberTurnSettledEventPayload {
memberName?: string;
sourceId?: string;
provider?: string;
}
interface MemberWorkSyncRosterSource {
loadActiveMemberNames(teamName: string): Promise<string[]>;
}
interface MemberWorkSyncMemberStorageMaterializer {
materializeMember(teamName: string, memberName: string): Promise<void>;
}
const TEAM_WIDE_REASONS: Partial<Record<TeamChangeEvent['type'], MemberWorkSyncTriggerReason>> = {
config: 'config_changed',
task: 'task_changed',
'task-log-change': 'runtime_activity',
'log-source-change': 'runtime_activity',
process: 'runtime_activity',
'lead-activity': 'runtime_activity',
};
function parseInboxRecipient(detail: string | undefined): string | null {
if (!detail) {
return null;
}
const match = /^inboxes\/(.+)\.json$/.exec(detail);
return match?.[1]?.trim() || null;
}
function parseToolActivity(detail: string | undefined): ToolActivityEventPayload | null {
if (!detail) {
return null;
}
try {
const parsed = JSON.parse(detail) as ToolActivityEventPayload;
return parsed && typeof parsed === 'object' ? parsed : null;
} catch {
return null;
}
}
function parseMemberTurnSettled(detail: string | undefined): MemberTurnSettledEventPayload | null {
if (!detail) {
return null;
}
try {
const parsed = JSON.parse(detail) as MemberTurnSettledEventPayload;
return parsed && typeof parsed === 'object' ? parsed : null;
} catch {
return null;
}
}
export class MemberWorkSyncTeamChangeRouter {
constructor(
private readonly rosterSource: MemberWorkSyncRosterSource,
private readonly queue: MemberWorkSyncEventQueue,
private readonly materializer?: MemberWorkSyncMemberStorageMaterializer
) {}
async enqueueStartupScan(teamNames: string[]): Promise<void> {
await Promise.allSettled(
teamNames.map((teamName) => this.enqueueTeam(teamName, 'startup_scan', 30_000))
);
}
noteTeamChange(event: TeamChangeEvent): void {
if (event.type === 'lead-activity' && event.detail === 'offline') {
this.queue.dropTeam(event.teamName);
return;
}
if (event.type === 'member-spawn') {
const memberName = event.detail?.trim();
if (memberName) {
this.queue.enqueue({
teamName: event.teamName,
memberName,
triggerReason: 'member_spawned',
runAfterMs: 30_000,
});
} else {
void this.enqueueTeam(event.teamName, 'member_spawned', 30_000).catch(() => undefined);
}
return;
}
if (event.type === 'tool-activity') {
const payload = parseToolActivity(event.detail);
if (payload?.action === 'finish' && payload.memberName) {
this.queue.enqueue({
teamName: event.teamName,
memberName: payload.memberName,
triggerReason: 'tool_finished',
});
}
return;
}
if (event.type === 'member-turn-settled') {
const payload = parseMemberTurnSettled(event.detail);
const memberName = payload?.memberName?.trim();
if (memberName) {
this.queue.enqueue({
teamName: event.teamName,
memberName,
triggerReason: 'turn_settled',
});
}
return;
}
if (event.type === 'inbox' || event.type === 'lead-message') {
const recipient = parseInboxRecipient(event.detail);
if (recipient) {
this.queue.enqueue({
teamName: event.teamName,
memberName: recipient,
triggerReason: 'inbox_changed',
});
}
return;
}
const teamWideReason = TEAM_WIDE_REASONS[event.type];
if (teamWideReason) {
void this.enqueueTeam(event.teamName, teamWideReason).catch(() => undefined);
}
}
private async enqueueTeam(
teamName: string,
triggerReason: MemberWorkSyncTriggerReason,
runAfterMs?: number
): Promise<void> {
const activeMembers = await this.rosterSource.loadActiveMemberNames(teamName);
const materializer = this.materializer;
if (materializer) {
await Promise.allSettled(
activeMembers.map((memberName) => materializer.materializeMember(teamName, memberName))
);
}
for (const memberName of activeMembers) {
this.queue.enqueue({ teamName, memberName, triggerReason, runAfterMs });
}
}
}