@townco/agent 0.1.140 → 0.1.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -9,14 +9,22 @@ import type { Sandbox } from "@e2b/code-interpreter";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function getSessionSandbox(apiKey: string): Promise<Sandbox>;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Pause a session's sandbox (called on session end or cleanup).
|
|
13
|
+
* The sandbox can be resumed later using the persisted sandboxId.
|
|
14
|
+
* State is preserved for up to 30 days.
|
|
14
15
|
*/
|
|
15
16
|
export declare function destroySessionSandbox(sessionId: string): Promise<void>;
|
|
16
17
|
/**
|
|
17
18
|
* Check if a session has an active sandbox.
|
|
18
19
|
*/
|
|
19
20
|
export declare function hasSessionSandbox(sessionId: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Clear the in-memory sandbox reference for a session.
|
|
23
|
+
* Call this when a sandbox operation fails due to the sandbox being
|
|
24
|
+
* paused/expired (e.g., "sandbox is probably not running anymore").
|
|
25
|
+
* The next call to getSessionSandbox() will attempt to reconnect.
|
|
26
|
+
*/
|
|
27
|
+
export declare function clearStaleSandbox(sessionId: string): void;
|
|
20
28
|
/**
|
|
21
29
|
* Get an existing sandbox by sessionId without creating a new one.
|
|
22
30
|
* Returns undefined if no sandbox exists for this session.
|
|
@@ -10,8 +10,8 @@ const sandboxActivity = new Map();
|
|
|
10
10
|
const cleanupTimeouts = new Map();
|
|
11
11
|
// Map sessionId -> Promise<Sandbox> for in-flight creations (prevents race condition)
|
|
12
12
|
const sandboxCreationPromises = new Map();
|
|
13
|
-
// Sandbox timeout in milliseconds (default:
|
|
14
|
-
const SANDBOX_TIMEOUT_MS =
|
|
13
|
+
// Sandbox timeout in milliseconds (default: 24 hours)
|
|
14
|
+
const SANDBOX_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
15
15
|
/**
|
|
16
16
|
* Collect environment variables that should be passed to E2B sandbox
|
|
17
17
|
* for tool usage (image generation, etc.)
|
|
@@ -76,6 +76,7 @@ async function createSandboxForSession(sessionId, apiKey) {
|
|
|
76
76
|
const { Sandbox: SandboxClass } = await import("@e2b/code-interpreter");
|
|
77
77
|
const sandbox = await SandboxClass.connect(persistedSandboxId, {
|
|
78
78
|
apiKey,
|
|
79
|
+
timeoutMs: SANDBOX_TIMEOUT_MS,
|
|
79
80
|
});
|
|
80
81
|
logger.info("Successfully reconnected to sandbox", {
|
|
81
82
|
sessionId,
|
|
@@ -121,7 +122,11 @@ async function createSandboxForSession(sessionId, apiKey) {
|
|
|
121
122
|
config.template = templateId;
|
|
122
123
|
logger.info("Using custom E2B template", { templateId });
|
|
123
124
|
}
|
|
124
|
-
const sandbox = await SandboxClass.
|
|
125
|
+
const sandbox = await SandboxClass.betaCreate({
|
|
126
|
+
...config,
|
|
127
|
+
autoPause: true,
|
|
128
|
+
timeoutMs: SANDBOX_TIMEOUT_MS,
|
|
129
|
+
});
|
|
125
130
|
logger.info("Created new sandbox", {
|
|
126
131
|
sessionId,
|
|
127
132
|
sandboxId: sandbox.sandboxId,
|
|
@@ -197,18 +202,23 @@ export async function getSessionSandbox(apiKey) {
|
|
|
197
202
|
}
|
|
198
203
|
}
|
|
199
204
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
205
|
+
* Pause a session's sandbox (called on session end or cleanup).
|
|
206
|
+
* The sandbox can be resumed later using the persisted sandboxId.
|
|
207
|
+
* State is preserved for up to 30 days.
|
|
202
208
|
*/
|
|
203
209
|
export async function destroySessionSandbox(sessionId) {
|
|
204
210
|
const sandbox = sessionSandboxes.get(sessionId);
|
|
205
211
|
if (sandbox) {
|
|
206
|
-
logger.info("
|
|
212
|
+
logger.info("Pausing sandbox", { sessionId, sandboxId: sandbox.sandboxId });
|
|
207
213
|
try {
|
|
208
|
-
await sandbox.
|
|
214
|
+
await sandbox.betaPause();
|
|
215
|
+
logger.info("Sandbox paused successfully", {
|
|
216
|
+
sessionId,
|
|
217
|
+
sandboxId: sandbox.sandboxId,
|
|
218
|
+
});
|
|
209
219
|
}
|
|
210
220
|
catch (error) {
|
|
211
|
-
logger.error("Error
|
|
221
|
+
logger.error("Error pausing sandbox", { sessionId, error });
|
|
212
222
|
}
|
|
213
223
|
sessionSandboxes.delete(sessionId);
|
|
214
224
|
sandboxActivity.delete(sessionId);
|
|
@@ -218,22 +228,7 @@ export async function destroySessionSandbox(sessionId) {
|
|
|
218
228
|
clearTimeout(timeout);
|
|
219
229
|
cleanupTimeouts.delete(sessionId);
|
|
220
230
|
}
|
|
221
|
-
//
|
|
222
|
-
// We do this without requiring session context since this can be called
|
|
223
|
-
// from HTTP endpoints that don't have session context
|
|
224
|
-
try {
|
|
225
|
-
// Try to get storage if we have context, but don't fail if we don't
|
|
226
|
-
if (hasSessionContext()) {
|
|
227
|
-
const storage = getSessionStorage();
|
|
228
|
-
if (storage) {
|
|
229
|
-
await storage.updateSandboxId(sessionId, undefined);
|
|
230
|
-
logger.debug("Cleared persisted sandboxId", { sessionId });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
logger.warn("Failed to clear persisted sandboxId", { sessionId, error });
|
|
236
|
-
}
|
|
231
|
+
// Note: We intentionally keep the sandboxId in storage so we can resume later
|
|
237
232
|
}
|
|
238
233
|
}
|
|
239
234
|
/**
|
|
@@ -267,6 +262,22 @@ function rescheduleCleanup(sessionId) {
|
|
|
267
262
|
export function hasSessionSandbox(sessionId) {
|
|
268
263
|
return sessionSandboxes.has(sessionId);
|
|
269
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Clear the in-memory sandbox reference for a session.
|
|
267
|
+
* Call this when a sandbox operation fails due to the sandbox being
|
|
268
|
+
* paused/expired (e.g., "sandbox is probably not running anymore").
|
|
269
|
+
* The next call to getSessionSandbox() will attempt to reconnect.
|
|
270
|
+
*/
|
|
271
|
+
export function clearStaleSandbox(sessionId) {
|
|
272
|
+
logger.info("Clearing stale sandbox reference", { sessionId });
|
|
273
|
+
sessionSandboxes.delete(sessionId);
|
|
274
|
+
sandboxActivity.delete(sessionId);
|
|
275
|
+
const timeout = cleanupTimeouts.get(sessionId);
|
|
276
|
+
if (timeout) {
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
cleanupTimeouts.delete(sessionId);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
270
281
|
/**
|
|
271
282
|
* Get an existing sandbox by sessionId without creating a new one.
|
|
272
283
|
* Returns undefined if no sandbox exists for this session.
|
|
@@ -4,7 +4,7 @@ import { getShedAuth } from "@townco/core/auth";
|
|
|
4
4
|
import { tool } from "langchain";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { createLogger } from "../../../logger.js";
|
|
7
|
-
import { getSessionSandbox } from "../../e2b-sandbox-manager";
|
|
7
|
+
import { clearStaleSandbox, getSessionSandbox, } from "../../e2b-sandbox-manager";
|
|
8
8
|
import { getEmitUpdate, getSessionContext, getToolOutputDir, hasSessionContext, } from "../../session-context";
|
|
9
9
|
const logger = createLogger("e2b-tools");
|
|
10
10
|
// Cached API key from Town proxy
|
|
@@ -48,6 +48,45 @@ export async function getTownE2BApiKey() {
|
|
|
48
48
|
_apiKeyFetchPromise = null;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if an error indicates the sandbox is stale (paused/expired).
|
|
53
|
+
* E2B throws this when the sandbox was auto-paused or timed out.
|
|
54
|
+
*/
|
|
55
|
+
function isStaleSandboxError(error) {
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
const msg = error.message.toLowerCase();
|
|
58
|
+
return (msg.includes("sandbox is probably not running") ||
|
|
59
|
+
msg.includes("sandbox not found") ||
|
|
60
|
+
msg.includes("not running anymore"));
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Execute a sandbox operation with automatic retry on stale sandbox errors.
|
|
66
|
+
* If the sandbox was auto-paused by E2B, this clears the stale reference
|
|
67
|
+
* and reconnects before retrying.
|
|
68
|
+
*/
|
|
69
|
+
async function withSandboxRetry(getSandbox, operation) {
|
|
70
|
+
let sandbox = await getSandbox();
|
|
71
|
+
try {
|
|
72
|
+
return await operation(sandbox);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (isStaleSandboxError(error)) {
|
|
76
|
+
logger.info("Sandbox appears stale (auto-paused), clearing and reconnecting...");
|
|
77
|
+
// Clear the stale sandbox reference
|
|
78
|
+
if (hasSessionContext()) {
|
|
79
|
+
const { sessionId } = getSessionContext();
|
|
80
|
+
clearStaleSandbox(sessionId);
|
|
81
|
+
}
|
|
82
|
+
// Get a fresh sandbox (will reconnect/resume the paused sandbox)
|
|
83
|
+
sandbox = await getSandbox();
|
|
84
|
+
// Retry the operation
|
|
85
|
+
return await operation(sandbox);
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
51
90
|
/**
|
|
52
91
|
* Helper to save image artifacts from code execution results.
|
|
53
92
|
*/
|
|
@@ -73,9 +112,8 @@ async function saveImageArtifact(base64Data, format) {
|
|
|
73
112
|
function makeE2BToolsInternal(getSandbox) {
|
|
74
113
|
// Tool 1: Run Code (Python or JavaScript)
|
|
75
114
|
const runCode = tool(async ({ code, language = "python" }) => {
|
|
76
|
-
const sandbox = await getSandbox();
|
|
77
115
|
try {
|
|
78
|
-
const result = await sandbox.runCode(code, { language });
|
|
116
|
+
const result = await withSandboxRetry(getSandbox, (sandbox) => sandbox.runCode(code, { language }));
|
|
79
117
|
// Format output
|
|
80
118
|
let output = "";
|
|
81
119
|
if (result.logs?.stdout && result.logs.stdout.length > 0) {
|
|
@@ -174,9 +212,8 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
174
212
|
};
|
|
175
213
|
// Tool 2: Run Bash Command
|
|
176
214
|
const runBash = tool(async ({ command }) => {
|
|
177
|
-
const sandbox = await getSandbox();
|
|
178
215
|
try {
|
|
179
|
-
const result = await sandbox.commands.run(command);
|
|
216
|
+
const result = await withSandboxRetry(getSandbox, (sandbox) => sandbox.commands.run(command));
|
|
180
217
|
let output = "";
|
|
181
218
|
if (result.stdout) {
|
|
182
219
|
output += result.stdout;
|
|
@@ -244,9 +281,8 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
244
281
|
};
|
|
245
282
|
// Tool 3: Read File from Sandbox
|
|
246
283
|
const readSandboxFile = tool(async ({ path: filePath }) => {
|
|
247
|
-
const sandbox = await getSandbox();
|
|
248
284
|
try {
|
|
249
|
-
const content = await sandbox.files.read(filePath);
|
|
285
|
+
const content = await withSandboxRetry(getSandbox, (sandbox) => sandbox.files.read(filePath));
|
|
250
286
|
return content;
|
|
251
287
|
}
|
|
252
288
|
catch (error) {
|
|
@@ -267,9 +303,8 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
267
303
|
readSandboxFile.icon = "FileText";
|
|
268
304
|
// Tool 4: Write File to Sandbox
|
|
269
305
|
const writeSandboxFile = tool(async ({ path: filePath, content }) => {
|
|
270
|
-
const sandbox = await getSandbox();
|
|
271
306
|
try {
|
|
272
|
-
await sandbox.files.write(filePath, content);
|
|
307
|
+
await withSandboxRetry(getSandbox, (sandbox) => sandbox.files.write(filePath, content));
|
|
273
308
|
// Emit file change notification
|
|
274
309
|
const emitUpdate = getEmitUpdate();
|
|
275
310
|
if (emitUpdate) {
|
|
@@ -312,13 +347,12 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
312
347
|
if (!hasSessionContext()) {
|
|
313
348
|
throw new Error("Sandbox_Share requires session context");
|
|
314
349
|
}
|
|
315
|
-
const sandbox = await getSandbox();
|
|
316
350
|
const { sessionId } = getSessionContext();
|
|
317
351
|
const toolOutputDir = getToolOutputDir("E2B");
|
|
318
352
|
try {
|
|
319
353
|
// Step 1: Download from sandbox to local artifacts
|
|
320
354
|
// Use base64 encoding to safely transfer binary data
|
|
321
|
-
const result = await sandbox.commands.run(`base64 ${sandboxPath}`);
|
|
355
|
+
const result = await withSandboxRetry(getSandbox, (sandbox) => sandbox.commands.run(`base64 ${sandboxPath}`));
|
|
322
356
|
if (result.exitCode !== 0) {
|
|
323
357
|
throw new Error(`Failed to read file: ${result.stderr}`);
|
|
324
358
|
}
|
|
@@ -405,13 +439,19 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
405
439
|
shareSandboxFile.icon = "Share";
|
|
406
440
|
// Tool 6: Load Library Documents to Sandbox
|
|
407
441
|
const loadLibraryDocuments = tool(async ({ document_ids }) => {
|
|
408
|
-
const sandbox = await getSandbox();
|
|
409
442
|
try {
|
|
410
443
|
const libraryApiUrl = process.env.LIBRARY_API_URL;
|
|
411
444
|
const libraryApiKey = process.env.LIBRARY_API_KEY;
|
|
412
445
|
if (!libraryApiUrl || !libraryApiKey) {
|
|
413
446
|
throw new Error("LIBRARY_API_URL and LIBRARY_API_KEY environment variables are required");
|
|
414
447
|
}
|
|
448
|
+
// Get sandbox (with retry in case it was auto-paused) to ensure it's active
|
|
449
|
+
// The library API needs the sandbox to be running to upload files
|
|
450
|
+
const sandbox = await withSandboxRetry(getSandbox, async (s) => {
|
|
451
|
+
// Run a simple command to verify the sandbox is actually running
|
|
452
|
+
await s.commands.run("true");
|
|
453
|
+
return s;
|
|
454
|
+
});
|
|
415
455
|
const response = await fetch(`${libraryApiUrl}/sandbox/upload_documents_to_sandbox`, {
|
|
416
456
|
method: "POST",
|
|
417
457
|
headers: {
|
|
@@ -475,7 +515,6 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
475
515
|
};
|
|
476
516
|
// Tool 7: Generate Image in Sandbox
|
|
477
517
|
const generateImage = tool(async ({ prompt }) => {
|
|
478
|
-
const sandbox = await getSandbox();
|
|
479
518
|
try {
|
|
480
519
|
// JavaScript script to call Gemini API using @google/genai
|
|
481
520
|
const escapedPrompt = prompt
|
|
@@ -566,10 +605,13 @@ async function generateImage() {
|
|
|
566
605
|
|
|
567
606
|
generateImage();
|
|
568
607
|
`;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
608
|
+
// Run all sandbox operations with retry support
|
|
609
|
+
const result = await withSandboxRetry(getSandbox, async (sandbox) => {
|
|
610
|
+
await sandbox.files.write("/home/user/gen_img.js", script);
|
|
611
|
+
// Install @google/genai if not already installed (should be pre-installed in template)
|
|
612
|
+
await sandbox.commands.run("cd /home/user && npm install @google/genai");
|
|
613
|
+
return sandbox.commands.run("cd /home/user && node gen_img.js");
|
|
614
|
+
});
|
|
573
615
|
logger.info("Image generation command result", {
|
|
574
616
|
exitCode: result.exitCode,
|
|
575
617
|
stdout: result.stdout,
|
|
@@ -110,6 +110,31 @@ function getSourceName(url) {
|
|
|
110
110
|
*/
|
|
111
111
|
function extractSourcesFromToolOutput(toolName, rawOutput, toolCallId, sourceCounter) {
|
|
112
112
|
const sources = [];
|
|
113
|
+
// Check for pre-extracted sources from runner (extracted before compaction)
|
|
114
|
+
// These preserve URLs that may be stripped during LLM compaction
|
|
115
|
+
if (rawOutput._compactionMeta &&
|
|
116
|
+
typeof rawOutput._compactionMeta === "object") {
|
|
117
|
+
const meta = rawOutput._compactionMeta;
|
|
118
|
+
if (meta.preExtractedSources && meta.preExtractedSources.length > 0) {
|
|
119
|
+
logger.info("Using pre-extracted sources from runner", {
|
|
120
|
+
toolName,
|
|
121
|
+
toolCallId,
|
|
122
|
+
sourcesCount: meta.preExtractedSources.length,
|
|
123
|
+
});
|
|
124
|
+
for (const s of meta.preExtractedSources) {
|
|
125
|
+
sources.push({
|
|
126
|
+
id: s.id,
|
|
127
|
+
url: s.url,
|
|
128
|
+
title: s.title,
|
|
129
|
+
toolCallId,
|
|
130
|
+
...(s.snippet && { snippet: s.snippet }),
|
|
131
|
+
...(s.favicon && { favicon: s.favicon }),
|
|
132
|
+
...(s.sourceName && { sourceName: s.sourceName }),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return sources;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
113
138
|
// Parse the actual output from the wrapper
|
|
114
139
|
// The runner wraps results as { content: JSON.stringify(actualResult) }
|
|
115
140
|
let actualOutput = rawOutput;
|