/** * Harness — provider-agnostic agent runtime contract. * * Each AI provider that ships an "agent" experience (Claude Agent SDK, * Codex app-server, …) implements this interface. The dispatcher in * `supervisor/bloby-agent.ts` selects which one to use based on * `cfg.ai.provider` so the rest of the supervisor never needs to care. * * The two execution shapes mirror what the supervisor already needs: * • startConversation / pushMessage / endConversation — long-lived chat * with streaming events. Used by the dashboard and admin WhatsApp. * • startBlobyAgentQuery / stopBlobyAgentQuery — one-shot request/response * used by customer WhatsApp and the scheduler. * * Event names emitted via the `onMessage` callback are the same vocabulary * `supervisor/index.ts` and `channels/manager.ts` already consume — every * harness translates its native events into this set: * bot:typing, bot:token, bot:tool, bot:response, bot:turn-complete, * bot:done, bot:error, bot:task-created, bot:task-progress, bot:task-done, * bot:conversation-ended. */ import type { SavedFile } from '../file-saver.js'; export interface RecentMessage { role: 'user' | 'assistant'; content: string; } export interface AgentAttachment { type: 'image' | 'file'; name: string; mediaType: string; data: string; // base64 } export type OnAgentMessage = (type: string, data: any) => void; export interface Harness { /* ── Live conversation API ── */ startConversation( conversationId: string, model: string, onMessage: OnAgentMessage, names?: { botName: string; humanName: string }, recentMessages?: RecentMessage[], ): Promise; pushMessage( conversationId: string, content: string, attachments?: AgentAttachment[], savedFiles?: SavedFile[], ): boolean; hasConversation(conversationId: string): boolean; endConversation(conversationId: string): void; endAllConversations(): void; isConversationBusy(conversationId: string): boolean; /** True if ANY conversation in this harness is mid-turn (no id — used to defer backend restarts). */ anyConversationBusy(): boolean; /** True if ANY one-shot query (startBlobyAgentQuery: pulse/cron, customer WhatsApp) is in flight. * These do NOT register as live conversations, so anyConversationBusy() can't see them — the * supervisor ORs this in so a queued self-update / backend restart defers past one-shot turns too. */ anyOneShotActive(): boolean; stopSubAgentTask(conversationId: string, taskId: string): Promise; warmUpForLiveConversation( model: string, names?: { botName: string; humanName: string }, ): Promise; /* ── One-shot API (request/response) ── */ 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; stopBlobyAgentQuery(conversationId: string): void; /* ── Workspace agent endpoint (POST /api/agent/query) ── */ runAgentQuery(req: AgentQueryRequest): Promise; } export interface AgentQueryRequest { message: string; /** Already-resolved system prompt content (not a path). Empty/omitted → use the harness's default coding-agent prompt. */ systemPrompt?: string; /** Provider-specific session id to resume (Claude: SDK session_id; Codex: threadId). */ sessionId?: string; /** Max turns (Claude only — Codex has no equivalent). */ maxTurns?: number; /** Hard timeout in ms. */ timeout?: number; } export interface AgentQueryResult { ok: boolean; response?: string; /** Provider-specific session id the caller can pass back to resume this conversation. */ sessionId?: string; toolsUsed?: string[]; usedFileTools?: boolean; error?: string; }