import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent"; import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { getExperimentalToolSampling } from "../tool-sampling.ts"; import { canExecuteNotebookControlInsideExec } from "../notebook-mode/control-contract.ts"; import type { SharedCodeModeRuntime } from "./shared-runtime.ts"; import type { NotebookControlRequest, NotebookControlResult, ProgrammaticCodeModeToolDefinition, ToolExecutionContext, } from "./types.ts"; export const NOTEBOOK_PARAMETERS = Type.Union([ Type.Object({ action: StringEnum(["status", "list"]), query: Type.Optional(Type.String()), }, { additionalProperties: false }), Type.Object({ action: StringEnum(["checkpoint", "restart", "diagnostics", "reset"]), }, { additionalProperties: false }), Type.Object({ action: StringEnum(["save", "load"]), name: Type.String(), }, { additionalProperties: false }), Type.Object({ action: StringEnum(["pin", "unpin", "release"]), names: Type.Array(Type.String(), { minItems: 1 }), }, { additionalProperties: false }), Type.Object({ action: Type.Literal("prune"), query: Type.String(), }, { additionalProperties: false }), ]); const NOTEBOOK_DESCRIPTION = "Control persistent notebook state: status inspects memory/bindings by query glob; checkpoint; pin/unpin/release names; prune unpinned matches; list/save/load profiles; restart; diagnostics; reset"; type NotebookToolParameters = { action: string; query?: string | undefined; name?: string | undefined; names?: string[] | undefined; }; export function registerNotebookTool(pi: ExtensionAPI, runtime: SharedCodeModeRuntime): void { const constrainedSampling = getExperimentalToolSampling("notebook"); pi.registerTool({ name: "notebook", label: "Notebook", description: NOTEBOOK_DESCRIPTION, promptSnippet: "Inspect, recover, or control notebook state", parameters: NOTEBOOK_PARAMETERS, ...(constrainedSampling ? { constrainedSampling } : {}), async execute(_id, params, signal, _onUpdate, ctx) { const result = await executeNotebookControl(runtime, params, { cwd: ctx.cwd, extensionContext: ctx, }, signal); return { content: [{ type: "text", text: result.message }], details: result.details, }; }, } satisfies ToolDefinition); } export function createNotebookControlProxy( runtime: SharedCodeModeRuntime, ): ProgrammaticCodeModeToolDefinition { return { name: "notebook", usage: "await tools.notebook({ action, query?, name?, names? })", description: NOTEBOOK_DESCRIPTION, deferLoading: true, kind: "function", inputSchema: NOTEBOOK_PARAMETERS, invoke: (input, context, signal) => executeNotebookExecControl(runtime, input as NotebookToolParameters, context, signal), }; } export async function executeNotebookControl( runtime: SharedCodeModeRuntime, params: NotebookToolParameters, context: ToolExecutionContext, signal?: AbortSignal, ): Promise { return executeNormalizedNotebookControl(runtime, normalizeNotebookRequest(params), context, signal); } export async function executeNotebookExecControl( runtime: SharedCodeModeRuntime, params: NotebookToolParameters, context: ToolExecutionContext, signal?: AbortSignal, ): Promise { const request = normalizeNotebookRequest(params); if (!canExecuteNotebookControlInsideExec(request)) return { message: `Notebook ${request.action} was not run because it needs the active exec cell to finish. After exec returns, call notebook with ${JSON.stringify(request)}.`, details: { notRun: true, action: request.action, retry: request }, }; return executeNormalizedNotebookControl(runtime, request, context, signal); } function executeNormalizedNotebookControl( runtime: SharedCodeModeRuntime, request: NotebookControlRequest, context: ToolExecutionContext, signal?: AbortSignal, ): Promise { return runtime.controlNotebook(request, context, signal); } export function normalizeNotebookRequest(params: NotebookToolParameters): NotebookControlRequest { params = { action: params.action, ...(params.query == null ? {} : { query: params.query }), ...(params.name == null ? {} : { name: params.name }), ...(params.names == null ? {} : { names: params.names }), }; if (params.action === "status" || params.action === "list") { if (params.name !== undefined || params.names !== undefined) throw new Error(`notebook ${params.action} accepts query only`); return { action: params.action, ...(params.query === undefined ? {} : { query: params.query }) }; } if (params.action === "save" || params.action === "load") { if (params.query !== undefined || params.names !== undefined) throw new Error(`notebook ${params.action} accepts name only`); if (!params.name) throw new Error(`notebook ${params.action} requires name`); return { action: params.action, name: params.name }; } if (params.action === "release" || params.action === "pin" || params.action === "unpin") { if (params.query !== undefined || params.name !== undefined) throw new Error(`notebook ${params.action} accepts names only`); if (!params.names?.length) throw new Error(`notebook ${params.action} requires at least one name`); return { action: params.action, names: [...new Set(params.names)] }; } if (params.action === "prune") { if (params.name !== undefined || params.names !== undefined) throw new Error("notebook prune accepts query only"); if (!params.query) throw new Error("notebook prune requires query"); return { action: "prune", query: params.query }; } if (params.action !== "checkpoint" && params.action !== "restart" && params.action !== "diagnostics" && params.action !== "reset") { throw new Error(`Unsupported notebook action: ${params.action}`); } if (params.query !== undefined || params.name !== undefined || params.names !== undefined) { throw new Error(`notebook ${params.action} accepts only action`); } return { action: params.action }; }