@townco/agent 0.1.119 → 0.1.121

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.
@@ -676,12 +676,23 @@ export class AgentAcpAdapter {
676
676
  sources: session.sources,
677
677
  nextId: session.sourceCounter + 1,
678
678
  };
679
- await this.storage.saveSession(sessionId, session.messages, session.context, citations);
679
+ // Pass session metadata for subagent sessions
680
+ const metadata = {
681
+ ...(session.parentSessionId && {
682
+ parentSessionId: session.parentSessionId,
683
+ }),
684
+ ...(session.isSubagentSession && {
685
+ isSubagentSession: session.isSubagentSession,
686
+ }),
687
+ };
688
+ await this.storage.saveSession(sessionId, session.messages, session.context, citations, Object.keys(metadata).length > 0 ? metadata : undefined);
680
689
  logger.debug("Saved session to disk", {
681
690
  sessionId,
682
691
  messageCount: session.messages.length,
683
692
  contextCount: session.context.length,
684
693
  citationsCount: session.sources.length,
694
+ isSubagentSession: session.isSubagentSession,
695
+ parentSessionId: session.parentSessionId,
685
696
  });
686
697
  }
687
698
  catch (error) {
@@ -1046,6 +1057,8 @@ export class AgentAcpAdapter {
1046
1057
  // If session not found (e.g., after server restart), create a new one
1047
1058
  if (!session) {
1048
1059
  logger.info(`Session ${params.sessionId} not found, creating new session`);
1060
+ const parentSessionId = params._meta?.parentSessionId;
1061
+ const isSubagentSession = params._meta?.isSubagentSession === true;
1049
1062
  session = {
1050
1063
  pendingPrompt: null,
1051
1064
  messages: [],
@@ -1054,6 +1067,9 @@ export class AgentAcpAdapter {
1054
1067
  isCancelled: false,
1055
1068
  sourceCounter: 0,
1056
1069
  sources: [],
1070
+ // Extract subagent metadata from _meta (only add if defined)
1071
+ ...(parentSessionId && { parentSessionId }),
1072
+ ...(isSubagentSession && { isSubagentSession }),
1057
1073
  };
1058
1074
  this.sessions.set(params.sessionId, session);
1059
1075
  }
@@ -1299,6 +1315,8 @@ export class AgentAcpAdapter {
1299
1315
  ...(this.agentDir ? { agentDir: this.agentDir } : {}),
1300
1316
  // Pass resolved context messages to agent
1301
1317
  contextMessages,
1318
+ // Pass context entries for hook execution (compaction tracking)
1319
+ contextEntries: session.context,
1302
1320
  // Pass abort signal for cancellation
1303
1321
  abortSignal: session.pendingPrompt?.signal,
1304
1322
  // Pass emitUpdate callback for file change events
@@ -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),
@@ -142,6 +142,10 @@ export interface SessionMetadata {
142
142
  agentName: string;
143
143
  /** E2B sandbox ID for persistent sandbox reconnection */
144
144
  sandboxId?: string | undefined;
145
+ /** Parent session ID for subagent sessions (enables session linking) */
146
+ parentSessionId?: string | undefined;
147
+ /** Flag indicating this is a subagent session (for UI filtering) */
148
+ isSubagentSession?: boolean | undefined;
145
149
  }
146
150
  /**
147
151
  * Complete session data stored in JSON files
@@ -207,7 +211,7 @@ export declare class SessionStorage {
207
211
  * Save a session to disk
208
212
  * Uses atomic write (write to temp file, then rename)
209
213
  */
210
- saveSession(sessionId: string, messages: SessionMessage[], context: ContextEntry[], citations?: CitationStorage): Promise<void>;
214
+ saveSession(sessionId: string, messages: SessionMessage[], context: ContextEntry[], citations?: CitationStorage, extraMetadata?: Partial<SessionMetadata>): Promise<void>;
211
215
  /**
212
216
  * Load a session from disk
213
217
  */
@@ -242,6 +246,8 @@ export declare class SessionStorage {
242
246
  updatedAt: string;
243
247
  messageCount: number;
244
248
  firstUserMessage?: string;
249
+ parentSessionId?: string;
250
+ isSubagentSession?: boolean;
245
251
  }>>;
246
252
  /**
247
253
  * Get the directory for storing large content files for a session (artifacts folder)
@@ -135,6 +135,8 @@ const sessionMetadataSchema = z.object({
135
135
  updatedAt: z.string(),
136
136
  agentName: z.string(),
137
137
  sandboxId: z.string().optional(),
138
+ parentSessionId: z.string().optional(),
139
+ isSubagentSession: z.boolean().optional(),
138
140
  });
139
141
  // Citation schemas - matches SourceSchema from packages/ui/src/core/schemas/source.ts
140
142
  const persistedCitationSourceSchema = z.object({
@@ -200,7 +202,7 @@ export class SessionStorage {
200
202
  * Save a session to disk
201
203
  * Uses atomic write (write to temp file, then rename)
202
204
  */
203
- async saveSession(sessionId, messages, context, citations) {
205
+ async saveSession(sessionId, messages, context, citations, extraMetadata) {
204
206
  // Debug: log subagent data being saved
205
207
  const messagesWithSubagents = messages.filter((msg) => msg.content.some((block) => block.type === "tool_call" &&
206
208
  "subagentMessages" in block &&
@@ -239,6 +241,7 @@ export class SessionStorage {
239
241
  createdAt: existingSession?.metadata.createdAt || now,
240
242
  updatedAt: now,
241
243
  agentName: this.agentName,
244
+ ...extraMetadata,
242
245
  },
243
246
  ...(citations && { citations }),
244
247
  };
@@ -386,6 +389,13 @@ export class SessionStorage {
386
389
  if (firstUserText && "text" in firstUserText) {
387
390
  entry.firstUserMessage = firstUserText.text.slice(0, 100);
388
391
  }
392
+ // Include sub-agent metadata if present
393
+ if (session.metadata.parentSessionId) {
394
+ entry.parentSessionId = session.metadata.parentSessionId;
395
+ }
396
+ if (session.metadata.isSubagentSession) {
397
+ entry.isSubagentSession = session.metadata.isSubagentSession;
398
+ }
389
399
  sessions.push(entry);
390
400
  }
391
401
  }
@@ -1,7 +1,7 @@
1
1
  import type { PromptRequest, PromptResponse, SessionNotification } from "@agentclientprotocol/sdk";
2
2
  import type { Span } from "@opentelemetry/api";
3
3
  import { z } from "zod";
4
- import type { ContentBlock } from "../acp-server/session-storage.js";
4
+ import type { ContentBlock, ContextEntry } from "../acp-server/session-storage.js";
5
5
  export declare const zAgentRunnerParams: z.ZodObject<{
6
6
  displayName: z.ZodOptional<z.ZodString>;
7
7
  version: z.ZodOptional<z.ZodString>;
@@ -99,6 +99,7 @@ export type InvokeRequest = Omit<PromptRequest, "_meta"> & {
99
99
  agentDir?: string;
100
100
  sessionMeta?: Record<string, unknown>;
101
101
  contextMessages?: SessionMessage[];
102
+ contextEntries?: ContextEntry[];
102
103
  configOverrides?: ConfigOverrides;
103
104
  /** Abort signal for cancellation - tools can listen for this to stop early */
104
105
  abortSignal?: AbortSignal;
@@ -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.
@@ -412,122 +412,36 @@ export class LangchainAgent {
412
412
  baseContextTokens,
413
413
  modelContextWindow,
414
414
  });
415
- // Wrap tools with response compaction if hook is configured
416
- const hooks = this.definition.hooks ?? [];
417
- const hasToolResponseHook = hooks.some((h) => h.type === "tool_response");
418
- const noSession = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true; // Subagents don't have session storage
419
- // Track cumulative tool output tokens in this turn for proper context calculation
420
- let cumulativeToolOutputTokens = 0;
415
+ // Hook execution removed from tool wrapper - hooks are now executed only at the adapter layer
416
+ // The adapter has proper MidTurnRestartError handling that can restart the turn
417
+ // Executing hooks here in the runner was causing restart signals to be caught as tool failures
421
418
  // Counter for subagent calls - used to create unique source ID ranges
422
419
  // Each subagent call gets a unique offset (1000, 2000, 3000, etc.)
423
420
  // to ensure sources never conflict with parent's sources (typically < 100)
424
421
  let subagentCallCounter = 0;
425
- let wrappedTools = enabledTools;
426
- if (hasToolResponseHook && !noSession) {
427
- const { countToolResultTokens } = await import("../../utils/token-counter.js");
428
- const { toolResponseCompactor } = await import("../hooks/predefined/tool-response-compactor.js");
429
- wrappedTools = enabledTools.map((originalTool) => {
430
- const wrappedFunc = async (input) => {
431
- // Execute the original tool
432
- const result = await originalTool.invoke(input);
433
- // Check if result should be compacted
434
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
435
- const rawOutput = { content: resultStr };
436
- const outputTokens = countToolResultTokens(rawOutput);
437
- // Skip compaction for small results (under 10k tokens)
438
- if (outputTokens < 10000) {
439
- // Still track this in cumulative total
440
- cumulativeToolOutputTokens += outputTokens;
441
- return result;
442
- }
443
- _logger.info("Tool wrapper: compacting large tool result", {
444
- toolName: originalTool.name,
445
- originalTokens: outputTokens,
446
- cumulativeToolOutputTokens,
447
- });
448
- // Calculate current context including all tool outputs so far in this turn
449
- // Uses accurate baseContextTokens calculated earlier (system prompt, tool overhead, MCP overhead, message history)
450
- // This ensures we account for multiple large tool calls in the same turn
451
- const currentTokens = baseContextTokens + cumulativeToolOutputTokens;
452
- // Build proper hook context with all required fields
453
- const hookContext = {
454
- session: {
455
- messages: req.contextMessages || [],
456
- context: [],
457
- requestParams: {
458
- hookSettings: hooks.find((h) => h.type === "tool_response")
459
- ?.setting,
460
- },
461
- },
462
- currentTokens,
463
- maxTokens: modelContextWindow,
464
- percentage: (currentTokens / modelContextWindow) * 100,
465
- model: this.definition.model,
466
- agent: this.definition,
467
- toolResponse: {
468
- toolCallId: "pending",
469
- toolName: originalTool.name,
470
- toolInput: input,
471
- rawOutput,
472
- outputTokens,
473
- },
474
- };
475
- // Call the tool response compactor directly
476
- const hookResult = await toolResponseCompactor(hookContext);
477
- // Extract modified output from metadata
478
- if (hookResult?.metadata?.modifiedOutput) {
479
- const modifiedOutput = hookResult.metadata
480
- .modifiedOutput;
481
- const compactedTokens = countToolResultTokens(modifiedOutput);
482
- // Update cumulative total with the compacted size (not original!)
483
- cumulativeToolOutputTokens += compactedTokens;
484
- _logger.info("Tool wrapper: compaction complete", {
485
- toolName: originalTool.name,
486
- originalTokens: outputTokens,
487
- compactedTokens,
488
- reduction: `${((1 - compactedTokens / outputTokens) * 100).toFixed(1)}%`,
489
- totalCumulativeTokens: cumulativeToolOutputTokens,
490
- });
491
- // Include compaction metadata in the output for the adapter to extract
492
- // Also include original content so adapter can store it
493
- const originalContentStr = typeof rawOutput === "object" &&
494
- rawOutput !== null &&
495
- "content" in rawOutput
496
- ? String(rawOutput.content)
497
- : JSON.stringify(rawOutput);
498
- const outputWithMeta = {
499
- ...modifiedOutput,
500
- _compactionMeta: {
501
- action: hookResult.metadata.action,
502
- originalTokens: hookResult.metadata.originalTokens,
503
- finalTokens: hookResult.metadata.finalTokens,
504
- tokensSaved: hookResult.metadata.tokensSaved,
505
- originalContent: originalContentStr,
506
- },
507
- };
508
- // Always return JSON string to preserve metadata
509
- return JSON.stringify(outputWithMeta);
510
- }
511
- // No compaction happened, count original size
512
- cumulativeToolOutputTokens += outputTokens;
513
- return result;
514
- };
515
- // Create new tool with wrapped function
516
- // biome-ignore lint/suspicious/noExplicitAny: Need to pass function with dynamic signature
517
- const wrappedTool = tool(wrappedFunc, {
518
- name: originalTool.name,
519
- description: originalTool.description,
520
- // biome-ignore lint/suspicious/noExplicitAny: Accessing internal schema property
521
- schema: originalTool.schema,
522
- });
523
- // Preserve metadata
524
- // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
525
- wrappedTool.prettyName = originalTool.prettyName;
526
- // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
527
- wrappedTool.icon = originalTool.icon;
528
- return wrappedTool;
422
+ // Simple tool wrapper that just passes through to the original tool
423
+ // All hook execution (compaction, restart logic) happens at the adapter layer
424
+ const wrappedTools = enabledTools.map((originalTool) => {
425
+ const wrappedFunc = async (input) => {
426
+ // Execute the original tool and return raw result
427
+ const result = await originalTool.invoke(input);
428
+ return result;
429
+ };
430
+ // Create new tool with wrapped function
431
+ // biome-ignore lint/suspicious/noExplicitAny: Need to pass function with dynamic signature
432
+ const wrappedTool = tool(wrappedFunc, {
433
+ name: originalTool.name,
434
+ description: originalTool.description,
435
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing internal schema property
436
+ schema: originalTool.schema,
529
437
  });
530
- }
438
+ // Preserve metadata
439
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
440
+ wrappedTool.prettyName = originalTool.prettyName;
441
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
442
+ wrappedTool.icon = originalTool.icon;
443
+ return wrappedTool;
444
+ });
531
445
  // Filter tools if running in subagent mode
532
446
  const isSubagent = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true;
533
447
  const filteredTools = isSubagent
@@ -50,11 +50,11 @@ export declare function makeTownE2BTools(): readonly [import("langchain").Dynami
50
50
  sandboxPath: string;
51
51
  fileName?: string | undefined;
52
52
  }, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
53
- document_id: z.ZodString;
53
+ document_ids: z.ZodArray<z.ZodString>;
54
54
  }, z.core.$strip>, {
55
- document_id: string;
55
+ document_ids: string[];
56
56
  }, {
57
- document_id: string;
57
+ document_ids: string[];
58
58
  }, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
59
59
  prompt: z.ZodString;
60
60
  }, z.core.$strip>, {
@@ -403,8 +403,8 @@ function makeE2BToolsInternal(getSandbox) {
403
403
  shareSandboxFile.prettyName = "Share from Sandbox";
404
404
  // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
405
405
  shareSandboxFile.icon = "Share";
406
- // Tool 6: Upload Library Document to Sandbox
407
- const uploadLibraryDocument = tool(async ({ document_id }) => {
406
+ // Tool 6: Load Library Documents to Sandbox
407
+ const loadLibraryDocuments = tool(async ({ document_ids }) => {
408
408
  const sandbox = await getSandbox();
409
409
  try {
410
410
  const libraryApiUrl = process.env.LIBRARY_API_URL;
@@ -412,14 +412,14 @@ function makeE2BToolsInternal(getSandbox) {
412
412
  if (!libraryApiUrl || !libraryApiKey) {
413
413
  throw new Error("LIBRARY_API_URL and LIBRARY_API_KEY environment variables are required");
414
414
  }
415
- const response = await fetch(`${libraryApiUrl}/sandbox/upload_document_to_sandbox`, {
415
+ const response = await fetch(`${libraryApiUrl}/sandbox/upload_documents_to_sandbox`, {
416
416
  method: "POST",
417
417
  headers: {
418
418
  "Content-Type": "application/json",
419
419
  Authorization: `Bearer ${libraryApiKey}`,
420
420
  },
421
421
  body: JSON.stringify({
422
- document_id,
422
+ document_ids,
423
423
  sandbox_id: sandbox.sandboxId,
424
424
  }),
425
425
  });
@@ -428,34 +428,50 @@ function makeE2BToolsInternal(getSandbox) {
428
428
  throw new Error(`Library API error: ${response.status} - ${text}`);
429
429
  }
430
430
  const result = await response.json();
431
- return result.path;
431
+ // Format the response
432
+ const successfulUploads = result.results
433
+ .filter((r) => r.status === "success")
434
+ .map((r) => r.file_path);
435
+ const failedUploads = result.results
436
+ .filter((r) => r.status === "error")
437
+ .map((r) => `${r.document_id}: ${r.error}`);
438
+ let output = `Status: ${result.status}\n`;
439
+ output += `Uploaded ${result.successful_uploads}/${result.total_requested} documents\n`;
440
+ if (successfulUploads.length > 0) {
441
+ output += `\nSuccessful uploads:\n${successfulUploads.join("\n")}`;
442
+ }
443
+ if (failedUploads.length > 0) {
444
+ output += `\nFailed uploads:\n${failedUploads.join("\n")}`;
445
+ }
446
+ return output;
432
447
  }
433
448
  catch (error) {
434
- logger.error("Error uploading library document to sandbox", {
449
+ logger.error("Error loading library documents to sandbox", {
435
450
  error,
436
- document_id,
451
+ document_ids,
437
452
  });
438
- return `Error uploading document: ${error instanceof Error ? error.message : String(error)}`;
453
+ return `Error loading documents: ${error instanceof Error ? error.message : String(error)}`;
439
454
  }
440
455
  }, {
441
- name: "Sandbox_UploadLibraryDocument",
442
- description: "Upload a document from the library to the cloud sandbox. " +
443
- "Use this to make library documents available for processing in the sandbox environment.",
456
+ name: "Sandbox_LoadLibraryDocuments",
457
+ description: "Load multiple documents from the library to the cloud sandbox. " +
458
+ "Use this to make library documents available for processing in the sandbox environment. " +
459
+ "Supports batch uploads - some documents may succeed while others fail.",
444
460
  schema: z.object({
445
- document_id: z
446
- .string()
447
- .describe("The ID of the document to upload from the library"),
461
+ document_ids: z
462
+ .array(z.string())
463
+ .describe("The IDs of the documents to load from the library"),
448
464
  }),
449
465
  });
450
466
  // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
451
- uploadLibraryDocument.prettyName = "Upload Library Document";
467
+ loadLibraryDocuments.prettyName = "Load Library Documents";
452
468
  // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
453
- uploadLibraryDocument.icon = "Upload";
469
+ loadLibraryDocuments.icon = "FileText";
454
470
  // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
455
- uploadLibraryDocument.verbiage = {
456
- active: "Uploading document {document_id}",
457
- past: "Uploaded document {document_id}",
458
- paramKey: "document_id",
471
+ loadLibraryDocuments.verbiage = {
472
+ active: "Loading {document_ids.length} documents",
473
+ past: "Loaded {document_ids.length} documents",
474
+ paramKey: "document_ids",
459
475
  };
460
476
  // Tool 7: Generate Image in Sandbox
461
477
  const generateImage = tool(async ({ prompt }) => {
@@ -634,7 +650,7 @@ generateImage();
634
650
  readSandboxFile,
635
651
  writeSandboxFile,
636
652
  shareSandboxFile,
637
- uploadLibraryDocument,
653
+ loadLibraryDocuments,
638
654
  generateImage,
639
655
  ];
640
656
  }
@@ -28,6 +28,12 @@ export interface SubagentMessage {
28
28
  content: string;
29
29
  contentBlocks: SubagentContentBlock[];
30
30
  toolCalls: SubagentToolCall[];
31
+ _meta?: {
32
+ semanticName?: string;
33
+ agentDefinitionName?: string;
34
+ currentActivity?: string;
35
+ statusGenerating?: boolean;
36
+ };
31
37
  }
32
38
  /**
33
39
  * Maps query hash to toolCallId.