import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { Api, Model, ServiceTier } from "@earendil-works/pi-ai"; import type { AgentSession } from "./agent-session.js"; import type { ToolDefinition } from "./extensions/index.js"; import type { HostRequestHandler } from "./kernel/index.js"; import { THINKING_LEVELS } from "./thinking-levels.js"; /** Request emitted by `rlm.run`; cellSourceCode preserves the spawning cell for display. */ export interface RlmRunRequest { prompt: string; kwargs: Record; cellSourceCode?: string; } interface RlmCreateSessionRequest { prompt: string; kwargs: Record; } export interface RlmCreateSessionResult { active_session_id: string; session_id: string; name: string; session_file: string; model: string; } export interface RlmSpawnHandle { rlm_child_id: string; name: string; session_dir: string; model: string; } export type RlmSubagentRegistryStatus = "running" | "completed" | "error"; export interface RlmSubagentRegistryEntry { rlm_child_id: string; active_session_id: string | null; session_id: string | null; session_name: string; session_dir: string; status: RlmSubagentRegistryStatus; } export interface RlmListSubagentsResult { subagents: RlmSubagentRegistryEntry[]; } export interface RlmDeleteSubagentResult { subagent: RlmSubagentRegistryEntry; outcome?: "deleted" | "skipped_running"; } export interface RlmModelMatch { provider: string; id: string; name: string; selector: string; } export interface RlmFindModelsResult { models: RlmModelMatch[]; } export type RlmRunHandler = (request: RlmRunRequest) => Promise>; type RlmCreateSessionHandler = (request: RlmCreateSessionRequest) => Promise; interface AsyncBashCompletionRequest { pid: number; command: string; exitCode: number; } type AsyncBashCompletionHandler = (request: AsyncBashCompletionRequest) => void | Promise; export type RlmListSubagentsHandler = () => RlmListSubagentsResult | Promise; export type RlmDeleteSubagentHandler = (target: string) => Promise; export type RlmFindModelsHandler = (query: string, limit: number) => RlmFindModelsResult | Promise; const RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH = 64; export const DEFAULT_RLM_MODEL_SEARCH_LIMIT = 8; export const MAX_RLM_MODEL_SEARCH_LIMIT = 20; export function normalizeRequestedRlmSubagentSessionName(value: unknown, operation = "rlm.run"): string | undefined { if (value === undefined) { return undefined; } if (typeof value !== "string") { throw new Error(`${operation} name must be a string`); } const name = value.trim(); if (!name) { throw new Error(`${operation} name must not be empty`); } if (name.length > RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH) { throw new Error(`${operation} name must be at most ${RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH} characters`); } return name; } export function normalizeRequestedRlmSubagentThinkingLevel( value: unknown, operation = "rlm.run", ): ThinkingLevel | undefined { if (value === undefined) { return undefined; } if (typeof value !== "string") { throw new Error(`${operation} thinking must be a string`); } const level = value.trim().toLowerCase(); if (!THINKING_LEVELS.includes(level as ThinkingLevel)) { throw new Error(`${operation} thinking must be one of: ${THINKING_LEVELS.join(", ")}`); } return level as ThinkingLevel; } export function normalizeRequestedRlmSubagentModel(value: unknown, operation = "rlm.run"): string | undefined { if (value === undefined) { return undefined; } if (typeof value !== "string") { throw new Error(`${operation} model must be a string`); } const model = value.trim(); if (!model) { throw new Error(`${operation} model must not be empty`); } return model; } /** Create a readable, collision-resistant default name usable as an agent-message selector. */ export function createDefaultRlmSubagentSessionName(prompt: string, childId: string): string { const promptSlug = prompt .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); const idSuffix = childId .replace(/^sub-/, "") .replace(/[^A-Za-z0-9]+/g, "") .slice(-8) || "child"; const fixedLength = "subagent--".length + idSuffix.length; const promptPart = (promptSlug || "worker") .slice(0, Math.max(1, RLM_SUBAGENT_SESSION_NAME_MAX_LENGTH - fixedLength)) .replace(/-+$/g, ""); return `subagent-${promptPart || "worker"}-${idSuffix}`; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function normalizeModelSearchText(value: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, ""); } export function findRlmModelMatches(query: string, models: Model[], limit: number): RlmModelMatch[] { const normalizedQuery = normalizeModelSearchText(query.trim()); return models .map((model) => { const selector = `${model.provider}/${model.id}`; const fields = [selector, model.id, model.name || model.id]; const normalizedFields = fields.map(normalizeModelSearchText); let score = normalizedQuery ? Number.POSITIVE_INFINITY : 0; if (normalizedQuery) { const exactIndex = normalizedFields.indexOf(normalizedQuery); const prefixIndex = normalizedFields.findIndex((field) => field.startsWith(normalizedQuery)); const partialIndex = normalizedFields.findIndex((field) => field.includes(normalizedQuery)); if (exactIndex >= 0) score = exactIndex; else if (prefixIndex >= 0) score = 3 + prefixIndex; else if (partialIndex >= 0) score = 6 + partialIndex; } return { model, selector, score }; }) .filter((candidate) => Number.isFinite(candidate.score)) .sort((a, b) => a.score - b.score || a.selector.localeCompare(b.selector)) .slice(0, limit) .map(({ model, selector }) => ({ provider: model.provider, id: model.id, name: model.name || model.id, selector, })); } export function createRlmCreateSessionHostHandler(handler: RlmCreateSessionHandler): HostRequestHandler { return async (payload) => { if (typeof payload.prompt !== "string") { throw new Error("rlm.create_session prompt must be a string"); } const kwargs = isRecord(payload.kwargs) ? payload.kwargs : {}; const result = await handler({ prompt: payload.prompt, kwargs }); return result as unknown as Record; }; } /** Adapt an RlmRunHandler into the typed `rlm.run` kernel host handler. */ export function createRlmRunHostHandler(handler: RlmRunHandler): HostRequestHandler { return async (payload) => { if (typeof payload.prompt !== "string") { throw new Error("rlm.run prompt must be a string"); } const kwargs = isRecord(payload.kwargs) ? payload.kwargs : {}; const cellSourceCode = typeof payload.cellSourceCode === "string" ? payload.cellSourceCode : undefined; const result = await handler({ prompt: payload.prompt, kwargs, cellSourceCode, }); return result as unknown as Record; }; } /** Adapt detached kernel bash completions into a validated host notification. */ export function createAsyncBashCompletionHostHandler(handler: AsyncBashCompletionHandler): HostRequestHandler { return async (payload) => { const { pid, command, exitCode } = payload; if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) { throw new Error("bash.completed pid must be a positive integer"); } if (typeof command !== "string" || !command) { throw new Error("bash.completed command must be a non-empty string"); } if (typeof exitCode !== "number" || !Number.isInteger(exitCode)) { throw new Error("bash.completed exitCode must be an integer"); } await handler({ pid, command, exitCode }); return {}; }; } /** Search a bounded authenticated model catalog without adding it to the system prompt. */ export function createRlmFindModelsHostHandler(handler: RlmFindModelsHandler): HostRequestHandler { return async (payload) => { if (typeof payload.query !== "string") { throw new Error("rlm.find_models query must be a string"); } const limit = payload.limit === undefined ? DEFAULT_RLM_MODEL_SEARCH_LIMIT : payload.limit; if (!Number.isInteger(limit) || (limit as number) < 1 || (limit as number) > MAX_RLM_MODEL_SEARCH_LIMIT) { throw new Error(`rlm.find_models limit must be an integer from 1 to ${MAX_RLM_MODEL_SEARCH_LIMIT}`); } return { models: (await handler(payload.query, limit as number)).models }; }; } /** Expose the current parent session's direct RLM child registry to its kernel. */ export function createRlmListSubagentsHostHandler(handler: RlmListSubagentsHandler): HostRequestHandler { return async () => { const { subagents } = await handler(); return { subagents }; }; } /** Delete one direct child selected from the current parent session's registry. */ export function createRlmDeleteSubagentHostHandler(handler: RlmDeleteSubagentHandler): HostRequestHandler { return async (payload) => { if (typeof payload.target !== "string" || !payload.target.trim()) { throw new Error("rlm.delete_subagent target must be a non-empty string"); } const { subagent, outcome } = await handler(payload.target.trim()); return outcome === undefined ? { subagent } : { subagent, outcome }; }; } export interface RlmSubagentRuntime { session: AgentSession; } export interface CreateRlmSubagentRuntimeOptions { parentSession: AgentSession; id: string; prompt: string; sessionName: string; sessionDir: string; model: Model; thinkingLevel: ThinkingLevel; serviceTier: ServiceTier; scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; activeToolNames: string[]; allowedToolNames?: string[]; customTools: ToolDefinition[]; includeGoals: boolean; includeCompactSkill: boolean; rlmDepth: number; rlmMaxDepth: number; rlmParentNodeId: string; /** Request ID of the parent model call whose tool call caused this spawn. */ spawnedByRequestId?: string; /** Source of the Python cell that spawned this subagent, for display. */ spawnCode?: string; /** Publish the session to the parent before a host makes the runtime addressable. */ onSessionPublished?: (session: AgentSession) => void; /** Remote host destination if this subagent's kernel should execute on a remote host over SSH. */ remoteHost?: string; } export interface CreateRlmRootSessionOptions { prompt: string; sessionName?: string; cwd: string; model: Model; thinkingLevel: ThinkingLevel; } export interface SubagentRuntimeHost { createRlmSubagentRuntime(options: CreateRlmSubagentRuntimeOptions): Promise; createRlmRootSession?(options: CreateRlmRootSessionOptions): Promise; /** Persist host-owned completion before the child becomes passivation-eligible. */ completeRlmSubagentRuntime?(childId: string, session: AgentSession): boolean; /** Release a host-owned child after its detached initial task settles. */ releaseRlmSubagentRuntime?: ( runtime: RlmSubagentRuntime, options: CreateRlmSubagentRuntimeOptions, status: "done" | "error" | "cancelled", ) => Promise; /** Close or remove the host-owned child; session is absent when a persisted child is still passive. */ deleteRlmSubagentRuntime(childId: string, session?: AgentSession): Promise; disposeRlmSubagentRuntimes?(): Promise; }