From 8291967bf16c4f6fa2c1f37056b147a5c95e78a1 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 09:22:41 -0500 Subject: [PATCH 01/20] feat: load sandbox from Recoup API snapshot Use snapshot-based sandbox creation when available, eliminating the file upload step. Falls back to fresh sandbox + file upload when no snapshot exists or on any failure. Co-Authored-By: Claude Opus 4.6 --- app/api/agent/route.ts | 32 +++++++++++++++++++++++++++----- lib/recoup-api/getSnapshotId.ts | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 lib/recoup-api/getSnapshotId.ts diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 022fa314..bb20113a 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -4,6 +4,7 @@ import { Sandbox } from "@vercel/sandbox"; import { readdirSync, readFileSync } from "fs"; import { dirname, join, relative } from "path"; import { fileURLToPath } from "url"; +import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "./_agent-data"); @@ -74,19 +75,40 @@ export async function POST(req: Request) { ); } + const bearerToken = authHeader.slice("Bearer ".length); + const { messages } = await req.json(); const lastUserMessage = messages .filter((m: { role: string }) => m.role === "user") .pop(); console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); - const sandbox = await Sandbox.create(); + const snapshotId = await getSnapshotId(bearerToken); + + let sandbox: Sandbox; + let usedSnapshot = false; + + if (snapshotId) { + try { + sandbox = await Sandbox.create({ + source: { type: "snapshot", snapshotId }, + }); + usedSnapshot = true; + } catch (err) { + console.warn("Snapshot sandbox creation failed, falling back:", err); + sandbox = await Sandbox.create(); + } + } else { + sandbox = await Sandbox.create(); + } try { - // Upload source files so the agent can explore them - const files = readSourceFiles(AGENT_DATA_DIR); - if (files.length > 0) { - await sandbox.writeFiles(files); + if (!usedSnapshot) { + // Upload source files so the agent can explore them + const files = readSourceFiles(AGENT_DATA_DIR); + if (files.length > 0) { + await sandbox.writeFiles(files); + } } const bashToolkit = await createBashTool({ diff --git a/lib/recoup-api/getSnapshotId.ts b/lib/recoup-api/getSnapshotId.ts new file mode 100644 index 00000000..ccc80e79 --- /dev/null +++ b/lib/recoup-api/getSnapshotId.ts @@ -0,0 +1,20 @@ +const RECOUP_API_URL = + process.env.RECOUP_API_URL || "https://recoup-api.vercel.app"; + +export async function getSnapshotId( + bearerToken: string, +): Promise { + try { + const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { + headers: { Authorization: `Bearer ${bearerToken}` }, + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) return null; + + const data = await response.json(); + return data?.snapshot_id ?? null; + } catch { + return null; + } +} From 16069ce1f685de368c113b1f038dc874a0b64522 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 09:44:54 -0500 Subject: [PATCH 02/20] refactor: extract sandbox creation into lib/sandbox/createNewSandbox.ts Move snapshot resolution, sandbox creation, and file upload fallback logic out of the route handler into a dedicated module (SRP). Co-Authored-By: Claude Opus 4.6 --- app/api/agent/route.ts | 62 ++------------------------------- lib/sandbox/createNewSandbox.ts | 56 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 59 deletions(-) create mode 100644 lib/sandbox/createNewSandbox.ts diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index bb20113a..f250eb09 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,10 +1,8 @@ import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai"; import { createBashTool } from "bash-tool"; -import { Sandbox } from "@vercel/sandbox"; -import { readdirSync, readFileSync } from "fs"; -import { dirname, join, relative } from "path"; +import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; +import { createNewSandbox } from "@/lib/sandbox/createNewSandbox"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "./_agent-data"); @@ -37,35 +35,6 @@ Use cat to read files. Use head, tail to read parts of large files. Keep responses concise. You have access to a full Linux environment with standard tools.`; -/** - * Recursively read all files from a directory, returning them in the format - * expected by Sandbox.writeFiles(). - */ -function readSourceFiles( - dir: string, - baseDir?: string -): Array<{ path: string; content: Buffer }> { - const base = baseDir ?? dir; - const files: Array<{ path: string; content: Buffer }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - // Skip node_modules and other large/irrelevant dirs - if (entry.name === "node_modules" || entry.name === ".git") continue; - files.push(...readSourceFiles(fullPath, base)); - } else { - const relPath = relative(base, fullPath); - files.push({ - path: join(SANDBOX_CWD, relPath), - content: readFileSync(fullPath), - }); - } - } - - return files; -} - export async function POST(req: Request) { const authHeader = req.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { @@ -83,34 +52,9 @@ export async function POST(req: Request) { .pop(); console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); - const snapshotId = await getSnapshotId(bearerToken); - - let sandbox: Sandbox; - let usedSnapshot = false; - - if (snapshotId) { - try { - sandbox = await Sandbox.create({ - source: { type: "snapshot", snapshotId }, - }); - usedSnapshot = true; - } catch (err) { - console.warn("Snapshot sandbox creation failed, falling back:", err); - sandbox = await Sandbox.create(); - } - } else { - sandbox = await Sandbox.create(); - } + const sandbox = await createNewSandbox(bearerToken, AGENT_DATA_DIR); try { - if (!usedSnapshot) { - // Upload source files so the agent can explore them - const files = readSourceFiles(AGENT_DATA_DIR); - if (files.length > 0) { - await sandbox.writeFiles(files); - } - } - const bashToolkit = await createBashTool({ sandbox, destination: SANDBOX_CWD, diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts new file mode 100644 index 00000000..bdc29e47 --- /dev/null +++ b/lib/sandbox/createNewSandbox.ts @@ -0,0 +1,56 @@ +import { Sandbox } from "@vercel/sandbox"; +import { readdirSync, readFileSync } from "fs"; +import { join, relative } from "path"; +import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; + +const SANDBOX_CWD = "/vercel/sandbox"; + +function readSourceFiles( + dir: string, + baseDir?: string, +): Array<{ path: string; content: Buffer }> { + const base = baseDir ?? dir; + const files: Array<{ path: string; content: Buffer }> = []; + + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + files.push(...readSourceFiles(fullPath, base)); + } else { + const relPath = relative(base, fullPath); + files.push({ + path: join(SANDBOX_CWD, relPath), + content: readFileSync(fullPath), + }); + } + } + + return files; +} + +export async function createNewSandbox( + bearerToken: string, + agentDataDir: string, +): Promise { + const snapshotId = await getSnapshotId(bearerToken); + + if (snapshotId) { + try { + return await Sandbox.create({ + source: { type: "snapshot", snapshotId }, + }); + } catch (err) { + console.warn("Snapshot sandbox creation failed, falling back:", err); + } + } + + const sandbox = await Sandbox.create(); + + const files = readSourceFiles(agentDataDir); + if (files.length > 0) { + await sandbox.writeFiles(files); + } + + return sandbox; +} From 7e4dea3e6ca334e7993bba5bf7a67839e6cf2a71 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 09:50:12 -0500 Subject: [PATCH 03/20] feat: delegate sandbox creation to Recoup API Instead of fetching a snapshot_id and creating sandboxes directly, call POST /api/sandboxes on the Recoup API which handles snapshot resolution internally, then connect via Sandbox.get(). Co-Authored-By: Claude Opus 4.6 --- .../{getSnapshotId.ts => createSandbox.ts} | 13 +++++++++---- lib/sandbox/createNewSandbox.ts | 12 +++++------- 2 files changed, 14 insertions(+), 11 deletions(-) rename lib/recoup-api/{getSnapshotId.ts => createSandbox.ts} (52%) diff --git a/lib/recoup-api/getSnapshotId.ts b/lib/recoup-api/createSandbox.ts similarity index 52% rename from lib/recoup-api/getSnapshotId.ts rename to lib/recoup-api/createSandbox.ts index ccc80e79..bd61f1c8 100644 --- a/lib/recoup-api/getSnapshotId.ts +++ b/lib/recoup-api/createSandbox.ts @@ -1,19 +1,24 @@ const RECOUP_API_URL = process.env.RECOUP_API_URL || "https://recoup-api.vercel.app"; -export async function getSnapshotId( +export async function createSandbox( bearerToken: string, ): Promise { try { const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { - headers: { Authorization: `Bearer ${bearerToken}` }, - signal: AbortSignal.timeout(5000), + method: "POST", + headers: { + Authorization: `Bearer ${bearerToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + signal: AbortSignal.timeout(15000), }); if (!response.ok) return null; const data = await response.json(); - return data?.snapshot_id ?? null; + return data?.sandboxes?.[0]?.sandboxId ?? null; } catch { return null; } diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index bdc29e47..933ce9dc 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -1,7 +1,7 @@ import { Sandbox } from "@vercel/sandbox"; import { readdirSync, readFileSync } from "fs"; import { join, relative } from "path"; -import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; +import { createSandbox } from "@/lib/recoup-api/createSandbox"; const SANDBOX_CWD = "/vercel/sandbox"; @@ -33,15 +33,13 @@ export async function createNewSandbox( bearerToken: string, agentDataDir: string, ): Promise { - const snapshotId = await getSnapshotId(bearerToken); + const sandboxId = await createSandbox(bearerToken); - if (snapshotId) { + if (sandboxId) { try { - return await Sandbox.create({ - source: { type: "snapshot", snapshotId }, - }); + return await Sandbox.get({ sandboxId }); } catch (err) { - console.warn("Snapshot sandbox creation failed, falling back:", err); + console.warn("Failed to connect to API sandbox, falling back:", err); } } From 248b54bdfb00bd99b04d5bec133992fe7cb469b3 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 09:55:02 -0500 Subject: [PATCH 04/20] fix: use env-based API URL to match Recoup-Chat pattern Switch between prod and test Recoup API based on NEXT_PUBLIC_VERCEL_ENV, matching the pattern in Recoup-Chat. Preview deployments now correctly hit test-recoup-api.vercel.app instead of production. Co-Authored-By: Claude Opus 4.6 --- lib/recoup-api/createSandbox.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/recoup-api/createSandbox.ts b/lib/recoup-api/createSandbox.ts index bd61f1c8..048f6cb2 100644 --- a/lib/recoup-api/createSandbox.ts +++ b/lib/recoup-api/createSandbox.ts @@ -1,5 +1,7 @@ -const RECOUP_API_URL = - process.env.RECOUP_API_URL || "https://recoup-api.vercel.app"; +const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; +const RECOUP_API_URL = IS_PROD + ? "https://recoup-api.vercel.app" + : "https://test-recoup-api.vercel.app"; export async function createSandbox( bearerToken: string, From 26f7783adf28c2f975dee9f9e74853b011fd27f6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 10:30:15 -0500 Subject: [PATCH 05/20] chore: add timing logs to identify latency bottleneck Log elapsed time for each step: POST /api/sandboxes, Sandbox.get, createBashTool, and fallback paths. Will help diagnose the 20s gap between sandbox creation and first bash execution. Co-Authored-By: Claude Opus 4.6 --- app/api/agent/route.ts | 4 ++++ lib/sandbox/createNewSandbox.ts | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index f250eb09..08a8df4e 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -52,13 +52,17 @@ export async function POST(req: Request) { .pop(); console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); + const t0 = Date.now(); const sandbox = await createNewSandbox(bearerToken, AGENT_DATA_DIR); + console.log(`[timing] createNewSandbox: ${Date.now() - t0}ms`); try { + const t1 = Date.now(); const bashToolkit = await createBashTool({ sandbox, destination: SANDBOX_CWD, }); + console.log(`[timing] createBashTool: ${Date.now() - t1}ms`); // Create a fresh agent per request for proper streaming const agent = new ToolLoopAgent({ diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index 933ce9dc..09d62ddc 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -33,21 +33,30 @@ export async function createNewSandbox( bearerToken: string, agentDataDir: string, ): Promise { + const t0 = Date.now(); const sandboxId = await createSandbox(bearerToken); + console.log(`[timing] POST /api/sandboxes: ${Date.now() - t0}ms (sandboxId: ${sandboxId})`); if (sandboxId) { try { - return await Sandbox.get({ sandboxId }); + const t1 = Date.now(); + const sandbox = await Sandbox.get({ sandboxId }); + console.log(`[timing] Sandbox.get: ${Date.now() - t1}ms`); + return sandbox; } catch (err) { console.warn("Failed to connect to API sandbox, falling back:", err); } } + const t2 = Date.now(); const sandbox = await Sandbox.create(); + console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); const files = readSourceFiles(agentDataDir); if (files.length > 0) { + const t3 = Date.now(); await sandbox.writeFiles(files); + console.log(`[timing] writeFiles (fallback): ${Date.now() - t3}ms`); } return sandbox; From d1ed53a32b8bc94793d2da069518e8abb21b5ae2 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 10:36:14 -0500 Subject: [PATCH 06/20] perf: skip tool discovery in createBashTool with static prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tool discovery runs `ls /usr/bin ...` in the sandbox which takes ~34s due to sandbox warm-up after snapshot restore. Provide a static toolPrompt to skip discovery entirely — the available tools on a node22 sandbox are always the same. Co-Authored-By: Claude Opus 4.6 --- app/api/agent/route.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 08a8df4e..b2043157 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -61,6 +61,10 @@ export async function POST(req: Request) { const bashToolkit = await createBashTool({ sandbox, destination: SANDBOX_CWD, + promptOptions: { + toolPrompt: + "Available tools: awk, cat, column, curl, cut, diff, find, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", + }, }); console.log(`[timing] createBashTool: ${Date.now() - t1}ms`); From 9ec3236ec7dad204a296956273c6ae3b053606ec Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 10:42:48 -0500 Subject: [PATCH 07/20] fix: always upload source files regardless of sandbox source The snapshot contains the user's Recoup Sandbox content, not the just-bash/bash-tool source files. Write agent data files to the sandbox in all cases so the agent can explore the source code. Co-Authored-By: Claude Opus 4.6 --- lib/sandbox/createNewSandbox.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index 09d62ddc..48f89114 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -33,6 +33,8 @@ export async function createNewSandbox( bearerToken: string, agentDataDir: string, ): Promise { + let sandbox: Sandbox; + const t0 = Date.now(); const sandboxId = await createSandbox(bearerToken); console.log(`[timing] POST /api/sandboxes: ${Date.now() - t0}ms (sandboxId: ${sandboxId})`); @@ -40,23 +42,25 @@ export async function createNewSandbox( if (sandboxId) { try { const t1 = Date.now(); - const sandbox = await Sandbox.get({ sandboxId }); + sandbox = await Sandbox.get({ sandboxId }); console.log(`[timing] Sandbox.get: ${Date.now() - t1}ms`); - return sandbox; } catch (err) { console.warn("Failed to connect to API sandbox, falling back:", err); + const t2 = Date.now(); + sandbox = await Sandbox.create(); + console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); } + } else { + const t2 = Date.now(); + sandbox = await Sandbox.create(); + console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); } - const t2 = Date.now(); - const sandbox = await Sandbox.create(); - console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); - const files = readSourceFiles(agentDataDir); if (files.length > 0) { const t3 = Date.now(); await sandbox.writeFiles(files); - console.log(`[timing] writeFiles (fallback): ${Date.now() - t3}ms`); + console.log(`[timing] writeFiles: ${Date.now() - t3}ms`); } return sandbox; From 99d05606b5f3125263b2ff32f0bbc8597183817b Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 10:43:30 -0500 Subject: [PATCH 08/20] revert: skip file upload when using snapshot sandbox Only upload source files in the fallback path (fresh sandbox). Snapshot sandboxes should have the needed files pre-baked. Co-Authored-By: Claude Opus 4.6 --- lib/sandbox/createNewSandbox.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index 48f89114..09d62ddc 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -33,8 +33,6 @@ export async function createNewSandbox( bearerToken: string, agentDataDir: string, ): Promise { - let sandbox: Sandbox; - const t0 = Date.now(); const sandboxId = await createSandbox(bearerToken); console.log(`[timing] POST /api/sandboxes: ${Date.now() - t0}ms (sandboxId: ${sandboxId})`); @@ -42,25 +40,23 @@ export async function createNewSandbox( if (sandboxId) { try { const t1 = Date.now(); - sandbox = await Sandbox.get({ sandboxId }); + const sandbox = await Sandbox.get({ sandboxId }); console.log(`[timing] Sandbox.get: ${Date.now() - t1}ms`); + return sandbox; } catch (err) { console.warn("Failed to connect to API sandbox, falling back:", err); - const t2 = Date.now(); - sandbox = await Sandbox.create(); - console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); } - } else { - const t2 = Date.now(); - sandbox = await Sandbox.create(); - console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); } + const t2 = Date.now(); + const sandbox = await Sandbox.create(); + console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); + const files = readSourceFiles(agentDataDir); if (files.length > 0) { const t3 = Date.now(); await sandbox.writeFiles(files); - console.log(`[timing] writeFiles: ${Date.now() - t3}ms`); + console.log(`[timing] writeFiles (fallback): ${Date.now() - t3}ms`); } return sandbox; From 3686ebf6bcc5d9e18b9c7bc0fca85930767e94ed Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 10:47:21 -0500 Subject: [PATCH 09/20] perf: warm up sandbox after snapshot restore Run a no-op command (true) right after Sandbox.get() to absorb the snapshot cold-start latency during setup rather than during the agent's first tool call. This eliminates the long pause users see after the first bash command appears on screen. Co-Authored-By: Claude Opus 4.6 --- lib/sandbox/createNewSandbox.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index 09d62ddc..4b1f6bc7 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -42,6 +42,11 @@ export async function createNewSandbox( const t1 = Date.now(); const sandbox = await Sandbox.get({ sandboxId }); console.log(`[timing] Sandbox.get: ${Date.now() - t1}ms`); + + const t2 = Date.now(); + await sandbox.runCommand("true"); + console.log(`[timing] sandbox warm-up: ${Date.now() - t2}ms`); + return sandbox; } catch (err) { console.warn("Failed to connect to API sandbox, falling back:", err); From 32c6dd4324488b6552b7f0057382392f4d0eb6a6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 10:58:38 -0500 Subject: [PATCH 10/20] perf: create snapshot sandbox locally instead of via Sandbox.get Use GET /api/sandboxes to fetch the snapshot_id, then create the sandbox locally with Sandbox.create({ source: { type: "snapshot" } }) instead of POST + Sandbox.get(). This tests whether the 33s cold start was caused by cross-project Sandbox.get() vs local creation. Co-Authored-By: Claude Opus 4.6 --- lib/recoup-api/getSnapshotId.ts | 22 ++++++++++++++++++++++ lib/sandbox/createNewSandbox.ts | 24 +++++++++++++----------- 2 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 lib/recoup-api/getSnapshotId.ts diff --git a/lib/recoup-api/getSnapshotId.ts b/lib/recoup-api/getSnapshotId.ts new file mode 100644 index 00000000..b0e7b1be --- /dev/null +++ b/lib/recoup-api/getSnapshotId.ts @@ -0,0 +1,22 @@ +const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; +const RECOUP_API_URL = IS_PROD + ? "https://recoup-api.vercel.app" + : "https://test-recoup-api.vercel.app"; + +export async function getSnapshotId( + bearerToken: string, +): Promise { + try { + const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { + headers: { Authorization: `Bearer ${bearerToken}` }, + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) return null; + + const data = await response.json(); + return data?.snapshot_id ?? null; + } catch { + return null; + } +} diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index 4b1f6bc7..3c566f50 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -1,7 +1,7 @@ import { Sandbox } from "@vercel/sandbox"; import { readdirSync, readFileSync } from "fs"; import { join, relative } from "path"; -import { createSandbox } from "@/lib/recoup-api/createSandbox"; +import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; const SANDBOX_CWD = "/vercel/sandbox"; @@ -34,14 +34,16 @@ export async function createNewSandbox( agentDataDir: string, ): Promise { const t0 = Date.now(); - const sandboxId = await createSandbox(bearerToken); - console.log(`[timing] POST /api/sandboxes: ${Date.now() - t0}ms (sandboxId: ${sandboxId})`); + const snapshotId = await getSnapshotId(bearerToken); + console.log(`[timing] GET /api/sandboxes: ${Date.now() - t0}ms (snapshotId: ${snapshotId})`); - if (sandboxId) { + if (snapshotId) { try { const t1 = Date.now(); - const sandbox = await Sandbox.get({ sandboxId }); - console.log(`[timing] Sandbox.get: ${Date.now() - t1}ms`); + const sandbox = await Sandbox.create({ + source: { type: "snapshot", snapshotId }, + }); + console.log(`[timing] Sandbox.create (snapshot): ${Date.now() - t1}ms`); const t2 = Date.now(); await sandbox.runCommand("true"); @@ -49,19 +51,19 @@ export async function createNewSandbox( return sandbox; } catch (err) { - console.warn("Failed to connect to API sandbox, falling back:", err); + console.warn("Snapshot sandbox creation failed, falling back:", err); } } - const t2 = Date.now(); + const t3 = Date.now(); const sandbox = await Sandbox.create(); - console.log(`[timing] Sandbox.create (fallback): ${Date.now() - t2}ms`); + console.log(`[timing] Sandbox.create (fresh): ${Date.now() - t3}ms`); const files = readSourceFiles(agentDataDir); if (files.length > 0) { - const t3 = Date.now(); + const t4 = Date.now(); await sandbox.writeFiles(files); - console.log(`[timing] writeFiles (fallback): ${Date.now() - t3}ms`); + console.log(`[timing] writeFiles: ${Date.now() - t4}ms`); } return sandbox; From 0f761370aca30aa2bf0facc99406d266987bec71 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:08:44 -0500 Subject: [PATCH 11/20] chore: remove dev timing logs and unused createSandbox Clean up all [timing] console.log statements and the warm-up command. Remove unused lib/recoup-api/createSandbox.ts (replaced by getSnapshotId). Co-Authored-By: Claude Opus 4.6 --- app/api/agent/route.ts | 4 ---- lib/recoup-api/createSandbox.ts | 27 --------------------------- lib/sandbox/createNewSandbox.ts | 16 +--------------- 3 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 lib/recoup-api/createSandbox.ts diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index b2043157..0f9673f7 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -52,12 +52,9 @@ export async function POST(req: Request) { .pop(); console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); - const t0 = Date.now(); const sandbox = await createNewSandbox(bearerToken, AGENT_DATA_DIR); - console.log(`[timing] createNewSandbox: ${Date.now() - t0}ms`); try { - const t1 = Date.now(); const bashToolkit = await createBashTool({ sandbox, destination: SANDBOX_CWD, @@ -66,7 +63,6 @@ export async function POST(req: Request) { "Available tools: awk, cat, column, curl, cut, diff, find, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", }, }); - console.log(`[timing] createBashTool: ${Date.now() - t1}ms`); // Create a fresh agent per request for proper streaming const agent = new ToolLoopAgent({ diff --git a/lib/recoup-api/createSandbox.ts b/lib/recoup-api/createSandbox.ts deleted file mode 100644 index 048f6cb2..00000000 --- a/lib/recoup-api/createSandbox.ts +++ /dev/null @@ -1,27 +0,0 @@ -const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; -const RECOUP_API_URL = IS_PROD - ? "https://recoup-api.vercel.app" - : "https://test-recoup-api.vercel.app"; - -export async function createSandbox( - bearerToken: string, -): Promise { - try { - const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { - method: "POST", - headers: { - Authorization: `Bearer ${bearerToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - signal: AbortSignal.timeout(15000), - }); - - if (!response.ok) return null; - - const data = await response.json(); - return data?.sandboxes?.[0]?.sandboxId ?? null; - } catch { - return null; - } -} diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index 3c566f50..bdc29e47 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -33,37 +33,23 @@ export async function createNewSandbox( bearerToken: string, agentDataDir: string, ): Promise { - const t0 = Date.now(); const snapshotId = await getSnapshotId(bearerToken); - console.log(`[timing] GET /api/sandboxes: ${Date.now() - t0}ms (snapshotId: ${snapshotId})`); if (snapshotId) { try { - const t1 = Date.now(); - const sandbox = await Sandbox.create({ + return await Sandbox.create({ source: { type: "snapshot", snapshotId }, }); - console.log(`[timing] Sandbox.create (snapshot): ${Date.now() - t1}ms`); - - const t2 = Date.now(); - await sandbox.runCommand("true"); - console.log(`[timing] sandbox warm-up: ${Date.now() - t2}ms`); - - return sandbox; } catch (err) { console.warn("Snapshot sandbox creation failed, falling back:", err); } } - const t3 = Date.now(); const sandbox = await Sandbox.create(); - console.log(`[timing] Sandbox.create (fresh): ${Date.now() - t3}ms`); const files = readSourceFiles(agentDataDir); if (files.length > 0) { - const t4 = Date.now(); await sandbox.writeFiles(files); - console.log(`[timing] writeFiles: ${Date.now() - t4}ms`); } return sandbox; From f9d0d835fd9af08dc290b3db4db6a5a0064e57fc Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:10:48 -0500 Subject: [PATCH 12/20] feat: add /new page with fresh sandbox for performance comparison - /new page uses /api/agent/new which creates a fresh Sandbox.create() + file upload (no snapshot) for A/B comparison against snapshot path - Terminal and agent-command accept configurable agentEndpoint prop Co-Authored-By: Claude Opus 4.6 --- app/api/agent/new/route.ts | 127 ++++++++++++++++++ app/components/Terminal.tsx | 6 +- .../terminal-parts/agent-command.ts | 3 +- app/new/page.tsx | 61 +++++++++ 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 app/api/agent/new/route.ts create mode 100644 app/new/page.tsx diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts new file mode 100644 index 00000000..6beb5dcf --- /dev/null +++ b/app/api/agent/new/route.ts @@ -0,0 +1,127 @@ +import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai"; +import { createBashTool } from "bash-tool"; +import { Sandbox } from "@vercel/sandbox"; +import { readdirSync, readFileSync } from "fs"; +import { dirname, join, relative } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const AGENT_DATA_DIR = join(__dirname, "../_agent-data"); +const SANDBOX_CWD = "/vercel/sandbox"; + +const SYSTEM_INSTRUCTIONS = `You are an expert on just-bash, a TypeScript bash interpreter with an in-memory virtual filesystem. + +You have access to a real bash sandbox with the full source code of: +- just-bash/ - The main bash interpreter +- bash-tool/ - AI SDK tool for bash + +The source files are located at ${SANDBOX_CWD}. + +Refer to the README.md of the projects to answer questions about just-bash and bash-tool +themselves which is your main focus. Never talk about this demo implementation unless asked explicitly. + +Use the sandbox to explore the source code, demonstrate commands, and help users understand: +- How to use just-bash and bash-tool +- Bash scripting in general +- The implementation details of just-bash + +Key features of just-bash: +- Pure TypeScript implementation (no WASM dependencies) +- In-memory virtual filesystem +- Supports common bash commands: ls, cat, grep, awk, sed, jq, etc. +- Custom command support via defineCommand +- Network access control with URL allowlists + +Use cat to read files. Use head, tail to read parts of large files. + +Keep responses concise. You have access to a full Linux environment with standard tools.`; + +function readSourceFiles( + dir: string, + baseDir?: string, +): Array<{ path: string; content: Buffer }> { + const base = baseDir ?? dir; + const files: Array<{ path: string; content: Buffer }> = []; + + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + files.push(...readSourceFiles(fullPath, base)); + } else { + const relPath = relative(base, fullPath); + files.push({ + path: join(SANDBOX_CWD, relPath), + content: readFileSync(fullPath), + }); + } + } + + return files; +} + +export async function POST(req: Request) { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return Response.json( + { error: "Unauthorized" }, + { status: 401 }, + ); + } + + const { messages } = await req.json(); + const lastUserMessage = messages + .filter((m: { role: string }) => m.role === "user") + .pop(); + console.log("Prompt (no snapshot):", lastUserMessage?.parts?.[0]?.text); + + const sandbox = await Sandbox.create(); + + const files = readSourceFiles(AGENT_DATA_DIR); + if (files.length > 0) { + await sandbox.writeFiles(files); + } + + try { + const bashToolkit = await createBashTool({ + sandbox, + destination: SANDBOX_CWD, + promptOptions: { + toolPrompt: + "Available tools: awk, cat, column, curl, cut, diff, find, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", + }, + }); + + const agent = new ToolLoopAgent({ + model: "claude-haiku-4-5", + instructions: SYSTEM_INSTRUCTIONS, + tools: { + bash: bashToolkit.tools.bash, + }, + stopWhen: stepCountIs(20), + }); + + const response = await createAgentUIStreamResponse({ + agent, + uiMessages: messages, + }); + + const body = response.body; + if (body) { + const transform = new TransformStream(); + body.pipeTo(transform.writable).finally(() => { + sandbox.stop().catch(() => {}); + }); + return new Response(transform.readable, { + headers: response.headers, + status: response.status, + }); + } + + sandbox.stop().catch(() => {}); + return response; + } catch (error) { + sandbox.stop().catch(() => {}); + throw error; + } +} diff --git a/app/components/Terminal.tsx b/app/components/Terminal.tsx index 0eba43fb..77e7508f 100644 --- a/app/components/Terminal.tsx +++ b/app/components/Terminal.tsx @@ -32,8 +32,10 @@ function getTheme(isDark: boolean) { export default function TerminalComponent({ getAccessToken, + agentEndpoint, }: { getAccessToken: () => Promise; + agentEndpoint?: string; }) { const terminalRef = useRef(null); @@ -51,7 +53,7 @@ export default function TerminalComponent({ // Create commands const { aboutCmd, installCmd, githubCmd } = createStaticCommands(); - const agentCmd = createAgentCommand(term, getAccessToken); + const agentCmd = createAgentCommand(term, getAccessToken, agentEndpoint); // Files from DOM const files = { @@ -114,7 +116,7 @@ export default function TerminalComponent({ colorSchemeQuery.removeEventListener("change", onColorSchemeChange); term.dispose(); }; - }, [getAccessToken]); + }, [getAccessToken, agentEndpoint]); return (
Promise, + agentEndpoint = "/api/agent", ) { const agentMessages: UIMessage[] = []; let messageIdCounter = 0; @@ -62,7 +63,7 @@ export function createAgentCommand( }; } - const response = await fetch("/api/agent", { + const response = await fetch(agentEndpoint, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/new/page.tsx b/app/new/page.tsx new file mode 100644 index 00000000..a6d5fee2 --- /dev/null +++ b/app/new/page.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePrivy } from "@privy-io/react-auth"; +import TerminalComponent from "../components/Terminal"; +import { TerminalData } from "../components/TerminalData"; + +export default function NewPage() { + const [mounted, setMounted] = useState(false); + const { ready, authenticated, login, getAccessToken } = usePrivy(); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted || !ready) { + return ; + } + + if (!authenticated) { + return ( + <> + +
+ +
+ + ); + } + + return ( + <> + + + + ); +} From b7b2983174619dbf06b5f46d0f140f31912cdb12 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:12:41 -0500 Subject: [PATCH 13/20] fix: add git to static tool prompt Co-Authored-By: Claude Opus 4.6 --- app/api/agent/new/route.ts | 2 +- app/api/agent/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts index 6beb5dcf..fba27ba3 100644 --- a/app/api/agent/new/route.ts +++ b/app/api/agent/new/route.ts @@ -88,7 +88,7 @@ export async function POST(req: Request) { destination: SANDBOX_CWD, promptOptions: { toolPrompt: - "Available tools: awk, cat, column, curl, cut, diff, find, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", + "Available tools: awk, cat, column, curl, cut, diff, find, git, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", }, }); diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 0f9673f7..2176ca5b 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -60,7 +60,7 @@ export async function POST(req: Request) { destination: SANDBOX_CWD, promptOptions: { toolPrompt: - "Available tools: awk, cat, column, curl, cut, diff, find, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", + "Available tools: awk, cat, column, curl, cut, diff, find, git, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", }, }); From 15201aca66008e2fd5be2625b9c3c78533784445 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:18:37 -0500 Subject: [PATCH 14/20] refactor: extract shared agent logic into lib to follow DRY Co-Authored-By: Claude Opus 4.6 --- app/api/agent/new/route.ts | 110 ++---------------------------- app/api/agent/route.ts | 78 +-------------------- lib/agent/constants.ts | 31 +++++++++ lib/agent/createAgentResponse.ts | 52 ++++++++++++++ lib/sandbox/createFreshSandbox.ts | 39 +++++++++++ lib/sandbox/createNewSandbox.ts | 38 +---------- 6 files changed, 131 insertions(+), 217 deletions(-) create mode 100644 lib/agent/constants.ts create mode 100644 lib/agent/createAgentResponse.ts create mode 100644 lib/sandbox/createFreshSandbox.ts diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts index fba27ba3..f89b2ef1 100644 --- a/app/api/agent/new/route.ts +++ b/app/api/agent/new/route.ts @@ -1,64 +1,10 @@ -import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai"; -import { createBashTool } from "bash-tool"; -import { Sandbox } from "@vercel/sandbox"; -import { readdirSync, readFileSync } from "fs"; -import { dirname, join, relative } from "path"; +import { dirname, join } from "path"; import { fileURLToPath } from "url"; +import { createFreshSandbox } from "@/lib/sandbox/createFreshSandbox"; +import { createAgentResponse } from "@/lib/agent/createAgentResponse"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "../_agent-data"); -const SANDBOX_CWD = "/vercel/sandbox"; - -const SYSTEM_INSTRUCTIONS = `You are an expert on just-bash, a TypeScript bash interpreter with an in-memory virtual filesystem. - -You have access to a real bash sandbox with the full source code of: -- just-bash/ - The main bash interpreter -- bash-tool/ - AI SDK tool for bash - -The source files are located at ${SANDBOX_CWD}. - -Refer to the README.md of the projects to answer questions about just-bash and bash-tool -themselves which is your main focus. Never talk about this demo implementation unless asked explicitly. - -Use the sandbox to explore the source code, demonstrate commands, and help users understand: -- How to use just-bash and bash-tool -- Bash scripting in general -- The implementation details of just-bash - -Key features of just-bash: -- Pure TypeScript implementation (no WASM dependencies) -- In-memory virtual filesystem -- Supports common bash commands: ls, cat, grep, awk, sed, jq, etc. -- Custom command support via defineCommand -- Network access control with URL allowlists - -Use cat to read files. Use head, tail to read parts of large files. - -Keep responses concise. You have access to a full Linux environment with standard tools.`; - -function readSourceFiles( - dir: string, - baseDir?: string, -): Array<{ path: string; content: Buffer }> { - const base = baseDir ?? dir; - const files: Array<{ path: string; content: Buffer }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === ".git") continue; - files.push(...readSourceFiles(fullPath, base)); - } else { - const relPath = relative(base, fullPath); - files.push({ - path: join(SANDBOX_CWD, relPath), - content: readFileSync(fullPath), - }); - } - } - - return files; -} export async function POST(req: Request) { const authHeader = req.headers.get("Authorization"); @@ -75,53 +21,7 @@ export async function POST(req: Request) { .pop(); console.log("Prompt (no snapshot):", lastUserMessage?.parts?.[0]?.text); - const sandbox = await Sandbox.create(); - - const files = readSourceFiles(AGENT_DATA_DIR); - if (files.length > 0) { - await sandbox.writeFiles(files); - } - - try { - const bashToolkit = await createBashTool({ - sandbox, - destination: SANDBOX_CWD, - promptOptions: { - toolPrompt: - "Available tools: awk, cat, column, curl, cut, diff, find, git, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", - }, - }); - - const agent = new ToolLoopAgent({ - model: "claude-haiku-4-5", - instructions: SYSTEM_INSTRUCTIONS, - tools: { - bash: bashToolkit.tools.bash, - }, - stopWhen: stepCountIs(20), - }); - - const response = await createAgentUIStreamResponse({ - agent, - uiMessages: messages, - }); + const sandbox = await createFreshSandbox(AGENT_DATA_DIR); - const body = response.body; - if (body) { - const transform = new TransformStream(); - body.pipeTo(transform.writable).finally(() => { - sandbox.stop().catch(() => {}); - }); - return new Response(transform.readable, { - headers: response.headers, - status: response.status, - }); - } - - sandbox.stop().catch(() => {}); - return response; - } catch (error) { - sandbox.stop().catch(() => {}); - throw error; - } + return createAgentResponse(sandbox, messages); } diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 2176ca5b..5f6624cf 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,39 +1,10 @@ -import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai"; -import { createBashTool } from "bash-tool"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { createNewSandbox } from "@/lib/sandbox/createNewSandbox"; +import { createAgentResponse } from "@/lib/agent/createAgentResponse"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "./_agent-data"); -const SANDBOX_CWD = "/vercel/sandbox"; - -const SYSTEM_INSTRUCTIONS = `You are an expert on just-bash, a TypeScript bash interpreter with an in-memory virtual filesystem. - -You have access to a real bash sandbox with the full source code of: -- just-bash/ - The main bash interpreter -- bash-tool/ - AI SDK tool for bash - -The source files are located at ${SANDBOX_CWD}. - -Refer to the README.md of the projects to answer questions about just-bash and bash-tool -themselves which is your main focus. Never talk about this demo implementation unless asked explicitly. - -Use the sandbox to explore the source code, demonstrate commands, and help users understand: -- How to use just-bash and bash-tool -- Bash scripting in general -- The implementation details of just-bash - -Key features of just-bash: -- Pure TypeScript implementation (no WASM dependencies) -- In-memory virtual filesystem -- Supports common bash commands: ls, cat, grep, awk, sed, jq, etc. -- Custom command support via defineCommand -- Network access control with URL allowlists - -Use cat to read files. Use head, tail to read parts of large files. - -Keep responses concise. You have access to a full Linux environment with standard tools.`; export async function POST(req: Request) { const authHeader = req.headers.get("Authorization"); @@ -54,50 +25,5 @@ export async function POST(req: Request) { const sandbox = await createNewSandbox(bearerToken, AGENT_DATA_DIR); - try { - const bashToolkit = await createBashTool({ - sandbox, - destination: SANDBOX_CWD, - promptOptions: { - toolPrompt: - "Available tools: awk, cat, column, curl, cut, diff, find, git, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more", - }, - }); - - // Create a fresh agent per request for proper streaming - const agent = new ToolLoopAgent({ - model: "claude-haiku-4-5", - instructions: SYSTEM_INSTRUCTIONS, - tools: { - bash: bashToolkit.tools.bash, - }, - stopWhen: stepCountIs(20), - }); - - const response = await createAgentUIStreamResponse({ - agent, - uiMessages: messages, - }); - - // Clean up sandbox after the stream finishes (not before). - // The original `finally` block killed the sandbox immediately when - // createAgentUIStreamResponse returned, before any tool calls ran. - const body = response.body; - if (body) { - const transform = new TransformStream(); - body.pipeTo(transform.writable).finally(() => { - sandbox.stop().catch(() => {}); - }); - return new Response(transform.readable, { - headers: response.headers, - status: response.status, - }); - } - - sandbox.stop().catch(() => {}); - return response; - } catch (error) { - sandbox.stop().catch(() => {}); - throw error; - } + return createAgentResponse(sandbox, messages); } diff --git a/lib/agent/constants.ts b/lib/agent/constants.ts new file mode 100644 index 00000000..baf72033 --- /dev/null +++ b/lib/agent/constants.ts @@ -0,0 +1,31 @@ +export const SANDBOX_CWD = "/vercel/sandbox"; + +export const TOOL_PROMPT = + "Available tools: awk, cat, column, curl, cut, diff, find, git, grep, head, jq, join, nl, node, od, paste, printf, rev, sed, sort, split, strings, tail, tee, tr, uniq, wc, xargs, xxd, and more"; + +export const SYSTEM_INSTRUCTIONS = `You are an expert on just-bash, a TypeScript bash interpreter with an in-memory virtual filesystem. + +You have access to a real bash sandbox with the full source code of: +- just-bash/ - The main bash interpreter +- bash-tool/ - AI SDK tool for bash + +The source files are located at ${SANDBOX_CWD}. + +Refer to the README.md of the projects to answer questions about just-bash and bash-tool +themselves which is your main focus. Never talk about this demo implementation unless asked explicitly. + +Use the sandbox to explore the source code, demonstrate commands, and help users understand: +- How to use just-bash and bash-tool +- Bash scripting in general +- The implementation details of just-bash + +Key features of just-bash: +- Pure TypeScript implementation (no WASM dependencies) +- In-memory virtual filesystem +- Supports common bash commands: ls, cat, grep, awk, sed, jq, etc. +- Custom command support via defineCommand +- Network access control with URL allowlists + +Use cat to read files. Use head, tail to read parts of large files. + +Keep responses concise. You have access to a full Linux environment with standard tools.`; diff --git a/lib/agent/createAgentResponse.ts b/lib/agent/createAgentResponse.ts new file mode 100644 index 00000000..74e50b06 --- /dev/null +++ b/lib/agent/createAgentResponse.ts @@ -0,0 +1,52 @@ +import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai"; +import { createBashTool } from "bash-tool"; +import { Sandbox } from "@vercel/sandbox"; +import { SANDBOX_CWD, SYSTEM_INSTRUCTIONS, TOOL_PROMPT } from "./constants"; + +export async function createAgentResponse( + sandbox: Sandbox, + messages: unknown[], +): Promise { + try { + const bashToolkit = await createBashTool({ + sandbox, + destination: SANDBOX_CWD, + promptOptions: { + toolPrompt: TOOL_PROMPT, + }, + }); + + const agent = new ToolLoopAgent({ + model: "claude-haiku-4-5", + instructions: SYSTEM_INSTRUCTIONS, + tools: { + bash: bashToolkit.tools.bash, + }, + stopWhen: stepCountIs(20), + }); + + const response = await createAgentUIStreamResponse({ + agent, + uiMessages: messages, + }); + + // Clean up sandbox after the stream finishes (not before). + const body = response.body; + if (body) { + const transform = new TransformStream(); + body.pipeTo(transform.writable).finally(() => { + sandbox.stop().catch(() => {}); + }); + return new Response(transform.readable, { + headers: response.headers, + status: response.status, + }); + } + + sandbox.stop().catch(() => {}); + return response; + } catch (error) { + sandbox.stop().catch(() => {}); + throw error; + } +} diff --git a/lib/sandbox/createFreshSandbox.ts b/lib/sandbox/createFreshSandbox.ts new file mode 100644 index 00000000..2beb6b08 --- /dev/null +++ b/lib/sandbox/createFreshSandbox.ts @@ -0,0 +1,39 @@ +import { Sandbox } from "@vercel/sandbox"; +import { readdirSync, readFileSync } from "fs"; +import { join, relative } from "path"; +import { SANDBOX_CWD } from "@/lib/agent/constants"; + +function readSourceFiles( + dir: string, + baseDir?: string, +): Array<{ path: string; content: Buffer }> { + const base = baseDir ?? dir; + const files: Array<{ path: string; content: Buffer }> = []; + + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + files.push(...readSourceFiles(fullPath, base)); + } else { + const relPath = relative(base, fullPath); + files.push({ + path: join(SANDBOX_CWD, relPath), + content: readFileSync(fullPath), + }); + } + } + + return files; +} + +export async function createFreshSandbox(agentDataDir: string): Promise { + const sandbox = await Sandbox.create(); + + const files = readSourceFiles(agentDataDir); + if (files.length > 0) { + await sandbox.writeFiles(files); + } + + return sandbox; +} diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createNewSandbox.ts index bdc29e47..8d39aab0 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createNewSandbox.ts @@ -1,33 +1,6 @@ import { Sandbox } from "@vercel/sandbox"; -import { readdirSync, readFileSync } from "fs"; -import { join, relative } from "path"; import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; - -const SANDBOX_CWD = "/vercel/sandbox"; - -function readSourceFiles( - dir: string, - baseDir?: string, -): Array<{ path: string; content: Buffer }> { - const base = baseDir ?? dir; - const files: Array<{ path: string; content: Buffer }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === ".git") continue; - files.push(...readSourceFiles(fullPath, base)); - } else { - const relPath = relative(base, fullPath); - files.push({ - path: join(SANDBOX_CWD, relPath), - content: readFileSync(fullPath), - }); - } - } - - return files; -} +import { createFreshSandbox } from "./createFreshSandbox"; export async function createNewSandbox( bearerToken: string, @@ -45,12 +18,5 @@ export async function createNewSandbox( } } - const sandbox = await Sandbox.create(); - - const files = readSourceFiles(agentDataDir); - if (files.length > 0) { - await sandbox.writeFiles(files); - } - - return sandbox; + return createFreshSandbox(agentDataDir); } From adca78acdf4634de20d86595b38b4283221d60cb Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:22:47 -0500 Subject: [PATCH 15/20] refactor: extract shared TerminalPage component for auth boilerplate Co-Authored-By: Claude Opus 4.6 --- app/components/TerminalPage.tsx | 74 +++++++++++++++++++++++++++++++++ app/new/page.tsx | 60 +------------------------- app/page.tsx | 63 ++-------------------------- 3 files changed, 79 insertions(+), 118 deletions(-) create mode 100644 app/components/TerminalPage.tsx diff --git a/app/components/TerminalPage.tsx b/app/components/TerminalPage.tsx new file mode 100644 index 00000000..1f1a426a --- /dev/null +++ b/app/components/TerminalPage.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useEffect, useState, ReactNode } from "react"; +import { usePrivy } from "@privy-io/react-auth"; +import TerminalComponent from "./Terminal"; +import { TerminalData } from "./TerminalData"; + +export default function TerminalPage({ + agentEndpoint, + children, +}: { + agentEndpoint?: string; + children?: ReactNode; +}) { + const [mounted, setMounted] = useState(false); + const { ready, authenticated, login, getAccessToken } = usePrivy(); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted || !ready) { + return ( + <> + {children} + + + ); + } + + if (!authenticated) { + return ( + <> + {children} + +
+ +
+ + ); + } + + return ( + <> + {children} + + + + ); +} diff --git a/app/new/page.tsx b/app/new/page.tsx index a6d5fee2..16af2f34 100644 --- a/app/new/page.tsx +++ b/app/new/page.tsx @@ -1,61 +1,5 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { usePrivy } from "@privy-io/react-auth"; -import TerminalComponent from "../components/Terminal"; -import { TerminalData } from "../components/TerminalData"; +import TerminalPage from "../components/TerminalPage"; export default function NewPage() { - const [mounted, setMounted] = useState(false); - const { ready, authenticated, login, getAccessToken } = usePrivy(); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted || !ready) { - return ; - } - - if (!authenticated) { - return ( - <> - -
- -
- - ); - } - - return ( - <> - - - - ); + return ; } diff --git a/app/page.tsx b/app/page.tsx index cce7f4bd..2b61f47d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,9 +1,4 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { usePrivy } from "@privy-io/react-auth"; -import TerminalComponent from "./components/Terminal"; -import { TerminalData } from "./components/TerminalData"; +import TerminalPage from "./components/TerminalPage"; const NOSCRIPT_CONTENT = ` _ _ _ _ @@ -94,66 +89,14 @@ const NOSCRIPT_CONTENT = ` `; export default function Home() { - const [mounted, setMounted] = useState(false); - const { ready, authenticated, login, getAccessToken } = usePrivy(); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted || !ready) { - return ( - <> - - - - ); - } - - if (!authenticated) { - return ( - <> - -
- -
- - ); - } - return ( - <> + - - - + ); } From 8ea819cf2ab66128c7212b0fdae4cb8db50d0559 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:24:46 -0500 Subject: [PATCH 16/20] refactor: extract handleAgentRequest to DRY route boilerplate Co-Authored-By: Claude Opus 4.6 --- app/api/agent/new/route.ts | 20 ++------------------ app/api/agent/route.ts | 24 ++++-------------------- lib/agent/createAgentResponse.ts | 26 +++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts index f89b2ef1..643f4116 100644 --- a/app/api/agent/new/route.ts +++ b/app/api/agent/new/route.ts @@ -1,27 +1,11 @@ import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { createFreshSandbox } from "@/lib/sandbox/createFreshSandbox"; -import { createAgentResponse } from "@/lib/agent/createAgentResponse"; +import { handleAgentRequest } from "@/lib/agent/createAgentResponse"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "../_agent-data"); export async function POST(req: Request) { - const authHeader = req.headers.get("Authorization"); - if (!authHeader?.startsWith("Bearer ")) { - return Response.json( - { error: "Unauthorized" }, - { status: 401 }, - ); - } - - const { messages } = await req.json(); - const lastUserMessage = messages - .filter((m: { role: string }) => m.role === "user") - .pop(); - console.log("Prompt (no snapshot):", lastUserMessage?.parts?.[0]?.text); - - const sandbox = await createFreshSandbox(AGENT_DATA_DIR); - - return createAgentResponse(sandbox, messages); + return handleAgentRequest(req, () => createFreshSandbox(AGENT_DATA_DIR)); } diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 5f6624cf..6df7b5d7 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,29 +1,13 @@ import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { createNewSandbox } from "@/lib/sandbox/createNewSandbox"; -import { createAgentResponse } from "@/lib/agent/createAgentResponse"; +import { handleAgentRequest } from "@/lib/agent/createAgentResponse"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "./_agent-data"); export async function POST(req: Request) { - const authHeader = req.headers.get("Authorization"); - if (!authHeader?.startsWith("Bearer ")) { - return Response.json( - { error: "Unauthorized" }, - { status: 401 }, - ); - } - - const bearerToken = authHeader.slice("Bearer ".length); - - const { messages } = await req.json(); - const lastUserMessage = messages - .filter((m: { role: string }) => m.role === "user") - .pop(); - console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); - - const sandbox = await createNewSandbox(bearerToken, AGENT_DATA_DIR); - - return createAgentResponse(sandbox, messages); + return handleAgentRequest(req, (bearerToken) => + createNewSandbox(bearerToken, AGENT_DATA_DIR), + ); } diff --git a/lib/agent/createAgentResponse.ts b/lib/agent/createAgentResponse.ts index 74e50b06..03c451e0 100644 --- a/lib/agent/createAgentResponse.ts +++ b/lib/agent/createAgentResponse.ts @@ -3,7 +3,31 @@ import { createBashTool } from "bash-tool"; import { Sandbox } from "@vercel/sandbox"; import { SANDBOX_CWD, SYSTEM_INSTRUCTIONS, TOOL_PROMPT } from "./constants"; -export async function createAgentResponse( +type CreateSandbox = (bearerToken: string) => Promise; + +export async function handleAgentRequest( + req: Request, + createSandbox: CreateSandbox, +): Promise { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const bearerToken = authHeader.slice("Bearer ".length); + + const { messages } = await req.json(); + const lastUserMessage = messages + .filter((m: { role: string }) => m.role === "user") + .pop(); + console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); + + const sandbox = await createSandbox(bearerToken); + + return createAgentResponse(sandbox, messages); +} + +async function createAgentResponse( sandbox: Sandbox, messages: unknown[], ): Promise { From 31ce7116d94da2b6ecb1f838f38da426b4ec795b Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:26:55 -0500 Subject: [PATCH 17/20] refactor: centralize AGENT_DATA_DIR in constants Co-Authored-By: Claude Opus 4.6 --- app/api/agent/new/route.ts | 6 +----- app/api/agent/route.ts | 6 +----- lib/agent/constants.ts | 4 ++++ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts index 643f4116..f9545cff 100644 --- a/app/api/agent/new/route.ts +++ b/app/api/agent/new/route.ts @@ -1,10 +1,6 @@ -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; import { createFreshSandbox } from "@/lib/sandbox/createFreshSandbox"; import { handleAgentRequest } from "@/lib/agent/createAgentResponse"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const AGENT_DATA_DIR = join(__dirname, "../_agent-data"); +import { AGENT_DATA_DIR } from "@/lib/agent/constants"; export async function POST(req: Request) { return handleAgentRequest(req, () => createFreshSandbox(AGENT_DATA_DIR)); diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 6df7b5d7..427bc742 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,10 +1,6 @@ -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; import { createNewSandbox } from "@/lib/sandbox/createNewSandbox"; import { handleAgentRequest } from "@/lib/agent/createAgentResponse"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const AGENT_DATA_DIR = join(__dirname, "./_agent-data"); +import { AGENT_DATA_DIR } from "@/lib/agent/constants"; export async function POST(req: Request) { return handleAgentRequest(req, (bearerToken) => diff --git a/lib/agent/constants.ts b/lib/agent/constants.ts index baf72033..900abf02 100644 --- a/lib/agent/constants.ts +++ b/lib/agent/constants.ts @@ -1,3 +1,7 @@ +import { join } from "path"; + +export const AGENT_DATA_DIR = join(process.cwd(), "app/api/agent/_agent-data"); + export const SANDBOX_CWD = "/vercel/sandbox"; export const TOOL_PROMPT = From 73cc60d04dd73ec05ebf617f54dc6daa532bd011 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:29:50 -0500 Subject: [PATCH 18/20] refactor: rename createNewSandbox to createSnapshotSandbox Co-Authored-By: Claude Opus 4.6 --- app/api/agent/route.ts | 4 ++-- lib/sandbox/{createNewSandbox.ts => createSnapshotSandbox.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/sandbox/{createNewSandbox.ts => createSnapshotSandbox.ts} (92%) diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 427bc742..7cde8263 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,9 +1,9 @@ -import { createNewSandbox } from "@/lib/sandbox/createNewSandbox"; +import { createSnapshotSandbox } from "@/lib/sandbox/createSnapshotSandbox"; import { handleAgentRequest } from "@/lib/agent/createAgentResponse"; import { AGENT_DATA_DIR } from "@/lib/agent/constants"; export async function POST(req: Request) { return handleAgentRequest(req, (bearerToken) => - createNewSandbox(bearerToken, AGENT_DATA_DIR), + createSnapshotSandbox(bearerToken, AGENT_DATA_DIR), ); } diff --git a/lib/sandbox/createNewSandbox.ts b/lib/sandbox/createSnapshotSandbox.ts similarity index 92% rename from lib/sandbox/createNewSandbox.ts rename to lib/sandbox/createSnapshotSandbox.ts index 8d39aab0..a8a212b2 100644 --- a/lib/sandbox/createNewSandbox.ts +++ b/lib/sandbox/createSnapshotSandbox.ts @@ -2,7 +2,7 @@ import { Sandbox } from "@vercel/sandbox"; import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; import { createFreshSandbox } from "./createFreshSandbox"; -export async function createNewSandbox( +export async function createSnapshotSandbox( bearerToken: string, agentDataDir: string, ): Promise { From f321c8aede0ea51beeb83c54a515c97005b3107c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:31:39 -0500 Subject: [PATCH 19/20] refactor: extract readSourceFiles into its own lib file Co-Authored-By: Claude Opus 4.6 --- lib/sandbox/createFreshSandbox.ts | 28 +--------------------------- lib/sandbox/readSourceFiles.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 27 deletions(-) create mode 100644 lib/sandbox/readSourceFiles.ts diff --git a/lib/sandbox/createFreshSandbox.ts b/lib/sandbox/createFreshSandbox.ts index 2beb6b08..8026447c 100644 --- a/lib/sandbox/createFreshSandbox.ts +++ b/lib/sandbox/createFreshSandbox.ts @@ -1,31 +1,5 @@ import { Sandbox } from "@vercel/sandbox"; -import { readdirSync, readFileSync } from "fs"; -import { join, relative } from "path"; -import { SANDBOX_CWD } from "@/lib/agent/constants"; - -function readSourceFiles( - dir: string, - baseDir?: string, -): Array<{ path: string; content: Buffer }> { - const base = baseDir ?? dir; - const files: Array<{ path: string; content: Buffer }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === ".git") continue; - files.push(...readSourceFiles(fullPath, base)); - } else { - const relPath = relative(base, fullPath); - files.push({ - path: join(SANDBOX_CWD, relPath), - content: readFileSync(fullPath), - }); - } - } - - return files; -} +import { readSourceFiles } from "./readSourceFiles"; export async function createFreshSandbox(agentDataDir: string): Promise { const sandbox = await Sandbox.create(); diff --git a/lib/sandbox/readSourceFiles.ts b/lib/sandbox/readSourceFiles.ts new file mode 100644 index 00000000..3d0e5afc --- /dev/null +++ b/lib/sandbox/readSourceFiles.ts @@ -0,0 +1,27 @@ +import { readdirSync, readFileSync } from "fs"; +import { join, relative } from "path"; +import { SANDBOX_CWD } from "@/lib/agent/constants"; + +export function readSourceFiles( + dir: string, + baseDir?: string, +): Array<{ path: string; content: Buffer }> { + const base = baseDir ?? dir; + const files: Array<{ path: string; content: Buffer }> = []; + + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + files.push(...readSourceFiles(fullPath, base)); + } else { + const relPath = relative(base, fullPath); + files.push({ + path: join(SANDBOX_CWD, relPath), + content: readFileSync(fullPath), + }); + } + } + + return files; +} From 24feb3aa3552ceea7825e7fd741602c47afd7d5c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 9 Feb 2026 11:33:46 -0500 Subject: [PATCH 20/20] refactor: use POST /api/sandboxes + Sandbox.get for snapshot sandbox Co-Authored-By: Claude Opus 4.6 --- .../{getSnapshotId.ts => createSandbox.ts} | 13 +++++++++---- lib/sandbox/createSnapshotSandbox.ts | 12 +++++------- 2 files changed, 14 insertions(+), 11 deletions(-) rename lib/recoup-api/{getSnapshotId.ts => createSandbox.ts} (58%) diff --git a/lib/recoup-api/getSnapshotId.ts b/lib/recoup-api/createSandbox.ts similarity index 58% rename from lib/recoup-api/getSnapshotId.ts rename to lib/recoup-api/createSandbox.ts index b0e7b1be..24e13208 100644 --- a/lib/recoup-api/getSnapshotId.ts +++ b/lib/recoup-api/createSandbox.ts @@ -3,19 +3,24 @@ const RECOUP_API_URL = IS_PROD ? "https://recoup-api.vercel.app" : "https://test-recoup-api.vercel.app"; -export async function getSnapshotId( +export async function createSandbox( bearerToken: string, ): Promise { try { const response = await fetch(`${RECOUP_API_URL}/api/sandboxes`, { - headers: { Authorization: `Bearer ${bearerToken}` }, - signal: AbortSignal.timeout(5000), + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${bearerToken}`, + }, + body: JSON.stringify({}), + signal: AbortSignal.timeout(10000), }); if (!response.ok) return null; const data = await response.json(); - return data?.snapshot_id ?? null; + return data?.sandboxes?.[0]?.sandboxId ?? null; } catch { return null; } diff --git a/lib/sandbox/createSnapshotSandbox.ts b/lib/sandbox/createSnapshotSandbox.ts index a8a212b2..935b753e 100644 --- a/lib/sandbox/createSnapshotSandbox.ts +++ b/lib/sandbox/createSnapshotSandbox.ts @@ -1,20 +1,18 @@ import { Sandbox } from "@vercel/sandbox"; -import { getSnapshotId } from "@/lib/recoup-api/getSnapshotId"; +import { createSandbox } from "@/lib/recoup-api/createSandbox"; import { createFreshSandbox } from "./createFreshSandbox"; export async function createSnapshotSandbox( bearerToken: string, agentDataDir: string, ): Promise { - const snapshotId = await getSnapshotId(bearerToken); + const sandboxId = await createSandbox(bearerToken); - if (snapshotId) { + if (sandboxId) { try { - return await Sandbox.create({ - source: { type: "snapshot", snapshotId }, - }); + return await Sandbox.get({ sandboxId }); } catch (err) { - console.warn("Snapshot sandbox creation failed, falling back:", err); + console.warn("Snapshot sandbox connection failed, falling back:", err); } }