Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160import { parseSSEStream, type ExecEvent } from "@cloudflare/sandbox";import { tool } from "ai";import { z } from "zod";import { SHELL_INPUT_BYTES, SHELL_OUTPUT_BYTES, SHELL_MAX_MS, type ShellResult,} from "../shared/shell";import type { ToolActivityDescriptor } from "./tool-activity";
export const shellInput = z .object({ command: z .string() .min(1) .max(SHELL_INPUT_BYTES) .refine( (value) => new TextEncoder().encode(value).byteLength <= SHELL_INPUT_BYTES, "Command must fit in 32 KiB", ) .describe( "A complete Bash command. For quote-heavy or multiline JavaScript, use a single-quoted heredoc such as node <<'JS' followed by the script and JS on its own line; do not wrap the script in nested node -e quotes.", ), timeoutMs: z.number().int().min(1000).max(SHELL_MAX_MS).default(30_000), }) .strict();
export interface ShellHost { reserve(): Promise<{ id: string; expiresAt: number }>; launch(id: string, command: string): Promise<ReadableStream<Uint8Array>>; close(id: string): Promise<boolean>;}
/** Ordinary native AI SDK preliminary results; Think owns transport/history. */export function createShellTool(host: (timeoutMs: number) => ShellHost) { return tool({ description: "Run a Bash command/script in a fresh temporary Linux sandbox with Node.js and Bun. Use heredocs to write and process temporary files in this invocation. No files persist between calls. Streams bounded stdout/stderr; kills the container at completion, cancellation or limit. No Python runtime is promised.", inputSchema: shellInput, execute: async function* ({ command, timeoutMs }, { abortSignal }) { const owner = host(timeoutMs); const controller = new AbortController(); let lease: { id: string; expiresAt: number } | undefined; let timer: ReturnType<typeof setTimeout> | undefined; let closing: Promise<boolean> | undefined; const close = () => lease ? (closing ??= owner.close(lease.id).catch(() => false)) : Promise.resolve(false); const cancel = () => { controller.abort("cancelled"); void close(); }; abortSignal?.addEventListener("abort", cancel, { once: true }); const result: ShellResult = { status: "running", stdout: "", stderr: "", outputBytes: 0, exitCode: null, truncated: false, cleanup: "pending", workspaceLifetime: "invocation", }; let lastYield = 0; try { if (abortSignal?.aborted) cancel(); controller.signal.throwIfAborted(); lease = await owner.reserve(); if (controller.signal.aborted) { await close(); controller.signal.throwIfAborted(); } timer = setTimeout( () => { controller.abort("timeout"); void close(); }, Math.max(0, lease.expiresAt - Date.now()), ); yield { ...result }; // Parent owns late launch settlement even if this facet is deleted. const stream = await owner.launch(lease.id, command); for await (const event of parseSSEStream<ExecEvent>( stream, controller.signal, )) { if (event.type === "stdout" || event.type === "stderr") { const bytes = new TextEncoder().encode(event.data ?? ""); const remaining = SHELL_OUTPUT_BYTES - result.outputBytes; // stream:true avoids emitting a replacement character for a split // final UTF-8 scalar at the byte boundary. result[event.type] += new TextDecoder().decode( bytes.subarray(0, remaining), { stream: bytes.byteLength > remaining }, ); result.outputBytes += Math.min(bytes.byteLength, remaining); if (bytes.byteLength >= remaining) { result.truncated = true; result.error = "output_limit"; controller.abort("output_limit"); void close(); break; } if (Date.now() - lastYield >= 250) { lastYield = Date.now(); yield { ...result }; } } else if (event.type === "complete") { result.exitCode = event.exitCode ?? event.result?.exitCode ?? null; result.status = result.exitCode === 0 ? "succeeded" : "failed"; if (result.status === "failed") result.error = "command_failed"; break; } else if (event.type === "error") { result.error = "unavailable"; break; } } } catch { result.error ??= "unavailable"; } finally { if (timer) clearTimeout(timer); abortSignal?.removeEventListener("abort", cancel); if (lease && Date.now() >= lease.expiresAt && !abortSignal?.aborted) result.error = "timeout"; if (controller.signal.aborted) result.error = controller.signal.reason as ShellResult["error"]; if (result.status === "running" || result.error) result.status = "failed"; result.error ??= result.status === "failed" ? "unavailable" : undefined; result.cleanup = (await close()) ? "closed" : "pending"; } yield { ...result }; }, });}
export const shellActivity: ToolActivityDescriptor = { kind: "shell", label: "Run temporary shell", outcome: (output) => (output as ShellResult).status === "succeeded" ? "succeeded" : "failed", outputSummary: (output) => (output as ShellResult).cleanup === "pending" ? "Command completed; workspace cleanup pending" : "Temporary shell completed", progress: (output) => { const value = output as ShellResult; return Number.isSafeInteger(value?.outputBytes) ? { text: "Receiving shell output", completed: value.outputBytes, total: SHELL_OUTPUT_BYTES, } : undefined; },};