@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.
- package/dist/acp-server/adapter.js +63 -43
- package/dist/acp-server/http.js +21 -0
- package/dist/runner/langchain/index.js +139 -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
- package/dist/runner/langchain/tools/artifacts.d.ts +0 -68
- package/dist/runner/langchain/tools/artifacts.js +0 -474
- package/dist/runner/langchain/tools/conversation_search.d.ts +0 -22
- package/dist/runner/langchain/tools/conversation_search.js +0 -137
- package/dist/runner/langchain/tools/generate_image.d.ts +0 -47
- package/dist/runner/langchain/tools/generate_image.js +0 -175
- package/dist/runner/langchain/tools/port-utils.d.ts +0 -8
- package/dist/runner/langchain/tools/port-utils.js +0 -35
|
@@ -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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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: {
|
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,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
|
-
|
|
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
|
-
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
|
1318
|
-
|
|
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
|
|
1340
|
-
|
|
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
|
|
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;
|