Files
fusion/packages/engine/src/runtime-resolution.ts
gsxdsm 26a14698c6 fix(engine): make pi.js imports static to fail fast on partial dist
Replace lazy `await import("./pi.js")` and `require("./pi.js")` calls in
runtime-resolution, agent-session-helpers, agent-heartbeat, and
cron-runner with top-level static imports. These dynamic imports were
documented as plugin-decoupling, but pi.js is already eagerly loaded
through index.ts re-exports and static imports in executor/merger/
reviewer/triage/mission-execution-loop, so the deferral never paid off
in practice.

The deferral did, however, introduce a TOCTOU race: a tsc rebuild that
momentarily emptied dist/pi.js would let the engine load fine and only
fail minutes later when the first session was created (e.g. FN-2860
errored two minutes into execution while pi.js was being rewritten).
With static imports, a missing/half-built dist now fails immediately at
process startup with a clear stack — verified by `mv dist/pi.js
dist/pi.js.bak` reproducing ERR_MODULE_NOT_FOUND on the first import of
runtime-resolution.js.

Also drops the DefaultPiRuntime.describeModelFn cache, which only
existed to paper over the require-on-first-call latency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:25:55 -07:00

342 lines
11 KiB
TypeScript

/**
* Runtime resolution utilities for selecting and instantiating agent runtimes.
*
* Provides a resolution layer that:
* 1. Looks up plugin-provided runtimes when a runtime hint is configured
* 2. Falls back to the default pi runtime when no hint is provided or lookup fails
* 3. Provides structured logging for debugging runtime selection decisions
*/
import type { AgentRuntime, AgentRuntimeOptions, AgentSessionResult } from "./agent-runtime.js";
import type { PluginRunner } from "./plugin-runner.js";
import type { AgentSession } from "@mariozechner/pi-coding-agent";
import { createLogger } from "./logger.js";
import { createFnAgent, promptWithFallback, describeModel } from "./pi.js";
/** Logger for the runtime resolution subsystem */
const runtimeLog = createLogger("runtime-resolver");
/**
* Session purpose for runtime selection context.
* Determines which runtime selection rules apply.
*/
export type SessionPurpose =
| "executor"
| "triage"
| "reviewer"
| "merger"
| "heartbeat"
| "validation";
/**
* Context for runtime resolution.
* Provides all information needed to select and configure a runtime.
*/
export interface RuntimeResolutionContext {
/** Purpose of the session (affects runtime selection behavior) */
sessionPurpose: SessionPurpose;
/** Optional runtime hint (runtimeId) from task/agent configuration.
* When provided and non-empty, the resolver attempts to find a matching
* plugin runtime before falling back to the default. */
runtimeHint?: string;
/** PluginRunner for looking up plugin-provided runtimes */
pluginRunner: PluginRunner;
}
/**
* Result of runtime resolution.
*/
export interface ResolvedRuntime {
/** The resolved runtime instance */
runtime: AgentRuntime;
/** Whether this runtime was explicitly configured via hint (vs. default) */
wasConfigured: boolean;
/** The runtime ID that was resolved */
runtimeId: string;
}
/**
* Reason for fallback when configured runtime is unavailable.
*/
export type FallbackReason =
/** Runtime hint was provided but no matching runtime was found */
| "not_found"
/** Runtime factory function threw an error during instantiation */
| "factory_error"
/** Runtime was found but failed to initialize */
| "init_error";
/**
* Default pi-based runtime implementation.
*
* This runtime wraps the existing createFnAgent + promptWithFallback
* implementation without any behavior changes. It serves as:
* 1. The default runtime when no runtime hint is configured
* 2. The fallback runtime when a configured plugin runtime is unavailable
*/
export class DefaultPiRuntime implements AgentRuntime {
readonly id = "pi";
readonly name = "Default PI Runtime";
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
return createFnAgent(options);
}
async promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
return promptWithFallback(session, prompt, options);
}
describeModel(session: AgentSession): string {
return describeModel(session);
}
}
/**
* Singleton instance of the default pi runtime.
* Reused across all resolution requests.
*/
let defaultPiRuntimeInstance: DefaultPiRuntime | null = null;
/**
* Get the singleton default pi runtime instance.
*/
export function getDefaultPiRuntime(): AgentRuntime {
if (!defaultPiRuntimeInstance) {
defaultPiRuntimeInstance = new DefaultPiRuntime();
}
return defaultPiRuntimeInstance;
}
/**
* Resolve a plugin runtime by its runtimeId.
*
* @param pluginRunner - PluginRunner for looking up runtimes
* @param runtimeId - The runtime ID to find
* @returns The resolved runtime wrapper, or null if not found
*/
async function resolvePluginRuntime(
pluginRunner: PluginRunner,
runtimeId: string,
): Promise<{ runtime: AgentRuntime; pluginId: string } | null> {
// Use the convenience method for single runtime lookup
const registration = pluginRunner.getRuntimeById(runtimeId);
if (!registration) {
return null;
}
const { pluginId, runtime } = registration;
runtimeLog.log(`Found plugin runtime "${runtimeId}" from plugin "${pluginId}"`);
try {
// Create plugin context for runtime factory
const pluginContext = await pluginRunner.createRuntimeContext(pluginId);
if (!pluginContext) {
runtimeLog.warn(`Plugin "${pluginId}" runtime factory context unavailable`);
return null;
}
// Instantiate the runtime via factory
const factoryResult = runtime.factory(pluginContext);
const instance = await (factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult));
if (!instance) {
runtimeLog.warn(`Plugin "${pluginId}" runtime factory returned null`);
return null;
}
// Wrap the plugin runtime to conform to AgentRuntime interface
// The plugin may return its own interface, so we adapt if needed
const wrappedRuntime = wrapPluginRuntime(instance, runtime.metadata.runtimeId, runtime.metadata.name);
return { runtime: wrappedRuntime, pluginId };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
runtimeLog.error(`Plugin "${pluginId}" runtime factory error: ${message}`);
return null;
}
}
/**
* Wrap a plugin runtime instance to conform to the AgentRuntime interface.
*
* Plugin runtimes may return their own interface types. This function
* adapts them to the standard AgentRuntime interface.
*/
function wrapPluginRuntime(
instance: unknown,
runtimeId: string,
runtimeName: string,
): AgentRuntime {
// If it's already an AgentRuntime, return as-is
if (isAgentRuntime(instance)) {
return instance;
}
// Otherwise, wrap in a compatibility layer
// The plugin should return something compatible with our interface
// but we provide a defensive fallback
runtimeLog.warn(`Plugin runtime "${runtimeId}" does not conform to AgentRuntime interface, wrapping with adapter`);
return {
id: runtimeId,
name: runtimeName,
createSession: async (options: AgentRuntimeOptions) => {
const adapter = instance as Record<string, unknown>;
if (typeof adapter.createSession === "function") {
const result = await adapter.createSession(options);
return {
session: (result as AgentSessionResult).session ?? (result as AgentSession),
sessionFile: (result as AgentSessionResult).sessionFile,
};
}
throw new Error(`Plugin runtime "${runtimeId}" does not implement createSession`);
},
promptWithFallback: async (session: AgentSession, prompt: string, options?: unknown) => {
const adapter = instance as Record<string, unknown>;
if (typeof adapter.promptWithFallback === "function") {
return adapter.promptWithFallback(session, prompt, options);
}
// Fallback to default pi promptWithFallback
return promptWithFallback(session, prompt, options);
},
describeModel: (session: AgentSession) => {
const adapter = instance as Record<string, unknown>;
if (typeof adapter.describeModel === "function") {
return (adapter.describeModel as (s: AgentSession) => string)(session);
}
// Fallback to default pi describeModel
return describeModel(session);
},
};
}
/**
* Type guard to check if an object conforms to AgentRuntime.
*/
function isAgentRuntime(obj: unknown): obj is AgentRuntime {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
typeof (obj as AgentRuntime).createSession === "function" &&
typeof (obj as AgentRuntime).promptWithFallback === "function" &&
typeof (obj as AgentRuntime).describeModel === "function"
);
}
/**
* Resolve an agent runtime based on the resolution context.
*
* Resolution algorithm:
* 1. If runtimeHint is provided and non-empty:
* a. Look up runtime by ID from plugin runner
* b. If found: instantiate and return with wasConfigured=true
* c. If not found: log structured warning, fall back to pi runtime
* 2. If no runtimeHint:
* a. Return the default pi runtime with wasConfigured=false
*
* @param context - Resolution context with purpose, hint, and plugin runner
* @returns The resolved runtime with metadata about how it was selected
*/
export async function resolveRuntime(context: RuntimeResolutionContext): Promise<ResolvedRuntime> {
const { sessionPurpose, runtimeHint, pluginRunner } = context;
// Case 1: No runtime hint provided — use default pi runtime
if (!runtimeHint || runtimeHint.trim() === "") {
runtimeLog.log(`[${sessionPurpose}] No runtime hint configured, using default pi runtime`);
return {
runtime: getDefaultPiRuntime(),
wasConfigured: false,
runtimeId: "pi",
};
}
// Case 2: Runtime hint provided — try to find matching plugin runtime
const runtimeId = runtimeHint.trim();
// Check if the hint is explicitly "pi" — use default runtime
if (runtimeId === "pi" || runtimeId === "default") {
runtimeLog.log(`[${sessionPurpose}] Runtime hint is "pi/default", using default pi runtime`);
return {
runtime: getDefaultPiRuntime(),
wasConfigured: true,
runtimeId: "pi",
};
}
// Look up the plugin runtime
try {
const resolved = await resolvePluginRuntime(pluginRunner, runtimeId);
if (resolved) {
runtimeLog.log(`[${sessionPurpose}] Using configured plugin runtime "${runtimeId}" from "${resolved.pluginId}"`);
return {
runtime: resolved.runtime,
wasConfigured: true,
runtimeId,
};
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
runtimeLog.error(`[${sessionPurpose}] Error resolving plugin runtime "${runtimeId}": ${message}`);
}
// Case 3: Runtime not found or error — fall back to pi with warning
logRuntimeFallback(sessionPurpose, runtimeId, "not_found");
return {
runtime: getDefaultPiRuntime(),
wasConfigured: false,
runtimeId: "pi",
};
}
/**
* Log structured fallback warning when configured runtime is unavailable.
*/
function logRuntimeFallback(
sessionPurpose: SessionPurpose,
requestedRuntimeId: string,
reason: FallbackReason,
): void {
runtimeLog.warn(
`[${sessionPurpose}] Runtime "${requestedRuntimeId}" unavailable (${reason}), falling back to default pi runtime`,
);
}
/**
* Build a RuntimeResolutionContext with a default plugin runner.
*
* When a subsystem has a pluginRunner, use this helper to create the context
* with sensible defaults. The runtimeHint should come from the task/agent
* configuration when available.
*
* @param sessionPurpose - The purpose of the session
* @param pluginRunner - The plugin runner for runtime lookup
* @param runtimeHint - Optional runtime hint from task/agent configuration
* @returns The resolution context
*/
export function buildRuntimeResolutionContext(
sessionPurpose: SessionPurpose,
pluginRunner: PluginRunner | undefined,
runtimeHint?: string,
): RuntimeResolutionContext {
return {
sessionPurpose,
runtimeHint,
pluginRunner: pluginRunner ?? createNoOpPluginRunner(),
};
}
/**
* Create a no-op plugin runner for subsystems that don't have access to the real one.
* This allows the resolver to still return the default pi runtime.
*/
function createNoOpPluginRunner(): PluginRunner {
return {
getPluginRuntimes: () => [],
getRuntimeById: () => undefined,
createRuntimeContext: async () => null,
} as unknown as PluginRunner;
}