/** * Shared Type Definitions for Multi-Agent Orchestration * * Defines the session-based multi-agent conversation system with * support for QA, Discussion, and Lecture session types. */ import type { UIMessage } from 'ai'; import type { CleanupSource } from '@/lib/playback/auto-resume'; import type { ThinkingConfig } from './provider'; // Session Types export type SessionType = 'qa' | 'discussion' | 'lecture'; export type SessionStatus = | 'idle' | 'active' | 'soft-closing' | 'interrupted' | 'completed' | 'error'; /** * Metadata attached to chat messages */ export interface ChatMessageMetadata { senderName?: string; senderAvatar?: string; originalRole?: 'teacher' | 'agent' | 'user'; actions?: MessageAction[]; agentId?: string; agentColor?: string; createdAt?: number; interrupted?: boolean; } /** * Action buttons that can be attached to messages */ export interface MessageAction { id: string; label: string; icon?: string; variant?: 'spotlight' | 'highlight' | 'reset' | 'insert' | 'draw'; } /** * Chat session representing a conversation with one or more agents */ export interface ChatSession { id: string; type: SessionType; title: string; status: SessionStatus; messages: UIMessage[]; config: SessionConfig; toolCalls: ToolCallRecord[]; pendingToolCalls: ToolCallRequest[]; createdAt: number; updatedAt: number; sceneId?: string; lastActionIndex?: number; endReason?: string; /** Absolute deadline for the client-side soft-closing grace window. */ softCloseDeadline?: number; directorState?: DirectorState; } export interface PiSessionBoundaryContext { isFirstRequestInLiveSession: true; previousEndSource?: CleanupSource; sameSceneAsPrevious?: boolean; } /** * Advance the session's conflict-order clock without trusting wall time to be * monotonic. Restored data may come from a clock ahead of this device, and the * local clock itself can move backwards. */ export function nextChatUpdatedAt( session: Pick, now = Date.now(), ): number { return Math.max(now, session.updatedAt + 1); } /** Apply a lifecycle transition and advance the same conflict-order clock. */ export function withChatSessionStatus( session: ChatSession, status: SessionStatus, now = Date.now(), ): ChatSession { return { ...session, status, updatedAt: nextChatUpdatedAt(session, now) }; } /** Advance conflict order once a streamed message segment is fully revealed. */ export function withChatSegmentSealed(session: ChatSession, now = Date.now()): ChatSession { return { ...session, updatedAt: nextChatUpdatedAt(session, now) }; } /** Advance conflict order only when paced text has actually finished revealing. */ export function withChatSegmentReveal( session: ChatSession, isComplete: boolean, now = Date.now(), ): ChatSession { return isComplete ? withChatSegmentSealed(session, now) : session; } /** Mark streams that cannot survive a reload as interrupted without stale ordering. */ export function interruptActiveChatSessions( sessions: ChatSession[], now = Date.now(), ): ChatSession[] { return sessions.map((session) => session.status === 'active' ? withChatSessionStatus(session, 'interrupted', now) : session, ); } /** * Session configuration */ export interface SessionConfig { agentIds: string[]; triggerAgentId?: string; // For discussion: first agent to speak defaultAgentId?: string; // For QA: the responding agent } /** * Pending tool call request sent to client for execution */ export interface ToolCallRequest { toolCallId: string; toolName: string; args: Record; agentId: string; status: 'pending' | 'executing'; requestedAt: number; } /** * Completed tool call record with result */ export interface ToolCallRecord { toolCallId: string; toolName: string; args: Record; agentId: string; result?: unknown; error?: string; status: 'pending' | 'executing' | 'completed' | 'failed'; requestedAt: number; completedAt?: number; } /** * Server-Sent Event types for streaming session updates */ export type SessionEvent = | { type: 'message'; data: UIMessage } | { type: 'tool_request'; data: { sessionId: string; toolCalls: ToolCallRequest[] }; } | { type: 'tool_complete'; data: ToolCallRecord } | { type: 'agent_switch'; data: { fromAgentId: string | null; toAgentId: string }; } | { type: 'session_status'; data: { status: SessionStatus; reason?: string } } | { type: 'error'; data: { message: string } } | { type: 'done'; data: SessionSummary } | { type: 'text_start'; data: { messageId: string; agentId: string; agentName: string }; } | { type: 'text_delta'; data: { messageId: string; delta: string } } | { type: 'text_end'; data: { messageId: string; content: string } }; /** * Summary data sent when session completes */ export interface SessionSummary { sessionId: string; totalTurns: number; totalMessages: number; totalToolCalls: number; endReason: string; } /** * Request body for creating a new session */ export interface CreateSessionRequest { type: SessionType; title?: string; trigger: { message?: string; agentIds: string[]; triggerAgentId?: string; }; } /** * Request body for sending a message to a session */ export interface SendMessageRequest { content: string; apiKey?: string; baseUrl?: string; model?: string; storeState: { stage: unknown; scenes: unknown[]; currentSceneId: string | null; mode: 'autonomous' | 'playback'; whiteboardOpen: boolean; }; } /** * Request body for submitting tool results */ export interface ToolResultsRequest { results: ToolCallRecord[]; } /** * Session list item (without full messages for efficiency) */ export interface SessionListItem { id: string; type: SessionType; title: string; status: SessionStatus; messageCount: number; toolCallCount: number; createdAt: number; updatedAt: number; } /** * Convert a full ChatSession to a list item (without messages) */ export function toSessionListItem(session: ChatSession): SessionListItem { return { id: session.id, type: session.type, title: session.title, status: session.status, messageCount: session.messages.length, toolCallCount: session.toolCalls.length, createdAt: session.createdAt, updatedAt: session.updatedAt, }; } /** * A single item in a lecture note — either speech text or an action badge. * Ordered to match the original action sequence in the scene. */ export type LectureNoteItem = | { kind: 'speech'; text: string; actionIndex: number; actionId: string; actionType: string; } | { kind: 'action'; type: string; label?: string; actionIndex: number; actionId: string; actionType: string; }; /** * A completed lecture note entry for one scene. * Built from Scene.actions, displayed in the Notes tab. */ export interface LectureNoteEntry { sceneId: string; sceneTitle: string; sceneOrder: number; items: LectureNoteItem[]; completedAt: number; } // ==================== Stateless Multi-Agent API Types ==================== import type { Stage, Scene, StageMode } from '@/lib/types/stage'; import type { SceneOutline } from '@/lib/types/generation'; import type { AgentTurnSummary, WhiteboardActionRecord } from '@/lib/orchestration/types'; import type { DirectorCompactionTrace } from '@/lib/chat/pi/director-compaction'; import type { DirectorToolTraceEntry } from '@/lib/chat/pi/types'; import type { BaiduSubSources, WebSearchProviderId } from '@/lib/web-search/types'; /** * Accumulated director state passed between per-agent requests. * Client-maintained — backend is stateless. */ export interface DirectorState { turnCount: number; agentResponses: AgentTurnSummary[]; whiteboardLedger: WhiteboardActionRecord[]; } /** Browser-selected identity for one PPT element. Content is resolved by the Host. */ export interface SlideElementReference { kind: 'slide_element'; sceneId: string; elementId: string; } /** * Request body for the stateless chat API * All state is sent from the client on each request */ export interface StatelessChatRequest { /** Conversation history (client-maintained) */ messages: UIMessage[]; /** Current application state */ storeState: { stage: Stage | null; scenes: Scene[]; /** Thin course map available to the Pi Director before it reads any scene. */ outlines?: SceneOutline[]; currentSceneId: string | null; mode: StageMode; whiteboardOpen: boolean; /** Browser-owned manual visibility revision captured for this request. */ whiteboardManualVisibilityRevision?: number; /** * Post-submit quiz state for the CURRENT scene, hydrated by the client * from localStorage when the active scene is a graded quiz. Lets the * agent give targeted feedback on the student's actual answers * (correct/incorrect, written response, AI grader comment) instead of * guessing. Absent when the student has not submitted yet, or when the * active scene is not a quiz. */ quizResults?: { sceneId: string; answers: Record; results: Array<{ questionId: string; correct: boolean | null; status: 'correct' | 'incorrect'; earned: number; aiComment?: string; }>; }; }; /** Optional Pi-only, identity-only reference to one slide element. */ elementReference?: SlideElementReference; /** Agent configuration */ config: { agentIds: string[]; sessionType?: 'qa' | 'discussion'; /** Discussion topic (for agent-initiated discussions) */ discussionTopic?: string; /** Discussion prompt (for agent-initiated discussions) */ discussionPrompt?: string; /** Which agent should speak first in a discussion */ triggerAgentId?: string; /** Full agent configs for generated (non-default) agents that aren't in the server-side registry */ agentConfigs?: Array<{ id: string; name: string; role: string; persona: string; avatar: string; color: string; allowedActions: string[]; priority: number; isGenerated?: boolean; boundStageId?: string; }>; /** Pi PoC: max child agent turns in one server-side loop. */ piMaxAgentTurns?: number; /** Pi PoC: max emitted actions per child agent turn. */ piMaxActionsPerAgent?: number; /** Pi PoC: opt in to whiteboard tools; defaults off to keep the first A/B pass comparable. */ piEnableWhiteboardTools?: boolean; }; /** Accumulated director state from previous per-agent requests */ directorState?: DirectorState; /** Pi-only context for the first request in a newly created live UI session. */ piSessionBoundary?: PiSessionBoundaryContext; /** User profile for personalization */ userProfile?: { nickname?: string; bio?: string; }; /** OpenAI-compatible API credentials */ apiKey: string; baseUrl?: string; model?: string; providerType?: string; /** * Opt-in: enable provider-side thinking for this request. Default is * `{ enabled: false }` (low-latency chat). Eval harness sets this to * `{ enabled: true }` when `EVAL_ENABLE_THINKING=1`. */ thinking?: ThinkingConfig; /** UI-selected per-model thinking config. Takes precedence over `thinking`. */ thinkingConfig?: ThinkingConfig; /** Toolbar-selected Web Search provider. Resolved server-side independently from the LLM. */ webSearchProviderId?: WebSearchProviderId; /** Selected provider credential only; server-managed credentials remain authoritative. */ webSearchApiKey?: string; /** Selected provider base URL only; validated server-side and ignored for managed providers. */ webSearchBaseUrl?: string; /** Selected Claude Web Search model only. */ webSearchModelId?: string; /** Selected Baidu Web Search sub-sources only. */ baiduSubSources?: BaiduSubSources; } /** * Parsed action from structured output */ export interface ParsedAction { actionId: string; actionName: string; params: Record; } /** @deprecated Use ParsedAction instead */ export type ParsedToolCall = ParsedAction; /** * Server-Sent Events for stateless chat API */ export type StatelessEvent = | { type: 'agent_start'; data: { messageId: string; agentId: string; agentName: string; agentAvatar?: string; agentColor?: string; }; } | { type: 'agent_end'; data: { messageId: string; agentId: string } } | { type: 'text_delta'; data: { content: string; messageId?: string } } | { type: 'action'; data: { actionId: string; actionName: string; params: Record; agentId: string; messageId?: string; }; } | { type: 'thinking'; data: { stage: 'director' | 'agent_loading'; agentId?: string }; } | { type: 'whiteboard'; data: | { kind: 'visibility_query'; queryId: string; stageId: string } | { kind: 'open' | 'close'; stageId: string; manualVisibilityRevision: number; } | { kind: 'projection'; stageId: string; lastSeq: number }; } | { type: 'cue_user'; data: { fromAgentId?: string; prompt?: string } } | { type: 'done'; data: { totalActions: number; totalAgents: number; agentHadContent?: boolean; cueUserReceived?: boolean; sessionClosed?: boolean; endReason?: string; directorCompaction?: DirectorCompactionTrace; directorToolTrace?: DirectorToolTraceEntry[]; directorState?: DirectorState; }; } | { type: 'error'; data: { message: string } };