/** * orchestrator.ts — High-level session management, state transitions, and * multi-turn coordination. This module owns the *what* and *when* of the * agentic loop: * 1. Manage session lifecycle (create, resume, terminate) * 2. Decide when to compact context * 3. Coordinate between multiple agents * 4. Handle task decomposition and sub-task routing * 5. Track overall progress and completion criteria * * The runner owns the *how*: turn-by-turn execution, tool dispatch, and * event emission. */ import type { LLMBackend } from './backendAdapter.js'; import { AgenticRunner, type RunResult, type RunnerEvent, type RunnerConfig, type ToolHandler } from './runner.js'; // ─── Session state ──────────────────────────────────────────────────────── export enum SessionState { /** Session has been created but not yet started */ Created = 'created', /** Session is actively running */ Running = 'running', /** Session is paused (waiting for user input or external event) */ Paused = 'paused', /** Session has completed successfully */ Completed = 'completed', /** Session has failed */ Failed = 'failed', /** Session has been terminated */ Terminated = 'terminated', } /** A single session in the orchestrator */ export interface Session { /** Unique session identifier */ id: string; /** Current session state */ state: SessionState; /** Session name / description */ name: string; /** Task description */ task: string; /** System prompt for the session */ systemPrompt: string; /** Registered tools for this session */ tools: Map; /** LLM backend for this session */ backend: LLMBackend; /** Messages in the conversation history */ messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; /** Current turn number */ turnCount: number; /** Maximum turns before forced stop */ maxTurns: number; /** Compaction strategy for this session */ compactionStrategy: CompactionStrategy; /** Completion criteria for the session */ completionCriteria?: string; /** Progress tracking */ progress: SessionProgress; /** Created timestamp */ createdAt: number; /** Last updated timestamp */ updatedAt: number; } /** Progress tracking for a session */ export interface SessionProgress { /** Steps completed */ completedSteps: string[]; /** Steps pending */ pendingSteps: string[]; /** Current step being worked on */ currentStep: string; /** Failed approaches to avoid */ failedApproaches: string[]; /** Next action to take */ nextAction: string; /** Modified files tracking */ modifiedFiles: Map; /** Total tool calls made */ toolCallCount: number; } /** Compaction strategy for managing context window */ export interface CompactionStrategy { /** Type of compaction */ type: 'summarize' | 'prune' | 'compress' | 'none'; /** Threshold for triggering compaction (in turns) */ threshold: number; /** Token budget after compaction */ tokenBudget: number; /** Custom compaction function */ custom?: (messages: Array<{ role: string; content: string }>) => Promise>; } // ─── Orchestrator ──────────────────────────────────────────────────────── export class SessionOrchestrator { private sessions: Map = new Map(); private onSessionEvent?: (event: SessionEvent) => void; constructor(onSessionEvent?: (event: SessionEvent) => void) { this.onSessionEvent = onSessionEvent; } /** * Create a new session with the given configuration. */ createSession(options: { id?: string; name: string; task: string; systemPrompt?: string; backend: LLMBackend; tools?: Map; maxTurns?: number; compactionStrategy?: CompactionStrategy; completionCriteria?: string; }): Session { const id = options.id ?? `session_${Date.now()}_${Math.random().toString(36).slice(2)}`; const session: Session = { id, state: SessionState.Created, name: options.name, task: options.task, systemPrompt: options.systemPrompt ?? '', backend: options.backend, tools: options.tools ?? new Map(), messages: [], turnCount: 0, maxTurns: options.maxTurns ?? 50, compactionStrategy: options.compactionStrategy ?? { type: 'none', threshold: 10, tokenBudget: 4096 }, completionCriteria: options.completionCriteria, progress: { completedSteps: [], pendingSteps: [options.task], currentStep: options.task, failedApproaches: [], nextAction: 'start', modifiedFiles: new Map(), toolCallCount: 0, }, createdAt: Date.now(), updatedAt: Date.now(), }; this.sessions.set(id, session); this.emit({ type: 'session-created', sessionId: id, session }); return session; } /** * Start a session — transitions it from Created to Running and begins * the agentic loop. */ async startSession(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session "${sessionId}" not found`); } if (session.state !== SessionState.Created && session.state !== SessionState.Paused) { throw new Error(`Cannot start session in state "${session.state}"`); } session.state = SessionState.Running; session.updatedAt = Date.now(); // Add system prompt as first message if (session.systemPrompt) { session.messages.push({ role: 'system', content: session.systemPrompt }); } this.emit({ type: 'session-started', sessionId, session }); // Begin the agentic loop await this.runLoop(session); return session; } /** * Run the agentic loop for a session: repeatedly call the runner until * completion criteria are met or max turns reached. */ private async runLoop(session: Session): Promise { while (session.state === SessionState.Running && session.turnCount < session.maxTurns) { session.turnCount++; // Check compaction threshold if (session.compactionStrategy.type !== 'none' && session.turnCount % session.compactionStrategy.threshold === 0) { await this.compactSession(session); } // Check completion criteria if (session.completionCriteria && this.checkCompletion(session)) { session.state = SessionState.Completed; session.updatedAt = Date.now(); this.emit({ type: 'session-completed', sessionId: session.id, session }); return; } // Run one turn const result = await this.runTurn(session); // Check for errors if (result.error) { session.state = SessionState.Failed; session.updatedAt = Date.now(); this.emit({ type: 'session-failed', sessionId: session.id, session, error: result.error }); return; } // Update progress session.progress.toolCallCount += result.toolCalls.length; session.updatedAt = Date.now(); } // Max turns reached if (session.turnCount >= session.maxTurns) { session.state = SessionState.Completed; session.updatedAt = Date.now(); this.emit({ type: 'session-completed', sessionId: session.id, session }); } } /** * Run a single turn: delegate to the runner and process the result. */ private async runTurn(session: Session): Promise { const runner = new AgenticRunner( { backend: session.backend, tools: session.tools, maxTurns: 1, streamEvents: true, }, (event: RunnerEvent) => { this.emit({ type: 'runner-event', sessionId: session.id, event }); }, ); const result = await runner.run(session.messages); // Add assistant message to history session.messages.push({ role: 'assistant', content: result.text }); // Add tool results to history for (const tr of result.toolResults) { session.messages.push({ role: 'system', content: `${tr.content}`, }); } return result; } /** * Compact the session's message history according to the compaction strategy. */ private async compactSession(session: Session): Promise { if (session.compactionStrategy.type === 'none') return; const messages = session.messages; if (session.compactionStrategy.custom) { session.messages = (await session.compactionStrategy.custom(messages)) as typeof session.messages; } else if (session.compactionStrategy.type === 'summarize') { // Simple summarize: keep system prompt + last N messages const keep = Math.min(session.compactionStrategy.tokenBudget, messages.length); session.messages = messages.slice(-keep); } else if (session.compactionStrategy.type === 'prune') { // Simple prune: keep system prompt + every other message session.messages = messages.filter((_, i) => i % 2 === 0); } } /** * Check if the session has met its completion criteria. */ private checkCompletion(session: Session): boolean { if (!session.completionCriteria) return false; // Simple keyword matching for now const criteria = session.completionCriteria.toLowerCase(); const lastMessage = session.messages[session.messages.length - 1]; if (!lastMessage) return false; return lastMessage.content.toLowerCase().includes(criteria); } /** * Pause a running session. */ pauseSession(sessionId: string): Session { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session "${sessionId}" not found`); } if (session.state !== SessionState.Running) { throw new Error(`Cannot pause session in state "${session.state}"`); } session.state = SessionState.Paused; session.updatedAt = Date.now(); this.emit({ type: 'session-paused', sessionId, session }); return session; } /** * Resume a paused session. */ async resumeSession(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session "${sessionId}" not found`); } if (session.state !== SessionState.Paused) { throw new Error(`Cannot resume session in state "${session.state}"`); } session.state = SessionState.Running; session.updatedAt = Date.now(); this.emit({ type: 'session-resumed', sessionId, session }); await this.runLoop(session); return session; } /** * Terminate a session. */ terminateSession(sessionId: string): Session { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session "${sessionId}" not found`); } session.state = SessionState.Terminated; session.updatedAt = Date.now(); this.emit({ type: 'session-terminated', sessionId, session }); return session; } /** * Get a session by ID. */ getSession(sessionId: string): Session | undefined { return this.sessions.get(sessionId); } /** * List all sessions. */ listSessions(): Session[] { return Array.from(this.sessions.values()); } /** * Delete a session. */ deleteSession(sessionId: string): void { this.sessions.delete(sessionId); } /** * Emit a session event. */ private emit(event: SessionEvent): void { if (this.onSessionEvent) { this.onSessionEvent(event); } } } // ─── Session events ──────────────────────────────────────────────────────── export interface SessionEvent { type: 'session-created' | 'session-started' | 'session-paused' | 'session-resumed' | 'session-completed' | 'session-failed' | 'session-terminated' | 'runner-event'; sessionId: string; session?: Session; event?: RunnerEvent; error?: string; }