import { IAgentLifecycleService, IAgentConversationUndoParticipantRegistry, IAgentLoopService, IAgentScopeContext, IAgentTaskService, IEventBus, INTERACTION_TAG_AGENT_ID, INTERACTION_TAG_SESSION_ID, ISessionMetadata, MAIN_AGENT_ID, interactions, toDisposable, type AgentMeta, type IDisposable, type IAgentScopeHandle, type Interaction, type ISessionScopeHandle, } from '@moonshot-ai/agent-core-v2'; import type { AgentDescriptor, TranscriptChangeEvent, TranscriptStore } from '@moonshot-ai/transcript'; import { legacyApprovalsOf } from '../legacyStatus/legacyActivity'; import { AgentTranscriptProjector, type ProjectorBusEvent, type ProjectorInteraction, } from './coreEventMap'; export interface TranscriptBindingLogger { warn(obj: unknown, msg: string): void; } export interface TranscriptBinding extends IDisposable { seedPendingInteractions(agentId?: string): void; } export function bindSessionTranscript( store: TranscriptStore, session: ISessionScopeHandle, logger?: TranscriptBindingLogger, onOps?: (event: TranscriptChangeEvent) => void, reconcileAfterUndo?: (agentId: string) => Promise, ): TranscriptBinding { const agents = session.accessor.get(IAgentLifecycleService); const pendingInteractions = (): readonly Interaction[] => interactions.findAll({ resolved: false, tags: { [INTERACTION_TAG_SESSION_ID]: session.id }, }); const disposables: IDisposable[] = []; const agentDisposables = new Map(); const subscribedAgents = new Set(); const projectors = new Map(); const interactionAgents = new Map(); const knownInteractions = new Set(); const unseeded = new Map(); const earlyResolves = new Map(); const seededAgents = new Set(); let seededAll = false; const isSeeded = (agentId: string): boolean => seededAll || seededAgents.has(agentId); const applyOps = (agentId: string, ops: ReturnType): void => { if (ops.length === 0) return; const result = store.ensureAgent(agentId).apply(ops); if (result.gap !== undefined) { logger?.warn( { sessionId: store.sessionId, agentId, gap: result.gap }, 'transcript: append gap — producer/consumer skew', ); return; } onOps?.({ agentId, ops }); }; const projectorFor = (agentId: string): AgentTranscriptProjector => { let projector = projectors.get(agentId); if (projector === undefined) { projector = new AgentTranscriptProjector(agentId, store.sessionId, { stepFrames: (turnId, stepId) => store.getAgent(agentId)?.getTurn(turnId)?.steps.find((s) => s.stepId === stepId)?.frames, toolFrame: (toolCallId) => { const transcript = store.getAgent(agentId); if (transcript === undefined) return undefined; for (const item of transcript.getItems()) { if (item.kind !== 'turn') continue; for (const step of item.steps) { for (const frame of step.frames) { if (frame.kind === 'tool' && frame.toolCallId === toolCallId) { return { turnId: item.turnId, stepId: step.stepId, frame }; } } } } return undefined; }, stepOrdinal: (turnId) => { const agentHandle = agents.handleOf(agentId); if (agentHandle === undefined) return undefined; const turn = agentHandle.accessor.get(IAgentLoopService)?.snapshot().turn; return turn === undefined || `t${turn.turnId}` !== turnId ? undefined : turn.step; }, activitySnapshot: () => agents.handleOf(agentId)?.accessor.get(IAgentLoopService)?.snapshot() ?? {}, pendingApprovals: () => { const agentHandle = agents.handleOf(agentId); return agentHandle === undefined ? [] : legacyApprovalsOf(agentHandle); }, turn: (turnId) => store.getAgent(agentId)?.getTurn(turnId), resolvePlanRevisionKey: (key) => agents.handleOf(agentId)?.accessor.get(IAgentScopeContext).scope(key) ?? key, }); const agentHandle = agents.handleOf(agentId); if (agentHandle !== undefined) { const tasks = agentHandle.accessor.get(IAgentTaskService)?.list() ?? []; for (const info of tasks) { if (info.kind === 'agent' && typeof info.agentId === 'string' && info.agentId.length > 0) { applyOps( agentId, projector.seedSubagentTask({ taskId: info.taskId, agentId: info.agentId, description: info.description, status: info.status, detached: info.detached ?? false, startedAt: info.startedAt, }), ); } } } projectors.set(agentId, projector); } return projector; }; const subscribeAgent = (handle: IAgentScopeHandle): void => { if (subscribedAgents.has(handle.id)) return; subscribedAgents.add(handle.id); const projector = projectorFor(handle.id); store.ensureAgent(handle.id, { agentId: handle.id }); const bus = handle.accessor.get(IEventBus); const busD = bus.subscribe((event) => applyOps(handle.id, projectorFor(handle.id).map(event as ProjectorBusEvent)), ); const loopStatus = handle.accessor.get(IAgentLoopService)?.snapshot(); if (loopStatus?.state === 'running' && loopStatus.activeTurnId !== undefined) { const promptId = loopStatus.activePromptId; projector.seedActiveTurn({ turnId: loopStatus.activeTurnId, promptId }); } const list = agentDisposables.get(handle.id) ?? []; list.push(busD); if (reconcileAfterUndo !== undefined) { list.push(handle.accessor.get(IAgentConversationUndoParticipantRegistry).register({ id: 'transcript', phase: 'after-flush', reconcileAfterUndo: async () => { await reconcileAfterUndo(handle.id); projectors.delete(handle.id); }, })); } agentDisposables.set(handle.id, list); }; const interactionAgentId = (interaction: Interaction): string => { const payloadAgent = (interaction.payload as { agentId?: unknown }).agentId; const tag = interaction.tags[INTERACTION_TAG_AGENT_ID]; return ( (typeof tag === 'string' ? tag : undefined) ?? (typeof payloadAgent === 'string' ? payloadAgent : undefined) ?? MAIN_AGENT_ID ); }; const announceInteraction = (interaction: Interaction): void => { if (interaction.kind !== 'approval' && interaction.kind !== 'question') return; const agentId = interactionAgentId(interaction); interactionAgents.set(interaction.id, agentId); const request: ProjectorInteraction = { id: interaction.id, kind: interaction.kind, payload: interaction.payload, createdAt: interaction.createdAt, }; applyOps(agentId, projectorFor(agentId).mapInteractionRequested(request)); }; const refreshDescriptors = (): void => { void session.accessor .get(ISessionMetadata) .read() .then((meta) => { for (const agentId of projectors.keys()) { store.describeAgent(descriptorFromMeta(agentId, meta.agents?.[agentId])); } }) .catch(() => { }); }; for (const agent of agents.list()) { const handle = agents.handleOf(agent.agentId); if (handle !== undefined) subscribeAgent(handle); } disposables.push( agents.onDidCreate((context) => { const handle = agents.handleOf(context.agentId); if (handle !== undefined) subscribeAgent(handle); seededAgents.add(context.agentId); refreshDescriptors(); }), agents.onDidClose((context) => { const agentId = context.agentId; for (const d of agentDisposables.get(agentId) ?? []) d.dispose(); agentDisposables.delete(agentId); subscribedAgents.delete(agentId); projectors.delete(agentId); store.markDisposed(agentId, new Date().toISOString()); }), ); for (const pending of pendingInteractions()) { if (pending.kind !== 'approval' && pending.kind !== 'question') continue; if (knownInteractions.has(pending.id)) continue; knownInteractions.add(pending.id); interactionAgents.set(pending.id, interactionAgentId(pending)); unseeded.set(pending.id, pending); } const seedPendingInteractions = (agentId?: string): void => { if (agentId === undefined) seededAll = true; else seededAgents.add(agentId); for (const [id, interaction] of unseeded) { if (agentId !== undefined && interactionAgents.get(id) !== agentId) continue; unseeded.delete(id); announceInteraction(interaction); const early = earlyResolves.get(id); if (early === undefined) continue; interactionAgents.delete(id); earlyResolves.delete(id); const projector = projectors.get(early.agentId); if (projector !== undefined) { applyOps(early.agentId, projector.mapInteractionResolved(id, early.response)); } } for (const pending of pendingInteractions()) { if (knownInteractions.has(pending.id)) continue; if (agentId !== undefined && interactionAgentId(pending) !== agentId) continue; knownInteractions.add(pending.id); announceInteraction(pending); } }; disposables.push( toDisposable( interactions.onDidChangePending(() => { for (const pending of pendingInteractions()) { if (knownInteractions.has(pending.id)) continue; const agentId = interactionAgentId(pending); knownInteractions.add(pending.id); if (!isSeeded(agentId)) { interactionAgents.set(pending.id, agentId); unseeded.set(pending.id, pending); continue; } announceInteraction(pending); } }), ), toDisposable( interactions.onDidResolve(({ id, response }) => { knownInteractions.delete(id); const agentId = interactionAgents.get(id); if (agentId === undefined) return; interactionAgents.delete(id); if (unseeded.has(id)) { earlyResolves.set(id, { agentId, response }); return; } const projector = projectors.get(agentId); if (projector === undefined) return; applyOps(agentId, projector.mapInteractionResolved(id, response)); }), ), ); refreshDescriptors(); return { seedPendingInteractions, dispose: () => { for (const d of disposables) d.dispose(); for (const list of agentDisposables.values()) { for (const d of list) d.dispose(); } agentDisposables.clear(); projectors.clear(); interactionAgents.clear(); knownInteractions.clear(); unseeded.clear(); earlyResolves.clear(); }, }; } export function descriptorFromMeta(agentId: string, meta: AgentMeta | undefined): AgentDescriptor { const parentFromLabels = meta?.labels?.['parentAgentId']; const swarmItem = meta?.labels?.['swarmItem'] ?? meta?.swarmItem; return { agentId, type: meta?.type ?? (agentId === MAIN_AGENT_ID ? 'main' : 'sub'), parentAgentId: parentFromLabels !== undefined && parentFromLabels.length > 0 ? parentFromLabels : (meta?.parentAgentId ?? undefined), label: swarmItem !== undefined && swarmItem.length > 0 ? swarmItem : undefined, }; }