@townco/agent 0.1.118 → 0.1.120

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.
@@ -259,19 +259,7 @@ export function createAcpHttpApp(agent, agentDir, agentName) {
259
259
  const stream = sseStreams.get(msgSessionId);
260
260
  if (stream) {
261
261
  try {
262
- // Check if this is a sources message
263
- const isSourcesMsg = rawMsg &&
264
- typeof rawMsg === "object" &&
265
- "params" in rawMsg &&
266
- rawMsg.params?.update?.sessionUpdate === "sources";
267
- if (isSourcesMsg) {
268
- const sourcesCount = rawMsg.params?.update?.sources?.length;
269
- logger.info("🔶 SENDING LARGE SOURCES VIA DIRECT SSE", {
270
- sessionId: msgSessionId,
271
- sourcesCount,
272
- compressedSize,
273
- });
274
- }
262
+ // Send large messages directly via SSE
275
263
  await stream.writeSSE({
276
264
  event: "message",
277
265
  data: JSON.stringify(rawMsg),
@@ -811,21 +799,7 @@ export function createAcpHttpApp(agent, agentDir, agentName) {
811
799
  return;
812
800
  }
813
801
  }
814
- // Check if this is a sources message and log prominently
815
- const isSourcesMsg = json &&
816
- typeof json === "object" &&
817
- "params" in json &&
818
- json.params?.update?.sessionUpdate === "sources";
819
- if (isSourcesMsg) {
820
- logger.info("🔶 SENDING SOURCES VIA SSE", {
821
- sessionId,
822
- channel,
823
- sourcesCount: json.params?.update?.sources?.length,
824
- });
825
- }
826
- else {
827
- logger.trace("Sending SSE message", { sessionId, channel });
828
- }
802
+ logger.trace("Sending SSE message", { sessionId, channel });
829
803
  await stream.writeSSE({
830
804
  event: "message",
831
805
  data: JSON.stringify(json),
@@ -3,6 +3,9 @@ import type { Sandbox } from "@e2b/code-interpreter";
3
3
  * Get or create an E2B sandbox for the current session.
4
4
  * Sandboxes are session-scoped and reused across tool calls.
5
5
  * Attempts to reconnect to persisted sandboxes when resuming sessions.
6
+ *
7
+ * Uses promise caching to prevent race conditions when multiple tools
8
+ * are called in parallel - all concurrent calls will share the same sandbox.
6
9
  */
7
10
  export declare function getSessionSandbox(apiKey: string): Promise<Sandbox>;
8
11
  /**
@@ -8,6 +8,8 @@ const sessionSandboxes = new Map();
8
8
  const sandboxActivity = new Map();
9
9
  // Map sessionId -> cleanup timeout handle
10
10
  const cleanupTimeouts = new Map();
11
+ // Map sessionId -> Promise<Sandbox> for in-flight creations (prevents race condition)
12
+ const sandboxCreationPromises = new Map();
11
13
  // Sandbox timeout in milliseconds (default: 15 minutes)
12
14
  const SANDBOX_TIMEOUT_MS = 15 * 60 * 1000;
13
15
  /**
@@ -58,24 +60,10 @@ function getSessionStorage() {
58
60
  }
59
61
  }
60
62
  /**
61
- * Get or create an E2B sandbox for the current session.
62
- * Sandboxes are session-scoped and reused across tool calls.
63
- * Attempts to reconnect to persisted sandboxes when resuming sessions.
63
+ * Internal function to create or reconnect to a sandbox for a session.
64
+ * This should only be called from getSessionSandbox() with proper promise tracking.
64
65
  */
65
- export async function getSessionSandbox(apiKey) {
66
- if (!hasSessionContext()) {
67
- throw new Error("E2B tools require session context");
68
- }
69
- const { sessionId } = getSessionContext();
70
- // Check for existing in-memory sandbox
71
- let sandbox = sessionSandboxes.get(sessionId);
72
- if (sandbox) {
73
- // Update activity timestamp and reschedule cleanup
74
- sandboxActivity.set(sessionId, Date.now());
75
- rescheduleCleanup(sessionId);
76
- logger.debug("Reusing existing sandbox", { sessionId });
77
- return sandbox;
78
- }
66
+ async function createSandboxForSession(sessionId, apiKey) {
79
67
  // Try to reconnect to persisted sandbox
80
68
  const storage = getSessionStorage();
81
69
  const persistedSandboxId = storage?.getSandboxId(sessionId);
@@ -86,7 +74,9 @@ export async function getSessionSandbox(apiKey) {
86
74
  });
87
75
  try {
88
76
  const { Sandbox: SandboxClass } = await import("@e2b/code-interpreter");
89
- sandbox = await SandboxClass.connect(persistedSandboxId, { apiKey });
77
+ const sandbox = await SandboxClass.connect(persistedSandboxId, {
78
+ apiKey,
79
+ });
90
80
  logger.info("Successfully reconnected to sandbox", {
91
81
  sessionId,
92
82
  sandboxId: persistedSandboxId,
@@ -131,7 +121,7 @@ export async function getSessionSandbox(apiKey) {
131
121
  config.template = templateId;
132
122
  logger.info("Using custom E2B template", { templateId });
133
123
  }
134
- sandbox = await SandboxClass.create(config);
124
+ const sandbox = await SandboxClass.create(config);
135
125
  logger.info("Created new sandbox", {
136
126
  sessionId,
137
127
  sandboxId: sandbox.sandboxId,
@@ -167,6 +157,45 @@ logging.basicConfig(
167
157
  scheduleCleanup(sessionId);
168
158
  return sandbox;
169
159
  }
160
+ /**
161
+ * Get or create an E2B sandbox for the current session.
162
+ * Sandboxes are session-scoped and reused across tool calls.
163
+ * Attempts to reconnect to persisted sandboxes when resuming sessions.
164
+ *
165
+ * Uses promise caching to prevent race conditions when multiple tools
166
+ * are called in parallel - all concurrent calls will share the same sandbox.
167
+ */
168
+ export async function getSessionSandbox(apiKey) {
169
+ if (!hasSessionContext()) {
170
+ throw new Error("E2B tools require session context");
171
+ }
172
+ const { sessionId } = getSessionContext();
173
+ // Check for existing in-memory sandbox
174
+ const existingSandbox = sessionSandboxes.get(sessionId);
175
+ if (existingSandbox) {
176
+ // Update activity timestamp and reschedule cleanup
177
+ sandboxActivity.set(sessionId, Date.now());
178
+ rescheduleCleanup(sessionId);
179
+ logger.debug("Reusing existing sandbox", { sessionId });
180
+ return existingSandbox;
181
+ }
182
+ // Check for in-flight creation (prevents race condition when tools run in parallel)
183
+ const existingPromise = sandboxCreationPromises.get(sessionId);
184
+ if (existingPromise) {
185
+ logger.debug("Waiting for in-flight sandbox creation", { sessionId });
186
+ return existingPromise;
187
+ }
188
+ // Create new sandbox with promise tracking
189
+ const creationPromise = createSandboxForSession(sessionId, apiKey);
190
+ sandboxCreationPromises.set(sessionId, creationPromise);
191
+ try {
192
+ const sandbox = await creationPromise;
193
+ return sandbox;
194
+ }
195
+ finally {
196
+ sandboxCreationPromises.delete(sessionId);
197
+ }
198
+ }
170
199
  /**
171
200
  * Explicitly destroy a session's sandbox (called on session end).
172
201
  * Also clears the persisted sandboxId from storage.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Supabase Storage-backed artifacts tool for agent backend
3
+ *
4
+ * Provides file storage capabilities using Supabase Storage with the following operations:
5
+ * - artifacts_cp: Copy files to/from Supabase Storage
6
+ * - artifacts_del: Delete files from Supabase Storage
7
+ * - artifacts_ls: List files in Supabase Storage
8
+ * - artifacts_url: Generate signed URLs
9
+ *
10
+ * Storage keys are scoped by: <deploying_user>/<agent_name>/<session_id>/<file_path>
11
+ */
12
+ import { z } from "zod";
13
+ /**
14
+ * Factory function to create the artifacts tools
15
+ * Returns an array of all four artifact management tools
16
+ */
17
+ export declare function makeArtifactsTools(): (import("langchain").DynamicStructuredTool<z.ZodObject<{
18
+ session_id: z.ZodOptional<z.ZodString>;
19
+ source: z.ZodString;
20
+ destination: z.ZodString;
21
+ direction: z.ZodEnum<{
22
+ download: "download";
23
+ upload: "upload";
24
+ }>;
25
+ }, z.core.$strip>, {
26
+ session_id: string;
27
+ source: string;
28
+ destination: string;
29
+ direction: "download" | "upload";
30
+ }, {
31
+ session_id?: string | undefined;
32
+ source: string;
33
+ destination: string;
34
+ direction: "download" | "upload";
35
+ }, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
36
+ session_id: z.ZodOptional<z.ZodString>;
37
+ path: z.ZodString;
38
+ }, z.core.$strip>, {
39
+ session_id: string;
40
+ path: string;
41
+ }, {
42
+ session_id?: string | undefined;
43
+ path: string;
44
+ }, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
45
+ session_id: z.ZodOptional<z.ZodString>;
46
+ path: z.ZodOptional<z.ZodString>;
47
+ recursive: z.ZodOptional<z.ZodBoolean>;
48
+ }, z.core.$strip>, {
49
+ session_id: string;
50
+ path?: string;
51
+ recursive?: boolean;
52
+ }, {
53
+ session_id?: string | undefined;
54
+ path?: string | undefined;
55
+ recursive?: boolean | undefined;
56
+ }, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
57
+ session_id: z.ZodOptional<z.ZodString>;
58
+ path: z.ZodString;
59
+ expires_in: z.ZodOptional<z.ZodNumber>;
60
+ }, z.core.$strip>, {
61
+ session_id: string;
62
+ path: string;
63
+ expires_in?: number;
64
+ }, {
65
+ session_id?: string | undefined;
66
+ path: string;
67
+ expires_in?: number | undefined;
68
+ }, string>)[];