import type { EvalStatusEvent, EvalView } from "@oh-my-pi/pi-tui/tools/eval"; import { Snowflake } from "@oh-my-pi/pi-utils"; import type { Subprocess } from "bun"; import type { ShadowBarrier, ShadowControlNode, ShadowOperation } from "./speculation/types.js"; import { type EvalDisplayOutput } from "./types.js"; export type KernelRuntimeEnv = Record; export interface KernelExecuteOptions { id?: string; /** Source filename for file-backed execution and tracebacks. */ filename?: string; /** Runtime working directory applied immediately before this request executes. */ cwd?: string; /** Managed runtime environment variables applied immediately before this request executes. */ env?: Record | Record; signal?: AbortSignal; onChunk?: (text: string) => Promise | void; onDisplay?: (output: EvalDisplayOutput) => Promise | void; timeoutMs?: number; silent?: boolean; storeHistory?: boolean; allowStdin?: boolean; } export interface KernelExecuteResult { status: "ok" | "error"; executionCount?: number; error?: { name: string; value: string; traceback: string[]; }; cancelled: boolean; timedOut: boolean; stdinRequested: boolean; /** True when an atomic retained-runtime admission check rejected this execution before user code ran. */ admissionRejected?: boolean; /** Tool-bridge envelope the runner sent in a `reply` frame (tool requests only). */ reply?: unknown; /** * True when the kernel subprocess was killed as part of settling this * execution (e.g. SIGINT was ignored and we escalated to shutdown, or the * kernel died unexpectedly). When false, the kernel remains reusable. */ kernelKilled?: boolean; } export interface KernelShutdownResult { confirmed: boolean; } export interface KernelShutdownOptions { signal?: AbortSignal; timeoutMs?: number; } /** Per-language lifecycle configuration consumed by each kernel's `start()`. */ export interface KernelStartOptions { cwd: string; env?: Record; /** Explicit interpreter path; skips discovery when set. */ interpreter?: string; signal?: AbortSignal; deadlineMs?: number; } /** Per-language configuration handed to {@link BaseKernel} by each subclass. */ export interface BaseKernelOptions { /** Human-readable language label used in log messages and errors. */ languageName: string; /** When true, every IPC frame is logged at debug level. */ traceIpc: boolean; /** Wire payload asking the runner to exit cleanly. */ exitPayload: string; /** How long to wait after SIGINT before escalating to subprocess termination. */ interruptEscalationMs: number; /** Default grace period applied by {@link BaseKernel.shutdown}. */ shutdownGraceMs: number; /** Serializes an execution request into the runner's wire protocol. */ buildPayload: (code: string, msgId: string, options?: TExecuteOptions) => string; /** Wire payload naming the execution a following SIGINT is meant for. */ interruptPayload?: (msgId: string) => string; } export type FrameType = "started" | "stdout" | "stderr" | "view" | "image" | "status" | "reply" | "error" | "done" | "shadow_snapshot" | "shadow_plan"; export interface Frame { type: FrameType; id?: string; data?: string; text?: string; view?: EvalView; image?: Record; event?: EvalStatusEvent; value?: unknown; ename?: string; evalue?: string; traceback?: string[]; status?: "ok" | "error"; operations?: ShadowOperation[]; controls?: ShadowControlNode[]; barrier?: ShadowBarrier | null; executionCount?: number; cancelled?: boolean; eligible?: boolean; reason?: string; revision?: number; digest?: string; admissionRejected?: boolean; values?: Record; } export declare function getRemainingTimeMs(deadlineMs?: number): number | undefined; /** * True when `pid` is safe to use as a process-group target for `kill(2)`. * * `process.kill(-pid, …)` is a group signal, and the degenerate targets are * catastrophic rather than merely useless: `-0` signals *our own* process group * (omp would kill itself along with the whole terminal job) and `-1` signals * every process the user is permitted to signal. Both must be rejected before * the negation is applied. */ export declare function isSignalableProcessGroup(pid: number | undefined): pid is number; /** * Signal the whole process group led by `pid`, returning true when a signal was * actually delivered. * * Kernels are spawned with `detached: true` on POSIX (see `shouldDetachKernel`), * so each runner calls `setsid()` and becomes the leader of its own session and * process group. Signalling only the direct PID therefore leaves anything the * runner itself spawned behind, and those orphans keep the kernel's pipes open * for the remainder of the omp process lifetime (#7714). * * Windows has no process groups, so this is a no-op there and callers keep * relying on the direct-PID kill. */ export declare function killProcessGroup(pid: number | undefined, signal: NodeJS.Signals): boolean; export declare function createAbortError(name: "AbortError" | "TimeoutError", message: string): Error; export declare function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void; export declare function isTimeoutReason(reason: unknown): boolean; /** * Shared subprocess-backed kernel machinery for the language runners. Each * language subclasses this, supplying its binary/runner via a static `start()` * and its wire protocol via {@link BaseKernelOptions.buildPayload}. The IPC loop * speaks NDJSON: the runner emits one JSON {@link Frame} per line; outbound * requests are serialized by `buildPayload` (which may itself be NDJSON or any * other line-delimited encoding). * * `TExecuteOptions` is the language's own execute-options type so each runner's * `buildPayload` sees its precise option shape (e.g. environment-map variants). */ export declare abstract class BaseKernel { #private; readonly id: string; constructor(id: string, options: BaseKernelOptions); setProcess(proc: Subprocess<"pipe", "pipe", "pipe">): void; isAlive(): boolean; /** * Settles once the kernel stops accepting work: subprocess exit or the start * of a shutdown. Retained-state reporting uses it to mark the kernel dead. */ whenDead(): Promise; execute(code: string, options?: TExecuteOptions): Promise; /** * Submit a raw runner request. Cancelling it settles it at once, without SIGINT; * a written request is followed by `cancelPayload`, when given, so the runner can stop its work. */ submitRequest(msgId: string, payload: string, options?: TExecuteOptions, cancelPayload?: string): Promise; requestControl(payload: string, id?: Snowflake, timeoutMs?: number): Promise; interrupt(): Promise; /** * Concurrent calls (e.g. several in-flight requests hitting the same broken pipe) share one * shutdown sequence. An unconfirmed shutdown clears the slot so a later call can retry. */ shutdown(options?: KernelShutdownOptions): Promise; executeWithBudget(code: string, signal: AbortSignal | undefined, timeoutMs: number, label: string): Promise; }