Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
26 kB · 718 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719import { existsSync } from "node:fs";import { join } from "node:path";import type { AgentTool } from "@earendil-works/pi-agent-core";import type { ImageContent, TextContent } from "@earendil-works/pi-ai";import { type Static, Type } from "typebox";import { IMAGE_MIME_TYPES } from "../../utils/mime.js";import { resolveKernelBashShell } from "../../utils/shell.js";import type { ExtensionContext, ToolDefinition } from "../extensions/types.js";import { withKernelBootPermit } from "../kernel/boot-gate.js";import type { KernelBootstrapProgressHandler } from "../kernel/bootstrap.js";import { type ExecuteResult, type HostRequestHandlers, type KernelAttachment, KernelBusyAfterInterruptError, type KernelClient, type KernelDiffDisplay, type KernelSentAgentMessage, type RemoteHostConfig, ReplKernelManager,} from "../kernel/index.js";import { manifestPathIn, type RestoreResult, snapshotPathIn } from "../kernel/state-snapshot.js";import type { PythonSkillRuntimeInfo } from "../skills.js";import { wrapToolDefinition } from "./tool-definition-wrapper.js";
const RLM_BOOTSTRAP_HEADER_CODE = `import asyncioimport os as _prime_agent_os
_prime_agent_os.environ["NO_COLOR"] = "1"`.trim();
const RLM_BOOTSTRAP_RUNTIME_CODE = `try: import rlm as _prime_agent_rlm_module rlm = _prime_agent_rlm_module.rlm bash = _prime_agent_rlm_module.bash import rlm.mcp as mcp import remoteexcept Exception as _prime_agent_rlm_error: _PRIME_AGENT_RLM_IMPORT_ERROR = str(_prime_agent_rlm_error)
class _PrimeAgentMissingRlm: def _raise_missing(self): raise RuntimeError( "prime-agent-runtime is not installed in this kernel. " "Remove ~/.prime/agent/kernel-venv so prime-agent can rebuild it, or set " "PRIME_AGENT_KERNEL_PYTHON to a kernel environment with prime-agent-runtime installed. " f"Import error: {_PRIME_AGENT_RLM_IMPORT_ERROR}" )
async def run(self, prompt, **kwargs): self._raise_missing()
async def find_models(self, query="", limit=8): self._raise_missing()
async def create_session(self, prompt, **kwargs): self._raise_missing()
async def list_subagents(self): self._raise_missing()
async def delete_subagent(self, target): self._raise_missing()
async def __call__(self, prompt, **kwargs): return await self.run(prompt, **kwargs)
rlm = _PrimeAgentMissingRlm()
def bash(command): rlm._raise_missing()`.trim();
export function buildRlmBootstrapCode(pythonSkills: readonly PythonSkillRuntimeInfo[] = []): string { const baseCode = [RLM_BOOTSTRAP_HEADER_CODE, RLM_BOOTSTRAP_RUNTIME_CODE].join("\n\n"); const importNames = [...new Set(pythonSkills.map((skill) => skill.importName))]; if (importNames.length === 0) { return baseCode; }
return `${baseCode}
import importlib as _prime_agent_importlibimport inspect as _prime_agent_inspectimport sys as _prime_agent_sysimport types as _prime_agent_types
class _PrimeAgentCallableSkillModule(_prime_agent_types.ModuleType): async def __call__(self, *args, **kwargs): result = self.run(*args, **kwargs) if _prime_agent_inspect.isawaitable(result): return await result return result
class _PrimeAgentUnavailableSkill: def __init__(self, name, error): self.__name__ = name self._prime_agent_import_error = error self.__doc__ = f"Python skill {name} is unavailable: {error}"
async def run(self, *args, **kwargs): raise RuntimeError( f"Python skill {self.__name__} is unavailable in this kernel. " f"Import error: {self._prime_agent_import_error}" )
async def __call__(self, *args, **kwargs): return await self.run(*args, **kwargs)
def __repr__(self): return f"<unavailable Python skill {self.__name__!r}: {self._prime_agent_import_error}>"
def _prime_agent_wrap_skill_module(module): run = getattr(module, "run", None) if not callable(run): return module if isinstance(module, _PrimeAgentCallableSkillModule): return module wrapped = _PrimeAgentCallableSkillModule(module.__name__) wrapped.__dict__.update(module.__dict__) try: wrapped.__signature__ = _prime_agent_inspect.signature(run) except Exception: pass doc = getattr(run, "__doc__", None) if doc: wrapped.__doc__ = doc _prime_agent_sys.modules[module.__name__] = wrapped return wrapped
_PRIME_AGENT_SKILL_IMPORT_ERRORS = {}
for _prime_agent_skill_name in ${JSON.stringify(importNames)}: try: globals()[_prime_agent_skill_name] = _prime_agent_wrap_skill_module( _prime_agent_importlib.import_module(_prime_agent_skill_name) ) except Exception as _prime_agent_skill_error: _PRIME_AGENT_SKILL_IMPORT_ERRORS[_prime_agent_skill_name] = str(_prime_agent_skill_error) globals()[_prime_agent_skill_name] = _PrimeAgentUnavailableSkill( _prime_agent_skill_name, str(_prime_agent_skill_error), )
if "search" in globals() and hasattr(globals()["search"], "ls"): globals()["ls"] = globals()["search"].ls`.trim();}
const ipythonSchema = Type.Object({ code: Type.String({ description: "Python code to execute in the persistent Python REPL. Use the target project's own environment for project imports, tests, scripts, CLIs, and dependency checks instead of direct kernel imports.", }),});
const BUSY_KERNEL_WAIT_CHOICE = "Wait and preserve state";const BUSY_KERNEL_KILL_CHOICE = "Kill kernel and restart";const BUSY_KERNEL_PROMPT = [ "Interrupted Python cell is still running", "Ctrl+C sent an interrupt, but the previous cell has not stopped yet. A new command cannot start until it finishes.", "Waiting preserves the current kernel state. Killing restarts the kernel and loses in-memory variables, imports, and running tasks.",].join("\n");const KERNEL_RESTART_NOTICE = [ "<ipython_kernel_reset>", "The Python kernel was restarted after a previous interrupted cell kept running. Variables, imports, async tasks, and open resources from before the restart are no longer available; recreate them before using them.", "</ipython_kernel_reset>",].join("\n");
function createAbortError(): Error { return new Error("Python execution aborted");}
function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined, onAbort?: () => void): Promise<T> { if (!signal) { return promise; } if (signal.aborted) { onAbort?.(); return Promise.reject(createAbortError()); } return new Promise<T>((resolve, reject) => { let settled = false; const cleanup = () => signal.removeEventListener("abort", abort); const abort = () => { if (settled) { return; } settled = true; cleanup(); onAbort?.(); reject(createAbortError()); }; signal.addEventListener("abort", abort, { once: true }); promise.then( (value) => { if (settled) { return; } settled = true; cleanup(); resolve(value); }, (error: unknown) => { if (settled) { return; } settled = true; cleanup(); reject(error); }, ); });}
function createLinkedAbortSignal(sources: readonly (AbortSignal | undefined)[]): { signal: AbortSignal; cleanup: () => void;} { const controller = new AbortController(); const cleanups: Array<() => void> = []; const abort = () => controller.abort(); for (const source of sources) { if (!source) { continue; } if (source.aborted) { controller.abort(); continue; } const listener = () => abort(); source.addEventListener("abort", listener, { once: true }); cleanups.push(() => source.removeEventListener("abort", listener)); } return { signal: controller.signal, cleanup: () => { for (const cleanup of cleanups) { cleanup(); } }, };}
function setWorkingMessage(ctx: ExtensionContext | undefined, message?: string): void { try { ctx?.ui.setWorkingMessage(message); } catch { // Stale UI context; cosmetic only. }}
export type IpythonToolInput = Static<typeof ipythonSchema>;
export interface IpythonToolDetails { durationMs?: number; status?: "ok" | "error" | "aborted" | "starting"; errorEname?: string; stdout?: string; stderr?: string; result?: string; /** Output that arrived without this cell's id (threads, other cells' leftovers), shown separately from stdout. */ backgroundOutput?: string; /** Diffs streamed from file edits, rendered by the cell view. */ diffs?: KernelDiffDisplay[]; /** Media attachments loaded into context (e.g. by the attach-image skill). */ attachments?: KernelAttachment[]; /** Agent messages sent from this cell. */ sentAgentMessages?: KernelSentAgentMessage[]; /** True when this result came after killing and restarting a busy kernel. */ kernelRestarted?: boolean; error?: { ename: string; evalue: string; traceback: string[]; };}
export interface IpythonToolOptions { /** Python override. Must have prime-agent-runtime installed. */ python?: string; env?: Record<string, string>; /** Command prefix prepended to every bash() command. */ commandPrefix?: string; /** Shell used by bash(). */ shellPath?: string; sessionId?: string; /** Typed host request handlers for the kernel↔host bridge (rlm.run, goal.*, …). */ hostHandlers?: HostRequestHandlers; pythonSkills?: readonly PythonSkillRuntimeInfo[]; /** Per-session artifact dir where the kernel namespace snapshot is stored. Omit to disable snapshots. */ snapshotDir?: string; /** Resolves before this kernel starts — e.g. the previous provisioner's dispose, so a * /reload's old-kernel snapshot flush can't race the new kernel's restore. */ readyGate?: Promise<unknown>; /** * Fires once per kernel start when a previous session's namespace was revived * (some names restored or some failed), so the session can tell the model. */ onRestore?: (result: RestoreResult) => void; onLateSentAgentMessage?: (toolCallId: string, message: KernelSentAgentMessage) => void; /** Remote host configuration when running on a remote host over SSH. */ remote?: RemoteHostConfig; /** Shared provisioner owning the kernel lifecycle. When provided, the remaining options are ignored. */ provisioner?: IpythonKernelProvisioner;}
/** * Owns the lazy create+start+runtime-bootstrap of one session's Python kernel. * * Concurrent ensure() calls await the same in-flight startup, a failed startup * clears the memo so the next call retries fresh, and progress listeners can * attach mid-flight (a tool call racing a background prewarm()). */export class IpythonKernelProvisioner { private managerPromise?: Promise<KernelClient>; private startedManager?: KernelClient; private readonly startupListeners = new Set<KernelBootstrapProgressHandler>(); private lastStartupMessage?: string; private _lastRestore?: RestoreResult; private readonly disposeController = new AbortController(); /** Snapshot policy of the dispose that aborted a startup, honored by startKernel's failure teardown. */ private disposeSnapshot = true;
constructor( private readonly cwd: string, private readonly options?: Omit<IpythonToolOptions, "provisioner">, ) {}
/** The kernel manager, once a startup has completed successfully. */ get manager(): KernelClient | undefined { return this.startedManager; }
/** Result of reviving a prior session's namespace on the last kernel start, if any. */ get lastRestore(): RestoreResult | undefined { return this._lastRestore; }
/** Start the kernel in the background. Failures are swallowed here and surface on the next ensure(). */ prewarm(): void { void this.ensure().catch(() => {}); }
/** Whether a kernel has finished starting and is currently running. */ get hasRunningKernel(): boolean { return this.startedManager?.isRunning ?? false; }
/** Remove live variables above the snapshot's per-variable size limit. */ async pruneOversizedVariables(): Promise<string[] | null> { const m = this.startedManager ?? (await this.managerPromise?.catch(() => undefined)); const result = await m?.pruneOversizedVariables(); return result ? (result.pruned ?? []) : null; }
/** Live user-defined names in the kernel namespace, or null if listing failed / no kernel. */ async listNamespaceNames(signal?: AbortSignal): Promise<string[] | null> { const m = this.startedManager ?? (await this.managerPromise?.catch(() => undefined)); return (await m?.listNamespaceNames(signal)) ?? null; }
/** Dispose the kernel owned by this provisioner, including one still starting up. */ async dispose(options?: { snapshot?: boolean }): Promise<void> { this.disposeSnapshot = options?.snapshot ?? true; // Drops a still-queued boot out of the semaphore and short-circuits an // in-flight startKernel before it spawns, so a disposed session's boot // doesn't waste a slot during a fan-out. this.disposeController.abort(); const pending = this.managerPromise; this.managerPromise = undefined; this.startedManager = undefined; if (!pending) return; try { const m = await pending; await m.shutdown({ snapshot: this.disposeSnapshot, drainHostRequests: true }); } catch { // a failed startup already cleaned up after itself } }
async kill(): Promise<void> { const pending = this.managerPromise; this.managerPromise = undefined; this.startedManager = undefined; if (!pending) return; try { const m = await pending; await m.kill(); } catch { // a failed startup already cleaned up after itself } }
ensure(onProgress?: KernelBootstrapProgressHandler, signal?: AbortSignal): Promise<KernelClient> { if (signal?.aborted) { return Promise.reject(createAbortError()); } // Only a terminally dead kernel drops the memo; a repairing manager (idle/starting) recovers itself. if (this.startedManager?.isDefunct) { this.managerPromise = undefined; this.startedManager = undefined; } let cleanupProgressListener: (() => void) | undefined; if (onProgress && !this.startedManager) { this.startupListeners.add(onProgress); cleanupProgressListener = () => { this.startupListeners.delete(onProgress); signal?.removeEventListener("abort", cleanupProgressListener!); }; signal?.addEventListener("abort", cleanupProgressListener, { once: true }); // Joining an in-flight startup: replay the current stage. if (this.managerPromise && this.lastStartupMessage) { onProgress(this.lastStartupMessage); } } if (!this.managerPromise) { const startup = this.startKernel(signal); this.managerPromise = startup; startup.then( (m) => { if (this.managerPromise === startup) { this.startedManager = m; } this.settleStartup(); }, () => { // Clear the memo so the next ensure() retries instead of // rethrowing a cached rejection forever. if (this.managerPromise === startup) { this.managerPromise = undefined; } this.settleStartup(); }, ); } return raceWithAbort(this.managerPromise, signal).finally(() => { cleanupProgressListener?.(); }); }
private settleStartup(): void { this.startupListeners.clear(); this.lastStartupMessage = undefined; }
private emitStartupProgress(message: string): void { this.lastStartupMessage = message; for (const listener of [...this.startupListeners]) { listener(message); } }
private async startKernel(signal?: AbortSignal): Promise<KernelClient> { const startupAbort = createLinkedAbortSignal([this.disposeController.signal, signal]); const startupSignal = startupAbort.signal; // Wait for a previous provisioner (e.g. on /reload) to finish disposing — and // flushing its final snapshot — before we read that snapshot back, so the two // kernels can't race over the same on-disk file. Guarded so the common // no-gate path stays synchronous (callers rely on prompt startup progress). try { if (this.options?.readyGate) { await raceWithAbort( this.options.readyGate.catch(() => {}), startupSignal, ); } const snapshotDir = this.options?.snapshotDir; // Always inject an absolute trusted shell (undefined only on win32 // without bash, where the runtime's teaching error fires instead). const shellPath = resolveKernelBashShell(this.options?.shellPath); const commandPrefix = this.options?.commandPrefix; const bootstrapCode = buildRlmBootstrapCode(this.options?.pythonSkills); const m = new ReplKernelManager({ python: this.options?.python, cwd: this.cwd, // bash() reads these to pick its shell and command prefix. env: { ...this.options?.env, ...(shellPath ? { PRIME_AGENT_BASH_SHELL: shellPath } : {}), ...(commandPrefix ? { PRIME_AGENT_BASH_COMMAND_PREFIX: commandPrefix } : {}), }, sessionId: this.options?.sessionId, hostHandlers: this.options?.hostHandlers, pythonSkills: this.options?.pythonSkills, // Only persistent sessions (which have an artifact dir) get a revivable snapshot. snapshot: snapshotDir ? { path: snapshotPathIn(snapshotDir), manifestPath: manifestPathIn(snapshotDir) } : undefined, stderrLogPath: snapshotDir ? join(snapshotDir, "kernel-stderr.log") : undefined, bootstrapCode, remote: this.options?.remote, }); let pendingRestore: RestoreResult | undefined; try { // Emitted synchronously (before the permit await) so a listener attaching // mid-flight can replay the current stage. this.emitStartupProgress("Starting Python kernel..."); // Only the process spawn + port resolve contends for OS resources under a // fan-out, and it is bounded by start()'s own timeouts — so the permit // covers only start(). Restore/bootstrap run per-kernel afterwards and are // unbounded execute()s; holding the global permit across them could pin it // forever on a wedged bootstrap and starve every other session's boot. await withKernelBootPermit(() => { // Disposed while queued for the permit — don't spawn a kernel nobody wants. if (startupSignal.aborted) throw new Error("Kernel provisioner disposed before start"); return m.start({ onBootstrapProgress: (message) => this.emitStartupProgress(message), signal: startupSignal, }); }, startupSignal); // Revive a prior session's namespace before the bootstrap, so the bootstrap // then overwrites live handles (rlm, skills) on top of anything restored. if (snapshotDir) { const snapshotExisted = existsSync(snapshotPathIn(snapshotDir)); this.emitStartupProgress("Restoring Python state..."); const restore = await raceWithAbort(m.restoreState(), startupSignal); if (snapshotExisted) { pendingRestore = restore ?? { restored: [], failed: [], path: snapshotPathIn(snapshotDir) }; } } this.emitStartupProgress("Preparing Python runtime..."); const bootstrap = await m.execute(bootstrapCode, { signal: startupSignal, }); if (bootstrap.status !== "ok") { const details = [bootstrap.stderr, bootstrap.error?.traceback.join("\n")].filter(Boolean).join("\n"); throw new Error(`Failed to initialize rlm runtime in the Python kernel:\n${details}`); } } catch (error) { // Never leak the kernel process if startup fails after spawn — and never // surface the failure before the teardown (final snapshot flush included) // finished, or a replacement provisioner gated on this dispose could // race the still-flushing kernel over the same snapshot files. await m.shutdown({ snapshot: this.disposeSnapshot, drainHostRequests: true }).catch(() => undefined); throw error; } // Only tell the model what was revived once the kernel is actually usable — // a notice claiming restored state must never outlive a failed bootstrap. if (pendingRestore) { this._lastRestore = pendingRestore; this.options?.onRestore?.(pendingRestore); } return m; } finally { startupAbort.cleanup(); } }}
async function chooseBusyKernelAction( ctx: ExtensionContext | undefined, signal: AbortSignal | undefined,): Promise<"wait" | "kill" | "cancel"> { if (!ctx?.hasUI) { return "cancel"; } const choice = await ctx.ui.select(BUSY_KERNEL_PROMPT, [BUSY_KERNEL_WAIT_CHOICE, BUSY_KERNEL_KILL_CHOICE], { signal, }); if (choice === BUSY_KERNEL_WAIT_CHOICE) { return "wait"; } if (choice === BUSY_KERNEL_KILL_CHOICE) { return "kill"; } return "cancel";}
async function executeWithBusyKernelChoice( provisioner: IpythonKernelProvisioner, reportStartupProgress: KernelBootstrapProgressHandler, toolCallId: string, code: string, signal: AbortSignal | undefined, onStream: (chunk: string, name: "stdout" | "stderr") => void, onWorkingMessage: (message?: string) => void, onLateSentAgentMessage: ((toolCallId: string, message: KernelSentAgentMessage) => void) | undefined, ctx: ExtensionContext | undefined,): Promise<{ result: ExecuteResult; kernelRestarted: boolean }> { let kernelRestarted = false; while (true) { const m = await provisioner.ensure(reportStartupProgress, signal); try { return { result: await m.execute(code, { signal, onStream, onLateSentAgentMessage: onLateSentAgentMessage ? (message) => onLateSentAgentMessage(toolCallId, message) : undefined, }), kernelRestarted, }; } catch (error) { if (!(error instanceof KernelBusyAfterInterruptError) || signal?.aborted) { throw error; } const action = await chooseBusyKernelAction(ctx, signal); if (action === "wait") { onWorkingMessage("Waiting for Python kernel..."); continue; } if (action === "kill") { onWorkingMessage("Restarting Python kernel..."); await provisioner.kill(); kernelRestarted = true; continue; } throw error; } }}
/** Turn kernel image attachments into `ImageContent` blocks; non-image types are dropped. */export function imageBlocksFromAttachments(attachments: readonly KernelAttachment[] | undefined): ImageContent[] { if (!attachments) return []; return attachments .filter((a) => IMAGE_MIME_TYPES.has(a.mimeType)) .map((a) => ({ type: "image", data: a.data, mimeType: a.mimeType }));}
export function createIpythonToolDefinition( cwd: string, options?: IpythonToolOptions,): ToolDefinition<typeof ipythonSchema, IpythonToolDetails> { const provisioner = options?.provisioner ?? new IpythonKernelProvisioner(cwd, options);
return { name: "ipython", label: "ipython", description: "Execute Python code in a persistent Python REPL. Top-level `await` is supported. Variables, imports, and loaded data persist across calls, and are revived on a best-effort basis when a session is resumed (objects that cannot be serialized are dropped and reported). Run shell commands with `bash('cmd')` / `await bash('cmd')`. Project imports, tests, scripts, CLIs, and dependency checks should run through the target project's own environment.", promptSnippet: "ipython - persistent Python REPL for code, state, and bash() orchestration", // The kernel is single-threaded — pi must not run two ipython calls in parallel within a batch. executionMode: "sequential", parameters: ipythonSchema, execute: async (toolCallId, params, signal, onUpdate, ctx) => { let hasWorkingMessage = false; const setToolWorkingMessage = (message?: string) => { setWorkingMessage(ctx, message); hasWorkingMessage = message !== undefined; }; const reportStartupProgress: KernelBootstrapProgressHandler = (message) => { setToolWorkingMessage(message); onUpdate?.({ content: [{ type: "text", text: message }], details: { status: "starting" }, }); };
try { const { result: r, kernelRestarted } = await executeWithBusyKernelChoice( provisioner, reportStartupProgress, toolCallId, params.code, signal, (chunk) => { onUpdate?.({ content: [{ type: "text", text: chunk }], details: { status: "ok" }, }); }, setToolWorkingMessage, options?.onLateSentAgentMessage, ctx, );
let text = r.stdout; if (r.stderr) text += (text ? "\n" : "") + r.stderr; if (r.result) text += (text ? "\n" : "") + r.result; if (r.status === "error" && r.error) { text += (text ? "\n" : "") + r.error.traceback.join("\n"); } if (r.backgroundOutput) { text += `${text ? "\n" : ""}[background output (unattributed)]\n${r.backgroundOutput}`; } if (kernelRestarted) { text = text ? `${KERNEL_RESTART_NOTICE}\n\n${text}` : KERNEL_RESTART_NOTICE; }
const imageBlocks = imageBlocksFromAttachments(r.attachments); const content: (TextContent | ImageContent)[] = [{ type: "text", text: text || "" }, ...imageBlocks];
return { content, details: { durationMs: r.durationMs, status: r.status, errorEname: r.error?.ename, stdout: r.stdout, stderr: r.stderr, result: r.result, backgroundOutput: r.backgroundOutput, diffs: r.diffs, attachments: r.attachments, sentAgentMessages: r.sentAgentMessages, kernelRestarted, error: r.error, }, isError: r.status === "error" || r.status === "aborted", }; } finally { if (hasWorkingMessage) { setToolWorkingMessage(); } } }, };}
export function createIpythonTool(cwd: string, options?: IpythonToolOptions): AgentTool<typeof ipythonSchema> { return wrapToolDefinition(createIpythonToolDefinition(cwd, options));}