@townco/agent 0.1.111 → 0.1.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-server/adapter.js +83 -20
- package/dist/acp-server/http.js +21 -0
- package/dist/runner/langchain/index.js +179 -116
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +474 -0
- package/dist/runner/langchain/tools/conversation_search.d.ts +22 -0
- package/dist/runner/langchain/tools/conversation_search.js +137 -0
- package/dist/runner/langchain/tools/generate_image.d.ts +47 -0
- package/dist/runner/langchain/tools/generate_image.js +175 -0
- 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
|
}
|
|
@@ -2111,6 +2124,56 @@ export class AgentAcpAdapter {
|
|
|
2111
2124
|
},
|
|
2112
2125
|
});
|
|
2113
2126
|
}
|
|
2127
|
+
// Proactive compaction based on actual LLM token usage
|
|
2128
|
+
// If actual input tokens from LLM exceed threshold, compact now to prevent overflow on next turn
|
|
2129
|
+
const PROACTIVE_COMPACTION_THRESHOLD = 70; // Trigger at 70% of context window
|
|
2130
|
+
const modelContextWindow = getModelContextWindow(this.agent.definition.model);
|
|
2131
|
+
if (turnTokenUsage.inputTokens > 0 && !this.noSession) {
|
|
2132
|
+
const actualPercentage = (turnTokenUsage.inputTokens / modelContextWindow) * 100;
|
|
2133
|
+
const latestContext = session.context.length > 0
|
|
2134
|
+
? session.context[session.context.length - 1]
|
|
2135
|
+
: undefined;
|
|
2136
|
+
const estimatedTokens = latestContext?.context_size.totalEstimated ?? 0;
|
|
2137
|
+
// Log the difference between estimated and actual tokens
|
|
2138
|
+
const tokenDifference = turnTokenUsage.inputTokens - estimatedTokens;
|
|
2139
|
+
const differencePercentage = estimatedTokens > 0
|
|
2140
|
+
? ((tokenDifference / estimatedTokens) * 100).toFixed(1)
|
|
2141
|
+
: "N/A";
|
|
2142
|
+
logger.info("Token usage comparison (actual vs estimated)", {
|
|
2143
|
+
sessionId: params.sessionId,
|
|
2144
|
+
actualInputTokens: turnTokenUsage.inputTokens,
|
|
2145
|
+
estimatedTokens,
|
|
2146
|
+
tokenDifference,
|
|
2147
|
+
differencePercentage: `${differencePercentage}%`,
|
|
2148
|
+
actualPercentage: `${actualPercentage.toFixed(1)}%`,
|
|
2149
|
+
modelContextWindow,
|
|
2150
|
+
threshold: `${PROACTIVE_COMPACTION_THRESHOLD}%`,
|
|
2151
|
+
});
|
|
2152
|
+
// Proactively compact if actual tokens exceed threshold
|
|
2153
|
+
if (actualPercentage >= PROACTIVE_COMPACTION_THRESHOLD) {
|
|
2154
|
+
logger.warn("Actual token usage exceeds threshold - proactively compacting for next turn", {
|
|
2155
|
+
sessionId: params.sessionId,
|
|
2156
|
+
actualPercentage: `${actualPercentage.toFixed(1)}%`,
|
|
2157
|
+
threshold: `${PROACTIVE_COMPACTION_THRESHOLD}%`,
|
|
2158
|
+
actualTokens: turnTokenUsage.inputTokens,
|
|
2159
|
+
modelContextWindow,
|
|
2160
|
+
});
|
|
2161
|
+
// Force compaction to reduce context for next turn
|
|
2162
|
+
const compactionResult = await this.forceCompaction(session, params.sessionId);
|
|
2163
|
+
if (compactionResult.success) {
|
|
2164
|
+
logger.info("Proactive compaction succeeded", {
|
|
2165
|
+
sessionId: params.sessionId,
|
|
2166
|
+
});
|
|
2167
|
+
// Save the updated session with new context entry
|
|
2168
|
+
await this.saveSessionToDisk(params.sessionId, session);
|
|
2169
|
+
}
|
|
2170
|
+
else {
|
|
2171
|
+
logger.warn("Proactive compaction failed - continuing without compaction", {
|
|
2172
|
+
sessionId: params.sessionId,
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2114
2177
|
session.pendingPrompt = null;
|
|
2115
2178
|
return {
|
|
2116
2179
|
stopReason: "end_turn",
|
|
@@ -2118,8 +2181,8 @@ export class AgentAcpAdapter {
|
|
|
2118
2181
|
}
|
|
2119
2182
|
/**
|
|
2120
2183
|
* Force compaction of the session context.
|
|
2121
|
-
* Used for error recovery when the Claude API returns "prompt is too long"
|
|
2122
|
-
*
|
|
2184
|
+
* Used for error recovery when the Claude API returns "prompt is too long",
|
|
2185
|
+
* and for proactive compaction when actual token usage exceeds threshold.
|
|
2123
2186
|
*/
|
|
2124
2187
|
async forceCompaction(session, sessionId) {
|
|
2125
2188
|
logger.info("Force compaction started", {
|
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
|
}
|
|
@@ -1225,9 +1241,10 @@ export class LangchainAgent {
|
|
|
1225
1241
|
continue;
|
|
1226
1242
|
}
|
|
1227
1243
|
// Check if the tool execution failed
|
|
1228
|
-
// LangChain may set status: "error" OR the content
|
|
1244
|
+
// LangChain may set status: "error" OR the content looks like an error message
|
|
1245
|
+
// Matches: "Error...", "Error:...", "SomethingError:..." (e.g. RateLimitError:, TypeError:)
|
|
1229
1246
|
const contentLooksLikeError = typeof aiMessage.content === "string" &&
|
|
1230
|
-
aiMessage.content.trim()
|
|
1247
|
+
/^(\w*Error:|Error\b)/i.test(aiMessage.content.trim());
|
|
1231
1248
|
const isError = aiMessage.status === "error" || contentLooksLikeError;
|
|
1232
1249
|
const status = isError ? "failed" : "completed";
|
|
1233
1250
|
telemetry.log(isError ? "error" : "info", `Tool call ${status}`, {
|
|
@@ -1311,15 +1328,62 @@ export class LangchainAgent {
|
|
|
1311
1328
|
}
|
|
1312
1329
|
// Yield any remaining pending subagent connection updates after stream ends
|
|
1313
1330
|
yield* yieldPendingSubagentUpdates();
|
|
1331
|
+
// Keep polling for subagent messages after stream ends
|
|
1332
|
+
// This ensures we capture messages that arrive after LangChain stream completes
|
|
1333
|
+
_logger.info("[SUBAGENT] Starting post-stream polling for subagent messages", {
|
|
1334
|
+
sessionId: req.sessionId,
|
|
1335
|
+
activeSubagentCount: activeSubagentToolCalls.size,
|
|
1336
|
+
});
|
|
1337
|
+
const checkInterval = 100; // Check every 100ms
|
|
1338
|
+
const maxIdleTime = 5000; // Stop if no messages for 5 seconds (fallback)
|
|
1339
|
+
const maxWaitTime = 300000; // Absolute max 5 minutes
|
|
1340
|
+
const startTime = Date.now();
|
|
1341
|
+
let lastMessageTime = Date.now();
|
|
1342
|
+
while (Date.now() - startTime < maxWaitTime) {
|
|
1343
|
+
// Check if all subagent tool calls have completed
|
|
1344
|
+
if (activeSubagentToolCalls.size > 0 &&
|
|
1345
|
+
completedSubagentToolCalls.size >= activeSubagentToolCalls.size) {
|
|
1346
|
+
_logger.info("[SUBAGENT] All subagent tool calls completed", {
|
|
1347
|
+
sessionId: req.sessionId,
|
|
1348
|
+
totalSubagents: activeSubagentToolCalls.size,
|
|
1349
|
+
completedCount: completedSubagentToolCalls.size,
|
|
1350
|
+
});
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
// Check if there are pending messages
|
|
1354
|
+
if (subagentMessagesQueue.length > 0) {
|
|
1355
|
+
_logger.info("[SUBAGENT] Found pending messages, yielding", {
|
|
1356
|
+
sessionId: req.sessionId,
|
|
1357
|
+
queueLength: subagentMessagesQueue.length,
|
|
1358
|
+
});
|
|
1359
|
+
yield* yieldPendingSubagentUpdates();
|
|
1360
|
+
lastMessageTime = Date.now();
|
|
1361
|
+
}
|
|
1362
|
+
// Stop if we haven't seen messages for maxIdleTime (fallback for no subagents)
|
|
1363
|
+
if (Date.now() - lastMessageTime > maxIdleTime) {
|
|
1364
|
+
_logger.info("[SUBAGENT] No new messages for 5 seconds, finishing", {
|
|
1365
|
+
sessionId: req.sessionId,
|
|
1366
|
+
hasActiveSubagents: activeSubagentToolCalls.size > 0,
|
|
1367
|
+
});
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
// Wait a bit before checking again
|
|
1371
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1372
|
+
}
|
|
1373
|
+
if (Date.now() - startTime >= maxWaitTime) {
|
|
1374
|
+
_logger.warn("[SUBAGENT] Timeout waiting for subagents", {
|
|
1375
|
+
sessionId: req.sessionId,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
// Final yield of any remaining messages
|
|
1379
|
+
yield* yieldPendingSubagentUpdates();
|
|
1380
|
+
_logger.info("[SUBAGENT] Finished post-stream polling", {
|
|
1381
|
+
sessionId: req.sessionId,
|
|
1382
|
+
});
|
|
1314
1383
|
// Now that content streaming is complete, yield all buffered tool call notifications
|
|
1315
1384
|
yield* flushPendingToolCalls();
|
|
1316
|
-
// Clean up subagent event
|
|
1317
|
-
|
|
1318
|
-
subagentEvents.off("messages", onSubagentMessages);
|
|
1319
|
-
// Cancel any pending wait
|
|
1320
|
-
if (subagentUpdateResolver) {
|
|
1321
|
-
subagentUpdateResolver = null;
|
|
1322
|
-
}
|
|
1385
|
+
// Clean up subagent event listener from invocation-scoped emitter
|
|
1386
|
+
subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
|
|
1323
1387
|
// Clean up any remaining iteration span
|
|
1324
1388
|
otelCallbacks?.cleanup();
|
|
1325
1389
|
// Log successful completion
|
|
@@ -1335,9 +1399,8 @@ export class LangchainAgent {
|
|
|
1335
1399
|
};
|
|
1336
1400
|
}
|
|
1337
1401
|
catch (error) {
|
|
1338
|
-
// Clean up subagent event
|
|
1339
|
-
|
|
1340
|
-
subagentEvents.off("messages", onSubagentMessages);
|
|
1402
|
+
// Clean up subagent event listener on error from invocation-scoped emitter
|
|
1403
|
+
subagentInvCtx.subagentEventEmitter.off("messages", onSubagentMessages);
|
|
1341
1404
|
// Clean up any remaining iteration span
|
|
1342
1405
|
otelCallbacks?.cleanup();
|
|
1343
1406
|
// Check if this is a context overflow error - wrap it for retry handling
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Storage-backed artifacts tool for agent backend
|
|
3
|
+
*
|
|
4
|
+
* Provides file storage capabilities using Supabase Storage with the following operations:
|
|
5
|
+
* - artifacts_cp: Copy files to/from Supabase Storage
|
|
6
|
+
* - artifacts_del: Delete files from Supabase Storage
|
|
7
|
+
* - artifacts_ls: List files in Supabase Storage
|
|
8
|
+
* - artifacts_url: Generate signed URLs
|
|
9
|
+
*
|
|
10
|
+
* Storage keys are scoped by: <deploying_user>/<agent_name>/<session_id>/<file_path>
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
/**
|
|
14
|
+
* Factory function to create the artifacts tools
|
|
15
|
+
* Returns an array of all four artifact management tools
|
|
16
|
+
*/
|
|
17
|
+
export declare function makeArtifactsTools(): (import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
18
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
19
|
+
source: z.ZodString;
|
|
20
|
+
destination: z.ZodString;
|
|
21
|
+
direction: z.ZodEnum<{
|
|
22
|
+
download: "download";
|
|
23
|
+
upload: "upload";
|
|
24
|
+
}>;
|
|
25
|
+
}, z.core.$strip>, {
|
|
26
|
+
session_id: string;
|
|
27
|
+
source: string;
|
|
28
|
+
destination: string;
|
|
29
|
+
direction: "download" | "upload";
|
|
30
|
+
}, {
|
|
31
|
+
session_id?: string | undefined;
|
|
32
|
+
source: string;
|
|
33
|
+
destination: string;
|
|
34
|
+
direction: "download" | "upload";
|
|
35
|
+
}, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
36
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
37
|
+
path: z.ZodString;
|
|
38
|
+
}, z.core.$strip>, {
|
|
39
|
+
session_id: string;
|
|
40
|
+
path: string;
|
|
41
|
+
}, {
|
|
42
|
+
session_id?: string | undefined;
|
|
43
|
+
path: string;
|
|
44
|
+
}, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
45
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
46
|
+
path: z.ZodOptional<z.ZodString>;
|
|
47
|
+
recursive: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
}, z.core.$strip>, {
|
|
49
|
+
session_id: string;
|
|
50
|
+
path?: string;
|
|
51
|
+
recursive?: boolean;
|
|
52
|
+
}, {
|
|
53
|
+
session_id?: string | undefined;
|
|
54
|
+
path?: string | undefined;
|
|
55
|
+
recursive?: boolean | undefined;
|
|
56
|
+
}, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
57
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
58
|
+
path: z.ZodString;
|
|
59
|
+
expires_in: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
}, z.core.$strip>, {
|
|
61
|
+
session_id: string;
|
|
62
|
+
path: string;
|
|
63
|
+
expires_in?: number;
|
|
64
|
+
}, {
|
|
65
|
+
session_id?: string | undefined;
|
|
66
|
+
path: string;
|
|
67
|
+
expires_in?: number | undefined;
|
|
68
|
+
}, string>)[];
|