@townco/agent 0.1.112 → 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.
- package/dist/acp-server/adapter.js +31 -18
- package/dist/acp-server/http.js +21 -0
- package/dist/runner/langchain/index.js +176 -114
- package/dist/runner/langchain/tools/subagent-connections.d.ts +7 -22
- package/dist/runner/langchain/tools/subagent-connections.js +26 -50
- package/dist/runner/langchain/tools/subagent.js +137 -378
- package/dist/runner/session-context.d.ts +18 -0
- package/dist/runner/session-context.js +35 -0
- package/dist/telemetry/setup.js +21 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/acp-server/http.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
subagentUpdateResolver = resolve;
|
|
182
|
-
});
|
|
179
|
+
subagentMessagesQueue.push(event);
|
|
183
180
|
};
|
|
184
|
-
|
|
181
|
+
subagentInvCtx.subagentEventEmitter.on("messages", onSubagentMessages);
|
|
182
|
+
// Helper to check and yield all pending subagent message updates
|
|
185
183
|
async function* yieldPendingSubagentUpdates() {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1312,15 +1328,62 @@ export class LangchainAgent {
|
|
|
1312
1328
|
}
|
|
1313
1329
|
// Yield any remaining pending subagent connection updates after stream ends
|
|
1314
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
|
+
});
|
|
1315
1383
|
// Now that content streaming is complete, yield all buffered tool call notifications
|
|
1316
1384
|
yield* flushPendingToolCalls();
|
|
1317
|
-
// Clean up subagent event
|
|
1318
|
-
|
|
1319
|
-
subagentEvents.off("messages", onSubagentMessages);
|
|
1320
|
-
// Cancel any pending wait
|
|
1321
|
-
if (subagentUpdateResolver) {
|
|
1322
|
-
subagentUpdateResolver = null;
|
|
1323
|
-
}
|
|
1385
|
+
// Clean up subagent event listener from invocation-scoped emitter
|
|
1386
|
+
subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
|
|
1324
1387
|
// Clean up any remaining iteration span
|
|
1325
1388
|
otelCallbacks?.cleanup();
|
|
1326
1389
|
// Log successful completion
|
|
@@ -1336,9 +1399,8 @@ export class LangchainAgent {
|
|
|
1336
1399
|
};
|
|
1337
1400
|
}
|
|
1338
1401
|
catch (error) {
|
|
1339
|
-
// Clean up subagent event
|
|
1340
|
-
|
|
1341
|
-
subagentEvents.off("messages", onSubagentMessages);
|
|
1402
|
+
// Clean up subagent event listener on error from invocation-scoped emitter
|
|
1403
|
+
subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
|
|
1342
1404
|
// Clean up any remaining iteration span
|
|
1343
1405
|
otelCallbacks?.cleanup();
|
|
1344
1406
|
// 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
|
|
55
|
-
* Emits an event
|
|
56
|
-
|
|
57
|
-
|
|
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;
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
2
|
import { createLogger } from "@townco/core";
|
|
3
|
+
import { getInvocationContext } from "../../session-context.js";
|
|
4
4
|
const logger = createLogger("subagent-connections");
|
|
5
|
-
/**
|
|
6
|
-
* Event emitter for subagent connection events.
|
|
7
|
-
* The runner listens to these events and emits tool_call_update.
|
|
8
|
-
*/
|
|
9
|
-
export const subagentEvents = new EventEmitter();
|
|
10
5
|
/**
|
|
11
6
|
* Maps query hash to toolCallId.
|
|
12
7
|
* Set by the runner when it sees a subagent tool_call.
|
|
8
|
+
* This is still global because the registration happens in the parent runner
|
|
9
|
+
* before subagent execution begins.
|
|
13
10
|
*/
|
|
14
11
|
export const queryToToolCallId = new Map();
|
|
15
|
-
/**
|
|
16
|
-
* Maps query hash to resolved toolCallId (preserved after connection event).
|
|
17
|
-
* Used to correlate messages when the tool completes.
|
|
18
|
-
*/
|
|
19
|
-
const queryToResolvedToolCallId = new Map();
|
|
20
12
|
/**
|
|
21
13
|
* Generate a hash from the query string for correlation.
|
|
22
14
|
*/
|
|
@@ -29,61 +21,45 @@ export function hashQuery(query) {
|
|
|
29
21
|
return hash;
|
|
30
22
|
}
|
|
31
23
|
/**
|
|
32
|
-
* Called by the subagent tool
|
|
33
|
-
* Emits an event
|
|
24
|
+
* Called by the subagent tool during execution to emit incremental messages.
|
|
25
|
+
* Emits an event with the messages for live streaming to the UI.
|
|
26
|
+
* Uses the invocation context's EventEmitter to ensure messages go to the correct parent.
|
|
27
|
+
* @param completed - If true, signals that the subagent stream has finished
|
|
34
28
|
*/
|
|
35
|
-
export function
|
|
36
|
-
logger.info("emitSubagentConnection called", {
|
|
37
|
-
queryHash,
|
|
38
|
-
port: connectionInfo.port,
|
|
39
|
-
sessionId: connectionInfo.sessionId,
|
|
40
|
-
registeredHashes: Array.from(queryToToolCallId.keys()),
|
|
41
|
-
});
|
|
29
|
+
export function emitSubagentMessages(queryHash, messages, completed = false) {
|
|
42
30
|
const toolCallId = queryToToolCallId.get(queryHash);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
toolCallId,
|
|
47
|
-
port: connectionInfo.port,
|
|
48
|
-
sessionId: connectionInfo.sessionId,
|
|
49
|
-
});
|
|
50
|
-
subagentEvents.emit("connection", {
|
|
51
|
-
toolCallId,
|
|
52
|
-
...connectionInfo,
|
|
53
|
-
});
|
|
54
|
-
// Preserve the toolCallId for message emission, but remove from pending lookup
|
|
55
|
-
queryToResolvedToolCallId.set(queryHash, toolCallId);
|
|
56
|
-
queryToToolCallId.delete(queryHash);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
logger.warn("No toolCallId found for queryHash", {
|
|
31
|
+
const invocationCtx = getInvocationContext();
|
|
32
|
+
if (!invocationCtx) {
|
|
33
|
+
logger.warn("✗ No invocation context found - cannot emit subagent messages", {
|
|
60
34
|
queryHash,
|
|
61
|
-
|
|
35
|
+
hasToolCallId: !!toolCallId,
|
|
62
36
|
});
|
|
37
|
+
return;
|
|
63
38
|
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Called by the subagent tool when it completes with accumulated messages.
|
|
67
|
-
* Emits an event with the messages for session storage.
|
|
68
|
-
*/
|
|
69
|
-
export function emitSubagentMessages(queryHash, messages) {
|
|
70
|
-
const toolCallId = queryToResolvedToolCallId.get(queryHash);
|
|
71
39
|
if (toolCallId) {
|
|
72
|
-
|
|
40
|
+
const firstMessage = messages[0];
|
|
41
|
+
logger.info("✓ Emitting subagent messages for live streaming", {
|
|
73
42
|
queryHash,
|
|
74
43
|
toolCallId,
|
|
75
44
|
messageCount: messages.length,
|
|
45
|
+
hasContent: firstMessage ? firstMessage.content.length > 0 : false,
|
|
46
|
+
hasToolCalls: firstMessage ? firstMessage.toolCalls.length > 0 : false,
|
|
47
|
+
completed,
|
|
48
|
+
invocationId: invocationCtx.invocationId,
|
|
76
49
|
});
|
|
77
|
-
|
|
50
|
+
// Emit to the parent's invocation-scoped EventEmitter
|
|
51
|
+
invocationCtx.subagentEventEmitter.emit("messages", {
|
|
78
52
|
toolCallId,
|
|
79
53
|
messages,
|
|
54
|
+
completed,
|
|
80
55
|
});
|
|
81
|
-
// Clean up the resolved mapping
|
|
82
|
-
queryToResolvedToolCallId.delete(queryHash);
|
|
83
56
|
}
|
|
84
57
|
else {
|
|
85
|
-
logger.warn("No
|
|
58
|
+
logger.warn("✗ No toolCallId found for messages emission (RACE CONDITION)", {
|
|
86
59
|
queryHash,
|
|
60
|
+
registeredHashes: Array.from(queryToToolCallId.keys()),
|
|
61
|
+
mapSize: queryToToolCallId.size,
|
|
62
|
+
invocationId: invocationCtx.invocationId,
|
|
87
63
|
});
|
|
88
64
|
}
|
|
89
65
|
}
|