export type AgentStatusValue = "pending" | "running" | "completed" | "failed"; export interface AgentInfo { story_id: string; agent_name: string; status: AgentStatusValue; session_id: string | null; worktree_path: string | null; base_branch: string | null; log_session_id: string | null; } export interface AgentEvent { type: | "status" | "output" | "thinking" | "agent_json" | "done" | "error" | "warning"; story_id?: string; agent_name?: string; status?: string; text?: string; data?: unknown; session_id?: string | null; message?: string; } export interface AgentConfigInfo { name: string; role: string; model: string | null; allowed_tools: string[] | null; max_turns: number | null; max_budget_usd: number | null; } const DEFAULT_API_BASE = "/api"; function buildApiUrl(path: string, baseUrl = DEFAULT_API_BASE): string { return `${baseUrl}${path}`; } async function requestJson( path: string, options: RequestInit = {}, baseUrl = DEFAULT_API_BASE, ): Promise { const res = await fetch(buildApiUrl(path, baseUrl), { headers: { "Content-Type": "application/json", ...(options.headers ?? {}), }, ...options, }); if (!res.ok) { const text = await res.text(); throw new Error(text || `Request failed (${res.status})`); } return res.json() as Promise; } export const agentsApi = { startAgent(storyId: string, agentName?: string, baseUrl?: string) { return requestJson( "/agents/start", { method: "POST", body: JSON.stringify({ story_id: storyId, agent_name: agentName, }), }, baseUrl, ); }, stopAgent(storyId: string, agentName: string, baseUrl?: string) { return requestJson( "/agents/stop", { method: "POST", body: JSON.stringify({ story_id: storyId, agent_name: agentName, }), }, baseUrl, ); }, listAgents(baseUrl?: string) { return requestJson("/agents", {}, baseUrl); }, getAgentConfig(baseUrl?: string) { return requestJson("/agents/config", {}, baseUrl); }, reloadConfig(baseUrl?: string) { return requestJson( "/agents/config/reload", { method: "POST" }, baseUrl, ); }, getAgentOutput(storyId: string, agentName: string, baseUrl?: string) { return requestJson<{ output: string }>( `/agents/${encodeURIComponent(storyId)}/${encodeURIComponent(agentName)}/output`, {}, baseUrl, ); }, }; /** * Subscribe to SSE events for a running agent. * Returns a cleanup function to close the connection. */ export function subscribeAgentStream( storyId: string, agentName: string, onEvent: (event: AgentEvent) => void, onError?: (error: Event) => void, ): () => void { const url = `/agents/${encodeURIComponent(storyId)}/${encodeURIComponent(agentName)}/stream`; const eventSource = new EventSource(url); eventSource.onmessage = (e) => { try { const data = JSON.parse(e.data) as AgentEvent; onEvent(data); // Close on terminal events if ( data.type === "done" || data.type === "error" || (data.type === "status" && data.status === "stopped") ) { eventSource.close(); } } catch (err) { console.error("Failed to parse agent event:", err); } }; eventSource.onerror = (e) => { onError?.(e); eventSource.close(); }; return () => { eventSource.close(); }; }