@townco/agent 0.1.112 → 0.1.114

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.
@@ -273,14 +273,12 @@ export class AgentAcpAdapter {
273
273
  "url" in result &&
274
274
  typeof result.url === "string") {
275
275
  // Use the citationId from the tool output if available
276
- let citationId;
277
- if (typeof result.citationId === "number") {
278
- citationId = String(result.citationId);
279
- }
280
- else {
281
- session.sourceCounter++;
282
- citationId = String(session.sourceCounter);
283
- }
276
+ const citationId = typeof result.citationId === "number"
277
+ ? String(result.citationId)
278
+ : (() => {
279
+ session.sourceCounter++;
280
+ return String(session.sourceCounter);
281
+ })();
284
282
  const url = result.url;
285
283
  const title = typeof result.title === "string" ? result.title : "Untitled";
286
284
  const snippet = typeof result.text === "string"
@@ -420,14 +418,11 @@ export class AgentAcpAdapter {
420
418
  if (!docUrl && !docTitle)
421
419
  return null;
422
420
  // Use document_id as the citation ID if available, otherwise use counter
423
- let citationId;
424
- if (docId) {
425
- citationId = docId;
426
- }
427
- else {
428
- session.sourceCounter++;
429
- citationId = String(session.sourceCounter);
430
- }
421
+ const citationId = docId ||
422
+ (() => {
423
+ session.sourceCounter++;
424
+ return String(session.sourceCounter);
425
+ })();
431
426
  // Extract snippet from summary or content
432
427
  let snippet;
433
428
  if (typeof doc.summary === "string") {
@@ -1075,7 +1070,7 @@ export class AgentAcpAdapter {
1075
1070
  }
1076
1071
  logger.info("User message received", {
1077
1072
  sessionId: params.sessionId,
1078
- messagePreview: userMessageText.slice(0, 100),
1073
+ message: userMessageText,
1079
1074
  noSession: this.noSession,
1080
1075
  });
1081
1076
  // Only store messages if session persistence is enabled
@@ -1134,12 +1129,40 @@ export class AgentAcpAdapter {
1134
1129
  // Build ordered content blocks for the assistant response
1135
1130
  const contentBlocks = [];
1136
1131
  let pendingText = "";
1132
+ // Buffer for logging agent response in readable chunks
1133
+ let logBuffer = "";
1134
+ const flushLogBuffer = (force = false) => {
1135
+ if (logBuffer.length === 0)
1136
+ return;
1137
+ if (force) {
1138
+ // Flush everything
1139
+ logger.info("Agent response", {
1140
+ sessionId: params.sessionId,
1141
+ text: logBuffer,
1142
+ });
1143
+ logBuffer = "";
1144
+ }
1145
+ else {
1146
+ // Only flush complete lines (up to and including the last newline)
1147
+ const lastNewline = logBuffer.lastIndexOf("\n");
1148
+ if (lastNewline !== -1) {
1149
+ const completeLines = logBuffer.slice(0, lastNewline + 1);
1150
+ logBuffer = logBuffer.slice(lastNewline + 1);
1151
+ logger.info("Agent response", {
1152
+ sessionId: params.sessionId,
1153
+ text: completeLines,
1154
+ });
1155
+ }
1156
+ }
1157
+ };
1137
1158
  // Helper function to flush pending text as a TextBlock
1138
1159
  const flushPendingText = () => {
1139
1160
  if (pendingText.length > 0) {
1140
1161
  contentBlocks.push({ type: "text", text: pendingText });
1141
1162
  pendingText = "";
1142
1163
  }
1164
+ // Force flush any remaining log buffer when text block completes
1165
+ flushLogBuffer(true);
1143
1166
  };
1144
1167
  // Helper to save cancelled message to session
1145
1168
  const saveCancelledMessage = async () => {
@@ -1332,6 +1355,11 @@ export class AgentAcpAdapter {
1332
1355
  const content = msg.content;
1333
1356
  if (content.type === "text" && typeof content.text === "string") {
1334
1357
  pendingText += content.text;
1358
+ // Buffer for logging - flush on newlines
1359
+ logBuffer += content.text;
1360
+ if (logBuffer.includes("\n")) {
1361
+ flushLogBuffer();
1362
+ }
1335
1363
  }
1336
1364
  }
1337
1365
  // Debug: log if this chunk has tokenUsage in _meta
@@ -1405,6 +1433,13 @@ export class AgentAcpAdapter {
1405
1433
  if (toolCallMsg.rawInput) {
1406
1434
  toolCall.rawInput = toolCallMsg.rawInput;
1407
1435
  }
1436
+ // Log tool call start
1437
+ logger.info("Tool call started", {
1438
+ sessionId: params.sessionId,
1439
+ toolCallId: toolCall.id,
1440
+ tool: toolCall.title,
1441
+ prettyName: toolCall.prettyName,
1442
+ });
1408
1443
  contentBlocks.push(toolCall);
1409
1444
  }
1410
1445
  // Handle tool_call_update - update existing ToolCallBlock
@@ -1431,6 +1466,17 @@ export class AgentAcpAdapter {
1431
1466
  if (toolCallBlock.status === "completed" ||
1432
1467
  toolCallBlock.status === "failed") {
1433
1468
  toolCallBlock.completedAt = Date.now();
1469
+ // Log tool call completion
1470
+ logger.info("Tool call completed", {
1471
+ sessionId: params.sessionId,
1472
+ toolCallId: updateMsg.toolCallId,
1473
+ tool: toolCallBlock.title,
1474
+ status: toolCallBlock.status,
1475
+ durationMs: toolCallBlock.startedAt
1476
+ ? toolCallBlock.completedAt - toolCallBlock.startedAt
1477
+ : undefined,
1478
+ error: toolCallBlock.error,
1479
+ });
1434
1480
  }
1435
1481
  const meta = updateMsg._meta;
1436
1482
  // Update batchId from _meta (comes from tool_call_update after preliminary tool_call)
@@ -1444,37 +1490,11 @@ export class AgentAcpAdapter {
1444
1490
  toolCallBlock.subagentSessionId = meta.subagentSessionId;
1445
1491
  }
1446
1492
  if (meta?.subagentMessages) {
1447
- logger.info("Storing subagent messages for session replay", {
1448
- toolCallId: updateMsg.toolCallId,
1449
- messageCount: meta.subagentMessages.length,
1450
- });
1451
1493
  toolCallBlock.subagentMessages = meta.subagentMessages;
1452
1494
  }
1453
1495
  }
1454
1496
  // Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
1455
1497
  if (updateMsg._meta) {
1456
- logger.info("Forwarding tool_call_update with _meta to client", {
1457
- toolCallId: updateMsg.toolCallId,
1458
- status: updateMsg.status,
1459
- _meta: updateMsg._meta,
1460
- });
1461
- this.connection.sessionUpdate({
1462
- sessionId: params.sessionId,
1463
- update: {
1464
- sessionUpdate: "tool_call_update",
1465
- toolCallId: updateMsg.toolCallId,
1466
- status: updateMsg.status,
1467
- _meta: updateMsg._meta,
1468
- },
1469
- });
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
1498
  this.connection.sessionUpdate({
1479
1499
  sessionId: params.sessionId,
1480
1500
  update: {
@@ -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,43 @@ 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
206
184
  while (subagentMessagesQueue.length > 0) {
207
185
  const messagesUpdate = subagentMessagesQueue.shift();
208
186
  if (!messagesUpdate)
209
187
  continue;
210
- _logger.info("Yielding queued subagent messages update", {
188
+ _logger.debug("[SUBAGENT] Yielding queued subagent messages update", {
189
+ sessionId: req.sessionId,
211
190
  toolCallId: messagesUpdate.toolCallId,
212
191
  messageCount: messagesUpdate.messages.length,
213
192
  });
214
- yield {
193
+ const updateToYield = {
215
194
  sessionUpdate: "tool_call_update",
216
195
  toolCallId: messagesUpdate.toolCallId,
217
196
  _meta: {
@@ -219,6 +198,7 @@ export class LangchainAgent {
219
198
  subagentMessages: messagesUpdate.messages,
220
199
  },
221
200
  };
201
+ yield updateToYield;
222
202
  }
223
203
  }
224
204
  // Add agent.session_id as a base attribute so it propagates to all child spans
@@ -871,53 +851,63 @@ export class LangchainAgent {
871
851
  callbacks: [otelCallbacks],
872
852
  }));
873
853
  _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,
854
+ // Merge the LangChain stream with subagent event stream
855
+ // This allows both to yield concurrently without polling
856
+ async function* mergeStreams() {
857
+ const streamIterator = (await stream)[Symbol.asyncIterator]();
858
+ const _pending = [];
859
+ let streamDone = false;
860
+ let subagentListenerActive = true;
861
+ // Create a promise that resolves when the next subagent event arrives
862
+ function createSubagentEventPromise() {
863
+ return new Promise((resolve) => {
864
+ const handler = (event) => {
865
+ subagentInvCtx?.subagentEventEmitter.off("messages", handler);
866
+ resolve({ source: "subagent", value: event, done: false });
867
+ };
868
+ subagentInvCtx?.subagentEventEmitter.once("messages", handler);
899
869
  });
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
870
  }
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;
871
+ // Start listening for subagent events
872
+ let subagentPromise = createSubagentEventPromise();
873
+ while (!streamDone || subagentListenerActive) {
874
+ // Race between next stream item and next subagent event
875
+ const streamPromise = streamIterator.next().then((result) => ({
876
+ source: "stream",
877
+ value: result.value,
878
+ done: result.done ?? false,
879
+ }));
880
+ const result = await Promise.race([streamPromise, subagentPromise]);
881
+ if (result.source === "stream") {
882
+ if (result.done) {
883
+ streamDone = true;
884
+ // Continue to drain remaining subagent events
885
+ subagentListenerActive = false;
886
+ // Yield any remaining queued messages
887
+ while (subagentMessagesQueue.length > 0) {
888
+ yield { source: "subagent" };
889
+ }
890
+ break;
891
+ }
892
+ yield { source: "stream", value: result.value };
893
+ }
894
+ else if (result.source === "subagent") {
895
+ // Subagent event arrived - it's already in the queue
896
+ yield { source: "subagent" };
897
+ // Start listening for the next subagent event
898
+ subagentPromise = createSubagentEventPromise();
899
+ }
918
900
  }
919
- // Also yield any queued subagent updates before processing stream item
920
- yield* yieldPendingSubagentUpdates();
901
+ }
902
+ // Iterate through the merged stream
903
+ for await (const item of mergeStreams()) {
904
+ if (item.source === "subagent") {
905
+ // Yield any queued subagent messages
906
+ yield* yieldPendingSubagentUpdates();
907
+ continue;
908
+ }
909
+ // Process the stream item
910
+ const streamItem = item.value;
921
911
  // biome-ignore lint/suspicious/noExplicitAny: LangChain stream items are tuples with dynamic types
922
912
  const [streamMode, chunk] = streamItem;
923
913
  if (streamMode === "updates") {
@@ -1020,10 +1010,19 @@ export class LangchainAgent {
1020
1010
  typeof toolCall.args.query === "string") {
1021
1011
  const qHash = hashQuery(toolCall.args.query);
1022
1012
  queryToToolCallId.set(qHash, toolCall.id);
1023
- telemetry.log("info", "Registered subagent query hash mapping", {
1013
+ activeSubagentToolCalls.add(toolCall.id);
1014
+ telemetry.log("info", "✓ Registered subagent query hash mapping", {
1024
1015
  queryHash: qHash,
1025
1016
  toolCallId: toolCall.id,
1026
1017
  queryPreview: toolCall.args.query.slice(0, 50),
1018
+ timestamp: new Date().toISOString(),
1019
+ });
1020
+ }
1021
+ else {
1022
+ telemetry.log("warn", "✗ Subagent tool call missing query parameter", {
1023
+ toolCallId: toolCall.id,
1024
+ hasQuery: "query" in (toolCall.args || {}),
1025
+ argsKeys: Object.keys(toolCall.args || {}),
1027
1026
  });
1028
1027
  }
1029
1028
  }
@@ -1312,15 +1311,42 @@ export class LangchainAgent {
1312
1311
  }
1313
1312
  // Yield any remaining pending subagent connection updates after stream ends
1314
1313
  yield* yieldPendingSubagentUpdates();
1314
+ // Keep polling for subagent messages after stream ends
1315
+ // This ensures we capture messages that arrive after LangChain stream completes
1316
+ const checkInterval = 100; // Check every 100ms
1317
+ const maxIdleTime = 5000; // Stop if no messages for 5 seconds (fallback)
1318
+ const maxWaitTime = 300000; // Absolute max 5 minutes
1319
+ const startTime = Date.now();
1320
+ let lastMessageTime = Date.now();
1321
+ while (Date.now() - startTime < maxWaitTime) {
1322
+ // Check if all subagent tool calls have completed
1323
+ if (activeSubagentToolCalls.size > 0 &&
1324
+ completedSubagentToolCalls.size >= activeSubagentToolCalls.size) {
1325
+ break;
1326
+ }
1327
+ // Check if there are pending messages
1328
+ if (subagentMessagesQueue.length > 0) {
1329
+ yield* yieldPendingSubagentUpdates();
1330
+ lastMessageTime = Date.now();
1331
+ }
1332
+ // Stop if we haven't seen messages for maxIdleTime (fallback for no subagents)
1333
+ if (Date.now() - lastMessageTime > maxIdleTime) {
1334
+ break;
1335
+ }
1336
+ // Wait a bit before checking again
1337
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
1338
+ }
1339
+ if (Date.now() - startTime >= maxWaitTime) {
1340
+ _logger.warn("[SUBAGENT] Timeout waiting for subagents", {
1341
+ sessionId: req.sessionId,
1342
+ });
1343
+ }
1344
+ // Final yield of any remaining messages
1345
+ yield* yieldPendingSubagentUpdates();
1315
1346
  // Now that content streaming is complete, yield all buffered tool call notifications
1316
1347
  yield* flushPendingToolCalls();
1317
- // Clean up subagent event listeners
1318
- subagentEvents.off("connection", onSubagentConnection);
1319
- subagentEvents.off("messages", onSubagentMessages);
1320
- // Cancel any pending wait
1321
- if (subagentUpdateResolver) {
1322
- subagentUpdateResolver = null;
1323
- }
1348
+ // Clean up subagent event listener from invocation-scoped emitter
1349
+ subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
1324
1350
  // Clean up any remaining iteration span
1325
1351
  otelCallbacks?.cleanup();
1326
1352
  // Log successful completion
@@ -1336,9 +1362,8 @@ export class LangchainAgent {
1336
1362
  };
1337
1363
  }
1338
1364
  catch (error) {
1339
- // Clean up subagent event listeners on error
1340
- subagentEvents.off("connection", onSubagentConnection);
1341
- subagentEvents.off("messages", onSubagentMessages);
1365
+ // Clean up subagent event listener on error from invocation-scoped emitter
1366
+ subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
1342
1367
  // Clean up any remaining iteration span
1343
1368
  otelCallbacks?.cleanup();
1344
1369
  // Check if this is a context overflow error - wrap it for retry handling
@@ -1,12 +1,3 @@
1
- import { EventEmitter } from "node:events";
2
- /**
3
- * Registry for subagent connection info.
4
- * Maps query hash to connection info so the runner can emit tool_call_update.
5
- */
6
- export interface SubagentConnectionInfo {
7
- port: number;
8
- sessionId: string;
9
- }
10
1
  /**
11
2
  * Sub-agent tool call tracked during streaming
12
3
  */
@@ -36,14 +27,11 @@ export interface SubagentMessage {
36
27
  contentBlocks: SubagentContentBlock[];
37
28
  toolCalls: SubagentToolCall[];
38
29
  }
39
- /**
40
- * Event emitter for subagent connection events.
41
- * The runner listens to these events and emits tool_call_update.
42
- */
43
- export declare const subagentEvents: EventEmitter<[never]>;
44
30
  /**
45
31
  * Maps query hash to toolCallId.
46
32
  * Set by the runner when it sees a subagent tool_call.
33
+ * This is still global because the registration happens in the parent runner
34
+ * before subagent execution begins.
47
35
  */
48
36
  export declare const queryToToolCallId: Map<string, string>;
49
37
  /**
@@ -51,12 +39,9 @@ export declare const queryToToolCallId: Map<string, string>;
51
39
  */
52
40
  export declare function hashQuery(query: string): string;
53
41
  /**
54
- * Called by the subagent tool when connection is established.
55
- * Emits an event that the runner can listen to.
56
- */
57
- export declare function emitSubagentConnection(queryHash: string, connectionInfo: SubagentConnectionInfo): void;
58
- /**
59
- * Called by the subagent tool when it completes with accumulated messages.
60
- * Emits an event with the messages for session storage.
42
+ * Called by the subagent tool during execution to emit incremental messages.
43
+ * Emits an event with the messages for live streaming to the UI.
44
+ * Uses the invocation context's EventEmitter to ensure messages go to the correct parent.
45
+ * @param completed - If true, signals that the subagent stream has finished
61
46
  */
62
- export declare function emitSubagentMessages(queryHash: string, messages: SubagentMessage[]): void;
47
+ export declare function emitSubagentMessages(queryHash: string, messages: SubagentMessage[], completed?: boolean): void;