@townco/agent 0.1.107 → 0.1.108
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 +169 -16
- 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 +13 -2
- 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("/");
|
|
@@ -1221,6 +1226,17 @@ export class AgentAcpAdapter {
|
|
|
1221
1226
|
}
|
|
1222
1227
|
: null,
|
|
1223
1228
|
});
|
|
1229
|
+
// Create emitUpdate callback for tools to emit session updates
|
|
1230
|
+
const emitUpdate = (update) => {
|
|
1231
|
+
logger.debug("Adapter emitting session update", {
|
|
1232
|
+
sessionId: params.sessionId,
|
|
1233
|
+
updateType: update?.sessionUpdate,
|
|
1234
|
+
});
|
|
1235
|
+
this.connection.sessionUpdate({
|
|
1236
|
+
sessionId: params.sessionId,
|
|
1237
|
+
update: update,
|
|
1238
|
+
});
|
|
1239
|
+
};
|
|
1224
1240
|
const invokeParams = {
|
|
1225
1241
|
prompt: params.prompt,
|
|
1226
1242
|
sessionId: params.sessionId,
|
|
@@ -1231,6 +1247,8 @@ export class AgentAcpAdapter {
|
|
|
1231
1247
|
contextMessages,
|
|
1232
1248
|
// Pass abort signal for cancellation
|
|
1233
1249
|
abortSignal: session.pendingPrompt?.signal,
|
|
1250
|
+
// Pass emitUpdate callback for file change events
|
|
1251
|
+
emitUpdate,
|
|
1234
1252
|
};
|
|
1235
1253
|
// Only add sessionMeta if it's defined
|
|
1236
1254
|
if (session.requestParams._meta) {
|
|
@@ -1664,6 +1682,26 @@ export class AgentAcpAdapter {
|
|
|
1664
1682
|
tokensSaved: hookResult.metadata.tokensSaved,
|
|
1665
1683
|
summaryTokens: hookResult.metadata.summaryTokens,
|
|
1666
1684
|
});
|
|
1685
|
+
// Generate hookId for the notification pair
|
|
1686
|
+
const hookId = `hook_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1687
|
+
// Send hook_triggered notification immediately to show "Compacting Context..."
|
|
1688
|
+
// This uses the same visualization as regular context compaction
|
|
1689
|
+
this.connection.sessionUpdate({
|
|
1690
|
+
sessionId: params.sessionId,
|
|
1691
|
+
update: {
|
|
1692
|
+
sessionUpdate: "hook_notification",
|
|
1693
|
+
id: hookId,
|
|
1694
|
+
notification: {
|
|
1695
|
+
type: "hook_triggered",
|
|
1696
|
+
hookType: "context_size",
|
|
1697
|
+
callback: "compaction_tool",
|
|
1698
|
+
metadata: {
|
|
1699
|
+
midTurn: true,
|
|
1700
|
+
},
|
|
1701
|
+
triggeredAt: Date.now(),
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
});
|
|
1667
1705
|
// Store the pending tool output in session for replay after restart
|
|
1668
1706
|
// We'll include this as part of the compacted context
|
|
1669
1707
|
session.pendingToolOutput = {
|
|
@@ -1673,7 +1711,8 @@ export class AgentAcpAdapter {
|
|
|
1673
1711
|
};
|
|
1674
1712
|
// Throw an error to abort the current turn
|
|
1675
1713
|
// The handlePrompt method will catch this and restart
|
|
1676
|
-
|
|
1714
|
+
// Pass the hookId so the catch block can send the hook_completed notification
|
|
1715
|
+
throw new MidTurnRestartError("Context compacted mid-turn, restart required", hookResult.newContextEntry, hookId);
|
|
1677
1716
|
}
|
|
1678
1717
|
}
|
|
1679
1718
|
}
|
|
@@ -1946,14 +1985,26 @@ export class AgentAcpAdapter {
|
|
|
1946
1985
|
});
|
|
1947
1986
|
// Clear the pending tool output since it's now part of the context
|
|
1948
1987
|
delete session.pendingToolOutput;
|
|
1949
|
-
//
|
|
1988
|
+
// Send hook_completed notification to mark compaction as done
|
|
1989
|
+
// The hook_triggered notification was already sent before the error was thrown
|
|
1990
|
+
const summaryTokens = err.newContextEntry.context_size?.totalEstimated ?? 0;
|
|
1950
1991
|
this.connection.sessionUpdate({
|
|
1951
1992
|
sessionId: params.sessionId,
|
|
1952
1993
|
update: {
|
|
1953
|
-
sessionUpdate: "
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1994
|
+
sessionUpdate: "hook_notification",
|
|
1995
|
+
id: err.hookId,
|
|
1996
|
+
notification: {
|
|
1997
|
+
type: "hook_completed",
|
|
1998
|
+
hookType: "context_size",
|
|
1999
|
+
callback: "compaction_tool",
|
|
2000
|
+
metadata: {
|
|
2001
|
+
action: "compacted",
|
|
2002
|
+
midTurn: true,
|
|
2003
|
+
messagesRemoved: err.newContextEntry.compactedUpTo ?? 0,
|
|
2004
|
+
summaryTokens,
|
|
2005
|
+
summaryGenerated: true,
|
|
2006
|
+
},
|
|
2007
|
+
completedAt: Date.now(),
|
|
1957
2008
|
},
|
|
1958
2009
|
},
|
|
1959
2010
|
});
|
|
@@ -1961,6 +2012,35 @@ export class AgentAcpAdapter {
|
|
|
1961
2012
|
// _promptImpl will resolve the updated context from session.context
|
|
1962
2013
|
return this._promptImpl(params);
|
|
1963
2014
|
}
|
|
2015
|
+
// Handle context overflow with compaction and retry
|
|
2016
|
+
if (err instanceof ContextOverflowError) {
|
|
2017
|
+
// Track retry count to prevent infinite loops (max 3 retries)
|
|
2018
|
+
const retryCount = session
|
|
2019
|
+
._overflowRetryCount ?? 0;
|
|
2020
|
+
if (retryCount >= 3) {
|
|
2021
|
+
logger.error("Max overflow retry count reached - giving up", {
|
|
2022
|
+
sessionId: params.sessionId,
|
|
2023
|
+
retryCount,
|
|
2024
|
+
});
|
|
2025
|
+
throw err.originalError;
|
|
2026
|
+
}
|
|
2027
|
+
session._overflowRetryCount = retryCount + 1;
|
|
2028
|
+
logger.warn("Context overflow detected - forcing compaction and retry", {
|
|
2029
|
+
sessionId: params.sessionId,
|
|
2030
|
+
retryAttempt: retryCount + 1,
|
|
2031
|
+
});
|
|
2032
|
+
// Force compaction
|
|
2033
|
+
const compactionResult = await this.forceCompaction(session, params.sessionId);
|
|
2034
|
+
if (compactionResult.success) {
|
|
2035
|
+
// Retry with compacted context
|
|
2036
|
+
return this._promptImpl(params);
|
|
2037
|
+
}
|
|
2038
|
+
// Compaction failed - throw original error
|
|
2039
|
+
logger.error("Force compaction failed", {
|
|
2040
|
+
sessionId: params.sessionId,
|
|
2041
|
+
});
|
|
2042
|
+
throw err.originalError;
|
|
2043
|
+
}
|
|
1964
2044
|
throw err;
|
|
1965
2045
|
}
|
|
1966
2046
|
// Store the complete assistant response in session messages
|
|
@@ -2035,6 +2115,79 @@ export class AgentAcpAdapter {
|
|
|
2035
2115
|
stopReason: "end_turn",
|
|
2036
2116
|
};
|
|
2037
2117
|
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Force compaction of the session context.
|
|
2120
|
+
* Used for error recovery when the Claude API returns "prompt is too long".
|
|
2121
|
+
* This bypasses the normal threshold check and always compacts.
|
|
2122
|
+
*/
|
|
2123
|
+
async forceCompaction(session, sessionId) {
|
|
2124
|
+
logger.info("Force compaction started", {
|
|
2125
|
+
sessionId,
|
|
2126
|
+
messageCount: session.messages.length,
|
|
2127
|
+
contextEntries: session.context.length,
|
|
2128
|
+
});
|
|
2129
|
+
try {
|
|
2130
|
+
// Create a hook configuration that forces compaction
|
|
2131
|
+
// Using callbacks array which accepts Record<string, unknown> for settings
|
|
2132
|
+
const forceCompactionHook = {
|
|
2133
|
+
type: "context_size",
|
|
2134
|
+
callbacks: [
|
|
2135
|
+
{
|
|
2136
|
+
name: "compaction_tool",
|
|
2137
|
+
setting: {
|
|
2138
|
+
forceCompact: true,
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
],
|
|
2142
|
+
};
|
|
2143
|
+
// Create notification callback
|
|
2144
|
+
const sendHookNotification = (notification) => {
|
|
2145
|
+
this.connection.sessionUpdate({
|
|
2146
|
+
sessionId,
|
|
2147
|
+
update: {
|
|
2148
|
+
sessionUpdate: "hook_notification",
|
|
2149
|
+
id: `hook_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
2150
|
+
notification,
|
|
2151
|
+
},
|
|
2152
|
+
});
|
|
2153
|
+
};
|
|
2154
|
+
// Create hook executor with force compaction settings
|
|
2155
|
+
const hookExecutor = new HookExecutor([forceCompactionHook], this.agent.definition.model, (callbackRef) => loadHookCallback(callbackRef, this.agentDir), sendHookNotification, this.agent.definition);
|
|
2156
|
+
// Create read-only session view for hooks
|
|
2157
|
+
const readonlySession = {
|
|
2158
|
+
messages: session.messages,
|
|
2159
|
+
context: session.context,
|
|
2160
|
+
requestParams: session.requestParams,
|
|
2161
|
+
};
|
|
2162
|
+
// Get current token estimate - use a high value to ensure compaction triggers
|
|
2163
|
+
const latestContext = session.context.length > 0
|
|
2164
|
+
? session.context[session.context.length - 1]
|
|
2165
|
+
: undefined;
|
|
2166
|
+
const currentTokens = latestContext?.context_size.totalEstimated ?? 0;
|
|
2167
|
+
// Execute hooks
|
|
2168
|
+
const hookResult = await hookExecutor.executeHooks(readonlySession, currentTokens);
|
|
2169
|
+
// Apply new context entries
|
|
2170
|
+
if (hookResult.newContextEntries.length > 0) {
|
|
2171
|
+
session.context.push(...hookResult.newContextEntries);
|
|
2172
|
+
logger.info("Force compaction succeeded", {
|
|
2173
|
+
sessionId,
|
|
2174
|
+
newContextEntries: hookResult.newContextEntries.length,
|
|
2175
|
+
});
|
|
2176
|
+
return { success: true };
|
|
2177
|
+
}
|
|
2178
|
+
logger.warn("Force compaction produced no new context entries", {
|
|
2179
|
+
sessionId,
|
|
2180
|
+
});
|
|
2181
|
+
return { success: false };
|
|
2182
|
+
}
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
logger.error("Force compaction error", {
|
|
2185
|
+
sessionId,
|
|
2186
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2187
|
+
});
|
|
2188
|
+
return { success: false };
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2038
2191
|
/**
|
|
2039
2192
|
* Execute hooks if configured for this agent
|
|
2040
2193
|
* 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
|
*/
|