// ============================================================
// Quay — Agent Runner v3
// Parallel ensemble + Query-Type Presets + Cost Tracking + Persistent Memory
//
// All vault-sourced improvements wired in:
// - P0: Confidence Voting (parallel ensemble)
// - P1: Query-Type Presets (auto-routing)
// - P2: Cost-Per-Query Tracking
// - P3: Persistent Agent Memory (ReasoningBank)
// ============================================================

import { v4 as uuidv4 } from 'uuid';
import { dbq } from '../db/index.js';
import { sseBroadcaster } from '../sse/index.js';
import { mcpRegistry } from '../mcp/index.js';
import { memoryTree } from '../memory/memoryTree.js';
import type { MCPToolResult } from '../mcp/index.js';

// ----------------------------------------------------------------
// Import Features
// ----------------------------------------------------------------
import {
  createVerificationRequest,
  type VerificationConfig,
} from '../features/verification.js';

import {
  createSnapshot,
} from '../features/determinism.js';

import {
  updateHealthStatus,
  detectFailure,
  getRecoveryStrategy,
} from '../features/selfHealing.js';

import {
  buildGroundedPrompt,
  type KnowledgeBaseConfig,
} from '../features/agentRAG.js';

// ----------------------------------------------------------------
// Import Routing & Memory (from vault)
// ----------------------------------------------------------------
import {
  classifyQuery,
  selectModelForQuery,
  QUERY_PRESETS,
  MODEL_PROFILES,
  costTracker,
  type CostTracker,
} from '../../lib/routing.js';

import {
  PersistentMemory,
  persistentMemory,
  type Trajectory,
  type MemoryBlock,
} from '../../lib/memory.js';

// ----------------------------------------------------------------
// Import Ensemble (THE KEY DIFFERENTIATOR)
// ----------------------------------------------------------------
import {
  EnsembleOrchestrator,
  type EnsembleConfig,
} from '../../lib/ensemble.js';

import {
  MultiRoundDialogOptimizer,
} from '../../lib/dialog.js';

// ----------------------------------------------------------------
// LLM Response
// ----------------------------------------------------------------

export interface LLMResponse {
  thought: string;
  toolCalls: Array<{ name: string; arguments: Record<string, unknown> }>;
  isDone: boolean;
  finalAnswer?: string;
  usage?: { inputTokens: number; outputTokens: number };
  ensembleInfo?: {
    winner: string;
    confidence: number;
    totalCost: number;
    modelsCalled: number;
  };
}

// ----------------------------------------------------------------
// Model Pricing
// ----------------------------------------------------------------

const MODEL_PRICING: Record<string, { input: number; output: number }> = {
  'claude-3-5-sonnet-latest': { input: 3, output: 15 },
  'claude-sonnet-4-20250514': { input: 3, output: 15 },
  'claude-3-5-haiku-latest': { input: 0.8, output: 4 },
  'gpt-4o': { input: 2.5, output: 10 },
  'gpt-4o-mini': { input: 0.15, output: 0.6 },
  'gpt-4-turbo': { input: 10, output: 30 },
  'gpt-4': { input: 30, output: 60 },
  'llama-3.3-70b-versatile': { input: 0.2, output: 0.2 },
  'llama-3.1-8b-instant': { input: 0.05, output: 0.08 },
  'mixtral-8x7b-32768': { input: 0.24, output: 0.24 },
  'llama3': { input: 0, output: 0 },
  'llama3.1': { input: 0, output: 0 },
  'codellama': { input: 0, output: 0 },
  'qwen2.5-coder': { input: 0, output: 0 },
  'qwen2.5': { input: 0, output: 0 },
  'gemini-2.5-flash': { input: 0.1, output: 0.4 },
  'gemini-1.5-pro': { input: 1.25, output: 5 },
  'glm-4': { input: 0.1, output: 0.3 },
  'glm-4-flash': { input: 0.0, output: 0.0 },
  'glm-5': { input: 0.5, output: 1.5 },
};

function modelPrice(model: string): { input: number; output: number } {
  return MODEL_PRICING[model] ?? { input: 0, output: 0 };
}

function calculateCost(model: string, tokensIn: number, tokensOut: number): number {
  const p = modelPrice(model);
  return (tokensIn * p.input + tokensOut * p.output) / 1_000_000;
}

