/** * Agent harness dispatcher. * * Picks the right harness implementation based on `cfg.ai.provider` and * forwards every call. The supervisor, channels, and scheduler still import * from `./bloby-agent.js` exactly as they did before — this file is a * compatibility shim so the harness split is invisible to callers. * * Provider routing: * anthropic (or empty/default) → Claude Agent SDK harness — unchanged * openai → Codex app-server harness — Phase 2 * * Cleanup operations (`endAllConversations`, `endConversation(id)`) are * fanned out to *both* harnesses so a stale conversation on a previously * active provider can't outlive a provider switch. */ import * as claude from './harnesses/claude.js'; import * as codex from './harnesses/codex.js'; import * as pi from './harnesses/pi/index.js'; import type { Harness, OnAgentMessage, RecentMessage, AgentAttachment, AgentQueryRequest, AgentQueryResult } from './harnesses/types.js'; import type { SavedFile } from './file-saver.js'; import { loadConfig } from '../shared/config.js'; export type { RecentMessage, AgentAttachment, AgentQueryRequest, AgentQueryResult }; const HARNESSES: Record = { anthropic: claude, openai: codex, pi: pi as unknown as Harness, }; /** Resolve the harness for the currently-configured provider. */ function activeHarness(): Harness { let provider = ''; try { provider = loadConfig().ai?.provider || ''; } catch { // Pre-onboard or transient config error: fall through to default. } return HARNESSES[provider] ?? claude; } /* ── Live conversation API ─────────────────────────────────────────────── */ export function startConversation( conversationId: string, model: string, onMessage: OnAgentMessage, names?: { botName: string; humanName: string }, recentMessages?: RecentMessage[], ): Promise { return activeHarness().startConversation(conversationId, model, onMessage, names, recentMessages); } export function pushMessage( conversationId: string, content: string, attachments?: AgentAttachment[], savedFiles?: SavedFile[], ): boolean { // Push to whichever harness owns this conversation; routing by current // config alone could miss a conversation that was started under a previous // provider (rare but possible during a switch). for (const h of Object.values(HARNESSES)) { if (h.hasConversation(conversationId)) { return h.pushMessage(conversationId, content, attachments, savedFiles); } } return activeHarness().pushMessage(conversationId, content, attachments, savedFiles); } export function hasConversation(conversationId: string): boolean { return Object.values(HARNESSES).some((h) => h.hasConversation(conversationId)); } export function endConversation(conversationId: string): void { for (const h of Object.values(HARNESSES)) h.endConversation(conversationId); } export function endAllConversations(): void { for (const h of Object.values(HARNESSES)) h.endAllConversations(); } export function isConversationBusy(conversationId: string): boolean { return Object.values(HARNESSES).some((h) => h.isConversationBusy(conversationId)); } /** True if ANY conversation in ANY harness is mid-turn. Lets the supervisor defer backend * restarts during channel/Alexa turns, which don't set the dashboard's agentQueryActive flag. */ export function anyOneShotActive(): boolean { return Object.values(HARNESSES).some((h) => h.anyOneShotActive()); } export function anyConversationBusy(): boolean { return Object.values(HARNESSES).some((h) => h.anyConversationBusy()); } export async function stopSubAgentTask(conversationId: string, taskId: string): Promise { for (const h of Object.values(HARNESSES)) { if (h.hasConversation(conversationId)) { await h.stopSubAgentTask(conversationId, taskId); return; } } } export function warmUpForLiveConversation( model: string, names?: { botName: string; humanName: string }, ): Promise { return activeHarness().warmUpForLiveConversation(model, names); } /* ── One-shot API ──────────────────────────────────────────────────────── */ export function startBlobyAgentQuery( conversationId: string, prompt: string, model: string, onMessage: OnAgentMessage, attachments?: AgentAttachment[], savedFiles?: SavedFile[], names?: { botName: string; humanName: string }, recentMessages?: RecentMessage[], supportPrompt?: string, maxTurns?: number, ): Promise { return activeHarness().startBlobyAgentQuery( conversationId, prompt, model, onMessage, attachments, savedFiles, names, recentMessages, supportPrompt, maxTurns, ); } export function stopBlobyAgentQuery(conversationId: string): void { for (const h of Object.values(HARNESSES)) h.stopBlobyAgentQuery(conversationId); } /* ── Workspace agent endpoint ──────────────────────────────────────────── */ export function runAgentQuery(req: AgentQueryRequest): Promise { return activeHarness().runAgentQuery(req); }