feat(backends): multi-backend intelligence layer — LLM + Agent backends, persona-loading agents, honest CLI commands

Add IntelligenceBackend abstraction with two categories:
- LLMBackend (OllamaLocal, OllamaCloud): CI runs tool loop, provides tools, constructs prompts
- AgentBackend (Opencode): agent runs own tool loop, CI serializes request

Refactor all 18 agents from hardcoded stubs to persona loaders that delegate
to the active backend or fail honestly when no backend is available.

Refactor OrchestratorAgent.executeStage() from monolithic switch to agent
delegation via STAGE_AGENT_MAP for intelligent stages (research, plan, execute,
verify), with mechanical stages (specify, clarify, complete) staying inline.

Wire CLI commands with --backend flag and auto-detection (opencode →
ollama-local → ollama-cloud). Harden rollback/ship with real git operations.
No command returns fake success.
This commit is contained in:
CI
2026-05-29 15:58:34 +00:00
parent ddf04792c7
commit 940b85bfae
33 changed files with 1828 additions and 100 deletions
+137
View File
@@ -0,0 +1,137 @@
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 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<boolean>;
execute(request: BackendRequest): Promise<BackendResult>;
}
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 OpencodeBackendConfig {
enabled: boolean;
executable?: string;
}
export interface BackendConfigSection {
provider: "auto" | "opencode" | "ollama-local" | "ollama-cloud";
fallback?: "opencode" | "ollama-local" | "ollama-cloud";
agent_backends: {
opencode?: OpencodeBackendConfig;
};
llm_backends: {
"ollama-local"?: OllamaLocalConfig;
"ollama-cloud"?: OllamaCloudConfig;
};
}
export const DEFAULT_BACKEND_CONFIG: BackendConfigSection = {
provider: "auto",
agent_backends: {
opencode: { enabled: true },
},
llm_backends: {
"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,
},
},
};
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. Run Ollama locally: ollama serve\n` +
` 3. 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,
};
}