@townco/agent 0.1.113 → 0.1.115
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 +143 -51
- package/dist/acp-server/session-storage.d.ts +2 -0
- package/dist/acp-server/session-storage.js +24 -0
- package/dist/runner/langchain/index.js +27 -101
- package/dist/runner/langchain/model-factory.d.ts +1 -1
- package/dist/runner/langchain/model-factory.js +2 -2
- package/dist/runner/langchain/tools/e2b.js +2 -2
- package/dist/runner/langchain/tools/subagent-connections.js +0 -10
- package/dist/runner/langchain/tools/subagent.js +44 -0
- package/dist/runner/langchain/tools/web_search.js +4 -4
- 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") {
|
|
@@ -884,6 +879,8 @@ export class AgentAcpAdapter {
|
|
|
884
879
|
hasSubagentSessionId: !!block.subagentSessionId,
|
|
885
880
|
hasSubagentMessages: !!block.subagentMessages,
|
|
886
881
|
subagentMessagesCount: block.subagentMessages?.length,
|
|
882
|
+
firstSubagentMessageContentLength: block.subagentMessages?.[0]?.content?.length,
|
|
883
|
+
firstSubagentMessageBlocksCount: block.subagentMessages?.[0]?.contentBlocks?.length,
|
|
887
884
|
blockMeta: block._meta,
|
|
888
885
|
replayMeta,
|
|
889
886
|
});
|
|
@@ -1075,7 +1072,7 @@ export class AgentAcpAdapter {
|
|
|
1075
1072
|
}
|
|
1076
1073
|
logger.info("User message received", {
|
|
1077
1074
|
sessionId: params.sessionId,
|
|
1078
|
-
|
|
1075
|
+
message: userMessageText,
|
|
1079
1076
|
noSession: this.noSession,
|
|
1080
1077
|
});
|
|
1081
1078
|
// Only store messages if session persistence is enabled
|
|
@@ -1134,12 +1131,40 @@ export class AgentAcpAdapter {
|
|
|
1134
1131
|
// Build ordered content blocks for the assistant response
|
|
1135
1132
|
const contentBlocks = [];
|
|
1136
1133
|
let pendingText = "";
|
|
1134
|
+
// Buffer for logging agent response in readable chunks
|
|
1135
|
+
let logBuffer = "";
|
|
1136
|
+
const flushLogBuffer = (force = false) => {
|
|
1137
|
+
if (logBuffer.length === 0)
|
|
1138
|
+
return;
|
|
1139
|
+
if (force) {
|
|
1140
|
+
// Flush everything
|
|
1141
|
+
logger.info("Agent response", {
|
|
1142
|
+
sessionId: params.sessionId,
|
|
1143
|
+
text: logBuffer,
|
|
1144
|
+
});
|
|
1145
|
+
logBuffer = "";
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
// Only flush complete lines (up to and including the last newline)
|
|
1149
|
+
const lastNewline = logBuffer.lastIndexOf("\n");
|
|
1150
|
+
if (lastNewline !== -1) {
|
|
1151
|
+
const completeLines = logBuffer.slice(0, lastNewline + 1);
|
|
1152
|
+
logBuffer = logBuffer.slice(lastNewline + 1);
|
|
1153
|
+
logger.info("Agent response", {
|
|
1154
|
+
sessionId: params.sessionId,
|
|
1155
|
+
text: completeLines,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1137
1160
|
// Helper function to flush pending text as a TextBlock
|
|
1138
1161
|
const flushPendingText = () => {
|
|
1139
1162
|
if (pendingText.length > 0) {
|
|
1140
1163
|
contentBlocks.push({ type: "text", text: pendingText });
|
|
1141
1164
|
pendingText = "";
|
|
1142
1165
|
}
|
|
1166
|
+
// Force flush any remaining log buffer when text block completes
|
|
1167
|
+
flushLogBuffer(true);
|
|
1143
1168
|
};
|
|
1144
1169
|
// Helper to save cancelled message to session
|
|
1145
1170
|
const saveCancelledMessage = async () => {
|
|
@@ -1332,6 +1357,11 @@ export class AgentAcpAdapter {
|
|
|
1332
1357
|
const content = msg.content;
|
|
1333
1358
|
if (content.type === "text" && typeof content.text === "string") {
|
|
1334
1359
|
pendingText += content.text;
|
|
1360
|
+
// Buffer for logging - flush on newlines
|
|
1361
|
+
logBuffer += content.text;
|
|
1362
|
+
if (logBuffer.includes("\n")) {
|
|
1363
|
+
flushLogBuffer();
|
|
1364
|
+
}
|
|
1335
1365
|
}
|
|
1336
1366
|
}
|
|
1337
1367
|
// Debug: log if this chunk has tokenUsage in _meta
|
|
@@ -1405,28 +1435,20 @@ export class AgentAcpAdapter {
|
|
|
1405
1435
|
if (toolCallMsg.rawInput) {
|
|
1406
1436
|
toolCall.rawInput = toolCallMsg.rawInput;
|
|
1407
1437
|
}
|
|
1438
|
+
// Log tool call start
|
|
1439
|
+
logger.info("Tool call started", {
|
|
1440
|
+
sessionId: params.sessionId,
|
|
1441
|
+
toolCallId: toolCall.id,
|
|
1442
|
+
tool: toolCall.title,
|
|
1443
|
+
prettyName: toolCall.prettyName,
|
|
1444
|
+
});
|
|
1408
1445
|
contentBlocks.push(toolCall);
|
|
1409
1446
|
}
|
|
1410
1447
|
// Handle tool_call_update - update existing ToolCallBlock
|
|
1411
1448
|
if ("sessionUpdate" in msg &&
|
|
1412
1449
|
msg.sessionUpdate === "tool_call_update") {
|
|
1413
1450
|
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
|
-
});
|
|
1423
1451
|
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
|
-
});
|
|
1430
1452
|
if (toolCallBlock) {
|
|
1431
1453
|
if (updateMsg.status) {
|
|
1432
1454
|
toolCallBlock.status =
|
|
@@ -1446,6 +1468,17 @@ export class AgentAcpAdapter {
|
|
|
1446
1468
|
if (toolCallBlock.status === "completed" ||
|
|
1447
1469
|
toolCallBlock.status === "failed") {
|
|
1448
1470
|
toolCallBlock.completedAt = Date.now();
|
|
1471
|
+
// Log tool call completion
|
|
1472
|
+
logger.info("Tool call completed", {
|
|
1473
|
+
sessionId: params.sessionId,
|
|
1474
|
+
toolCallId: updateMsg.toolCallId,
|
|
1475
|
+
tool: toolCallBlock.title,
|
|
1476
|
+
status: toolCallBlock.status,
|
|
1477
|
+
durationMs: toolCallBlock.startedAt
|
|
1478
|
+
? toolCallBlock.completedAt - toolCallBlock.startedAt
|
|
1479
|
+
: undefined,
|
|
1480
|
+
error: toolCallBlock.error,
|
|
1481
|
+
});
|
|
1449
1482
|
}
|
|
1450
1483
|
const meta = updateMsg._meta;
|
|
1451
1484
|
// Update batchId from _meta (comes from tool_call_update after preliminary tool_call)
|
|
@@ -1459,30 +1492,35 @@ export class AgentAcpAdapter {
|
|
|
1459
1492
|
toolCallBlock.subagentSessionId = meta.subagentSessionId;
|
|
1460
1493
|
}
|
|
1461
1494
|
if (meta?.subagentMessages) {
|
|
1462
|
-
logger.info("[SUBAGENT]
|
|
1495
|
+
logger.info("[SUBAGENT-ADAPTER] Updating toolCallBlock with subagent messages", {
|
|
1463
1496
|
sessionId: params.sessionId,
|
|
1464
|
-
toolCallId:
|
|
1465
|
-
|
|
1466
|
-
|
|
1497
|
+
toolCallId: toolCallBlock.id,
|
|
1498
|
+
messagesReceived: meta.subagentMessages.length,
|
|
1499
|
+
firstMessageContentLength: meta.subagentMessages[0]?.content?.length,
|
|
1500
|
+
firstMessageBlocksCount: meta.subagentMessages[0]?.contentBlocks?.length,
|
|
1501
|
+
existingMessagesCount: toolCallBlock.subagentMessages?.length || 0,
|
|
1467
1502
|
});
|
|
1468
1503
|
toolCallBlock.subagentMessages = meta.subagentMessages;
|
|
1469
|
-
logger.info("[SUBAGENT]
|
|
1504
|
+
logger.info("[SUBAGENT-ADAPTER] ToolCallBlock updated", {
|
|
1470
1505
|
sessionId: params.sessionId,
|
|
1471
|
-
toolCallId:
|
|
1472
|
-
|
|
1506
|
+
toolCallId: toolCallBlock.id,
|
|
1507
|
+
toolCallBlockMessagesCount: toolCallBlock.subagentMessages?.length,
|
|
1508
|
+
toolCallBlockFirstMessageLength: toolCallBlock.subagentMessages?.[0]?.content?.length,
|
|
1473
1509
|
});
|
|
1474
1510
|
}
|
|
1511
|
+
if (meta?.subagentCompleted !== undefined) {
|
|
1512
|
+
toolCallBlock.subagentCompleted = meta.subagentCompleted;
|
|
1513
|
+
}
|
|
1475
1514
|
}
|
|
1476
1515
|
// Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
|
|
1477
1516
|
if (updateMsg._meta) {
|
|
1478
|
-
|
|
1479
|
-
|
|
1517
|
+
const subagentCompletedValue = updateMsg._meta
|
|
1518
|
+
?.subagentCompleted;
|
|
1519
|
+
logger.info("[DEBUG] Adapter forwarding tool_call_update to client", {
|
|
1480
1520
|
toolCallId: updateMsg.toolCallId,
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
subagentMessageCount: updateMsg._meta?.subagentMessages
|
|
1485
|
-
?.length || 0,
|
|
1521
|
+
hasSubagentCompleted: subagentCompletedValue !== undefined,
|
|
1522
|
+
subagentCompleted: subagentCompletedValue,
|
|
1523
|
+
metaKeys: Object.keys(updateMsg._meta),
|
|
1486
1524
|
});
|
|
1487
1525
|
this.connection.sessionUpdate({
|
|
1488
1526
|
sessionId: params.sessionId,
|
|
@@ -1490,13 +1528,10 @@ export class AgentAcpAdapter {
|
|
|
1490
1528
|
sessionUpdate: "tool_call_update",
|
|
1491
1529
|
toolCallId: updateMsg.toolCallId,
|
|
1492
1530
|
status: updateMsg.status,
|
|
1531
|
+
subagentCompleted: subagentCompletedValue,
|
|
1493
1532
|
_meta: updateMsg._meta,
|
|
1494
1533
|
},
|
|
1495
1534
|
});
|
|
1496
|
-
logger.info("[SUBAGENT] Successfully forwarded to client", {
|
|
1497
|
-
sessionId: params.sessionId,
|
|
1498
|
-
toolCallId: updateMsg.toolCallId,
|
|
1499
|
-
});
|
|
1500
1535
|
}
|
|
1501
1536
|
}
|
|
1502
1537
|
// Handle tool_output - update ToolCallBlock with output content
|
|
@@ -1816,7 +1851,9 @@ export class AgentAcpAdapter {
|
|
|
1816
1851
|
// We store the raw output here for session persistence
|
|
1817
1852
|
// Create mid-turn context snapshot after tool completes
|
|
1818
1853
|
if (!this.noSession) {
|
|
1819
|
-
|
|
1854
|
+
// DON'T flush here - text should only be flushed when tool_call arrives or at end
|
|
1855
|
+
// Flushing here causes text that arrives between tool outputs to be lost
|
|
1856
|
+
// because the final message assembly replaces the entire message
|
|
1820
1857
|
// Update or create the partial assistant message in the messages array
|
|
1821
1858
|
const partialAssistantMessage = {
|
|
1822
1859
|
role: "assistant",
|
|
@@ -1983,6 +2020,13 @@ export class AgentAcpAdapter {
|
|
|
1983
2020
|
}
|
|
1984
2021
|
// Capture the return value (PromptResponse with tokenUsage)
|
|
1985
2022
|
_agentResponse = iterResult.value;
|
|
2023
|
+
logger.info("[MSG_ACCUMULATION] Generator loop completed", {
|
|
2024
|
+
sessionId: params.sessionId,
|
|
2025
|
+
pendingTextLength: pendingText.length,
|
|
2026
|
+
pendingTextPreview: pendingText.slice(0, 200),
|
|
2027
|
+
contentBlocksCount: contentBlocks.length,
|
|
2028
|
+
contentBlockTypes: contentBlocks.map((b) => b.type),
|
|
2029
|
+
});
|
|
1986
2030
|
// Flush any remaining pending text
|
|
1987
2031
|
flushPendingText();
|
|
1988
2032
|
}
|
|
@@ -2060,6 +2104,45 @@ export class AgentAcpAdapter {
|
|
|
2060
2104
|
// Store the complete assistant response in session messages
|
|
2061
2105
|
// Only store if session persistence is enabled
|
|
2062
2106
|
if (!this.noSession && contentBlocks.length > 0) {
|
|
2107
|
+
logger.info("[MSG_ACCUMULATION] Constructing final assistant message", {
|
|
2108
|
+
sessionId: params.sessionId,
|
|
2109
|
+
contentBlocksCount: contentBlocks.length,
|
|
2110
|
+
contentBlockTypes: contentBlocks.map((b) => b.type),
|
|
2111
|
+
contentBlockSummary: contentBlocks.map((b) => {
|
|
2112
|
+
if (b.type === "text") {
|
|
2113
|
+
return {
|
|
2114
|
+
type: "text",
|
|
2115
|
+
length: b.text.length,
|
|
2116
|
+
preview: b.text.slice(0, 100),
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
if (b.type === "tool_call") {
|
|
2120
|
+
return {
|
|
2121
|
+
type: "tool_call",
|
|
2122
|
+
id: b.id,
|
|
2123
|
+
title: b.title,
|
|
2124
|
+
status: b.status,
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
return { type: b.type };
|
|
2128
|
+
}),
|
|
2129
|
+
});
|
|
2130
|
+
// Debug: log contentBlocks before creating assistant message
|
|
2131
|
+
const subagentToolCalls = contentBlocks.filter((b) => b.type === "tool_call" && !!b.subagentMessages);
|
|
2132
|
+
if (subagentToolCalls.length > 0) {
|
|
2133
|
+
logger.info("[SUBAGENT-SESSION-SAVE] Creating assistant message with subagent tool calls", {
|
|
2134
|
+
sessionId: params.sessionId,
|
|
2135
|
+
totalContentBlocks: contentBlocks.length,
|
|
2136
|
+
subagentToolCallsCount: subagentToolCalls.length,
|
|
2137
|
+
subagentToolCallsDetails: subagentToolCalls.map((tc) => ({
|
|
2138
|
+
id: tc.id,
|
|
2139
|
+
title: tc.title,
|
|
2140
|
+
messagesCount: tc.subagentMessages?.length,
|
|
2141
|
+
firstMessageContentLength: tc.subagentMessages?.[0]?.content?.length,
|
|
2142
|
+
firstMessageBlocksCount: tc.subagentMessages?.[0]?.contentBlocks?.length,
|
|
2143
|
+
})),
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2063
2146
|
const assistantMessage = {
|
|
2064
2147
|
role: "assistant",
|
|
2065
2148
|
content: contentBlocks,
|
|
@@ -2069,10 +2152,19 @@ export class AgentAcpAdapter {
|
|
|
2069
2152
|
const lastMessage = session.messages[session.messages.length - 1];
|
|
2070
2153
|
if (lastMessage && lastMessage.role === "assistant") {
|
|
2071
2154
|
// Update the existing message instead of adding a duplicate
|
|
2155
|
+
logger.debug("[MSG_ACCUMULATION] Updating existing assistant message", {
|
|
2156
|
+
sessionId: params.sessionId,
|
|
2157
|
+
previousContentBlocks: lastMessage.content.length,
|
|
2158
|
+
newContentBlocks: contentBlocks.length,
|
|
2159
|
+
});
|
|
2072
2160
|
session.messages[session.messages.length - 1] = assistantMessage;
|
|
2073
2161
|
}
|
|
2074
2162
|
else {
|
|
2075
2163
|
// Add new message (no mid-turn updates occurred)
|
|
2164
|
+
logger.debug("[MSG_ACCUMULATION] Adding new assistant message", {
|
|
2165
|
+
sessionId: params.sessionId,
|
|
2166
|
+
messageIndex: session.messages.length,
|
|
2167
|
+
});
|
|
2076
2168
|
session.messages.push(assistantMessage);
|
|
2077
2169
|
}
|
|
2078
2170
|
// Create context snapshot based on previous context
|
|
@@ -79,6 +79,8 @@ export interface ToolCallBlock {
|
|
|
79
79
|
subagentSessionId?: string | undefined;
|
|
80
80
|
/** Stored sub-agent messages for replay */
|
|
81
81
|
subagentMessages?: SubagentMessage[] | undefined;
|
|
82
|
+
/** Whether the sub-agent has completed */
|
|
83
|
+
subagentCompleted?: boolean | undefined;
|
|
82
84
|
}
|
|
83
85
|
export type ContentBlock = TextBlock | ImageBlock | ToolCallBlock;
|
|
84
86
|
/**
|
|
@@ -78,6 +78,7 @@ const toolCallBlockSchema = z.object({
|
|
|
78
78
|
subagentPort: z.number().optional(),
|
|
79
79
|
subagentSessionId: z.string().optional(),
|
|
80
80
|
subagentMessages: z.array(subagentMessageSchema).optional(),
|
|
81
|
+
subagentCompleted: z.boolean().optional(),
|
|
81
82
|
_meta: z
|
|
82
83
|
.object({
|
|
83
84
|
truncationWarning: z.string().optional(),
|
|
@@ -183,6 +184,29 @@ export class SessionStorage {
|
|
|
183
184
|
* Uses atomic write (write to temp file, then rename)
|
|
184
185
|
*/
|
|
185
186
|
async saveSession(sessionId, messages, context) {
|
|
187
|
+
// Debug: log subagent data being saved
|
|
188
|
+
const messagesWithSubagents = messages.filter((msg) => msg.content.some((block) => block.type === "tool_call" &&
|
|
189
|
+
"subagentMessages" in block &&
|
|
190
|
+
block.subagentMessages));
|
|
191
|
+
if (messagesWithSubagents.length > 0) {
|
|
192
|
+
console.log("[SUBAGENT-STORAGE] Saving session with subagent messages", {
|
|
193
|
+
sessionId,
|
|
194
|
+
totalMessages: messages.length,
|
|
195
|
+
messagesWithSubagents: messagesWithSubagents.length,
|
|
196
|
+
subagentDetails: messagesWithSubagents.map((msg) => ({
|
|
197
|
+
role: msg.role,
|
|
198
|
+
toolCallsWithSubagents: msg.content
|
|
199
|
+
.filter((block) => block.type === "tool_call" &&
|
|
200
|
+
!!("subagentMessages" in block && block.subagentMessages))
|
|
201
|
+
.map((tc) => ({
|
|
202
|
+
id: tc.id,
|
|
203
|
+
title: tc.title,
|
|
204
|
+
messagesCount: tc.subagentMessages?.length,
|
|
205
|
+
firstMessageContentLength: tc.subagentMessages?.[0]?.content?.length,
|
|
206
|
+
})),
|
|
207
|
+
})),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
186
210
|
this.ensureSessionDir(sessionId);
|
|
187
211
|
const sessionPath = this.getSessionPath(sessionId);
|
|
188
212
|
const tempPath = `${sessionPath}.tmp`;
|
|
@@ -3,7 +3,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
5
5
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
6
|
-
import {
|
|
6
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
7
7
|
import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { ContextOverflowError, SUBAGENT_MODE_KEY, } from "../../acp-server/adapter";
|
|
@@ -160,62 +160,29 @@ export class LangchainAgent {
|
|
|
160
160
|
// Listen for subagent messages events (for live streaming)
|
|
161
161
|
// Use the invocation-scoped EventEmitter to ensure messages route correctly
|
|
162
162
|
const onSubagentMessages = (event) => {
|
|
163
|
-
_logger.info("✓ Received subagent messages event from scoped emitter", {
|
|
164
|
-
toolCallId: event.toolCallId,
|
|
165
|
-
messageCount: event.messages.length,
|
|
166
|
-
completed: event.completed,
|
|
167
|
-
invocationId: subagentInvCtx.invocationId,
|
|
168
|
-
sessionId: req.sessionId,
|
|
169
|
-
});
|
|
170
163
|
// Track completion
|
|
171
164
|
if (event.completed) {
|
|
172
165
|
completedSubagentToolCalls.add(event.toolCallId);
|
|
173
|
-
_logger.info("✓ Subagent stream completed", {
|
|
174
|
-
toolCallId: event.toolCallId,
|
|
175
|
-
sessionId: req.sessionId,
|
|
176
|
-
totalCompleted: completedSubagentToolCalls.size,
|
|
177
|
-
});
|
|
178
166
|
}
|
|
179
167
|
subagentMessagesQueue.push(event);
|
|
180
168
|
};
|
|
181
169
|
subagentInvCtx.subagentEventEmitter.on("messages", onSubagentMessages);
|
|
182
170
|
// Helper to check and yield all pending subagent message updates
|
|
183
171
|
async function* yieldPendingSubagentUpdates() {
|
|
184
|
-
_logger.info("[SUBAGENT] yieldPendingSubagentUpdates called", {
|
|
185
|
-
sessionId: req.sessionId,
|
|
186
|
-
queueLength: subagentMessagesQueue.length,
|
|
187
|
-
});
|
|
188
172
|
while (subagentMessagesQueue.length > 0) {
|
|
189
173
|
const messagesUpdate = subagentMessagesQueue.shift();
|
|
190
174
|
if (!messagesUpdate)
|
|
191
175
|
continue;
|
|
192
|
-
_logger.info("[SUBAGENT] Yielding queued subagent messages update", {
|
|
193
|
-
sessionId: req.sessionId,
|
|
194
|
-
toolCallId: messagesUpdate.toolCallId,
|
|
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,
|
|
199
|
-
});
|
|
200
176
|
const updateToYield = {
|
|
201
177
|
sessionUpdate: "tool_call_update",
|
|
202
178
|
toolCallId: messagesUpdate.toolCallId,
|
|
203
179
|
_meta: {
|
|
204
180
|
messageId: req.messageId,
|
|
205
181
|
subagentMessages: messagesUpdate.messages,
|
|
182
|
+
subagentCompleted: messagesUpdate.completed,
|
|
206
183
|
},
|
|
207
184
|
};
|
|
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
185
|
yield updateToYield;
|
|
215
|
-
_logger.info("[SUBAGENT] Successfully yielded update", {
|
|
216
|
-
sessionId: req.sessionId,
|
|
217
|
-
toolCallId: messagesUpdate.toolCallId,
|
|
218
|
-
});
|
|
219
186
|
}
|
|
220
187
|
}
|
|
221
188
|
// Add agent.session_id as a base attribute so it propagates to all child spans
|
|
@@ -643,7 +610,7 @@ export class LangchainAgent {
|
|
|
643
610
|
// - "gemini-2.0-flash" → Google Generative AI
|
|
644
611
|
// - "vertex-gemini-2.0-flash" → Vertex AI (strips prefix)
|
|
645
612
|
// - "claude-sonnet-4-5-20250929" → Anthropic
|
|
646
|
-
const model = createModelFromString(effectiveModel);
|
|
613
|
+
const model = await createModelFromString(effectiveModel);
|
|
647
614
|
const agentConfig = {
|
|
648
615
|
model,
|
|
649
616
|
tools: finalTools,
|
|
@@ -857,17 +824,11 @@ export class LangchainAgent {
|
|
|
857
824
|
}
|
|
858
825
|
// Create the stream within the invocation context so AsyncLocalStorage
|
|
859
826
|
// propagates the context to all tool executions and callbacks
|
|
860
|
-
_logger.info("Starting agent.stream", {
|
|
861
|
-
messageCount: messages.length,
|
|
862
|
-
effectiveModel,
|
|
863
|
-
sessionId: req.sessionId,
|
|
864
|
-
});
|
|
865
827
|
const stream = context.with(invocationContext, () => agent.stream({ messages }, {
|
|
866
828
|
streamMode: ["updates", "messages"],
|
|
867
829
|
recursionLimit: 200,
|
|
868
830
|
callbacks: [otelCallbacks],
|
|
869
831
|
}));
|
|
870
|
-
_logger.info("agent.stream created, starting iteration");
|
|
871
832
|
// Merge the LangChain stream with subagent event stream
|
|
872
833
|
// This allows both to yield concurrently without polling
|
|
873
834
|
async function* mergeStreams() {
|
|
@@ -887,16 +848,20 @@ export class LangchainAgent {
|
|
|
887
848
|
}
|
|
888
849
|
// Start listening for subagent events
|
|
889
850
|
let subagentPromise = createSubagentEventPromise();
|
|
851
|
+
// Create the first stream promise
|
|
852
|
+
let streamPromise = streamIterator.next().then((result) => ({
|
|
853
|
+
source: "stream",
|
|
854
|
+
value: result.value,
|
|
855
|
+
done: result.done ?? false,
|
|
856
|
+
}));
|
|
890
857
|
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
858
|
const result = await Promise.race([streamPromise, subagentPromise]);
|
|
898
859
|
if (result.source === "stream") {
|
|
899
860
|
if (result.done) {
|
|
861
|
+
// Yield the final value if it exists (don't skip the last message)
|
|
862
|
+
if (result.value !== undefined) {
|
|
863
|
+
yield { source: "stream", value: result.value };
|
|
864
|
+
}
|
|
900
865
|
streamDone = true;
|
|
901
866
|
// Continue to drain remaining subagent events
|
|
902
867
|
subagentListenerActive = false;
|
|
@@ -907,6 +872,12 @@ export class LangchainAgent {
|
|
|
907
872
|
break;
|
|
908
873
|
}
|
|
909
874
|
yield { source: "stream", value: result.value };
|
|
875
|
+
// Create next stream promise after processing this one
|
|
876
|
+
streamPromise = streamIterator.next().then((result) => ({
|
|
877
|
+
source: "stream",
|
|
878
|
+
value: result.value,
|
|
879
|
+
done: result.done ?? false,
|
|
880
|
+
}));
|
|
910
881
|
}
|
|
911
882
|
else if (result.source === "subagent") {
|
|
912
883
|
// Subagent event arrived - it's already in the queue
|
|
@@ -917,6 +888,7 @@ export class LangchainAgent {
|
|
|
917
888
|
}
|
|
918
889
|
}
|
|
919
890
|
// Iterate through the merged stream
|
|
891
|
+
let messageCount = 0;
|
|
920
892
|
for await (const item of mergeStreams()) {
|
|
921
893
|
if (item.source === "subagent") {
|
|
922
894
|
// Yield any queued subagent messages
|
|
@@ -925,6 +897,7 @@ export class LangchainAgent {
|
|
|
925
897
|
}
|
|
926
898
|
// Process the stream item
|
|
927
899
|
const streamItem = item.value;
|
|
900
|
+
messageCount++;
|
|
928
901
|
// biome-ignore lint/suspicious/noExplicitAny: LangChain stream items are tuples with dynamic types
|
|
929
902
|
const [streamMode, chunk] = streamItem;
|
|
930
903
|
if (streamMode === "updates") {
|
|
@@ -1028,11 +1001,10 @@ export class LangchainAgent {
|
|
|
1028
1001
|
const qHash = hashQuery(toolCall.args.query);
|
|
1029
1002
|
queryToToolCallId.set(qHash, toolCall.id);
|
|
1030
1003
|
activeSubagentToolCalls.add(toolCall.id);
|
|
1031
|
-
telemetry.log("info", "
|
|
1032
|
-
queryHash: qHash,
|
|
1004
|
+
telemetry.log("info", "Subagent invoked", {
|
|
1033
1005
|
toolCallId: toolCall.id,
|
|
1034
|
-
|
|
1035
|
-
|
|
1006
|
+
agentName: agentName,
|
|
1007
|
+
sessionId: req.sessionId,
|
|
1036
1008
|
});
|
|
1037
1009
|
}
|
|
1038
1010
|
else {
|
|
@@ -1118,30 +1090,12 @@ export class LangchainAgent {
|
|
|
1118
1090
|
turnTokenUsage.totalTokens += messageTokenUsage.totalTokens ?? 0;
|
|
1119
1091
|
countedMessageIds.add(aiMessage.id);
|
|
1120
1092
|
}
|
|
1121
|
-
if (messageTokenUsage) {
|
|
1122
|
-
const contentType = typeof aiMessage.content;
|
|
1123
|
-
const contentIsArray = Array.isArray(aiMessage.content);
|
|
1124
|
-
const contentLength = contentIsArray
|
|
1125
|
-
? aiMessage.content.length
|
|
1126
|
-
: typeof aiMessage.content === "string"
|
|
1127
|
-
? aiMessage.content.length
|
|
1128
|
-
: -1;
|
|
1129
|
-
_logger.debug("messageTokenUsage", {
|
|
1130
|
-
messageTokenUsage,
|
|
1131
|
-
contentType,
|
|
1132
|
-
isArray: contentIsArray,
|
|
1133
|
-
length: contentLength,
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
1093
|
// If we have tokenUsage but no content, send a token-only chunk
|
|
1137
1094
|
if (messageTokenUsage &&
|
|
1138
1095
|
(typeof aiMessage.content === "string"
|
|
1139
1096
|
? aiMessage.content === ""
|
|
1140
1097
|
: Array.isArray(aiMessage.content) &&
|
|
1141
1098
|
aiMessage.content.length === 0)) {
|
|
1142
|
-
_logger.debug("sending token-only chunk", {
|
|
1143
|
-
messageTokenUsage,
|
|
1144
|
-
});
|
|
1145
1099
|
const msgToYield = {
|
|
1146
1100
|
sessionUpdate: "agent_message_chunk",
|
|
1147
1101
|
content: {
|
|
@@ -1330,56 +1284,28 @@ export class LangchainAgent {
|
|
|
1330
1284
|
yield* yieldPendingSubagentUpdates();
|
|
1331
1285
|
// Keep polling for subagent messages after stream ends
|
|
1332
1286
|
// 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
1287
|
const checkInterval = 100; // Check every 100ms
|
|
1338
1288
|
const maxIdleTime = 5000; // Stop if no messages for 5 seconds (fallback)
|
|
1339
1289
|
const maxWaitTime = 300000; // Absolute max 5 minutes
|
|
1340
1290
|
const startTime = Date.now();
|
|
1341
1291
|
let lastMessageTime = Date.now();
|
|
1292
|
+
let iterations = 0;
|
|
1342
1293
|
while (Date.now() - startTime < maxWaitTime) {
|
|
1343
|
-
|
|
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
|
-
}
|
|
1294
|
+
iterations++;
|
|
1353
1295
|
// Check if there are pending messages
|
|
1354
1296
|
if (subagentMessagesQueue.length > 0) {
|
|
1355
|
-
_logger.info("[SUBAGENT] Found pending messages, yielding", {
|
|
1356
|
-
sessionId: req.sessionId,
|
|
1357
|
-
queueLength: subagentMessagesQueue.length,
|
|
1358
|
-
});
|
|
1359
1297
|
yield* yieldPendingSubagentUpdates();
|
|
1360
1298
|
lastMessageTime = Date.now();
|
|
1361
1299
|
}
|
|
1362
1300
|
// Stop if we haven't seen messages for maxIdleTime (fallback for no subagents)
|
|
1363
1301
|
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
1302
|
break;
|
|
1369
1303
|
}
|
|
1370
1304
|
// Wait a bit before checking again
|
|
1371
1305
|
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1372
1306
|
}
|
|
1373
|
-
if (Date.now() - startTime >= maxWaitTime) {
|
|
1374
|
-
_logger.warn("[SUBAGENT] Timeout waiting for subagents", {
|
|
1375
|
-
sessionId: req.sessionId,
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
1307
|
// Final yield of any remaining messages
|
|
1379
1308
|
yield* yieldPendingSubagentUpdates();
|
|
1380
|
-
_logger.info("[SUBAGENT] Finished post-stream polling", {
|
|
1381
|
-
sessionId: req.sessionId,
|
|
1382
|
-
});
|
|
1383
1309
|
// Now that content streaming is complete, yield all buffered tool call notifications
|
|
1384
1310
|
yield* flushPendingToolCalls();
|
|
1385
1311
|
// Clean up subagent event listener from invocation-scoped emitter
|
|
@@ -1435,7 +1361,7 @@ const makeMcpToolsClient = async (mcpConfigs) => {
|
|
|
1435
1361
|
const mcpServers = await Promise.all((mcpConfigs ?? []).map(async (config) => {
|
|
1436
1362
|
if (typeof config === "string") {
|
|
1437
1363
|
// String configs use the centralized MCP proxy with auth
|
|
1438
|
-
const shedAuth = await
|
|
1364
|
+
const shedAuth = await getShedAuth();
|
|
1439
1365
|
if (!shedAuth) {
|
|
1440
1366
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use cloud MCP servers.");
|
|
1441
1367
|
}
|
|
@@ -15,7 +15,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
|
|
|
15
15
|
* - Provider prefix: "google_vertexai:gemini-2.0-flash", "google_genai:gemini-2.0-flash", "anthropic:claude-3-5-sonnet"
|
|
16
16
|
* - Proxied: "town-claude-sonnet-4-5-20250929" (uses TOWN_SHED_URL or defaults to localhost:3000)
|
|
17
17
|
*/
|
|
18
|
-
export declare function createModelFromString(modelString: string): BaseChatModel
|
|
18
|
+
export declare function createModelFromString(modelString: string): Promise<BaseChatModel>;
|
|
19
19
|
/**
|
|
20
20
|
* Helper function to detect if a model string is for a specific provider
|
|
21
21
|
*/
|
|
@@ -22,11 +22,11 @@ const logger = createLogger("model-factory");
|
|
|
22
22
|
* - Provider prefix: "google_vertexai:gemini-2.0-flash", "google_genai:gemini-2.0-flash", "anthropic:claude-3-5-sonnet"
|
|
23
23
|
* - Proxied: "town-claude-sonnet-4-5-20250929" (uses TOWN_SHED_URL or defaults to localhost:3000)
|
|
24
24
|
*/
|
|
25
|
-
export function createModelFromString(modelString) {
|
|
25
|
+
export async function createModelFromString(modelString) {
|
|
26
26
|
// Check for town- prefix for proxied models via shed
|
|
27
27
|
if (modelString.startsWith("town-")) {
|
|
28
28
|
const actualModel = modelString.slice(5); // strip "town-"
|
|
29
|
-
const shedAuth = getShedAuth();
|
|
29
|
+
const shedAuth = await getShedAuth();
|
|
30
30
|
if (!shedAuth) {
|
|
31
31
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY.");
|
|
32
32
|
}
|