@townco/agent 0.1.106 → 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.
@@ -31,7 +31,16 @@ export interface CitationSource {
31
31
  */
32
32
  export declare class MidTurnRestartError extends Error {
33
33
  newContextEntry: ContextEntry;
34
- constructor(message: string, newContextEntry: ContextEntry);
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
- constructor(message, newContextEntry) {
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
- throw new MidTurnRestartError("Context compacted mid-turn, restart required", hookResult.newContextEntry);
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
- // Notify UI that a restart is happening
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: "agent_message_chunk",
1954
- content: {
1955
- type: "text",
1956
- text: "\n\n---\n*Context compacted mid-turn. Continuing from summary...*\n\n",
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
@@ -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<"artifacts">, z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"town_generate_image">, z.ZodLiteral<"browser">, z.ZodLiteral<"document_extract">, z.ZodLiteral<"code_sandbox">]>, z.ZodObject<{
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
  */