// ----------------------------------------------------------------
// Sandbox Execution
// ----------------------------------------------------------------

interface SandboxExecResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

async function execInSandbox(sandboxId: string, command: string): Promise<SandboxExecResult> {
  const [sandbox] = dbq.select<{ id: string; endpoint: string | null; status: string }>('sandboxes', { id: sandboxId });
  if (!sandbox) throw new Error(`Sandbox not found: ${sandboxId}`);
  if (!sandbox.endpoint) throw new Error(`Sandbox endpoint not set: ${sandboxId}`);

  const res = await fetch(`${sandbox.endpoint}/exec`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ command, timeout: 300 }),
  });
  if (!res.ok) throw new Error(`Sandbox exec failed: ${res.status} ${await res.text()}`);
  return res.json() as Promise<SandboxExecResult>;
}

// ----------------------------------------------------------------
// Default Ensemble Configs
// ----------------------------------------------------------------

const DEFAULT_ENSEMBLE: EnsembleConfig = {
  models: [
    { provider: 'openai', model: 'gpt-4o-mini', weight: 0.4 },
    { provider: 'anthropic', model: 'claude-3-haiku', weight: 0.3 },
    { provider: 'google', model: 'gemini-2.5-flash', weight: 0.3 },
  ],
  votingStrategy: 'confidence',
  mergeStrategy: 'best',
  maxTokens: 4096,
  timeoutMs: 30000,
};

const QUALITY_ENSEMBLE: EnsembleConfig = {
  models: [
    { provider: 'openai', model: 'gpt-4o', weight: 0.4 },
    { provider: 'anthropic', model: 'claude-3-5-sonnet', weight: 0.4 },
    { provider: 'google', model: 'gemini-2.5-flash', weight: 0.2 },
  ],
  votingStrategy: 'shapley',
  mergeStrategy: 'best',
  maxTokens: 4096,
  timeoutMs: 45000,
};

const CODE_ENSEMBLE: EnsembleConfig = {
  models: [
    { provider: 'openai', model: 'gpt-4o-mini', weight: 0.5 },
    { provider: 'anthropic', model: 'claude-3-5-sonnet', weight: 0.5 },
  ],
  votingStrategy: 'confidence',
  mergeStrategy: 'best',
  maxTokens: 4096,
  timeoutMs: 30000,
};

// ----------------------------------------------------------------
// Agent Runner v3 - Fully Wired
// ----------------------------------------------------------------

export class AgentRunner {
  maxSteps = 20;
  dialogOptimizer = new MultiRoundDialogOptimizer();
  memory: PersistentMemory;

  // Feature configs
  verificationConfig: VerificationConfig = {
    enabled: true,
    approvalThreshold: 'critical',
    criticalActions: ['deploy', 'delete', 'rm', 'drop', 'truncate', 'shutdown', 'restart'],
    autoApproveBelow: 0.05,
    useA3MRouting: true,
  };

  knowledgeBaseConfig: KnowledgeBaseConfig = {
    defaultThreshold: 0.7,
    maxChunks: 5,
    embeddingModel: 'text-embedding-3-small',
  };

  constructor() {
    this.memory = persistentMemory;
  }

