@townco/agent 0.1.107 → 0.1.109
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.
- package/dist/acp-server/adapter.d.ts +11 -1
- package/dist/acp-server/adapter.js +171 -17
- package/dist/acp-server/http.js +146 -0
- package/dist/acp-server/session-storage.d.ts +11 -0
- package/dist/acp-server/session-storage.js +45 -0
- package/dist/runner/agent-runner.d.ts +13 -4
- package/dist/runner/e2b-sandbox-manager.d.ts +8 -0
- package/dist/runner/e2b-sandbox-manager.js +163 -2
- package/dist/runner/hooks/predefined/compaction-tool.js +52 -39
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/langchain/index.js +43 -38
- package/dist/runner/langchain/tools/e2b.d.ts +10 -0
- package/dist/runner/langchain/tools/e2b.js +323 -32
- package/dist/runner/session-context.d.ts +17 -0
- package/dist/runner/session-context.js +35 -0
- package/dist/runner/tools.d.ts +2 -5
- package/dist/runner/tools.js +1 -15
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/dist/runner/langchain/tools/artifacts.d.ts +0 -68
- package/dist/runner/langchain/tools/artifacts.js +0 -474
- package/dist/runner/langchain/tools/generate_image.d.ts +0 -47
- package/dist/runner/langchain/tools/generate_image.js +0 -175
|
@@ -31,7 +31,16 @@ export interface CitationSource {
|
|
|
31
31
|
*/
|
|
32
32
|
export declare class MidTurnRestartError extends Error {
|
|
33
33
|
newContextEntry: ContextEntry;
|
|
34
|
-
|
|
34
|
+
hookId: string;
|
|
35
|
+
constructor(message: string, newContextEntry: ContextEntry, hookId: string);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Error thrown when the Claude API returns "prompt is too long" error.
|
|
39
|
+
* The adapter catches this error, forces context compaction, and retries.
|
|
40
|
+
*/
|
|
41
|
+
export declare class ContextOverflowError extends Error {
|
|
42
|
+
originalError: Error;
|
|
43
|
+
constructor(originalError: Error);
|
|
35
44
|
}
|
|
36
45
|
/** Adapts an Agent to speak the ACP protocol */
|
|
37
46
|
export declare class AgentAcpAdapter implements acp.Agent {
|
|
@@ -104,6 +113,7 @@ export declare class AgentAcpAdapter implements acp.Agent {
|
|
|
104
113
|
setSessionMode(_params: acp.SetSessionModeRequest): Promise<acp.SetSessionModeResponse>;
|
|
105
114
|
prompt(params: acp.PromptRequest): Promise<acp.PromptResponse>;
|
|
106
115
|
private _promptImpl;
|
|
116
|
+
private forceCompaction;
|
|
107
117
|
private executeHooksIfConfigured;
|
|
108
118
|
private _executeHooksImpl;
|
|
109
119
|
cancel(params: acp.CancelNotification): Promise<void>;
|
|
@@ -20,10 +20,24 @@ export const SUBAGENT_MODE_KEY = "town.com/isSubagent";
|
|
|
20
20
|
*/
|
|
21
21
|
export class MidTurnRestartError extends Error {
|
|
22
22
|
newContextEntry;
|
|
23
|
-
|
|
23
|
+
hookId;
|
|
24
|
+
constructor(message, newContextEntry, hookId) {
|
|
24
25
|
super(message);
|
|
25
26
|
this.name = "MidTurnRestartError";
|
|
26
27
|
this.newContextEntry = newContextEntry;
|
|
28
|
+
this.hookId = hookId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Error thrown when the Claude API returns "prompt is too long" error.
|
|
33
|
+
* The adapter catches this error, forces context compaction, and retries.
|
|
34
|
+
*/
|
|
35
|
+
export class ContextOverflowError extends Error {
|
|
36
|
+
originalError;
|
|
37
|
+
constructor(originalError) {
|
|
38
|
+
super(`Context overflow: ${originalError.message}`);
|
|
39
|
+
this.name = "ContextOverflowError";
|
|
40
|
+
this.originalError = originalError;
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
/**
|
|
@@ -179,15 +193,6 @@ export class AgentAcpAdapter {
|
|
|
179
193
|
...(tool.icon ? { icon: tool.icon } : {}),
|
|
180
194
|
};
|
|
181
195
|
}
|
|
182
|
-
else if (tool.type === "filesystem") {
|
|
183
|
-
// Filesystem is a tool group - dynamically get children
|
|
184
|
-
const children = getToolGroupChildren("filesystem");
|
|
185
|
-
return {
|
|
186
|
-
name: "filesystem",
|
|
187
|
-
description: "File system access tools",
|
|
188
|
-
...(children ? { children } : {}),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
196
|
else if (tool.type === "custom") {
|
|
192
197
|
// Custom tools from module paths - extract name from path
|
|
193
198
|
const pathParts = tool.modulePath.split("/");
|
|
@@ -347,7 +352,8 @@ export class AgentAcpAdapter {
|
|
|
347
352
|
// These return structured data with document_url, title, summary, document_id, etc.
|
|
348
353
|
const isLibraryTool = toolName.startsWith("library__") ||
|
|
349
354
|
toolName.includes("get_document") ||
|
|
350
|
-
toolName.includes("retrieve_document")
|
|
355
|
+
toolName.includes("retrieve_document") ||
|
|
356
|
+
toolName.includes("bibliotecha");
|
|
351
357
|
logger.info("Library tool check", {
|
|
352
358
|
toolName,
|
|
353
359
|
isLibraryTool,
|
|
@@ -1221,6 +1227,17 @@ export class AgentAcpAdapter {
|
|
|
1221
1227
|
}
|
|
1222
1228
|
: null,
|
|
1223
1229
|
});
|
|
1230
|
+
// Create emitUpdate callback for tools to emit session updates
|
|
1231
|
+
const emitUpdate = (update) => {
|
|
1232
|
+
logger.debug("Adapter emitting session update", {
|
|
1233
|
+
sessionId: params.sessionId,
|
|
1234
|
+
updateType: update?.sessionUpdate,
|
|
1235
|
+
});
|
|
1236
|
+
this.connection.sessionUpdate({
|
|
1237
|
+
sessionId: params.sessionId,
|
|
1238
|
+
update: update,
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1224
1241
|
const invokeParams = {
|
|
1225
1242
|
prompt: params.prompt,
|
|
1226
1243
|
sessionId: params.sessionId,
|
|
@@ -1231,6 +1248,8 @@ export class AgentAcpAdapter {
|
|
|
1231
1248
|
contextMessages,
|
|
1232
1249
|
// Pass abort signal for cancellation
|
|
1233
1250
|
abortSignal: session.pendingPrompt?.signal,
|
|
1251
|
+
// Pass emitUpdate callback for file change events
|
|
1252
|
+
emitUpdate,
|
|
1234
1253
|
};
|
|
1235
1254
|
// Only add sessionMeta if it's defined
|
|
1236
1255
|
if (session.requestParams._meta) {
|
|
@@ -1664,6 +1683,26 @@ export class AgentAcpAdapter {
|
|
|
1664
1683
|
tokensSaved: hookResult.metadata.tokensSaved,
|
|
1665
1684
|
summaryTokens: hookResult.metadata.summaryTokens,
|
|
1666
1685
|
});
|
|
1686
|
+
// Generate hookId for the notification pair
|
|
1687
|
+
const hookId = `hook_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1688
|
+
// Send hook_triggered notification immediately to show "Compacting Context..."
|
|
1689
|
+
// This uses the same visualization as regular context compaction
|
|
1690
|
+
this.connection.sessionUpdate({
|
|
1691
|
+
sessionId: params.sessionId,
|
|
1692
|
+
update: {
|
|
1693
|
+
sessionUpdate: "hook_notification",
|
|
1694
|
+
id: hookId,
|
|
1695
|
+
notification: {
|
|
1696
|
+
type: "hook_triggered",
|
|
1697
|
+
hookType: "context_size",
|
|
1698
|
+
callback: "compaction_tool",
|
|
1699
|
+
metadata: {
|
|
1700
|
+
midTurn: true,
|
|
1701
|
+
},
|
|
1702
|
+
triggeredAt: Date.now(),
|
|
1703
|
+
},
|
|
1704
|
+
},
|
|
1705
|
+
});
|
|
1667
1706
|
// Store the pending tool output in session for replay after restart
|
|
1668
1707
|
// We'll include this as part of the compacted context
|
|
1669
1708
|
session.pendingToolOutput = {
|
|
@@ -1673,7 +1712,8 @@ export class AgentAcpAdapter {
|
|
|
1673
1712
|
};
|
|
1674
1713
|
// Throw an error to abort the current turn
|
|
1675
1714
|
// The handlePrompt method will catch this and restart
|
|
1676
|
-
|
|
1715
|
+
// Pass the hookId so the catch block can send the hook_completed notification
|
|
1716
|
+
throw new MidTurnRestartError("Context compacted mid-turn, restart required", hookResult.newContextEntry, hookId);
|
|
1677
1717
|
}
|
|
1678
1718
|
}
|
|
1679
1719
|
}
|
|
@@ -1946,14 +1986,26 @@ export class AgentAcpAdapter {
|
|
|
1946
1986
|
});
|
|
1947
1987
|
// Clear the pending tool output since it's now part of the context
|
|
1948
1988
|
delete session.pendingToolOutput;
|
|
1949
|
-
//
|
|
1989
|
+
// Send hook_completed notification to mark compaction as done
|
|
1990
|
+
// The hook_triggered notification was already sent before the error was thrown
|
|
1991
|
+
const summaryTokens = err.newContextEntry.context_size?.totalEstimated ?? 0;
|
|
1950
1992
|
this.connection.sessionUpdate({
|
|
1951
1993
|
sessionId: params.sessionId,
|
|
1952
1994
|
update: {
|
|
1953
|
-
sessionUpdate: "
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1995
|
+
sessionUpdate: "hook_notification",
|
|
1996
|
+
id: err.hookId,
|
|
1997
|
+
notification: {
|
|
1998
|
+
type: "hook_completed",
|
|
1999
|
+
hookType: "context_size",
|
|
2000
|
+
callback: "compaction_tool",
|
|
2001
|
+
metadata: {
|
|
2002
|
+
action: "compacted",
|
|
2003
|
+
midTurn: true,
|
|
2004
|
+
messagesRemoved: err.newContextEntry.compactedUpTo ?? 0,
|
|
2005
|
+
summaryTokens,
|
|
2006
|
+
summaryGenerated: true,
|
|
2007
|
+
},
|
|
2008
|
+
completedAt: Date.now(),
|
|
1957
2009
|
},
|
|
1958
2010
|
},
|
|
1959
2011
|
});
|
|
@@ -1961,6 +2013,35 @@ export class AgentAcpAdapter {
|
|
|
1961
2013
|
// _promptImpl will resolve the updated context from session.context
|
|
1962
2014
|
return this._promptImpl(params);
|
|
1963
2015
|
}
|
|
2016
|
+
// Handle context overflow with compaction and retry
|
|
2017
|
+
if (err instanceof ContextOverflowError) {
|
|
2018
|
+
// Track retry count to prevent infinite loops (max 3 retries)
|
|
2019
|
+
const retryCount = session
|
|
2020
|
+
._overflowRetryCount ?? 0;
|
|
2021
|
+
if (retryCount >= 3) {
|
|
2022
|
+
logger.error("Max overflow retry count reached - giving up", {
|
|
2023
|
+
sessionId: params.sessionId,
|
|
2024
|
+
retryCount,
|
|
2025
|
+
});
|
|
2026
|
+
throw err.originalError;
|
|
2027
|
+
}
|
|
2028
|
+
session._overflowRetryCount = retryCount + 1;
|
|
2029
|
+
logger.warn("Context overflow detected - forcing compaction and retry", {
|
|
2030
|
+
sessionId: params.sessionId,
|
|
2031
|
+
retryAttempt: retryCount + 1,
|
|
2032
|
+
});
|
|
2033
|
+
// Force compaction
|
|
2034
|
+
const compactionResult = await this.forceCompaction(session, params.sessionId);
|
|
2035
|
+
if (compactionResult.success) {
|
|
2036
|
+
// Retry with compacted context
|
|
2037
|
+
return this._promptImpl(params);
|
|
2038
|
+
}
|
|
2039
|
+
// Compaction failed - throw original error
|
|
2040
|
+
logger.error("Force compaction failed", {
|
|
2041
|
+
sessionId: params.sessionId,
|
|
2042
|
+
});
|
|
2043
|
+
throw err.originalError;
|
|
2044
|
+
}
|
|
1964
2045
|
throw err;
|
|
1965
2046
|
}
|
|
1966
2047
|
// Store the complete assistant response in session messages
|
|
@@ -2035,6 +2116,79 @@ export class AgentAcpAdapter {
|
|
|
2035
2116
|
stopReason: "end_turn",
|
|
2036
2117
|
};
|
|
2037
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Force compaction of the session context.
|
|
2121
|
+
* Used for error recovery when the Claude API returns "prompt is too long".
|
|
2122
|
+
* This bypasses the normal threshold check and always compacts.
|
|
2123
|
+
*/
|
|
2124
|
+
async forceCompaction(session, sessionId) {
|
|
2125
|
+
logger.info("Force compaction started", {
|
|
2126
|
+
sessionId,
|
|
2127
|
+
messageCount: session.messages.length,
|
|
2128
|
+
contextEntries: session.context.length,
|
|
2129
|
+
});
|
|
2130
|
+
try {
|
|
2131
|
+
// Create a hook configuration that forces compaction
|
|
2132
|
+
// Using callbacks array which accepts Record<string, unknown> for settings
|
|
2133
|
+
const forceCompactionHook = {
|
|
2134
|
+
type: "context_size",
|
|
2135
|
+
callbacks: [
|
|
2136
|
+
{
|
|
2137
|
+
name: "compaction_tool",
|
|
2138
|
+
setting: {
|
|
2139
|
+
forceCompact: true,
|
|
2140
|
+
},
|
|
2141
|
+
},
|
|
2142
|
+
],
|
|
2143
|
+
};
|
|
2144
|
+
// Create notification callback
|
|
2145
|
+
const sendHookNotification = (notification) => {
|
|
2146
|
+
this.connection.sessionUpdate({
|
|
2147
|
+
sessionId,
|
|
2148
|
+
update: {
|
|
2149
|
+
sessionUpdate: "hook_notification",
|
|
2150
|
+
id: `hook_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
2151
|
+
notification,
|
|
2152
|
+
},
|
|
2153
|
+
});
|
|
2154
|
+
};
|
|
2155
|
+
// Create hook executor with force compaction settings
|
|
2156
|
+
const hookExecutor = new HookExecutor([forceCompactionHook], this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir), sendHookNotification, this.agent.definition);
|
|
2157
|
+
// Create read-only session view for hooks
|
|
2158
|
+
const readonlySession = {
|
|
2159
|
+
messages: session.messages,
|
|
2160
|
+
context: session.context,
|
|
2161
|
+
requestParams: session.requestParams,
|
|
2162
|
+
};
|
|
2163
|
+
// Get current token estimate - use a high value to ensure compaction triggers
|
|
2164
|
+
const latestContext = session.context.length > 0
|
|
2165
|
+
? session.context[session.context.length - 1]
|
|
2166
|
+
: undefined;
|
|
2167
|
+
const currentTokens = latestContext?.context_size.totalEstimated ?? 0;
|
|
2168
|
+
// Execute hooks
|
|
2169
|
+
const hookResult = await hookExecutor.executeHooks(readonlySession, currentTokens);
|
|
2170
|
+
// Apply new context entries
|
|
2171
|
+
if (hookResult.newContextEntries.length > 0) {
|
|
2172
|
+
session.context.push(...hookResult.newContextEntries);
|
|
2173
|
+
logger.info("Force compaction succeeded", {
|
|
2174
|
+
sessionId,
|
|
2175
|
+
newContextEntries: hookResult.newContextEntries.length,
|
|
2176
|
+
});
|
|
2177
|
+
return { success: true };
|
|
2178
|
+
}
|
|
2179
|
+
logger.warn("Force compaction produced no new context entries", {
|
|
2180
|
+
sessionId,
|
|
2181
|
+
});
|
|
2182
|
+
return { success: false };
|
|
2183
|
+
}
|
|
2184
|
+
catch (error) {
|
|
2185
|
+
logger.error("Force compaction error", {
|
|
2186
|
+
sessionId,
|
|
2187
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2188
|
+
});
|
|
2189
|
+
return { success: false };
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2038
2192
|
/**
|
|
2039
2193
|
* Execute hooks if configured for this agent
|
|
2040
2194
|
* Returns new context entries that should be appended to session.context
|
package/dist/acp-server/http.js
CHANGED
|
@@ -9,6 +9,7 @@ import { cors } from "hono/cors";
|
|
|
9
9
|
import { streamSSE } from "hono/streaming";
|
|
10
10
|
import { createLogger, isSubagent } from "../logger.js";
|
|
11
11
|
import { makeRunnerFromDefinition } from "../runner";
|
|
12
|
+
import { destroySessionSandbox, getExistingSandbox, hasSessionSandbox, } from "../runner/e2b-sandbox-manager";
|
|
12
13
|
import { AgentAcpAdapter } from "./adapter";
|
|
13
14
|
import { SessionStorage } from "./session-storage";
|
|
14
15
|
const logger = createLogger("http");
|
|
@@ -510,6 +511,151 @@ export function createAcpHttpApp(agent, agentDir, agentName) {
|
|
|
510
511
|
}, 500);
|
|
511
512
|
}
|
|
512
513
|
});
|
|
514
|
+
// Sandbox file listing endpoints
|
|
515
|
+
app.get("/sandbox/status", async (c) => {
|
|
516
|
+
const sessionId = c.req.query("sessionId");
|
|
517
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
518
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const status = hasSessionSandbox(sessionId) ? "ready" : "none";
|
|
522
|
+
return c.json({ status });
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
logger.error("Error checking sandbox status", { error, sessionId });
|
|
526
|
+
return c.json({ error: "Failed to check sandbox status" }, 500);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
app.get("/sandbox/files", async (c) => {
|
|
530
|
+
const sessionId = c.req.query("sessionId");
|
|
531
|
+
const path = c.req.query("path") || "/home/user";
|
|
532
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
533
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
// Check if sandbox exists
|
|
537
|
+
if (!hasSessionSandbox(sessionId)) {
|
|
538
|
+
return c.json({ files: [] }); // No sandbox yet
|
|
539
|
+
}
|
|
540
|
+
// Get existing sandbox by sessionId
|
|
541
|
+
const sandbox = getExistingSandbox(sessionId);
|
|
542
|
+
if (!sandbox) {
|
|
543
|
+
return c.json({ files: [] }); // Sandbox no longer exists
|
|
544
|
+
}
|
|
545
|
+
// List directory using find command with detailed info
|
|
546
|
+
const result = await sandbox.commands.run(`find "${path}" -maxdepth 1 -printf '%p\\t%s\\t%T@\\t%y\\n' 2>/dev/null || echo ""`);
|
|
547
|
+
if (result.exitCode !== 0 && result.stderr) {
|
|
548
|
+
logger.warn("Error listing sandbox files", {
|
|
549
|
+
sessionId,
|
|
550
|
+
path,
|
|
551
|
+
stderr: result.stderr,
|
|
552
|
+
});
|
|
553
|
+
return c.json({ files: [] });
|
|
554
|
+
}
|
|
555
|
+
// Parse output
|
|
556
|
+
const files = result.stdout
|
|
557
|
+
.split("\n")
|
|
558
|
+
.filter((line) => line.trim())
|
|
559
|
+
.map((line) => {
|
|
560
|
+
const [fullPath, size, mtime, type] = line.split("\t");
|
|
561
|
+
// Skip if any required field is missing
|
|
562
|
+
if (!fullPath || !size || !mtime || !type)
|
|
563
|
+
return null;
|
|
564
|
+
const name = fullPath.split("/").pop() || "";
|
|
565
|
+
// Skip . and .. entries, hidden files (starting with .), and the root path itself
|
|
566
|
+
if (name === "." ||
|
|
567
|
+
name === ".." ||
|
|
568
|
+
name.startsWith(".") ||
|
|
569
|
+
fullPath === path)
|
|
570
|
+
return null;
|
|
571
|
+
return {
|
|
572
|
+
name,
|
|
573
|
+
path: fullPath,
|
|
574
|
+
type: type === "d" ? "dir" : "file",
|
|
575
|
+
size: Number.parseInt(size, 10) || 0,
|
|
576
|
+
lastModified: Number.parseFloat(mtime) * 1000,
|
|
577
|
+
};
|
|
578
|
+
})
|
|
579
|
+
.filter(Boolean); // Remove null entries
|
|
580
|
+
return c.json({ files });
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
// Handle NotFoundError - sandbox no longer exists on E2B
|
|
584
|
+
if (error &&
|
|
585
|
+
typeof error === "object" &&
|
|
586
|
+
"name" in error &&
|
|
587
|
+
error.name === "NotFoundError") {
|
|
588
|
+
logger.warn("Sandbox not found on E2B, cleaning up local reference", {
|
|
589
|
+
sessionId,
|
|
590
|
+
});
|
|
591
|
+
// Remove stale sandbox from local cache
|
|
592
|
+
await destroySessionSandbox(sessionId);
|
|
593
|
+
return c.json({ files: [] });
|
|
594
|
+
}
|
|
595
|
+
logger.error("Error listing sandbox files", { error, sessionId, path });
|
|
596
|
+
return c.json({ error: "Failed to list sandbox files" }, 500);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
// Download file content from sandbox
|
|
600
|
+
app.get("/sandbox/download", async (c) => {
|
|
601
|
+
const sessionId = c.req.query("sessionId");
|
|
602
|
+
const filePath = c.req.query("path");
|
|
603
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
604
|
+
return c.json({ error: "sessionId required" }, 400);
|
|
605
|
+
}
|
|
606
|
+
if (!filePath || typeof filePath !== "string") {
|
|
607
|
+
return c.json({ error: "path required" }, 400);
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
// Check if sandbox exists
|
|
611
|
+
if (!hasSessionSandbox(sessionId)) {
|
|
612
|
+
return c.json({ error: "Sandbox not found" }, 404);
|
|
613
|
+
}
|
|
614
|
+
// Get existing sandbox by sessionId
|
|
615
|
+
const sandbox = getExistingSandbox(sessionId);
|
|
616
|
+
if (!sandbox) {
|
|
617
|
+
return c.json({ error: "Sandbox no longer exists" }, 404);
|
|
618
|
+
}
|
|
619
|
+
// Read file content using cat command
|
|
620
|
+
const result = await sandbox.commands.run(`cat "${filePath}"`);
|
|
621
|
+
if (result.exitCode !== 0) {
|
|
622
|
+
logger.warn("Error reading sandbox file", {
|
|
623
|
+
sessionId,
|
|
624
|
+
filePath,
|
|
625
|
+
stderr: result.stderr,
|
|
626
|
+
});
|
|
627
|
+
return c.json({ error: "Failed to read file" }, 500);
|
|
628
|
+
}
|
|
629
|
+
const fileName = filePath.split("/").pop() || "download";
|
|
630
|
+
// Return the file content
|
|
631
|
+
return new Response(result.stdout, {
|
|
632
|
+
headers: {
|
|
633
|
+
"Content-Type": "application/octet-stream",
|
|
634
|
+
"Content-Disposition": `attachment; filename="${fileName}"`,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
// Handle NotFoundError - sandbox no longer exists on E2B
|
|
640
|
+
if (error &&
|
|
641
|
+
typeof error === "object" &&
|
|
642
|
+
"name" in error &&
|
|
643
|
+
error.name === "NotFoundError") {
|
|
644
|
+
logger.warn("Sandbox not found on E2B, cleaning up local reference", {
|
|
645
|
+
sessionId,
|
|
646
|
+
});
|
|
647
|
+
// Remove stale sandbox from local cache
|
|
648
|
+
await destroySessionSandbox(sessionId);
|
|
649
|
+
return c.json({ error: "Sandbox not found" }, 404);
|
|
650
|
+
}
|
|
651
|
+
logger.error("Error downloading sandbox file", {
|
|
652
|
+
error,
|
|
653
|
+
sessionId,
|
|
654
|
+
filePath,
|
|
655
|
+
});
|
|
656
|
+
return c.json({ error: "Failed to download file" }, 500);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
513
659
|
// Serve static files from agent directory (for generated images, etc.)
|
|
514
660
|
if (agentDir) {
|
|
515
661
|
app.get("/static/*", async (c) => {
|
|
@@ -136,6 +136,8 @@ export interface SessionMetadata {
|
|
|
136
136
|
createdAt: string;
|
|
137
137
|
updatedAt: string;
|
|
138
138
|
agentName: string;
|
|
139
|
+
/** E2B sandbox ID for persistent sandbox reconnection */
|
|
140
|
+
sandboxId?: string | undefined;
|
|
139
141
|
}
|
|
140
142
|
/**
|
|
141
143
|
* Complete session data stored in JSON files
|
|
@@ -237,4 +239,13 @@ export declare class SessionStorage {
|
|
|
237
239
|
* Check if original content exists for a tool call
|
|
238
240
|
*/
|
|
239
241
|
hasToolOriginal(sessionId: string, toolName: string, toolCallId: string): boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Update sandbox ID for a session
|
|
244
|
+
* Used when creating/reconnecting to E2B sandboxes
|
|
245
|
+
*/
|
|
246
|
+
updateSandboxId(sessionId: string, sandboxId: string | undefined): Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* Get sandbox ID for a session
|
|
249
|
+
*/
|
|
250
|
+
getSandboxId(sessionId: string): string | undefined;
|
|
240
251
|
}
|
|
@@ -131,6 +131,7 @@ const sessionMetadataSchema = z.object({
|
|
|
131
131
|
createdAt: z.string(),
|
|
132
132
|
updatedAt: z.string(),
|
|
133
133
|
agentName: z.string(),
|
|
134
|
+
sandboxId: z.string().optional(),
|
|
134
135
|
});
|
|
135
136
|
const storedSessionSchema = z.object({
|
|
136
137
|
sessionId: z.string(),
|
|
@@ -404,4 +405,48 @@ export class SessionStorage {
|
|
|
404
405
|
hasToolOriginal(sessionId, toolName, toolCallId) {
|
|
405
406
|
return existsSync(this.getToolOriginalPath(sessionId, toolName, toolCallId));
|
|
406
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Update sandbox ID for a session
|
|
410
|
+
* Used when creating/reconnecting to E2B sandboxes
|
|
411
|
+
*/
|
|
412
|
+
async updateSandboxId(sessionId, sandboxId) {
|
|
413
|
+
const session = this.loadSessionSync(sessionId);
|
|
414
|
+
if (!session) {
|
|
415
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
416
|
+
}
|
|
417
|
+
// Update metadata with new sandboxId and save the session
|
|
418
|
+
const updatedSession = {
|
|
419
|
+
...session,
|
|
420
|
+
metadata: {
|
|
421
|
+
...session.metadata,
|
|
422
|
+
sandboxId,
|
|
423
|
+
updatedAt: new Date().toISOString(),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
// Use atomic write (write to temp file, then rename)
|
|
427
|
+
this.ensureSessionDir(sessionId);
|
|
428
|
+
const sessionPath = this.getSessionPath(sessionId);
|
|
429
|
+
const tempPath = `${sessionPath}.tmp`;
|
|
430
|
+
try {
|
|
431
|
+
writeFileSync(tempPath, JSON.stringify(updatedSession, null, 2), "utf-8");
|
|
432
|
+
if (existsSync(sessionPath)) {
|
|
433
|
+
unlinkSync(sessionPath);
|
|
434
|
+
}
|
|
435
|
+
writeFileSync(sessionPath, readFileSync(tempPath, "utf-8"), "utf-8");
|
|
436
|
+
unlinkSync(tempPath);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (existsSync(tempPath)) {
|
|
440
|
+
unlinkSync(tempPath);
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`Failed to update sandboxId for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Get sandbox ID for a session
|
|
447
|
+
*/
|
|
448
|
+
getSandboxId(sessionId) {
|
|
449
|
+
const session = this.loadSessionSync(sessionId);
|
|
450
|
+
return session?.metadata.sandboxId;
|
|
451
|
+
}
|
|
407
452
|
}
|
|
@@ -9,12 +9,9 @@ export declare const zAgentRunnerParams: z.ZodObject<{
|
|
|
9
9
|
suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
10
|
systemPrompt: z.ZodNullable<z.ZodString>;
|
|
11
11
|
model: z.ZodString;
|
|
12
|
-
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"
|
|
12
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"browser">, z.ZodLiteral<"document_extract">, z.ZodLiteral<"code_sandbox">]>, z.ZodObject<{
|
|
13
13
|
type: z.ZodLiteral<"custom">;
|
|
14
14
|
modulePath: z.ZodString;
|
|
15
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
16
|
-
type: z.ZodLiteral<"filesystem">;
|
|
17
|
-
working_directory: z.ZodOptional<z.ZodString>;
|
|
18
15
|
}, z.core.$strip>, z.ZodObject<{
|
|
19
16
|
type: z.ZodLiteral<"direct">;
|
|
20
17
|
name: z.ZodString;
|
|
@@ -81,6 +78,16 @@ export interface SessionMessage {
|
|
|
81
78
|
content: ContentBlock[];
|
|
82
79
|
timestamp: string;
|
|
83
80
|
}
|
|
81
|
+
export interface SandboxFilesChangedNotification {
|
|
82
|
+
sessionUpdate: "sandbox_files_changed";
|
|
83
|
+
paths?: string[];
|
|
84
|
+
_meta?: {
|
|
85
|
+
source?: "tool" | "poll";
|
|
86
|
+
toolName?: string;
|
|
87
|
+
isReplay?: boolean;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export type SessionUpdateNotification = SandboxFilesChangedNotification;
|
|
84
91
|
export interface ConfigOverrides {
|
|
85
92
|
model?: string;
|
|
86
93
|
systemPrompt?: string;
|
|
@@ -97,6 +104,8 @@ export type InvokeRequest = Omit<PromptRequest, "_meta"> & {
|
|
|
97
104
|
abortSignal?: AbortSignal;
|
|
98
105
|
/** Selected prompt parameters for this message. Maps parameter ID to selected option ID. */
|
|
99
106
|
promptParameters?: Record<string, string>;
|
|
107
|
+
/** Callback for tools to emit session updates (e.g., file changes) */
|
|
108
|
+
emitUpdate?: (update: SessionUpdateNotification) => void;
|
|
100
109
|
};
|
|
101
110
|
export interface TokenUsage {
|
|
102
111
|
inputTokens?: number;
|
|
@@ -2,16 +2,24 @@ import type { Sandbox } from "@e2b/code-interpreter";
|
|
|
2
2
|
/**
|
|
3
3
|
* Get or create an E2B sandbox for the current session.
|
|
4
4
|
* Sandboxes are session-scoped and reused across tool calls.
|
|
5
|
+
* Attempts to reconnect to persisted sandboxes when resuming sessions.
|
|
5
6
|
*/
|
|
6
7
|
export declare function getSessionSandbox(apiKey: string): Promise<Sandbox>;
|
|
7
8
|
/**
|
|
8
9
|
* Explicitly destroy a session's sandbox (called on session end).
|
|
10
|
+
* Also clears the persisted sandboxId from storage.
|
|
9
11
|
*/
|
|
10
12
|
export declare function destroySessionSandbox(sessionId: string): Promise<void>;
|
|
11
13
|
/**
|
|
12
14
|
* Check if a session has an active sandbox.
|
|
13
15
|
*/
|
|
14
16
|
export declare function hasSessionSandbox(sessionId: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Get an existing sandbox by sessionId without creating a new one.
|
|
19
|
+
* Returns undefined if no sandbox exists for this session.
|
|
20
|
+
* Used by HTTP endpoints that don't have session context.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getExistingSandbox(sessionId: string): Sandbox | undefined;
|
|
15
23
|
/**
|
|
16
24
|
* Get the number of active sandboxes (for debugging/monitoring).
|
|
17
25
|
*/
|