@townco/agent 0.1.111 → 0.1.113

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.
@@ -1411,7 +1411,22 @@ export class AgentAcpAdapter {
1411
1411
  if ("sessionUpdate" in msg &&
1412
1412
  msg.sessionUpdate === "tool_call_update") {
1413
1413
  const updateMsg = msg;
1414
+ logger.info("[SUBAGENT] Adapter received tool_call_update", {
1415
+ sessionId: params.sessionId,
1416
+ toolCallId: updateMsg.toolCallId,
1417
+ status: updateMsg.status,
1418
+ hasMeta: !!updateMsg._meta,
1419
+ hasSubagentMessages: !!updateMsg._meta?.subagentMessages,
1420
+ subagentMessageCount: updateMsg._meta?.subagentMessages?.length ||
1421
+ 0,
1422
+ });
1414
1423
  const toolCallBlock = contentBlocks.find((block) => block.type === "tool_call" && block.id === updateMsg.toolCallId);
1424
+ logger.info("[SUBAGENT] Tool call block lookup result", {
1425
+ sessionId: params.sessionId,
1426
+ toolCallId: updateMsg.toolCallId,
1427
+ found: !!toolCallBlock,
1428
+ blockStatus: toolCallBlock?.status,
1429
+ });
1415
1430
  if (toolCallBlock) {
1416
1431
  if (updateMsg.status) {
1417
1432
  toolCallBlock.status =
@@ -1444,19 +1459,30 @@ export class AgentAcpAdapter {
1444
1459
  toolCallBlock.subagentSessionId = meta.subagentSessionId;
1445
1460
  }
1446
1461
  if (meta?.subagentMessages) {
1447
- logger.info("Storing subagent messages for session replay", {
1462
+ logger.info("[SUBAGENT] Storing subagent messages for session replay", {
1463
+ sessionId: params.sessionId,
1448
1464
  toolCallId: updateMsg.toolCallId,
1449
1465
  messageCount: meta.subagentMessages.length,
1466
+ contentPreview: meta.subagentMessages[0]?.content?.substring(0, 100),
1450
1467
  });
1451
1468
  toolCallBlock.subagentMessages = meta.subagentMessages;
1469
+ logger.info("[SUBAGENT] Successfully stored messages", {
1470
+ sessionId: params.sessionId,
1471
+ toolCallId: updateMsg.toolCallId,
1472
+ storedCount: toolCallBlock.subagentMessages?.length,
1473
+ });
1452
1474
  }
1453
1475
  }
1454
1476
  // Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
1455
1477
  if (updateMsg._meta) {
1456
- logger.info("Forwarding tool_call_update with _meta to client", {
1478
+ logger.info("[SUBAGENT] Forwarding tool_call_update with _meta to client", {
1479
+ sessionId: params.sessionId,
1457
1480
  toolCallId: updateMsg.toolCallId,
1458
1481
  status: updateMsg.status,
1459
- _meta: updateMsg._meta,
1482
+ hasSubagentMessages: !!updateMsg._meta
1483
+ ?.subagentMessages,
1484
+ subagentMessageCount: updateMsg._meta?.subagentMessages
1485
+ ?.length || 0,
1460
1486
  });
1461
1487
  this.connection.sessionUpdate({
1462
1488
  sessionId: params.sessionId,
@@ -1467,22 +1493,9 @@ export class AgentAcpAdapter {
1467
1493
  _meta: updateMsg._meta,
1468
1494
  },
1469
1495
  });
1470
- }
1471
- // Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
1472
- if (updateMsg._meta) {
1473
- logger.info("Forwarding tool_call_update with _meta to client", {
1474
- toolCallId: updateMsg.toolCallId,
1475
- status: updateMsg.status,
1476
- _meta: updateMsg._meta,
1477
- });
1478
- this.connection.sessionUpdate({
1496
+ logger.info("[SUBAGENT] Successfully forwarded to client", {
1479
1497
  sessionId: params.sessionId,
1480
- update: {
1481
- sessionUpdate: "tool_call_update",
1482
- toolCallId: updateMsg.toolCallId,
1483
- status: updateMsg.status,
1484
- _meta: updateMsg._meta,
1485
- },
1498
+ toolCallId: updateMsg.toolCallId,
1486
1499
  });
1487
1500
  }
1488
1501
  }
@@ -2111,6 +2124,56 @@ export class AgentAcpAdapter {
2111
2124
  },
2112
2125
  });
2113
2126
  }
2127
+ // Proactive compaction based on actual LLM token usage
2128
+ // If actual input tokens from LLM exceed threshold, compact now to prevent overflow on next turn
2129
+ const PROACTIVE_COMPACTION_THRESHOLD = 70; // Trigger at 70% of context window
2130
+ const modelContextWindow = getModelContextWindow(this.agent.definition.model);
2131
+ if (turnTokenUsage.inputTokens > 0 && !this.noSession) {
2132
+ const actualPercentage = (turnTokenUsage.inputTokens / modelContextWindow) * 100;
2133
+ const latestContext = session.context.length > 0
2134
+ ? session.context[session.context.length - 1]
2135
+ : undefined;
2136
+ const estimatedTokens = latestContext?.context_size.totalEstimated ?? 0;
2137
+ // Log the difference between estimated and actual tokens
2138
+ const tokenDifference = turnTokenUsage.inputTokens - estimatedTokens;
2139
+ const differencePercentage = estimatedTokens > 0
2140
+ ? ((tokenDifference / estimatedTokens) * 100).toFixed(1)
2141
+ : "N/A";
2142
+ logger.info("Token usage comparison (actual vs estimated)", {
2143
+ sessionId: params.sessionId,
2144
+ actualInputTokens: turnTokenUsage.inputTokens,
2145
+ estimatedTokens,
2146
+ tokenDifference,
2147
+ differencePercentage: `${differencePercentage}%`,
2148
+ actualPercentage: `${actualPercentage.toFixed(1)}%`,
2149
+ modelContextWindow,
2150
+ threshold: `${PROACTIVE_COMPACTION_THRESHOLD}%`,
2151
+ });
2152
+ // Proactively compact if actual tokens exceed threshold
2153
+ if (actualPercentage >= PROACTIVE_COMPACTION_THRESHOLD) {
2154
+ logger.warn("Actual token usage exceeds threshold - proactively compacting for next turn", {
2155
+ sessionId: params.sessionId,
2156
+ actualPercentage: `${actualPercentage.toFixed(1)}%`,
2157
+ threshold: `${PROACTIVE_COMPACTION_THRESHOLD}%`,
2158
+ actualTokens: turnTokenUsage.inputTokens,
2159
+ modelContextWindow,
2160
+ });
2161
+ // Force compaction to reduce context for next turn
2162
+ const compactionResult = await this.forceCompaction(session, params.sessionId);
2163
+ if (compactionResult.success) {
2164
+ logger.info("Proactive compaction succeeded", {
2165
+ sessionId: params.sessionId,
2166
+ });
2167
+ // Save the updated session with new context entry
2168
+ await this.saveSessionToDisk(params.sessionId, session);
2169
+ }
2170
+ else {
2171
+ logger.warn("Proactive compaction failed - continuing without compaction", {
2172
+ sessionId: params.sessionId,
2173
+ });
2174
+ }
2175
+ }
2176
+ }
2114
2177
  session.pendingPrompt = null;
2115
2178
  return {
2116
2179
  stopReason: "end_turn",
@@ -2118,8 +2181,8 @@ export class AgentAcpAdapter {
2118
2181
  }
2119
2182
  /**
2120
2183
  * 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.
2184
+ * Used for error recovery when the Claude API returns "prompt is too long",
2185
+ * and for proactive compaction when actual token usage exceeds threshold.
2123
2186
  */
2124
2187
  async forceCompaction(session, sessionId) {
2125
2188
  logger.info("Force compaction started", {
@@ -406,6 +406,27 @@ export function createAcpHttpApp(agent, agentDir, agentName) {
406
406
  allowMethods: ["GET", "POST", "OPTIONS"],
407
407
  }));
408
408
  app.get("/health", (c) => c.json({ ok: true }));
409
+ // Receive browser console logs and write to agent.log
410
+ const browserLogger = createLogger("gui-console");
411
+ app.post("/logs/browser", async (c) => {
412
+ try {
413
+ const { logs } = await c.req.json();
414
+ if (Array.isArray(logs)) {
415
+ for (const log of logs) {
416
+ const level = log.level;
417
+ const method = browserLogger[level];
418
+ if (typeof method === "function") {
419
+ method.call(browserLogger, log.message, log.metadata);
420
+ }
421
+ }
422
+ }
423
+ return c.json({ ok: true });
424
+ }
425
+ catch (error) {
426
+ logger.error("Failed to process browser logs", { error });
427
+ return c.json({ ok: false }, 400);
428
+ }
429
+ });
409
430
  // List available sessions
410
431
  app.get("/sessions", async (c) => {
411
432
  if (!agentDir || !agentName) {
@@ -1,3 +1,4 @@
1
+ import { EventEmitter } from "node:events";
1
2
  import { mkdir } from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { MultiServerMCPClient } from "@langchain/mcp-adapters";
@@ -11,7 +12,7 @@ import { telemetry } from "../../telemetry/index.js";
11
12
  import { calculateContextSize } from "../../utils/context-size-calculator.js";
12
13
  import { getModelContextWindow } from "../hooks/constants.js";
13
14
  import { isContextOverflowError } from "../hooks/predefined/context-validator.js";
14
- import { bindGeneratorToAbortSignal, bindGeneratorToEmitUpdate, bindGeneratorToSessionContext, getAbortSignal, runWithAbortSignal, } from "../session-context";
15
+ import { bindGeneratorToAbortSignal, bindGeneratorToEmitUpdate, bindGeneratorToInvocationContext, bindGeneratorToSessionContext, getAbortSignal, getInvocationContext, runWithAbortSignal, } from "../session-context";
15
16
  import { loadCustomToolModule, } from "../tool-loader.js";
16
17
  import { createModelFromString, detectProvider } from "./model-factory.js";
17
18
  import { makeOtelCallbacks } from "./otel-callbacks.js";
@@ -19,7 +20,7 @@ import { makeBrowserTools } from "./tools/browser";
19
20
  import { makeDocumentExtractTool } from "./tools/document_extract";
20
21
  import { makeTownE2BTools } from "./tools/e2b";
21
22
  import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
22
- import { hashQuery, queryToToolCallId, subagentEvents, } from "./tools/subagent-connections";
23
+ import { hashQuery, queryToToolCallId, } from "./tools/subagent-connections";
23
24
  import { makeTodoWriteTool, TODO_WRITE_TOOL_NAME } from "./tools/todo";
24
25
  import { makeTownWebSearchTools, makeWebSearchTools } from "./tools/web_search";
25
26
  const _logger = createLogger("agent-runner");
@@ -82,6 +83,14 @@ export class LangchainAgent {
82
83
  async *invokeInternal(req) {
83
84
  // Start with the base generator
84
85
  let generator = this.invokeWithContext(req);
86
+ // Create invocation-scoped context for subagent event routing
87
+ // This must be done here so it's available before invokeWithContext starts
88
+ const subagentInvocationContext = {
89
+ invocationId: req.sessionId,
90
+ subagentEventEmitter: new EventEmitter(),
91
+ };
92
+ // Bind invocation context first so it's available to all nested operations
93
+ generator = bindGeneratorToInvocationContext(subagentInvocationContext, generator);
85
94
  // Bind abort signal if available (for cancellation support)
86
95
  if (req.abortSignal) {
87
96
  generator = bindGeneratorToAbortSignal(req.abortSignal, generator);
@@ -108,6 +117,11 @@ export class LangchainAgent {
108
117
  return yield* generator;
109
118
  }
110
119
  async *invokeWithContext(req) {
120
+ // Get subagent invocation context from AsyncLocalStorage (created in invokeInternal)
121
+ const subagentInvCtx = getInvocationContext();
122
+ if (!subagentInvCtx) {
123
+ throw new Error("No subagent invocation context available - invokeWithContext called outside of invocation binding");
124
+ }
111
125
  // Derive the parent OTEL context for this invocation.
112
126
  // If this is a subagent and the parent process propagated an OTEL trace
113
127
  // context via sessionMeta.otelTraceContext, use that as the parent;
@@ -140,78 +154,50 @@ export class LangchainAgent {
140
154
  // Clear the buffer after flushing
141
155
  pendingToolCallNotifications.length = 0;
142
156
  }
143
- const subagentUpdateQueue = [];
144
- let subagentUpdateResolver = null;
145
157
  const subagentMessagesQueue = [];
146
- // Listen for subagent connection events - resolve any waiting promise immediately
147
- const onSubagentConnection = (event) => {
148
- _logger.info("Received subagent connection event", {
149
- toolCallId: event.toolCallId,
150
- port: event.port,
151
- sessionId: event.sessionId,
152
- });
153
- if (subagentUpdateResolver) {
154
- // If someone is waiting, resolve immediately
155
- const resolver = subagentUpdateResolver;
156
- subagentUpdateResolver = null;
157
- resolver(event);
158
- }
159
- else {
160
- // Otherwise queue for later
161
- subagentUpdateQueue.push(event);
162
- }
163
- };
164
- subagentEvents.on("connection", onSubagentConnection);
165
- // Listen for subagent messages events (for session storage)
158
+ const completedSubagentToolCalls = new Set();
159
+ const activeSubagentToolCalls = new Set();
160
+ // Listen for subagent messages events (for live streaming)
161
+ // Use the invocation-scoped EventEmitter to ensure messages route correctly
166
162
  const onSubagentMessages = (event) => {
167
- _logger.info("Received subagent messages event", {
163
+ _logger.info("Received subagent messages event from scoped emitter", {
168
164
  toolCallId: event.toolCallId,
169
165
  messageCount: event.messages.length,
166
+ completed: event.completed,
167
+ invocationId: subagentInvCtx.invocationId,
168
+ sessionId: req.sessionId,
170
169
  });
171
- subagentMessagesQueue.push(event);
172
- };
173
- subagentEvents.on("messages", onSubagentMessages);
174
- // Helper to get next subagent update (returns immediately if queued, otherwise waits)
175
- const waitForSubagentUpdate = () => {
176
- if (subagentUpdateQueue.length > 0) {
177
- // biome-ignore lint/style/noNonNullAssertion: We check length > 0, so shift() will return a value
178
- return Promise.resolve(subagentUpdateQueue.shift());
170
+ // Track completion
171
+ if (event.completed) {
172
+ completedSubagentToolCalls.add(event.toolCallId);
173
+ _logger.info("✓ Subagent stream completed", {
174
+ toolCallId: event.toolCallId,
175
+ sessionId: req.sessionId,
176
+ totalCompleted: completedSubagentToolCalls.size,
177
+ });
179
178
  }
180
- return new Promise((resolve) => {
181
- subagentUpdateResolver = resolve;
182
- });
179
+ subagentMessagesQueue.push(event);
183
180
  };
184
- // Helper to check and yield all pending subagent updates
181
+ subagentInvCtx.subagentEventEmitter.on("messages", onSubagentMessages);
182
+ // Helper to check and yield all pending subagent message updates
185
183
  async function* yieldPendingSubagentUpdates() {
186
- while (subagentUpdateQueue.length > 0) {
187
- const update = subagentUpdateQueue.shift();
188
- if (!update)
189
- continue;
190
- _logger.info("Yielding queued subagent connection update", {
191
- toolCallId: update.toolCallId,
192
- subagentPort: update.port,
193
- subagentSessionId: update.sessionId,
194
- });
195
- yield {
196
- sessionUpdate: "tool_call_update",
197
- toolCallId: update.toolCallId,
198
- _meta: {
199
- messageId: req.messageId,
200
- subagentPort: update.port,
201
- subagentSessionId: update.sessionId,
202
- },
203
- };
204
- }
205
- // Also yield any pending messages updates
184
+ _logger.info("[SUBAGENT] yieldPendingSubagentUpdates called", {
185
+ sessionId: req.sessionId,
186
+ queueLength: subagentMessagesQueue.length,
187
+ });
206
188
  while (subagentMessagesQueue.length > 0) {
207
189
  const messagesUpdate = subagentMessagesQueue.shift();
208
190
  if (!messagesUpdate)
209
191
  continue;
210
- _logger.info("Yielding queued subagent messages update", {
192
+ _logger.info("[SUBAGENT] Yielding queued subagent messages update", {
193
+ sessionId: req.sessionId,
211
194
  toolCallId: messagesUpdate.toolCallId,
212
195
  messageCount: messagesUpdate.messages.length,
196
+ contentPreview: messagesUpdate.messages[0]?.content?.substring(0, 100),
197
+ hasContentBlocks: (messagesUpdate.messages[0]?.contentBlocks?.length ?? 0) > 0,
198
+ toolCallCount: messagesUpdate.messages[0]?.toolCalls?.length ?? 0,
213
199
  });
214
- yield {
200
+ const updateToYield = {
215
201
  sessionUpdate: "tool_call_update",
216
202
  toolCallId: messagesUpdate.toolCallId,
217
203
  _meta: {
@@ -219,6 +205,17 @@ export class LangchainAgent {
219
205
  subagentMessages: messagesUpdate.messages,
220
206
  },
221
207
  };
208
+ _logger.info("[SUBAGENT] About to yield update object", {
209
+ sessionId: req.sessionId,
210
+ toolCallId: messagesUpdate.toolCallId,
211
+ updateType: updateToYield.sessionUpdate,
212
+ hasMetaSubagentMessages: !!updateToYield._meta?.subagentMessages,
213
+ });
214
+ yield updateToYield;
215
+ _logger.info("[SUBAGENT] Successfully yielded update", {
216
+ sessionId: req.sessionId,
217
+ toolCallId: messagesUpdate.toolCallId,
218
+ });
222
219
  }
223
220
  }
224
221
  // Add agent.session_id as a base attribute so it propagates to all child spans
@@ -871,53 +868,63 @@ export class LangchainAgent {
871
868
  callbacks: [otelCallbacks],
872
869
  }));
873
870
  _logger.info("agent.stream created, starting iteration");
874
- // Use manual iteration with Promise.race to interleave stream events with subagent updates
875
- const streamIterator = (await stream)[Symbol.asyncIterator]();
876
- let streamDone = false;
877
- let pendingStreamPromise = null;
878
- while (!streamDone) {
879
- // Get or create the stream promise (reuse if still pending from last iteration)
880
- const nextStreamPromise = pendingStreamPromise ?? streamIterator.next();
881
- pendingStreamPromise = nextStreamPromise; // Track it
882
- // Create subagent wait promise (only if no queued updates)
883
- const subagentPromise = waitForSubagentUpdate();
884
- // Use Promise.race, but we need to handle both outcomes
885
- const result = await Promise.race([
886
- nextStreamPromise.then((r) => ({
887
- type: "stream",
888
- result: r,
889
- })),
890
- subagentPromise.then((u) => ({ type: "subagent", update: u })),
891
- ]);
892
- if (result.type === "subagent") {
893
- // Got a subagent update - yield it immediately
894
- const update = result.update;
895
- _logger.info("Yielding subagent connection update (via race)", {
896
- toolCallId: update.toolCallId,
897
- subagentPort: update.port,
898
- subagentSessionId: update.sessionId,
871
+ // Merge the LangChain stream with subagent event stream
872
+ // This allows both to yield concurrently without polling
873
+ async function* mergeStreams() {
874
+ const streamIterator = (await stream)[Symbol.asyncIterator]();
875
+ const _pending = [];
876
+ let streamDone = false;
877
+ let subagentListenerActive = true;
878
+ // Create a promise that resolves when the next subagent event arrives
879
+ function createSubagentEventPromise() {
880
+ return new Promise((resolve) => {
881
+ const handler = (event) => {
882
+ subagentInvCtx?.subagentEventEmitter.off("messages", handler);
883
+ resolve({ source: "subagent", value: event, done: false });
884
+ };
885
+ subagentInvCtx?.subagentEventEmitter.once("messages", handler);
899
886
  });
900
- yield {
901
- sessionUpdate: "tool_call_update",
902
- toolCallId: update.toolCallId,
903
- _meta: {
904
- messageId: req.messageId,
905
- subagentPort: update.port,
906
- subagentSessionId: update.sessionId,
907
- },
908
- };
909
- // Continue - the stream promise is still pending, will be reused
910
- continue;
911
887
  }
912
- // Got a stream item - clear the pending promise
913
- pendingStreamPromise = null;
914
- const { done, value: streamItem } = result.result;
915
- if (done) {
916
- streamDone = true;
917
- break;
888
+ // Start listening for subagent events
889
+ let subagentPromise = createSubagentEventPromise();
890
+ while (!streamDone || subagentListenerActive) {
891
+ // Race between next stream item and next subagent event
892
+ const streamPromise = streamIterator.next().then((result) => ({
893
+ source: "stream",
894
+ value: result.value,
895
+ done: result.done ?? false,
896
+ }));
897
+ const result = await Promise.race([streamPromise, subagentPromise]);
898
+ if (result.source === "stream") {
899
+ if (result.done) {
900
+ streamDone = true;
901
+ // Continue to drain remaining subagent events
902
+ subagentListenerActive = false;
903
+ // Yield any remaining queued messages
904
+ while (subagentMessagesQueue.length > 0) {
905
+ yield { source: "subagent" };
906
+ }
907
+ break;
908
+ }
909
+ yield { source: "stream", value: result.value };
910
+ }
911
+ else if (result.source === "subagent") {
912
+ // Subagent event arrived - it's already in the queue
913
+ yield { source: "subagent" };
914
+ // Start listening for the next subagent event
915
+ subagentPromise = createSubagentEventPromise();
916
+ }
918
917
  }
919
- // Also yield any queued subagent updates before processing stream item
920
- yield* yieldPendingSubagentUpdates();
918
+ }
919
+ // Iterate through the merged stream
920
+ for await (const item of mergeStreams()) {
921
+ if (item.source === "subagent") {
922
+ // Yield any queued subagent messages
923
+ yield* yieldPendingSubagentUpdates();
924
+ continue;
925
+ }
926
+ // Process the stream item
927
+ const streamItem = item.value;
921
928
  // biome-ignore lint/suspicious/noExplicitAny: LangChain stream items are tuples with dynamic types
922
929
  const [streamMode, chunk] = streamItem;
923
930
  if (streamMode === "updates") {
@@ -1020,10 +1027,19 @@ export class LangchainAgent {
1020
1027
  typeof toolCall.args.query === "string") {
1021
1028
  const qHash = hashQuery(toolCall.args.query);
1022
1029
  queryToToolCallId.set(qHash, toolCall.id);
1023
- telemetry.log("info", "Registered subagent query hash mapping", {
1030
+ activeSubagentToolCalls.add(toolCall.id);
1031
+ telemetry.log("info", "✓ Registered subagent query hash mapping", {
1024
1032
  queryHash: qHash,
1025
1033
  toolCallId: toolCall.id,
1026
1034
  queryPreview: toolCall.args.query.slice(0, 50),
1035
+ timestamp: new Date().toISOString(),
1036
+ });
1037
+ }
1038
+ else {
1039
+ telemetry.log("warn", "✗ Subagent tool call missing query parameter", {
1040
+ toolCallId: toolCall.id,
1041
+ hasQuery: "query" in (toolCall.args || {}),
1042
+ argsKeys: Object.keys(toolCall.args || {}),
1027
1043
  });
1028
1044
  }
1029
1045
  }
@@ -1225,9 +1241,10 @@ export class LangchainAgent {
1225
1241
  continue;
1226
1242
  }
1227
1243
  // Check if the tool execution failed
1228
- // LangChain may set status: "error" OR the content may start with "Error:"
1244
+ // LangChain may set status: "error" OR the content looks like an error message
1245
+ // Matches: "Error...", "Error:...", "SomethingError:..." (e.g. RateLimitError:, TypeError:)
1229
1246
  const contentLooksLikeError = typeof aiMessage.content === "string" &&
1230
- aiMessage.content.trim().startsWith("Error:");
1247
+ /^(\w*Error:|Error\b)/i.test(aiMessage.content.trim());
1231
1248
  const isError = aiMessage.status === "error" || contentLooksLikeError;
1232
1249
  const status = isError ? "failed" : "completed";
1233
1250
  telemetry.log(isError ? "error" : "info", `Tool call ${status}`, {
@@ -1311,15 +1328,62 @@ export class LangchainAgent {
1311
1328
  }
1312
1329
  // Yield any remaining pending subagent connection updates after stream ends
1313
1330
  yield* yieldPendingSubagentUpdates();
1331
+ // Keep polling for subagent messages after stream ends
1332
+ // This ensures we capture messages that arrive after LangChain stream completes
1333
+ _logger.info("[SUBAGENT] Starting post-stream polling for subagent messages", {
1334
+ sessionId: req.sessionId,
1335
+ activeSubagentCount: activeSubagentToolCalls.size,
1336
+ });
1337
+ const checkInterval = 100; // Check every 100ms
1338
+ const maxIdleTime = 5000; // Stop if no messages for 5 seconds (fallback)
1339
+ const maxWaitTime = 300000; // Absolute max 5 minutes
1340
+ const startTime = Date.now();
1341
+ let lastMessageTime = Date.now();
1342
+ while (Date.now() - startTime < maxWaitTime) {
1343
+ // Check if all subagent tool calls have completed
1344
+ if (activeSubagentToolCalls.size > 0 &&
1345
+ completedSubagentToolCalls.size >= activeSubagentToolCalls.size) {
1346
+ _logger.info("[SUBAGENT] All subagent tool calls completed", {
1347
+ sessionId: req.sessionId,
1348
+ totalSubagents: activeSubagentToolCalls.size,
1349
+ completedCount: completedSubagentToolCalls.size,
1350
+ });
1351
+ break;
1352
+ }
1353
+ // Check if there are pending messages
1354
+ if (subagentMessagesQueue.length > 0) {
1355
+ _logger.info("[SUBAGENT] Found pending messages, yielding", {
1356
+ sessionId: req.sessionId,
1357
+ queueLength: subagentMessagesQueue.length,
1358
+ });
1359
+ yield* yieldPendingSubagentUpdates();
1360
+ lastMessageTime = Date.now();
1361
+ }
1362
+ // Stop if we haven't seen messages for maxIdleTime (fallback for no subagents)
1363
+ if (Date.now() - lastMessageTime > maxIdleTime) {
1364
+ _logger.info("[SUBAGENT] No new messages for 5 seconds, finishing", {
1365
+ sessionId: req.sessionId,
1366
+ hasActiveSubagents: activeSubagentToolCalls.size > 0,
1367
+ });
1368
+ break;
1369
+ }
1370
+ // Wait a bit before checking again
1371
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
1372
+ }
1373
+ if (Date.now() - startTime >= maxWaitTime) {
1374
+ _logger.warn("[SUBAGENT] Timeout waiting for subagents", {
1375
+ sessionId: req.sessionId,
1376
+ });
1377
+ }
1378
+ // Final yield of any remaining messages
1379
+ yield* yieldPendingSubagentUpdates();
1380
+ _logger.info("[SUBAGENT] Finished post-stream polling", {
1381
+ sessionId: req.sessionId,
1382
+ });
1314
1383
  // Now that content streaming is complete, yield all buffered tool call notifications
1315
1384
  yield* flushPendingToolCalls();
1316
- // Clean up subagent event listeners
1317
- subagentEvents.off("connection", onSubagentConnection);
1318
- subagentEvents.off("messages", onSubagentMessages);
1319
- // Cancel any pending wait
1320
- if (subagentUpdateResolver) {
1321
- subagentUpdateResolver = null;
1322
- }
1385
+ // Clean up subagent event listener from invocation-scoped emitter
1386
+ subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
1323
1387
  // Clean up any remaining iteration span
1324
1388
  otelCallbacks?.cleanup();
1325
1389
  // Log successful completion
@@ -1335,9 +1399,8 @@ export class LangchainAgent {
1335
1399
  };
1336
1400
  }
1337
1401
  catch (error) {
1338
- // Clean up subagent event listeners on error
1339
- subagentEvents.off("connection", onSubagentConnection);
1340
- subagentEvents.off("messages", onSubagentMessages);
1402
+ // Clean up subagent event listener on error from invocation-scoped emitter
1403
+ subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
1341
1404
  // Clean up any remaining iteration span
1342
1405
  otelCallbacks?.cleanup();
1343
1406
  // Check if this is a context overflow error - wrap it for retry handling
@@ -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>)[];