  /**
   * Run agent with all vault improvements wired in
   */
  async run(
    task: { id: string; title: string; description?: string },
    agent: { id: string; name: string; type: string; provider: string; model: string; config: string },
    runId: string,
    instructions: string,
    allowedTools: string[],
    options?: {
      knowledgeBaseId?: string;
      verificationConfig?: Partial<VerificationConfig>;
      maxSteps?: number;
      useEnsemble?: boolean;
      ensembleConfig?: EnsembleConfig;
      conversationId?: string;
      useMemory?: boolean;
      budgetCap?: number;
    },
  ): Promise<{ success: boolean; error?: string; cost: number; tokensIn: number; tokensOut: number }> {
    const startTime = Date.now();
    let totalCost = 0;
    let totalTokensIn = 0;
    let totalTokensOut = 0;
    let stepIndex = 0;
    const maxSteps = options?.maxSteps ?? this.maxSteps;
    const conversationId = options?.conversationId ?? runId;

    // Classify query type (P1: Query-Type Presets)
    const queryType = classifyQuery(task.title + ' ' + (task.description || ''));
    const queryPreset = QUERY_PRESETS[queryType] || QUERY_PRESETS.general;

    // Select optimal model based on query type (P1)
    const { model: selectedModel } = selectModelForQuery(task.title + ' ' + (task.description || ''), {
      budgetCap: options?.budgetCap,
    });

    // Merge configs
    const verificationConfig = { ...this.verificationConfig, ...options?.verificationConfig };

    // Parse agent config
    let agentConfig: Record<string, unknown> = {};
    try { agentConfig = JSON.parse(agent.config); } catch { /* use defaults */ }

    // Build system prompt
    const systemPrompt = `You are ${agent.name}, a ${agent.type} agent.\n\n${instructions}\n\nYou have access to these tools. Use them to complete the task.\n\nWhen you use a tool, respond ONLY with:\n<tool_call>\n<name>tool_name</name>\n<args>{"key": "value"}</args>\n</tool_call>\n\n1. Tool calls in <tool_call><name>tool</name><args>{"key":"value"}</args></tool_call>\n3. When done: <done>your_answer</done>`;

    let currentPrompt = `Task: ${task.title}\n\n${task.description ?? ''}`;

    // Apply RAG grounding
    if (options?.knowledgeBaseId) {
      currentPrompt = await buildGroundedPrompt(options.knowledgeBaseId, currentPrompt, this.knowledgeBaseConfig);
    }

    // Enhance with persistent memory (P3)
    if (options?.useMemory !== false) {
      const memoryContext = this.memory.buildContext(agent.id, currentPrompt);
      if (memoryContext) {
        currentPrompt = memoryContext + '\n\n' + currentPrompt;
      }
    }

    // Enhance with dialog context
    currentPrompt = this.dialogOptimizer.buildEnhancedContext(conversationId) + '\n\n' + currentPrompt;

    // Initialize health
    updateHealthStatus(agent.id, { success: true, latencyMs: 0 });

    // Create ensemble orchestrator (P0)
    const useEnsemble = options?.useEnsemble ?? true;
    const ensembleConfig = options?.ensembleConfig ?? this.getEnsembleForTask(task.title, agent.type);
    const ensemble = new EnsembleOrchestrator(ensembleConfig);

    // Get API keys and base URLs
    const apiKeys = this.getApiKeys();
    const baseURLs = this.getBaseURLs();

    // Track trajectory for memory (P3)
    const trajectorySteps: Array<{
      step: number;
      action: string;
      observation: string;
      thought: string;
      success: boolean;
    }> = [];

    while (stepIndex < maxSteps) {
      const stepStartTime = Date.now();
      let llmResponse: LLMResponse;

      try {
        if (useEnsemble) {
          // PARALLEL ENSEMBLE EXECUTION (P0: THE KEY DIFFERENTIATOR)
          llmResponse = await this.runEnsemble(ensemble, currentPrompt, systemPrompt, apiKeys, baseURLs);
        } else {
          // Single model with query-type routing
          llmResponse = await this.callSingleModel(
            currentPrompt,
            systemPrompt,
            selectedModel.provider,
            selectedModel.model,
            agentConfig
          );
        }
      } catch (e) {
        // Self-healing: detect failure and apply recovery
        const failure = detectFailure(e as Error);
        const strategy = getRecoveryStrategy(failure.type);

        updateHealthStatus(agent.id, { error: true });
        this.recordFailureSync(runId, stepIndex, String(e));

        // Track failed trajectory (P3)
        if (options?.useMemory !== false) {
          trajectorySteps.push({
            step: stepIndex,
            action: 'error',
            observation: String(e),
            thought: llmResponse?.thought || '',
            success: false,
          });
        }

        // Record cost (P2)
        costTracker.record({
          model: selectedModel.model,
          provider: selectedModel.provider,
          queryType,
          tokensIn: totalTokensIn,
          tokensOut: totalTokensOut,
          costUSD: totalCost,
          latencyMs: Date.now() - stepStartTime,
          success: false,
        });

        if (strategy) {
          sseBroadcaster.broadcast('agent:recovery', {
            agentId: agent.id,
            failure: failure.type,
            strategy: strategy.action,
            runId,
          });
        }

        return { success: false, error: String(e), cost: totalCost, tokensIn: totalTokensIn, tokensOut: totalTokensOut };
      }

      // Track cost (P2)
      const stepCost = llmResponse.ensembleInfo?.totalCost ?? calculateCost(
        selectedModel.model,
        llmResponse.usage?.inputTokens ?? 0,
        llmResponse.usage?.outputTokens ?? 0
      );
      totalCost += stepCost;
      totalTokensIn += llmResponse.usage?.inputTokens ?? 0;
      totalTokensOut += llmResponse.usage?.outputTokens ?? 0;

      // Record cost to tracker (P2)
      costTracker.record({
        model: selectedModel.model,
        provider: selectedModel.provider,
        queryType,
        tokensIn: llmResponse.usage?.inputTokens ?? 0,
        tokensOut: llmResponse.usage?.outputTokens ?? 0,
        costUSD: stepCost,
        latencyMs: Date.now() - stepStartTime,
        success: true,
      });

      // Update health
      updateHealthStatus(agent.id, { success: true, latencyMs: Date.now() - stepStartTime });

      // Add dialog turn
      this.dialogOptimizer.addTurn(conversationId, 'assistant', llmResponse.thought, selectedModel.model);

      // Track trajectory step (P3)
      trajectorySteps.push({
        step: stepIndex,
        action: llmResponse.toolCalls.map(tc => tc.name).join(', ') || 'done',
        observation: '',
        thought: llmResponse.thought,
        success: true,
      });

      // Create deterministic snapshot
      const snapshot = createSnapshot(runId, stepIndex, {
        prompt: currentPrompt,
        context: {
          taskId: task.id,
          agentId: agent.id,
          knowledgeBaseId: options?.knowledgeBaseId,
          queryType,  // P1: Track query type
          ensembleInfo: llmResponse.ensembleInfo,
        },
        memory: memoryTree.getRecentMemories(runId),
        thought: llmResponse.thought,
        toolCalls: llmResponse.toolCalls,
        tokensIn: llmResponse.usage?.inputTokens ?? 0,
        tokensOut: llmResponse.usage?.outputTokens ?? 0,
        costUSD: totalCost,
        latencyMs: Date.now() - stepStartTime,
      });

      // Broadcast progress
      sseBroadcaster.broadcast('run:progress', {
        runId,
        stepIndex,
        thought: llmResponse.thought,
        snapshotId: snapshot.id,
        queryType,  // P1
        ensembleInfo: llmResponse.ensembleInfo,
      });

      if (llmResponse.isDone) {
        // Store trajectory in memory (P3)
        if (options?.useMemory !== false) {
          const trajectory: Trajectory = {
            id: runId,
            agentId: agent.id,
            taskId: task.id,
            query: task.title,
            steps: trajectorySteps,
            finalResult: llmResponse.finalAnswer || llmResponse.thought,
            success: true,
            costUSD: totalCost,
            durationMs: Date.now() - startTime,
            quality: 0.9,
            createdAt: startTime,
          };
          this.memory.storeTrajectory(trajectory);
        }

        dbq.update('runs', {
          state: 'DONE',
          completed_at: Date.now(),
          cost: totalCost,
          tokens_in: totalTokensIn,
          tokens_out: totalTokensOut,
        }, { id: runId });

        await memoryTree.updateAgentLongTerm(agent.id, { id: runId } as any, true);

        sseBroadcaster.broadcast('run:completed', {
          runId,
          cost: totalCost,
          durationMs: Date.now() - startTime,
          totalSteps: stepIndex + 1,
          queryType,  // P1
          ensembleInfo: llmResponse.ensembleInfo,
        });

        return { success: true, cost: totalCost, tokensIn: totalTokensIn, tokensOut: totalTokensOut };
      }

      // Execute tool calls
      for (const tc of llmResponse.toolCalls) {
        const toolName = tc.name.toLowerCase();
        const isCritical = verificationConfig.criticalActions.some(
          action => toolName.includes(action.toLowerCase())
        );

        // Human-in-Loop Verification
        if (isCritical && verificationConfig.enabled) {
          const estimatedCost = totalCost * 0.1;
          const verificationReq = await createVerificationRequest(
            agent.id,
            task.id,
            tc.name,
            `Executing ${tc.name} with args: ${JSON.stringify(tc.arguments)}`,
            estimatedCost,
            verificationConfig
          );

          if (verificationReq) {
            sseBroadcaster.broadcast('verification:pending', {
              requestId: verificationReq.id,
              agentId: agent.id,
              runId,
              action: tc.name,
              args: tc.arguments,
            });

            sseBroadcaster.broadcast('agent:paused', {
              runId,
              stepIndex,
              reason: 'verification_pending',
              requestId: verificationReq.id,
            });

            currentPrompt += `\n\n[${tc.name}] → VERIFICATION REQUIRED: Action paused for human approval. Request ID: ${verificationReq.id}`;
            continue;
          }
        }

        const toolResult = await this.executeTool(tc.name, tc.arguments);
        const resultStr = JSON.stringify(toolResult);
        currentPrompt += `\n\n[${tc.name}] → ${resultStr}`;

        // Update snapshot with observation
        snapshot.context = {
          ...snapshot.context,
          lastObservation: resultStr,
          lastToolCall: tc.name,
        };

        // Check for tool failure
        if (!toolResult.success && toolResult.error) {
          updateHealthStatus(agent.id, { error: true });

          trajectorySteps[trajectorySteps.length - 1].success = false;
          trajectorySteps[trajectorySteps.length - 1].observation = resultStr;

          const failure = detectFailure(new Error(toolResult.error));
          const strategy = getRecoveryStrategy(failure.type);

          if (strategy) {
            sseBroadcaster.broadcast('agent:recovery', {
              agentId: agent.id,
              failure: failure.type,
              strategy: strategy.action,
              runId,
              stepIndex,
            });
          }
        }

        dbq.update('step_attempts', {
          action: tc.name,
          action_args: JSON.stringify(tc.arguments),
          observation: resultStr,
        }, { id: snapshot.id });
      }

      stepIndex++;
    }

    // Store failed trajectory (P3)
    if (options?.useMemory !== false) {
      const trajectory: Trajectory = {
        id: runId,
        agentId: agent.id,
        taskId: task.id,
        query: task.title,
        steps: trajectorySteps,
        finalResult: 'Max steps exceeded',
        success: false,
        costUSD: totalCost,
        durationMs: Date.now() - startTime,
        quality: 0.3,
        createdAt: startTime,
      };
      this.memory.storeTrajectory(trajectory);
    }

    const err = 'Max steps exceeded';
    this.recordFailureSync(runId, stepIndex, err);
    updateHealthStatus(agent.id, { error: true });
    return { success: false, error: err, cost: totalCost, tokensIn: totalTokensIn, tokensOut: totalTokensOut };
  }

