Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/lib/agent/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import { analytics, groupsFromUser } from '@utils/analytics';
import { getUI } from '@ui';
import {
initializeAgent,
runAgent as executeAgent,
AgentErrorType,
AgentSignals,
buildWizardMetadata,
checkAllSettingsConflicts,
backupAndFixClaudeSettings,
restoreClaudeSettings,
} from './agent-interface';
import { selectRunner } from './runner';
import { getCloudUrlFromRegion } from '@utils/urls';
import {
evaluateWizardReadiness,
Expand Down Expand Up @@ -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') ||
Expand Down Expand Up @@ -372,7 +376,7 @@ export async function runProgram(
});

// 8. Run agent
const agentResult = await executeAgent(
const agentResult = await runner.run(
agent,
prompt,
sessionToOptions(session),
Expand Down
44 changes: 44 additions & 0 deletions src/lib/agent/runner/__tests__/select-runner.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> =
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);
});
});
17 changes: 17 additions & 0 deletions src/lib/agent/runner/anthropic-runner.ts
Original file line number Diff line number Diff line change
@@ -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<RunnerResult> {
return runAgent(...args);
}
}
64 changes: 64 additions & 0 deletions src/lib/agent/runner/index.ts
Original file line number Diff line number Diff line change
@@ -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<typeof runAgent>;
/** Result produced by a runner — mirrors `runAgent` exactly. */
export type RunnerResult = Awaited<ReturnType<typeof runAgent>>;

/**
* 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<RunnerResult>;
}

/**
* 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<string, string>,
): 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<string, string>): 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();
}
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, string>> = {
base: { VARIANT: 'base' },
Expand Down
Loading