/** * Helper functions for creating agent sessions with runtime resolution. * * These helpers wrap the runtime resolution pattern so that subsystems * don't need to duplicate the resolution logic. They use the resolver * to select the appropriate runtime and then delegate to it for session * creation and prompting. */ import type { AgentRuntimeOptions } from "./agent-runtime.js"; import type { PluginRunner } from "./plugin-runner.js"; import type { AgentSession } from "@mariozechner/pi-coding-agent"; import { resolveRuntime, buildRuntimeResolutionContext, type SessionPurpose } from "./runtime-resolution.js"; import { createLogger } from "./logger.js"; import { promptWithFallback, describeModel } from "./pi.js"; /** Logger for agent session helpers */ const sessionLog = createLogger("agent-session"); /** * Options for creating an agent session with runtime resolution. */ export interface ResolvedSessionOptions extends AgentRuntimeOptions { /** Session purpose for runtime selection */ sessionPurpose: SessionPurpose; /** Plugin runner for runtime lookup. When provided, enables plugin runtime selection. */ pluginRunner?: PluginRunner; /** Optional runtime hint from task/agent configuration */ runtimeHint?: string; /** * `beforeSpawnSession` is inherited from {@link AgentRuntimeOptions} — see * its definition there for the contract. Callers (e.g. the reviewer's * pause gate) throw from this callback to cancel session creation when * external state changed during the async setup window. Forwarded * verbatim to `runtime.createSession()`; the runtime is responsible for * invoking it at its latest synchronous point before the underlying LLM * session is instantiated. */ } /** * Result of creating an agent session with runtime resolution. */ export interface ResolvedSessionResult { /** The created agent session */ session: AgentSession; /** Path to the persisted session file (undefined for in-memory sessions) */ sessionFile?: string; /** The runtime ID that was used */ runtimeId: string; /** Whether the runtime was explicitly configured */ wasConfigured: boolean; } /** * Extract runtime hint from untyped runtimeConfig payload. * * @param runtimeConfig - Agent/task runtime configuration * @returns normalized runtime hint or undefined when missing/invalid */ export function extractRuntimeHint( runtimeConfig: Record | undefined, ): string | undefined { const hint = runtimeConfig?.runtimeHint; if (typeof hint !== "string") { return undefined; } const normalizedHint = hint.trim(); return normalizedHint.length > 0 ? normalizedHint : undefined; } /** * Create an agent session using runtime resolution. * * This function: * 1. Resolves the appropriate runtime based on sessionPurpose, runtimeHint, and pluginRunner * 2. Creates the session using the resolved runtime * 3. Returns the session along with metadata about which runtime was used * * @param options - Session creation options including purpose and runtime configuration * @returns Promise resolving to the session result with runtime metadata */ export async function createResolvedAgentSession( options: ResolvedSessionOptions, ): Promise { const { sessionPurpose, pluginRunner, runtimeHint, ...runtimeOptions } = options; // Build the resolution context const context = buildRuntimeResolutionContext(sessionPurpose, pluginRunner, runtimeHint); // Resolve the runtime const resolved = await resolveRuntime(context); sessionLog.log( `[${sessionPurpose}] Using runtime "${resolved.runtimeId}" (configured=${resolved.wasConfigured})`, ); // Forward `beforeSpawnSession` to the runtime so it fires at the true // latest sync point (just before LLM session instantiation) rather than // here, before the runtime's own awaited setup work runs. See // AgentRuntimeOptions.beforeSpawnSession for the contract. const result = await resolved.runtime.createSession(runtimeOptions); // Attach the resolved runtime's promptWithFallback as a bound method on the // session object when it is not already present. This is the dispatch hook // that pi.promptWithFallback (pi.ts:175) checks before falling through to its // own pi-native path. Plugin runtimes (hermes, openclaw, paperclip) do not // attach this method themselves; without it every prompt call would silently // bypass the plugin and go through pi's session.prompt() instead. // // The default pi runtime's createFnAgent (pi.ts:1143) already attaches // promptWithFallback to the session, so we only attach when it is absent. const session = result.session as AgentSession & { promptWithFallback?: unknown }; if (typeof session.promptWithFallback !== "function") { const runtime = resolved.runtime; // eslint-disable-next-line @typescript-eslint/no-explicit-any (session as any).promptWithFallback = ( prompt: string, options?: unknown, ) => runtime.promptWithFallback(session, prompt, options); } return { session: result.session, sessionFile: result.sessionFile, runtimeId: resolved.runtimeId, wasConfigured: resolved.wasConfigured, }; } /** * Prompt an agent session with automatic retry and compaction. * * This is a convenience wrapper that delegates to the runtime's promptWithFallback. * * @param session - The session to prompt * @param prompt - The prompt text * @param options - Optional prompt options (e.g., images) */ export async function promptWithAutoRetry( session: AgentSession, prompt: string, options?: unknown, ): Promise { return promptWithFallback(session, prompt, options); } /** * Get a human-readable model description from a session. * * @param session - The session to describe * @returns Model description string */ export async function describeAgentModel(session: AgentSession): Promise { return describeModel(session); }