diff --git a/package.json b/package.json
index 1eef290..81db356 100644
--- a/package.json
+++ b/package.json
@@ -37,23 +37,26 @@
   },
   "homepage": "https://github.com/answerlayer/sniffbench#readme",
   "dependencies": {
-    "commander": "^12.0.0",
+    "@anthropic-ai/claude-agent-sdk": "^0.1.61",
+    "@anthropic-ai/claude-code": "^2.0.61",
     "chalk": "^5.3.0",
+    "commander": "^12.0.0",
+    "dockerode": "^4.0.2",
     "ora": "^8.0.0",
     "yaml": "^2.3.4",
-    "dockerode": "^4.0.2"
+    "zod": "^4.1.13"
   },
   "devDependencies": {
-    "@types/node": "^20.10.0",
     "@types/dockerode": "^3.3.0",
+    "@types/jest": "^29.5.0",
+    "@types/node": "^20.10.0",
     "@typescript-eslint/eslint-plugin": "^6.13.0",
     "@typescript-eslint/parser": "^6.13.0",
     "eslint": "^8.54.0",
-    "prettier": "^3.1.0",
-    "typescript": "^5.3.0",
     "jest": "^29.7.0",
-    "@types/jest": "^29.5.0",
-    "ts-jest": "^29.1.0"
+    "prettier": "^3.1.0",
+    "ts-jest": "^29.1.0",
+    "typescript": "^5.3.0"
   },
   "engines": {
     "node": ">=18.0.0"
diff --git a/src/agents/claude-code.ts b/src/agents/claude-code.ts
index 2fd6048..c0f29a6 100644
--- a/src/agents/claude-code.ts
+++ b/src/agents/claude-code.ts
@@ -1,21 +1,54 @@
 /**
- * Claude Code agent wrapper
+ * Claude Code agent wrapper using the official SDK
  *
- * Wraps the Claude Code CLI (`claude`) to run prompts programmatically.
- * Uses the --print (-p) flag for non-interactive single-prompt execution.
+ * Uses @anthropic-ai/claude-agent-sdk for programmatic interaction
+ * with full metrics capture (tokens, cost, tool usage).
  */
 
 import { spawn } from 'child_process';
-import { AgentWrapper, AgentResult, AgentRunOptions } from './types';
+import {
+  AgentWrapper,
+  AgentResult,
+  AgentRunOptions,
+  AgentEvent,
+  ToolCall,
+  emptyAgentResult,
+} from './types.js';
+
+// SDK type imports
+type SDKMessage = import('@anthropic-ai/claude-agent-sdk').SDKMessage;
+type SDKResultMessage = import('@anthropic-ai/claude-agent-sdk').SDKResultMessage;
+type SDKAssistantMessage = import('@anthropic-ai/claude-agent-sdk').SDKAssistantMessage;
+type SDKSystemMessage = import('@anthropic-ai/claude-agent-sdk').SDKSystemMessage;
+type SDKUserMessage = import('@anthropic-ai/claude-agent-sdk').SDKUserMessage;
+type SDKPartialAssistantMessage = import('@anthropic-ai/claude-agent-sdk').SDKPartialAssistantMessage;
+type Options = import('@anthropic-ai/claude-agent-sdk').Options;
 
 /**
- * Claude Code agent wrapper
+ * Type guard for tool_use_result shape (SDK types this as `unknown`)
+ */
+interface ToolUseResult {
+  tool_use_id?: string;
+  content?: string;
+}
+
+function isToolUseResult(value: unknown): value is ToolUseResult {
+  if (!value || typeof value !== 'object') return false;
+  const obj = value as Record<string, unknown>;
+  return (
+    (obj.tool_use_id === undefined || typeof obj.tool_use_id === 'string') &&
+    (obj.content === undefined || typeof obj.content === 'string')
+  );
+}
+
+/**
+ * Claude Code agent wrapper using the official SDK
  */
 export class ClaudeCodeAgent implements AgentWrapper {
   name = 'claude-code';
   displayName = 'Claude Code';
 
-  /** Path to claude CLI (defaults to 'claude' in PATH) */
+  /** Path to claude CLI (for version check) */
   private cliPath: string;
 
   constructor(cliPath: string = 'claude') {
@@ -23,7 +56,7 @@ export class ClaudeCodeAgent implements AgentWrapper {
   }
 
   /**
-   * Check if Claude Code CLI is available
+   * Check if Claude Code is available
    */
   async isAvailable(): Promise<boolean> {
     try {
@@ -63,249 +96,296 @@ export class ClaudeCodeAgent implements AgentWrapper {
   }
 
   /**
-   * Run a prompt through Claude Code
-   *
-   * Uses `claude -p "prompt"` for non-interactive execution.
-   * The agent will explore the codebase and provide an answer.
+   * Run a prompt through Claude Code using the SDK
    */
   async run(prompt: string, options: AgentRunOptions): Promise<AgentResult> {
     const startTime = Date.now();
     const timeoutMs = options.timeoutMs || 300000; // 5 min default
 
-    return new Promise((resolve) => {
-      // Build command args
-      // -p: print mode (non-interactive, single prompt)
-      // --output-format stream-json: get real-time streaming JSON output
-      // --verbose: required for stream-json
-      // --include-partial-messages: stream text as it's generated
-      const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--include-partial-messages'];
-
-      const proc = spawn(this.cliPath, args, {
+    // Track tool calls
+    const toolCalls: ToolCall[] = [];
+    const toolStartTimes: Map<string, number> = new Map();
+    let model = 'unknown';
+    let sessionId = '';
+
+    try {
+      // Dynamic import of ESM SDK
+      const sdk = await import('@anthropic-ai/claude-agent-sdk');
+
+      // Build SDK options
+      // Note: env is passed directly without spreading process.env to avoid leaking secrets.
+      // SDK inherits process.env by default when env is undefined. If caller needs to add
+      // custom vars while preserving the environment, they should explicitly spread process.env.
+      const sdkOptions: Options = {
         cwd: options.cwd,
-        env: {
-          ...process.env,
-          ...options.env,
-          // Ensure non-interactive
-          CI: 'true',
-        },
-        stdio: ['pipe', 'pipe', 'pipe'],
-      });
+        permissionMode: options.permissionMode || 'acceptEdits',
+        allowedTools: options.allowedTools,
+        disallowedTools: options.disallowedTools,
+        maxBudgetUsd: options.maxBudgetUsd,
+        maxTurns: options.maxTurns,
+        model: options.model,
+        includePartialMessages: options.includePartialMessages ?? true,
+        env: options.env,
+        // Don't load user/project settings - isolation mode
+        settingSources: [],
+      };
+
+      // Set up abort controller for timeout
+      const abortController = new AbortController();
+      sdkOptions.abortController = abortController;
+
+      const timeoutId = setTimeout(() => {
+        abortController.abort();
+      }, timeoutMs);
 
-      // Close stdin immediately - claude -p doesn't need interactive input
-      proc.stdin?.end();
+      // Resolve includePartialMessages for consistent use
+      const includePartial = sdkOptions.includePartialMessages ?? true;
 
-      let stdout = '';
-      let stderr = '';
-      let timedOut = false;
-      let finalAnswer = '';
-      let lineBuffer = '';
+      // Run the query
+      const query = sdk.query({ prompt, options: sdkOptions });
 
-      proc.stdout?.on('data', (data) => {
-        const chunk = data.toString();
-        stdout += chunk;
-
-        // Parse streaming JSON - each line is a JSON object
-        lineBuffer += chunk;
-        const lines = lineBuffer.split('\n');
-        lineBuffer = lines.pop() || ''; // Keep incomplete line in buffer
-
-        for (const line of lines) {
-          if (!line.trim()) continue;
-          try {
-            const msg = JSON.parse(line);
-            const displayText = this.extractDisplayText(msg);
-            if (displayText && options.onOutput) {
-              options.onOutput(displayText);
-            }
-            // Capture final answer from result
-            if (msg.type === 'result' && msg.result) {
-              finalAnswer = msg.result;
-            }
-          } catch {
-            // Not valid JSON, output raw
-            options.onOutput?.(line + '\n');
+      let finalResult: SDKResultMessage | null = null;
+
+      try {
+        for await (const message of query) {
+          this.processMessage(message, options, toolCalls, toolStartTimes, includePartial, startTime, (m) => {
+            model = m;
+          }, (s) => {
+            sessionId = s;
+          });
+
+          // Capture result message
+          if (message.type === 'result') {
+            finalResult = message as SDKResultMessage;
           }
         }
-      });
-
-      proc.stderr?.on('data', (data) => {
-        const chunk = data.toString();
-        stderr += chunk;
-      });
+      } finally {
+        clearTimeout(timeoutId);
+      }
 
-      // Handle timeout
-      const timer = setTimeout(() => {
-        timedOut = true;
-        proc.kill('SIGTERM');
-        // Give it a moment to clean up, then force kill
-        setTimeout(() => {
-          proc.kill('SIGKILL');
-        }, 5000);
-      }, timeoutMs);
+      // Build result from SDK response
+      if (finalResult) {
+        const result = this.buildResult(finalResult, toolCalls, model, sessionId);
+        options.onEvent?.({ type: 'complete', result });
+        return result;
+      }
 
-      proc.on('close', (code) => {
-        clearTimeout(timer);
-        const durationMs = Date.now() - startTime;
-
-        // Try to extract tool usage from JSON output
-        const toolsUsed = this.parseToolsUsedFromJson(stdout);
-
-        resolve({
-          answer: finalAnswer || this.parseAnswer(stdout),
-          success: code === 0 && !timedOut,
-          error: timedOut ? 'Timed out' : (code !== 0 ? `Exit code: ${code}` : undefined),
-          timedOut,
-          durationMs,
-          toolsUsed,
-          stdout,
-          stderr,
-          exitCode: code,
-        });
+      // No result message - something went wrong
+      const errorResult = emptyAgentResult('No result received from SDK');
+      errorResult.durationMs = Date.now() - startTime;
+      errorResult.toolCalls = toolCalls;
+      errorResult.toolsUsed = [...new Set(toolCalls.map((t) => t.name))];
+      options.onEvent?.({ type: 'complete', result: errorResult });
+      return errorResult;
+
+    } catch (error) {
+      // Check for AbortError by name (DOMException may not inherit from Error)
+      const errorName = error && typeof error === 'object' && 'name' in error
+        ? (error as { name: unknown }).name
+        : undefined;
+      const isTimeout = errorName === 'AbortError';
+      const errorMessage = error instanceof Error
+        ? error.message
+        : (error && typeof error === 'object' && 'message' in error)
+          ? String((error as { message: unknown }).message)
+          : String(error);
+
+      options.onEvent?.({
+        type: 'error',
+        message: isTimeout ? 'Timed out' : errorMessage,
+        code: isTimeout ? 'TIMEOUT' : 'ERROR',
       });
 
-      proc.on('error', (err) => {
-        clearTimeout(timer);
-        const durationMs = Date.now() - startTime;
-
-        resolve({
-          answer: '',
-          success: false,
-          error: err.message,
-          timedOut: false,
-          durationMs,
-          stdout,
-          stderr,
-          exitCode: null,
-        });
-      });
-    });
-  }
+      const errorResult = emptyAgentResult(isTimeout ? 'Timed out' : errorMessage);
+      errorResult.timedOut = isTimeout;
+      errorResult.durationMs = Date.now() - startTime;
+      errorResult.toolCalls = toolCalls;
+      errorResult.toolsUsed = [...new Set(toolCalls.map((t) => t.name))];
+      errorResult.model = model;
 
-  /**
-   * Parse the answer from Claude Code output
-   *
-   * The -p flag outputs the response directly, but there may be
-   * some formatting or metadata to strip.
-   */
-  private parseAnswer(stdout: string): string {
-    // For now, return the full output
-    // TODO: Parse out any metadata/formatting if needed
-    return stdout.trim();
+      options.onEvent?.({ type: 'complete', result: errorResult });
+      return errorResult;
+    }
   }
 
   /**
-   * Extract displayable text from a stream-json message
+   * Process a streaming message from the SDK
    */
-  private extractDisplayText(msg: Record<string, unknown>): string | null {
-    // Handle streaming text deltas (with --include-partial-messages)
-    if (msg.type === 'stream_event') {
-      const event = msg.event as Record<string, unknown> | undefined;
-      if (event?.type === 'content_block_delta') {
-        const delta = event.delta as Record<string, unknown> | undefined;
-        if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
-          return delta.text;
+  private processMessage(
+    message: SDKMessage,
+    options: AgentRunOptions,
+    toolCalls: ToolCall[],
+    toolStartTimes: Map<string, number>,
+    includePartialMessages: boolean,
+    startTime: number,
+    setModel: (m: string) => void,
+    setSessionId: (s: string) => void,
+  ): void {
+    switch (message.type) {
+      case 'system': {
+        const sysMsg = message as SDKSystemMessage;
+        if (sysMsg.subtype === 'init') {
+          setModel(sysMsg.model);
+          setSessionId(sysMsg.session_id);
+          // Emit start event here with real model (not placeholder 'unknown')
+          options.onEvent?.({
+            type: 'start',
+            timestamp: startTime,
+            model: sysMsg.model,
+          });
         }
+        break;
       }
-      // Ignore content_block_start - we'll show details from assistant message
-    }
 
-    // Handle complete assistant messages with tool details
-    if (msg.type === 'assistant' && msg.message) {
-      const message = msg.message as Record<string, unknown>;
-      const content = message.content as Array<Record<string, unknown>> | undefined;
-      if (content && Array.isArray(content)) {
-        const textParts: string[] = [];
-        for (const part of content) {
-          if (part.type === 'tool_use') {
-            const input = part.input as Record<string, unknown> | undefined;
-            const formatted = this.formatToolCall(part.name as string, input);
-            if (formatted) {
-              textParts.push(formatted);
+      case 'assistant': {
+        const assistantMsg = message as SDKAssistantMessage;
+        // Process content blocks for tool usage
+        const content = assistantMsg.message?.content;
+        if (Array.isArray(content)) {
+          for (const block of content) {
+            if (block.type === 'tool_use') {
+              const toolCall: ToolCall = {
+                id: block.id,
+                name: block.name,
+                input: block.input as Record<string, unknown>,
+                timestamp: Date.now(),
+              };
+              toolCalls.push(toolCall);
+              toolStartTimes.set(block.id, Date.now());
+
+              options.onEvent?.({
+                type: 'tool_start',
+                tool: toolCall,
+              });
+            } else if (block.type === 'text') {
+              // Final text output - only emit if NOT streaming partial messages
+              // (otherwise we already streamed it via stream_event)
+              if (!includePartialMessages) {
+                options.onEvent?.({
+                  type: 'text_delta',
+                  text: (block as { text: string }).text,
+                });
+              }
+            } else if (block.type === 'thinking') {
+              // Only emit if NOT streaming partial messages (avoid duplicates)
+              if (!includePartialMessages) {
+                options.onEvent?.({
+                  type: 'thinking',
+                  text: (block as { thinking?: string }).thinking || '',
+                });
+              }
             }
           }
         }
-        if (textParts.length > 0) {
-          return '\n' + textParts.join('\n') + '\n';
-        }
+        break;
       }
-    }
-
-    // Handle tool results - just show checkmark, skip content
-    if (msg.type === 'user') {
-      const toolResult = msg.tool_use_result as Record<string, unknown> | undefined;
-      if (toolResult) {
-        return null; // Don't show tool results - too noisy
-      }
-    }
 
-    return null;
-  }
-
-  /**
-   * Format a tool call for display
-   */
-  private formatToolCall(name: string, input: Record<string, unknown> | undefined): string | null {
-    if (!input) return `  › ${name}`;
-
-    const dim = '\x1b[2m'; // dim
-    const reset = '\x1b[0m';
+      case 'user': {
+        // Tool results come back as user messages
+        const userMsg = message as SDKUserMessage;
+        const toolResult = userMsg.tool_use_result;
+
+        if (isToolUseResult(toolResult) && toolResult.tool_use_id) {
+          const toolId = toolResult.tool_use_id;
+          const startTime = toolStartTimes.get(toolId);
+          const durationMs = startTime ? Date.now() - startTime : 0;
+
+          // Update tool call with duration and result
+          const toolCall = toolCalls.find((t) => t.id === toolId);
+          if (toolCall) {
+            toolCall.durationMs = durationMs;
+            toolCall.success = true;
+            // Capture truncated result for display
+            if (toolResult.content) {
+              toolCall.result = toolResult.content.substring(0, 500);
+            }
+          }
 
-    switch (name) {
-      case 'Read': {
-        const path = (input.file_path as string || '').split('/').slice(-2).join('/');
-        return `  › Read ${dim}${path}${reset}`;
-      }
-      case 'Glob': {
-        return `  › Glob ${dim}${input.pattern || ''}${reset}`;
-      }
-      case 'Grep': {
-        return `  › Grep ${dim}"${input.pattern || ''}"${reset}`;
-      }
-      case 'Bash': {
-        const cmd = (input.command as string || '').substring(0, 50);
-        const truncated = (input.command as string || '').length > 50 ? '...' : '';
-        return `  › Bash ${dim}${cmd}${truncated}${reset}`;
-      }
-      case 'Edit':
-      case 'Write': {
-        const path = (input.file_path as string || '').split('/').slice(-2).join('/');
-        return `  › ${name} ${dim}${path}${reset}`;
+          options.onEvent?.({
+            type: 'tool_end',
+            toolId,
+            success: true,
+            durationMs,
+            result: toolResult.content?.substring(0, 200),
+          });
+        }
+        break;
       }
-      case 'Task': {
-        return `  › Task ${dim}${input.description || ''}${reset}`;
+
+      case 'stream_event': {
+        // Partial streaming messages
+        const partialMsg = message as SDKPartialAssistantMessage;
+        const event = partialMsg.event;
+
+        if (event?.type === 'content_block_delta') {
+          const delta = (event as { delta?: { type?: string; text?: string; thinking?: string } }).delta;
+          if (delta?.type === 'text_delta' && delta.text) {
+            options.onEvent?.({
+              type: 'text_delta',
+              text: delta.text,
+            });
+          } else if (delta?.type === 'thinking_delta' && delta.thinking) {
+            options.onEvent?.({
+              type: 'thinking',
+              text: delta.thinking,
+            });
+          }
+        }
+        break;
       }
-      default:
-        return `  › ${name}`;
     }
   }
 
   /**
-   * Parse tools used from JSON output
+   * Build AgentResult from SDK result message
    */
-  private parseToolsUsedFromJson(stdout: string): string[] {
-    const tools: Set<string> = new Set();
-
-    const lines = stdout.split('\n');
-    for (const line of lines) {
-      if (!line.trim()) continue;
-      try {
-        const msg = JSON.parse(line);
-        if (msg.type === 'assistant' && msg.message) {
-          const content = msg.message.content;
-          if (Array.isArray(content)) {
-            for (const part of content) {
-              if (part.type === 'tool_use' && part.name) {
-                tools.add(part.name);
-              }
-            }
-          }
-        }
-      } catch {
-        // Skip invalid JSON lines
-      }
+  private buildResult(
+    resultMsg: SDKResultMessage,
+    toolCalls: ToolCall[],
+    model: string,
+    sessionId: string,
+  ): AgentResult {
+    const usage = resultMsg.usage;
+    const isSuccess = resultMsg.subtype === 'success';
+
+    // Build token usage
+    const tokens = {
+      inputTokens: usage.input_tokens,
+      outputTokens: usage.output_tokens,
+      cacheReadTokens: usage.cache_read_input_tokens ?? 0,
+      cacheWriteTokens: usage.cache_creation_input_tokens ?? 0,
+      totalTokens: usage.input_tokens + usage.output_tokens,
+    };
+
+    // Build per-model usage (defensive: modelUsage may be absent)
+    const modelUsage: AgentResult['modelUsage'] = {};
+    for (const [modelName, mu] of Object.entries(resultMsg.modelUsage ?? {})) {
+      modelUsage[modelName] = {
+        inputTokens: mu.inputTokens,
+        outputTokens: mu.outputTokens,
+        cacheReadTokens: mu.cacheReadInputTokens,
+        cacheWriteTokens: mu.cacheCreationInputTokens,
+        costUsd: mu.costUSD,
+      };
     }
 
-    return Array.from(tools);
+    return {
+      answer: isSuccess ? (resultMsg as { result: string }).result : '',
+      success: isSuccess && !resultMsg.is_error,
+      error: !isSuccess ? (resultMsg as { errors?: string[] }).errors?.join(', ') : undefined,
+      timedOut: false,
+      durationMs: resultMsg.duration_ms,
+      tokens,
+      costUsd: resultMsg.total_cost_usd,
+      numTurns: resultMsg.num_turns,
+      toolCalls,
+      toolsUsed: [...new Set(toolCalls.map((t) => t.name))],
+      model,
+      modelUsage,
+      raw: {
+        sessionId,
+      },
+    };
   }
 }
 
diff --git a/src/agents/types.ts b/src/agents/types.ts
index d326684..70506e6 100644
--- a/src/agents/types.ts
+++ b/src/agents/types.ts
@@ -3,28 +3,61 @@
  *
  * Agents are coding assistants that can be evaluated by sniffbench.
  * Each agent wrapper provides a common interface for running prompts
- * and capturing results.
+ * and capturing results with full metrics.
  */
 
 /**
- * Options for running an agent
+ * Token usage breakdown from agent execution
  */
-export interface AgentRunOptions {
-  /** Working directory for the agent */
-  cwd: string;
-
-  /** Timeout in milliseconds */
-  timeoutMs?: number;
-
-  /** Environment variables to pass */
-  env?: Record<string, string>;
+export interface TokenUsage {
+  /** Input tokens consumed */
+  inputTokens: number;
+  /** Output tokens generated */
+  outputTokens: number;
+  /** Tokens read from cache */
+  cacheReadTokens: number;
+  /** Tokens written to cache */
+  cacheWriteTokens: number;
+  /** Total tokens (input + output) */
+  totalTokens: number;
+}
 
-  /** Callback for streaming output */
-  onOutput?: (chunk: string) => void;
+/**
+ * Tool invocation record
+ */
+export interface ToolCall {
+  /** Unique identifier for this tool call */
+  id: string;
+  /** Tool name (e.g., 'Read', 'Bash', 'Grep') */
+  name: string;
+  /** Tool input parameters */
+  input: Record<string, unknown>;
+  /** Timestamp when tool was invoked */
+  timestamp: number;
+  /** Duration of tool execution in milliseconds */
+  durationMs?: number;
+  /** Whether tool execution succeeded */
+  success?: boolean;
+  /** Truncated result from tool (for display) */
+  result?: string;
 }
 
 /**
- * Result from running an agent
+ * Streaming events emitted during agent execution.
+ * UI layer consumes these - agent-agnostic interface.
+ */
+export type AgentEvent =
+  | { type: 'start'; timestamp: number; model: string }
+  | { type: 'text_delta'; text: string }
+  | { type: 'tool_start'; tool: ToolCall }
+  | { type: 'tool_end'; toolId: string; success: boolean; durationMs: number; result?: string }
+  | { type: 'thinking'; text: string }
+  | { type: 'error'; message: string; code?: string }
+  | { type: 'status'; message: string }
+  | { type: 'complete'; result: AgentResult };
+
+/**
+ * Result from running an agent - enhanced with full metrics
  */
 export interface AgentResult {
   /** The agent's final answer/output */
@@ -42,20 +75,84 @@ export interface AgentResult {
   /** Duration in milliseconds */
   durationMs: number;
 
-  /** Tools/commands the agent used (if trackable) */
-  toolsUsed?: string[];
+  /** Token usage breakdown */
+  tokens: TokenUsage;
+
+  /** Cost in USD */
+  costUsd: number;
+
+  /** Number of conversation turns */
+  numTurns: number;
+
+  /** Structured tool call records */
+  toolCalls: ToolCall[];
+
+  /** Unique tool names used (convenience) */
+  toolsUsed: string[];
+
+  /** Model used for this run */
+  model: string;
+
+  /** Per-model usage breakdown (for multi-model runs) */
+  modelUsage?: Record<string, {
+    inputTokens: number;
+    outputTokens: number;
+    cacheReadTokens: number;
+    cacheWriteTokens: number;
+    costUsd: number;
+  }>;
+
+  /** Raw data for debugging */
+  raw?: {
+    stdout?: string;
+    stderr?: string;
+    exitCode?: number | null;
+    sessionId?: string;
+  };
+}
+
+/**
+ * Options for running an agent
+ */
+export interface AgentRunOptions {
+  /** Working directory for the agent */
+  cwd: string;
+
+  /** Timeout in milliseconds */
+  timeoutMs?: number;
+
+  /** Environment variables to pass */
+  env?: Record<string, string>;
+
+  /** Streaming event callback - receives typed events */
+  onEvent?: (event: AgentEvent) => void;
 
-  /** Tokens used (if trackable) */
-  tokensUsed?: number;
+  /**
+   * Permission mode for Claude Code SDK
+   * - 'default' - Standard behavior, prompts for dangerous operations
+   * - 'acceptEdits' - Auto-accept file edit operations
+   * - 'bypassPermissions' - Bypass all permission checks (use with caution)
+   * - 'plan' - Planning mode, no actual tool execution
+   */
+  permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
+
+  /** Tools to allow (whitelist) */
+  allowedTools?: string[];
+
+  /** Tools to disallow (blacklist) */
+  disallowedTools?: string[];
 
-  /** Raw stdout */
-  stdout: string;
+  /** Maximum budget in USD */
+  maxBudgetUsd?: number;
 
-  /** Raw stderr */
-  stderr: string;
+  /** Maximum number of turns */
+  maxTurns?: number;
 
-  /** Exit code */
-  exitCode: number | null;
+  /** Model to use (agent-specific) */
+  model?: string;
+
+  /** Include partial/streaming messages */
+  includePartialMessages?: boolean;
 }
 
 /**
@@ -79,7 +176,7 @@ export interface AgentWrapper {
    *
    * @param prompt - The prompt/question to send to the agent
    * @param options - Run options (cwd, timeout, etc.)
-   * @returns The agent's result
+   * @returns The agent's result with full metrics
    */
   run(prompt: string, options: AgentRunOptions): Promise<AgentResult>;
 }
@@ -100,3 +197,35 @@ export interface AgentRegistry {
   /** Find available agents on the system */
   findAvailable(): Promise<AgentWrapper[]>;
 }
+
+/**
+ * Create empty token usage object
+ */
+export function emptyTokenUsage(): TokenUsage {
+  return {
+    inputTokens: 0,
+    outputTokens: 0,
+    cacheReadTokens: 0,
+    cacheWriteTokens: 0,
+    totalTokens: 0,
+  };
+}
+
+/**
+ * Create empty agent result (for error cases)
+ */
+export function emptyAgentResult(error?: string): AgentResult {
+  return {
+    answer: '',
+    success: false,
+    error,
+    timedOut: false,
+    durationMs: 0,
+    tokens: emptyTokenUsage(),
+    costUsd: 0,
+    numTurns: 0,
+    toolCalls: [],
+    toolsUsed: [],
+    model: 'unknown',
+  };
+}
diff --git a/src/cli/commands/interview.ts b/src/cli/commands/interview.ts
index bfd7970..38bbda8 100644
--- a/src/cli/commands/interview.ts
+++ b/src/cli/commands/interview.ts
@@ -15,7 +15,7 @@ import * as readline from 'readline';
 import { box } from '../../utils/ui';
 import { loadCases, getDefaultCasesDir } from '../../cases';
 import { Case } from '../../cases/types';
-import { getAgent, AgentWrapper, AgentResult } from '../../agents';
+import { getAgent, AgentWrapper, AgentResult, AgentEvent } from '../../agents';
 
 /**
  * Exploration status messages - cycles through these while agent works
@@ -268,7 +268,7 @@ async function getAgentResponse(
   caseData: Case,
   agent: AgentWrapper,
   cwd: string,
-  onOutput?: (chunk: string) => void
+  onEvent?: (event: AgentEvent) => void
 ): Promise<AgentResult> {
   // Build the prompt for the agent
   // We frame it as a comprehension question about the codebase
@@ -283,7 +283,7 @@ Be concise but accurate. Focus on the key points with specific file references w
   const result = await agent.run(prompt, {
     cwd,
     timeoutMs: (caseData.expectations?.maxTimeSeconds || 300) * 1000,
-    onOutput,
+    onEvent,
   });
 
   return result;
@@ -324,28 +324,102 @@ async function runInterviewQuestion(
   let textOutputStarted = false;
 
   try {
-    const result = await getAgentResponse(caseData, agent, projectRoot, (chunk) => {
+    const result = await getAgentResponse(caseData, agent, projectRoot, (event) => {
       outputStarted = true;
 
-      // Check if this chunk contains tool calls (lines starting with ›)
-      // Tool calls are formatted like "  › Read src/file.ts"
-      const isToolCall = chunk.trim().startsWith('›');
-
-      if (isToolCall) {
-        // Extract tool call info and update spinner
-        const clean = chunk.trim().replace(/\x1b\[[0-9;]*m/g, '').substring(0, 80);
-        exploration.toolCalls.push(clean);
-        const state = EXPLORATION_STATES[0];
-        const baseText = `${chalk.bold.hex('#D97706')(agent.displayName)} ${state.color(state.text)}`;
-        exploration.spinner.text = `${baseText} ${chalk.dim(`(${exploration.toolCalls.length} tools)`)}`;
-      } else if (chunk.trim()) {
-        // This is text content - stop spinner and stream it
-        if (!textOutputStarted) {
-          textOutputStarted = true;
-          exploration.stop();
-          console.log(chalk.dim('\n  ─────────────────────────────────────────\n'));
+      switch (event.type) {
+        case 'tool_start': {
+          // Show tool with key input info
+          const input = event.tool.input;
+          let detail = '';
+
+          // Special handling for Task tool - show subagent type and prompt
+          if (event.tool.name === 'Task') {
+            const subagentType = input.subagent_type ? `[${input.subagent_type}]` : '';
+            const desc = input.description || '';
+            const prompt = input.prompt ? String(input.prompt).substring(0, 60) : '';
+            detail = `${subagentType} ${desc}`.trim();
+
+            exploration.toolCalls.push(`› ${event.tool.name}`);
+            exploration.spinner.stop();
+            console.log(chalk.yellow(`  ⚡ Task ${chalk.bold(subagentType)} ${chalk.dim(desc)}`));
+            if (prompt) {
+              console.log(chalk.dim(`     "${prompt}${String(input.prompt).length > 60 ? '...' : ''}"`));
+            }
+            exploration.spinner.start();
+          } else {
+            // Extract most useful input field for display
+            if (input.file_path) detail = String(input.file_path).split('/').slice(-2).join('/');
+            else if (input.pattern) detail = String(input.pattern);
+            else if (input.command) detail = String(input.command).substring(0, 50);
+            else if (input.query) detail = String(input.query).substring(0, 40);
+            else if (input.path) detail = String(input.path).split('/').slice(-2).join('/');
+
+            const toolInfo = detail ? `${event.tool.name} ${chalk.dim(detail)}` : event.tool.name;
+            exploration.toolCalls.push(`› ${event.tool.name}`);
+
+            // Stop spinner and show tool call
+            exploration.spinner.stop();
+            console.log(chalk.cyan(`  › ${toolInfo}`));
+            exploration.spinner.start();
+          }
+
+          const state = EXPLORATION_STATES[exploration.toolCalls.length % EXPLORATION_STATES.length];
+          const baseText = `${chalk.bold.hex('#D97706')(agent.displayName)} ${state.color(state.text)}`;
+          exploration.spinner.text = `${baseText} ${chalk.dim(`(${exploration.toolCalls.length} tools)`)}`;
+          break;
+        }
+
+        case 'tool_end': {
+          // Could show truncated result here if desired
+          break;
         }
-        process.stdout.write(chunk);
+
+        case 'thinking': {
+          // Show thinking/reasoning output between tool calls
+          const text = event.text.trim();
+          if (text) {
+            exploration.spinner.stop();
+            // Show first line or first 150 chars of thinking
+            const firstLine = text.split('\n')[0];
+            const display = firstLine.length > 150
+              ? firstLine.substring(0, 150) + '...'
+              : firstLine;
+            console.log(chalk.magenta(`  💭 ${display}`));
+            // If there's more content, indicate it
+            if (text.includes('\n') || text.length > 150) {
+              const lineCount = text.split('\n').length;
+              if (lineCount > 1) {
+                console.log(chalk.dim(`     (${lineCount} lines of reasoning)`));
+              }
+            }
+            exploration.spinner.start();
+          }
+          break;
+        }
+
+        case 'text_delta': {
+          // Stream text content to stdout
+          if (event.text.trim()) {
+            if (!textOutputStarted) {
+              textOutputStarted = true;
+              exploration.stop();
+              console.log(chalk.dim('\n  ─────────────────────────────────────────\n'));
+            }
+            process.stdout.write(event.text);
+          }
+          break;
+        }
+
+        case 'status': {
+          // Show status updates in spinner
+          exploration.spinner.text = `${chalk.bold.hex('#D97706')(agent.displayName)} ${chalk.cyan(event.message)}`;
+          break;
+        }
+
+        // Ignore other event types for now
+        default:
+          break;
       }
     });
 