  /**
   * Run ensemble: call multiple models in PARALLEL (P0)
   */
  private async runEnsemble(
    ensemble: EnsembleOrchestrator,
    prompt: string,
    systemPrompt: string,
    apiKeys: Record<string, string>,
    baseURLs: Record<string, string>
  ): Promise<LLMResponse> {
    const responses = await ensemble.executeEnsemble(prompt, systemPrompt, apiKeys, baseURLs);

    if (responses.length === 0) {
      throw new Error('All ensemble models failed');
    }

    const votingResult = ensemble.vote(responses);

    const content = votingResult.content;
    const toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
    const toolRegex = /<tool_call>\s*<name>([\w:]+)<\/name>\s*<args>([\s\S]*?)<\/args>\s*<\/tool_call>/g;
    let match;
    while ((match = toolRegex.exec(content)) !== null) {
      try { toolCalls.push({ name: match[1], arguments: JSON.parse(match[2]) }); } catch { /* skip */ }
    }

    const isDone = content.includes('<done>') || toolCalls.length === 0;
    const thought = content.split('<tool_call>')[0].trim();

    const totalTokensIn = responses.reduce((sum, r) => sum + r.tokensIn, 0);
    const totalTokensOut = responses.reduce((sum, r) => sum + r.tokensOut, 0);

    return {
      thought,
      toolCalls,
      isDone,
      finalAnswer: isDone ? content : undefined,
      usage: {
        inputTokens: totalTokensIn,
        outputTokens: totalTokensOut,
      },
      ensembleInfo: {
        winner: votingResult.winner,
        confidence: votingResult.confidence,
        totalCost: votingResult.totalCost,
        modelsCalled: responses.length,
      },
    };
  }

