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

204 lines
6 KiB
TypeScript

import { extractMemberWorkSyncTaskId } from './MemberWorkSyncTaskImpactResolver';
import type {
MemberWorkSyncEventQueue,
MemberWorkSyncTriggerReason,
} from '../../infrastructure/MemberWorkSyncEventQueue';
import type { MemberWorkSyncTaskImpactResolver } from './MemberWorkSyncTaskImpactResolver';
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',
'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,
private readonly taskImpactResolver?: MemberWorkSyncTaskImpactResolver
) {}
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);
void this.enqueueTeam(event.teamName, 'runtime_activity', 0).catch(() => undefined);
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 === 'task' || event.type === 'task-log-change') {
const triggerReason = event.type === 'task' ? 'task_changed' : 'runtime_activity';
void this.enqueueTaskRelatedMembers(event, triggerReason).catch(() =>
this.enqueueTeam(event.teamName, triggerReason).catch(() => undefined)
);
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 });
}
}
private async enqueueTaskRelatedMembers(
event: TeamChangeEvent,
triggerReason: MemberWorkSyncTriggerReason
): Promise<void> {
const taskId = extractMemberWorkSyncTaskId({
taskId: event.taskId,
detail: event.detail,
});
if (!taskId || !this.taskImpactResolver) {
await this.enqueueTeam(event.teamName, triggerReason);
return;
}
const impact = await this.taskImpactResolver.resolve({
teamName: event.teamName,
taskId,
});
if (impact.fallbackTeamWide) {
await this.enqueueTeam(event.teamName, triggerReason);
return;
}
const materializer = this.materializer;
if (materializer) {
await Promise.allSettled(
impact.memberNames.map((memberName) =>
materializer.materializeMember(event.teamName, memberName)
)
);
}
for (const memberName of impact.memberNames) {
this.queue.enqueue({
teamName: event.teamName,
memberName,
triggerReason,
});
}
}
}