import type { AgentToolResult, AgentToolUpdateCallback, } from "@earendil-works/pi-coding-agent"; import { type TUnsafe, Type } from "typebox"; import type { HostToKernelMessage, KernelToHostMessage, } from "../bridge/protocol.ts"; import type { TruncationMeta } from "../output/output-meta.ts"; export const evalLanguageOrder = ["py", "js", "rb", "ts"] as const; export type EvalLanguage = (typeof evalLanguageOrder)[number]; export type EnabledEvalLanguages = Readonly>; export function enabledLanguageList( enabled: EnabledEvalLanguages ): EvalLanguage[] { return evalLanguageOrder.filter((language) => enabled[language]); } export interface EvalToolInput { readonly action?: "run"; readonly code: string; readonly language: EvalLanguage; readonly on_timeout?: "detach" | "error"; readonly reset?: boolean; /** * Named strings exposed as `π.` inside the cell, useful for content * that is awkward to quote in code (long literals, file contents, prompts). */ readonly strings?: Readonly>; readonly timeout?: number; readonly title?: string; readonly tools?: readonly string[]; } export interface EvalControlInput { readonly action: "peek" | "stop"; readonly cell_id: string; } export type EvalToolRequest = EvalToolInput | EvalControlInput; const fullEvalInputSchema = Type.Object({ action: Type.Optional( Type.Union( [Type.Literal("run"), Type.Literal("peek"), Type.Literal("stop")], { description: "Defaults to run. peek and stop require cell_id.", } ) ), language: Type.Optional( Type.Union([ Type.Literal("py"), Type.Literal("js"), Type.Literal("rb"), Type.Literal("ts"), ]) ), code: Type.Optional(Type.String({ description: "Cell body, verbatim." })), strings: Type.Optional( Type.Record(Type.String(), Type.String(), { description: "Named strings exposed as π. inside the cell, useful for content that is awkward to quote in code.", }) ), title: Type.Optional(Type.String({ description: "Short transcript label." })), timeout: Type.Optional( Type.Number({ minimum: 1, description: "Timeout in seconds (default: the session cellTimeoutSeconds, 120). Exceeding it detaches in interactive sessions; set it higher for heavy compute or long tool calls.", }) ), on_timeout: Type.Optional( Type.Union([Type.Literal("detach"), Type.Literal("error")], { description: "Timeout behavior. Interactive sessions detach by default; print/json sessions error by default.", }) ), reset: Type.Optional( Type.Boolean({ description: "Reset this language kernel before running." }) ), cell_id: Type.Optional( Type.String({ description: "Detached eval cell id for peek or stop." }) ), tools: Type.Optional( Type.Array(Type.String({ minLength: 1 }), { description: "Tool names this cell may call. Omit to allow all active tools.", }) ), }); /** Runtime accepts a discriminated run/control union. */ export type EvalInputSchema = TUnsafe & Pick; export function createEvalInputSchema( enabled: EnabledEvalLanguages ): EvalInputSchema { const languages = enabledLanguageList(enabled); if (languages.length === 0) { throw new Error("eval requires at least one enabled language"); } const languageSchema = languages.length === 1 ? Type.Union([Type.Literal(languages[0])]) : Type.Union(languages.map((item) => Type.Literal(item))); return Type.Unsafe( Type.Object({ action: Type.Optional( Type.Union( [Type.Literal("run"), Type.Literal("peek"), Type.Literal("stop")], { description: "Defaults to run. peek and stop require cell_id.", } ) ), language: Type.Optional(languageSchema), code: Type.Optional(Type.String({ description: "Cell body, verbatim." })), strings: Type.Optional( Type.Record(Type.String(), Type.String(), { description: "Named strings exposed as π. inside the cell, useful for content that is awkward to quote in code.", }) ), title: Type.Optional( Type.String({ description: "Short transcript label." }) ), timeout: Type.Optional( Type.Number({ minimum: 1, description: "Timeout in seconds." }) ), on_timeout: Type.Optional( Type.Union([Type.Literal("detach"), Type.Literal("error")], { description: "Timeout behavior. Interactive sessions detach by default; print/json sessions error by default.", }) ), reset: Type.Optional( Type.Boolean({ description: "Reset this language kernel before running.", }) ), cell_id: Type.Optional( Type.String({ description: "Detached eval cell id for peek or stop." }) ), tools: Type.Optional( Type.Array(Type.String({ minLength: 1 }), { description: "Tool names this cell may call. Omit to allow all active tools.", }) ), }) ) as EvalInputSchema; } export type EvalKernelResult = Extract; export type EvalToolCallMessage = Extract< KernelToHostMessage, { type: "tool-call" } >; export interface EvalKernelRunInput { readonly cellId: string; readonly code: string; readonly strings?: Readonly>; readonly timeoutMs?: number; } export interface KernelInterruptHandle { /** Resolves once the kernel knows whether user state survived the interrupt. */ readonly stateRetained: Promise; } export interface EvalKernel { close: () => Promise; deliverToolReply: ( message: Extract ) => void; interrupt: (reason?: string) => Promise; reset: () => Promise; run: (input: EvalKernelRunInput) => Promise; } export interface EvalKernelManager { getKernel: ( language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void ) => Promise; } export type ExecuteTool = ( toolName: string, params: unknown, options?: { signal?: AbortSignal; onUpdate?: AgentToolUpdateCallback; activateInactiveTool?: boolean; } ) => Promise>; export interface EvalToolCallSummary { readonly args?: unknown; readonly argsTruncated?: boolean; readonly callId?: string; readonly durationMs?: number; readonly error?: string; readonly name: string; readonly ok: boolean; readonly resultPreview?: string; } export type EvalStatusEvent = { readonly op: string } & Readonly< Record >; /** * Per-active-cell hooks the eval tool registers with the session manager so * the HTTP bridge route (subprocess py/rb tool calls) records enrichment * into the cell's toolCalls and forwards agent() progress status events — the * same pipeline the JS kernel path gets from CellHandler. */ export interface BridgeCallSession { readonly emitStatus: (event: EvalStatusEvent) => void; readonly trackToolCall: (summary: EvalToolCallSummary) => void; } // Single source of truth for the cell lifecycle status vocabulary. Consumers // that only ever see a subset (foreground cells, detached cells) derive their // own types from this one via Exclude. export type EvalCellStatus = | "pending" | "running" | "detached" | "complete" | "error" | "cancelled"; export interface EvalCellResult { readonly code: string; readonly durationMs?: number; readonly exitCode?: number; readonly hasMarkdown?: boolean; readonly index: number; readonly language: EvalLanguage; readonly output: string; readonly status: EvalCellStatus; readonly statusEvents?: readonly EvalStatusEvent[]; readonly title?: string; } export interface EvalToolDetails { readonly cells?: readonly EvalCellResult[]; readonly durationMs: number; readonly isError?: boolean; readonly jsonOutputs?: readonly unknown[]; readonly language: EvalLanguage; readonly languages?: readonly EvalLanguage[]; readonly meta?: TruncationMeta; readonly notice?: string; readonly phase?: string; readonly statusEvents?: readonly EvalStatusEvent[]; readonly title?: string; readonly toolCalls: readonly EvalToolCallSummary[]; readonly truncated: boolean; } /** * Renderer state for one eval tool row (see TECHNICAL.md, Output and rendering): pi's * ToolRenderContext.state is a per-row object the host initializes as {}; the * port stores the self-driven spinner frame counter and its interval handle * there so the frame advances while the result streams and stops on the * terminal render. */ export interface EvalRenderState { /** Spinner frame index; undefined until the first streaming render. */ frame: number | undefined; /** Live spinner interval, or undefined once the row is terminal. */ interval: ReturnType | undefined; }