  /**
   * Call single model with query-type routing
   */
  private async callSingleModel(
    prompt: string,
    systemPrompt: string,
    provider: string,
    model: string,
    agentConfig: Record<string, unknown>
  ): Promise<LLMResponse> {
    const apiKeys = this.getApiKeys();
    const baseURLs = this.getBaseURLs();

    const config: LLMConfig = {
      provider,
      model,
      apiKey: apiKeys[provider],
      baseURL: baseURLs[provider] ?? agentConfig.baseURL as string | undefined ?? process.env.QUAY_DEFAULT_LLM_URL,
      temperature: agentConfig.temperature as number | undefined,
    };

    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
    };

    const body = {
      model: config.model,
      messages: [
        { role: 'system', content: systemPrompt },
        { role: 'user', content: prompt },
      ],
      max_tokens: 4096,
      temperature: config.temperature ?? 0.2,
    };

    const url = `${config.baseURL}/v1/chat/completions`;
    const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
    if (!res.ok) throw new Error(`LLM ${res.status}: ${await res.text()}`);

    const data = await res.json() as {
      choices: Array<{ message: { content: string } }>;
      usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number };
    };
    const content = data.choices[0]?.message?.content ?? '';

    const toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
    const toolRegex = /<tool_call>\s*<name>([\w:]+)<\/name>\s*<args>([\s\S]*?)<\/args>\s*<\/tool_call>/g;
    let match;
    while ((match = toolRegex.exec(content)) !== null) {
      try { toolCalls.push({ name: match[1], arguments: JSON.parse(match[2]) }); } catch { /* skip */ }
    }

    const isDone = content.includes('<done>') || toolCalls.length === 0;
    const thought = content.split('<tool_call>')[0].trim();

    return {
      thought,
      toolCalls,
      isDone,
      finalAnswer: isDone ? content : undefined,
      usage: data.usage ? {
        inputTokens: data.usage.prompt_tokens ?? 0,
        outputTokens: data.usage.completion_tokens ?? 0,
      } : undefined,
    };
  }

  private getApiKeys(): Record<string, string> {
    return {
      openai: process.env.OPENAI_API_KEY || '',
      anthropic: process.env.ANTHROPIC_API_KEY || '',
      google: process.env.GOOGLE_API_KEY || '',
      cerebras: process.env.CEREBRAS_API_KEY || '',
      groq: process.env.GROQ_API_KEY || '',
      together: process.env.TOGETHER_API_KEY || '',
      openrouter: process.env.OPENROUTER_API_KEY || '',
    };
  }

  private getBaseURLs(): Record<string, string> {
    return {
      openai: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
      anthropic: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
      google: process.env.GOOGLE_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta',
      cerebras: process.env.CEREBRAS_BASE_URL || 'https://api.cerebras.ai/v1',
      groq: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1',
      together: process.env.TOGETHER_BASE_URL || 'https://api.together.xyz/v1',
      openrouter: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
    };
  }

  private getEnsembleForTask(taskTitle: string, agentType: string): EnsembleConfig {
    const lower = taskTitle.toLowerCase();
    
    if (lower.includes('code') || lower.includes('debug') || agentType === 'coder') {
      return CODE_ENSEMBLE;
    }
    
    if (lower.includes('critical') || lower.includes('important') || lower.includes('security')) {
      return QUALITY_ENSEMBLE;
    }
    
    return DEFAULT_ENSEMBLE;
  }

  private async executeTool(
    name: string,
    args: Record<string, unknown>,
  ): Promise<MCPToolResult> {
    if (name === 'bash' || name === 'shell') {
      const cmd = (args.command ?? args.cmd ?? '') as string;
      const sandboxId = args.sandboxId as string;
      if (!sandboxId) return { tool: name, success: false, error: 'No sandboxId' };
      try {
        const result = await execInSandbox(sandboxId, cmd);
        return { tool: name, success: result.exitCode === 0, result };
      } catch (e) {
        return { tool: name, success: false, error: String(e) };
      }
    }
    return mcpRegistry.routeTool(name, args);
  }

  private recordFailureSync(runId: string, stepIndex: number, error: string): void {
    dbq.update('runs', { state: 'FAILED', completed_at: Date.now(), error }, { id: runId });
    dbq.insert('step_attempts', {
      id: uuidv4(), run_id: runId, step_index: stepIndex,
      thought: null, action: null, action_args: '{}', observation: null,
      error, started_at: Date.now(), completed_at: null,
    });
    sseBroadcaster.broadcast('run:failed', { runId, error, stepIndex });
  }
}

export const agentRunner = new AgentRunner();
