import { z } from "zod"; import { AgentName, AutonomyLevel, ModelProfile } from "../types/config.js"; import { AgentContext } from "../agents/base.js"; import { Decision } from "../types/decisions.js"; import { Escalation } from "../types/escalation.js"; export type BackendType = "llm" | "agent"; export const ArtifactSchema = z.object({ path: z.string().min(1, "Artifact path must not be empty"), content: z.string(), operation: z.enum(["create", "update", "delete"]), }); export const TokenUsageSchema = z.object({ input_tokens: z.number().min(0), output_tokens: z.number().min(0), total_tokens: z.number().min(0), estimated_cost_usd: z.number().min(0), }); export const BackendResultSchema = z.object({ success: z.boolean(), output: z.string(), artifacts: z.array(ArtifactSchema), decisions: z.array(z.unknown()), escalations: z.array(z.unknown()), usage: TokenUsageSchema, error: z.string().optional(), }).refine( (r) => !(r.success === true && r.error && r.error.length > 0), { message: "Result cannot be both success and have an error message" } ); export function validateBackendResult(raw: unknown): { result: BackendResult | null; errors: string[] } { const parseResult = BackendResultSchema.safeParse(raw); if (!parseResult.success) { return { result: null, errors: parseResult.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`), }; } const data = parseResult.data; if (!Array.isArray(data.artifacts)) { return { result: null, errors: ["artifacts: expected array"] }; } for (const a of data.artifacts) { if (a.path.includes("..")) { return { result: null, errors: [`artifacts: path "${a.path}" contains ".." (path traversal risk)`] }; } if (a.path.startsWith("/")) { return { result: null, errors: [`artifacts: path "${a.path}" is absolute (must be relative)`] }; } } return { result: data as BackendResult, errors: [] }; } export interface BackendRequest { persona: AgentName; workflow: string; task: string; context: AgentContext; autonomy: AutonomyLevel; } export interface Artifact { path: string; content: string; operation: "create" | "update" | "delete"; } export interface TokenUsage { input_tokens: number; output_tokens: number; total_tokens: number; estimated_cost_usd: number; } export interface BackendResult { success: boolean; output: string; artifacts: Artifact[]; decisions: Decision[]; escalations: Escalation[]; usage: TokenUsage; error?: string; } export interface IntelligenceBackend { readonly name: string; readonly type: BackendType; isAvailable(): Promise; execute(request: BackendRequest): Promise; } export interface LLMBackendConfig { base_url: string; model_profile: ModelProfile; model?: string; timeout_ms?: number; } export interface OllamaLocalConfig extends LLMBackendConfig { base_url: string; model_profile: ModelProfile; model?: string; timeout_ms?: number; } export interface OllamaCloudConfig extends LLMBackendConfig { base_url: string; api_key_env: string; model_profile: ModelProfile; model?: string; timeout_ms?: number; } export interface OpenAIConfig extends LLMBackendConfig { api_key_env: string; model: string; organization?: string; } export interface AnthropicConfig extends LLMBackendConfig { api_key_env: string; model: string; api_version?: string; } export interface OpencodeBackendConfig { enabled: boolean; executable?: string; } export interface BackendConfigSection { provider: "auto" | "opencode" | "openai" | "ollama-local" | "ollama-cloud" | "anthropic"; fallback?: "opencode" | "openai" | "ollama-local" | "ollama-cloud" | "anthropic"; agent_backends: { opencode?: OpencodeBackendConfig; }; llm_backends: { "openai"?: OpenAIConfig; "ollama-local"?: OllamaLocalConfig; "ollama-cloud"?: OllamaCloudConfig; "anthropic"?: AnthropicConfig; }; } export const DEFAULT_BACKEND_CONFIG: BackendConfigSection = { provider: "auto", agent_backends: { opencode: { enabled: true }, }, llm_backends: { "openai": { base_url: "https://api.openai.com/v1", api_key_env: "OPENAI_API_KEY", model: "gpt-4o", model_profile: "quality", timeout_ms: 60000, }, "ollama-local": { base_url: "http://localhost:11434", model_profile: "balanced", }, "ollama-cloud": { base_url: "", api_key_env: "OLLAMA_CLOUD_API_KEY", model_profile: "quality", timeout_ms: 60000, }, "anthropic": { base_url: "https://api.anthropic.com", api_key_env: "ANTHROPIC_API_KEY", model: "claude-sonnet-4-20250514", api_version: "2023-06-01", model_profile: "quality", timeout_ms: 60000, }, }, }; export class BackendUnavailableError extends Error { readonly backendName: string; readonly agentName?: string; constructor(backendName: string, agentName?: string) { const agentMsg = agentName ? ` (agent: ${agentName})` : ""; super( `Intelligence backend "${backendName}" is not available${agentMsg}. ` + `Configure one of:\n` + ` 1. Install opencode: npm i -g opencode\n` + ` 2. Set OPENAI_API_KEY for OpenAI API access\n` + ` 3. Set ANTHROPIC_API_KEY for Anthropic API access\n` + ` 4. Run Ollama locally: ollama serve\n` + ` 5. Set OLLAMA_CLOUD_API_KEY for remote inference` ); this.name = "BackendUnavailableError"; this.backendName = backendName; this.agentName = agentName; } } export function emptyTokenUsage(): TokenUsage { return { input_tokens: 0, output_tokens: 0, total_tokens: 0, estimated_cost_usd: 0 }; } export function emptyBackendResult(error?: string): BackendResult { return { success: false, output: "", artifacts: [], decisions: [], escalations: [], usage: emptyTokenUsage(), error, }; } export { ChatMessage, ChatCompletionResponse } from "./llm-base.js";