From f51ac73c5467226f58675f2ce3c0a5aed96e4edc Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 5 Jun 2026 18:27:44 -0400 Subject: [PATCH] feat: runner seam + multivariate wizard-runner flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route runProgram through a Runner abstraction selected by the multivariate wizard-runner flag (anthropic | pi | vercel, default anthropic). AnthropicRunner wraps runAgent verbatim; pi/vercel fall back to it until #523/#524 land. The active runner is logged every run. Pure refactor — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/agent/agent-runner.ts | 8 ++- .../runner/__tests__/select-runner.test.ts | 44 +++++++++++++ src/lib/agent/runner/anthropic-runner.ts | 17 +++++ src/lib/agent/runner/index.ts | 64 +++++++++++++++++++ src/lib/constants.ts | 2 + 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/lib/agent/runner/__tests__/select-runner.test.ts create mode 100644 src/lib/agent/runner/anthropic-runner.ts create mode 100644 src/lib/agent/runner/index.ts diff --git a/src/lib/agent/agent-runner.ts b/src/lib/agent/agent-runner.ts index 8e5aa905..8cf4809c 100644 --- a/src/lib/agent/agent-runner.ts +++ b/src/lib/agent/agent-runner.ts @@ -25,7 +25,6 @@ import { analytics, groupsFromUser } from '@utils/analytics'; import { getUI } from '@ui'; import { initializeAgent, - runAgent as executeAgent, AgentErrorType, AgentSignals, buildWizardMetadata, @@ -33,6 +32,7 @@ import { backupAndFixClaudeSettings, restoreClaudeSettings, } from './agent-interface'; +import { selectRunner } from './runner'; import { getCloudUrlFromRegion } from '@utils/urls'; import { evaluateWizardReadiness, @@ -304,6 +304,10 @@ export async function runProgram( const wizardFlags = await analytics.getAllFlagsForWizard(); const wizardMetadata = buildWizardMetadata(wizardFlags); + // Select the agent runner backend (anthropic | pi | vercel) from the flags + // evaluated above. Resolves to AnthropicRunner today; see runner/index.ts. + const runner = selectRunner(wizardFlags); + const mcpUrl = session.localMcp ? 'http://localhost:8787/mcp' : runtimeEnv('MCP_URL') || @@ -372,7 +376,7 @@ export async function runProgram( }); // 8. Run agent - const agentResult = await executeAgent( + const agentResult = await runner.run( agent, prompt, sessionToOptions(session), diff --git a/src/lib/agent/runner/__tests__/select-runner.test.ts b/src/lib/agent/runner/__tests__/select-runner.test.ts new file mode 100644 index 00000000..a77a6355 --- /dev/null +++ b/src/lib/agent/runner/__tests__/select-runner.test.ts @@ -0,0 +1,44 @@ +import { + selectRunner, + resolveRunnerVariant, + type WizardRunnerVariant, +} from '../index'; +import { AnthropicRunner } from '../anthropic-runner'; +import { WIZARD_RUNNER_FLAG_KEY } from '../../../constants'; + +// `runAgent` pulls in the full SDK-backed agent stack; runner selection is a +// pure choice over flags, so stub the module to keep this a focused unit test. +jest.mock('../../agent-interface', () => ({ + runAgent: jest.fn(), +})); + +describe('resolveRunnerVariant', () => { + const cases: Array<[string | undefined, WizardRunnerVariant]> = [ + ['anthropic', 'anthropic'], + ['pi', 'pi'], + ['vercel', 'vercel'], + ['something-else', 'anthropic'], + [undefined, 'anthropic'], + ]; + it.each(cases)('maps %p → %p', (value, expected) => { + const flags: Record = + value === undefined ? {} : { [WIZARD_RUNNER_FLAG_KEY]: value }; + expect(resolveRunnerVariant(flags)).toBe(expected); + }); +}); + +describe('selectRunner', () => { + // pi/vercel aren't implemented yet, so every variant resolves to AnthropicRunner. + it.each(['anthropic', 'pi', 'vercel', 'bogus'])( + 'returns AnthropicRunner for %p', + (variant) => { + expect( + selectRunner({ [WIZARD_RUNNER_FLAG_KEY]: variant }), + ).toBeInstanceOf(AnthropicRunner); + }, + ); + + it('defaults to AnthropicRunner when the flag is absent', () => { + expect(selectRunner({})).toBeInstanceOf(AnthropicRunner); + }); +}); diff --git a/src/lib/agent/runner/anthropic-runner.ts b/src/lib/agent/runner/anthropic-runner.ts new file mode 100644 index 00000000..035fd0e5 --- /dev/null +++ b/src/lib/agent/runner/anthropic-runner.ts @@ -0,0 +1,17 @@ +import { runAgent } from '../agent-interface'; +import type { Runner, RunnerRunArgs, RunnerResult } from './index'; + +/** + * The `anthropic` variant — today's execution path. Delegates verbatim to the + * SDK-backed `runAgent` in agent-interface.ts, which sets the gateway env vars, + * builds the MCP servers, and drives the `@anthropic-ai/claude-agent-sdk` + * `query()` loop. + * + * This is the control runner: a thin wrapper, not a rewrite, so the diff stays + * a pure seam. + */ +export class AnthropicRunner implements Runner { + run(...args: RunnerRunArgs): Promise { + return runAgent(...args); + } +} diff --git a/src/lib/agent/runner/index.ts b/src/lib/agent/runner/index.ts new file mode 100644 index 00000000..876d771c --- /dev/null +++ b/src/lib/agent/runner/index.ts @@ -0,0 +1,64 @@ +/** + * Runner seam. + * + * The program pipeline (agent-runner.ts) drives an agent run through a + * `Runner` rather than calling a model SDK directly. This is the seam the + * model-agnostic runner work hangs off: each backend wraps an existing agent + * SDK that owns the loop/tools/streaming, and `selectRunner` chooses between + * them by the multivariate `wizard-runner` flag. + * + * - `anthropic` — today's `@anthropic-ai/claude-agent-sdk` path (default). + * - `pi` — OpenAI Agents SDK (lands in #524). + * - `vercel` — Vercel AI SDK (lands in #523). + * + * This module is pure machinery — it carries no product knowledge. + */ +import { WIZARD_RUNNER_FLAG_KEY } from '../../constants'; +import { logToFile } from '../../../utils/debug'; +import type { runAgent } from '../agent-interface'; +import { AnthropicRunner } from './anthropic-runner'; + +/** The agent backends the `wizard-runner` flag can select. */ +export type WizardRunnerVariant = 'anthropic' | 'pi' | 'vercel'; + +/** Arguments accepted by a runner — mirrors `runAgent` exactly. */ +export type RunnerRunArgs = Parameters; +/** Result produced by a runner — mirrors `runAgent` exactly. */ +export type RunnerResult = Awaited>; + +/** + * Execution backend for an agent run. Implementations are interchangeable: + * they take the same arguments as `runAgent` and resolve to the same result, + * so the pipeline neither knows nor cares which one it holds. + */ +export interface Runner { + run(...args: RunnerRunArgs): Promise; +} + +/** + * Resolve the `wizard-runner` flag to a known variant. Anything unrecognized + * — including an absent value from a flag-fetch failure — resolves to + * `anthropic`, so the SDK path is always the safe default. + */ +export function resolveRunnerVariant( + flags: Record, +): WizardRunnerVariant { + const value = flags[WIZARD_RUNNER_FLAG_KEY]; + return value === 'pi' || value === 'vercel' ? value : 'anthropic'; +} + +/** + * Select the runner for this run and log which backend executed. + * + * The `pi` and `vercel` runners don't exist yet (#523/#524), so they fall back + * to `AnthropicRunner` — the flag is wired and observable, but selection is + * behavior-preserving until those runners land. + */ +export function selectRunner(flags: Record): Runner { + const variant = resolveRunnerVariant(flags); + // TODO(runners): return PiRunner / VercelRunner once #524 / #523 land. + const fallback = + variant !== 'anthropic' ? ' (fallback — not yet implemented)' : ''; + logToFile(`[runner] wizard-runner=${variant} → AnthropicRunner${fallback}`); + return new AnthropicRunner(); +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index bb7f3580..1be485a6 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -143,6 +143,8 @@ export const WIZARD_REMARK_EVENT_NAME = 'wizard remark'; export const WIZARD_VARIANT_FLAG_KEY = 'wizard-variant'; /** Feature flag key that gates the intro-screen "Tools" menu. */ export const WIZARD_TOOLS_MENU_FLAG_KEY = 'wizard-tools-menu'; +/** Multivariate flag selecting the agent runner backend (`anthropic` | `pi` | `vercel`). */ +export const WIZARD_RUNNER_FLAG_KEY = 'wizard-runner'; /** Variant key -> metadata for wizard run (VARIANT flag selects which entry to use). */ export const WIZARD_VARIANTS: Record> = { base: { VARIANT: 'base' },