@townco/agent 0.1.114 → 0.1.116
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 +11 -1
- 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 -64
- 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
|
@@ -879,6 +879,8 @@ export class AgentAcpAdapter {
|
|
|
879
879
|
hasSubagentSessionId: !!block.subagentSessionId,
|
|
880
880
|
hasSubagentMessages: !!block.subagentMessages,
|
|
881
881
|
subagentMessagesCount: block.subagentMessages?.length,
|
|
882
|
+
firstSubagentMessageContentLength: block.subagentMessages?.[0]?.content?.length,
|
|
883
|
+
firstSubagentMessageBlocksCount: block.subagentMessages?.[0]?.contentBlocks?.length,
|
|
882
884
|
blockMeta: block._meta,
|
|
883
885
|
replayMeta,
|
|
884
886
|
});
|
|
@@ -1492,15 +1494,21 @@ export class AgentAcpAdapter {
|
|
|
1492
1494
|
if (meta?.subagentMessages) {
|
|
1493
1495
|
toolCallBlock.subagentMessages = meta.subagentMessages;
|
|
1494
1496
|
}
|
|
1497
|
+
if (meta?.subagentCompleted !== undefined) {
|
|
1498
|
+
toolCallBlock.subagentCompleted = meta.subagentCompleted;
|
|
1499
|
+
}
|
|
1495
1500
|
}
|
|
1496
1501
|
// Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
|
|
1497
1502
|
if (updateMsg._meta) {
|
|
1503
|
+
const subagentCompletedValue = updateMsg._meta
|
|
1504
|
+
?.subagentCompleted;
|
|
1498
1505
|
this.connection.sessionUpdate({
|
|
1499
1506
|
sessionId: params.sessionId,
|
|
1500
1507
|
update: {
|
|
1501
1508
|
sessionUpdate: "tool_call_update",
|
|
1502
1509
|
toolCallId: updateMsg.toolCallId,
|
|
1503
1510
|
status: updateMsg.status,
|
|
1511
|
+
subagentCompleted: subagentCompletedValue,
|
|
1504
1512
|
_meta: updateMsg._meta,
|
|
1505
1513
|
},
|
|
1506
1514
|
});
|
|
@@ -1823,7 +1831,9 @@ export class AgentAcpAdapter {
|
|
|
1823
1831
|
// We store the raw output here for session persistence
|
|
1824
1832
|
// Create mid-turn context snapshot after tool completes
|
|
1825
1833
|
if (!this.noSession) {
|
|
1826
|
-
|
|
1834
|
+
// DON'T flush here - text should only be flushed when tool_call arrives or at end
|
|
1835
|
+
// Flushing here causes text that arrives between tool outputs to be lost
|
|
1836
|
+
// because the final message assembly replaces the entire message
|
|
1827
1837
|
// Update or create the partial assistant message in the messages array
|
|
1828
1838
|
const partialAssistantMessage = {
|
|
1829
1839
|
role: "assistant",
|
|
@@ -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,21 +160,9 @@ 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
|
};
|
|
@@ -185,17 +173,13 @@ export class LangchainAgent {
|
|
|
185
173
|
const messagesUpdate = subagentMessagesQueue.shift();
|
|
186
174
|
if (!messagesUpdate)
|
|
187
175
|
continue;
|
|
188
|
-
_logger.debug("[SUBAGENT] Yielding queued subagent messages update", {
|
|
189
|
-
sessionId: req.sessionId,
|
|
190
|
-
toolCallId: messagesUpdate.toolCallId,
|
|
191
|
-
messageCount: messagesUpdate.messages.length,
|
|
192
|
-
});
|
|
193
176
|
const updateToYield = {
|
|
194
177
|
sessionUpdate: "tool_call_update",
|
|
195
178
|
toolCallId: messagesUpdate.toolCallId,
|
|
196
179
|
_meta: {
|
|
197
180
|
messageId: req.messageId,
|
|
198
181
|
subagentMessages: messagesUpdate.messages,
|
|
182
|
+
subagentCompleted: messagesUpdate.completed,
|
|
199
183
|
},
|
|
200
184
|
};
|
|
201
185
|
yield updateToYield;
|
|
@@ -626,7 +610,7 @@ export class LangchainAgent {
|
|
|
626
610
|
// - "gemini-2.0-flash" → Google Generative AI
|
|
627
611
|
// - "vertex-gemini-2.0-flash" → Vertex AI (strips prefix)
|
|
628
612
|
// - "claude-sonnet-4-5-20250929" → Anthropic
|
|
629
|
-
const model = createModelFromString(effectiveModel);
|
|
613
|
+
const model = await createModelFromString(effectiveModel);
|
|
630
614
|
const agentConfig = {
|
|
631
615
|
model,
|
|
632
616
|
tools: finalTools,
|
|
@@ -840,17 +824,11 @@ export class LangchainAgent {
|
|
|
840
824
|
}
|
|
841
825
|
// Create the stream within the invocation context so AsyncLocalStorage
|
|
842
826
|
// propagates the context to all tool executions and callbacks
|
|
843
|
-
_logger.info("Starting agent.stream", {
|
|
844
|
-
messageCount: messages.length,
|
|
845
|
-
effectiveModel,
|
|
846
|
-
sessionId: req.sessionId,
|
|
847
|
-
});
|
|
848
827
|
const stream = context.with(invocationContext, () => agent.stream({ messages }, {
|
|
849
828
|
streamMode: ["updates", "messages"],
|
|
850
829
|
recursionLimit: 200,
|
|
851
830
|
callbacks: [otelCallbacks],
|
|
852
831
|
}));
|
|
853
|
-
_logger.info("agent.stream created, starting iteration");
|
|
854
832
|
// Merge the LangChain stream with subagent event stream
|
|
855
833
|
// This allows both to yield concurrently without polling
|
|
856
834
|
async function* mergeStreams() {
|
|
@@ -870,16 +848,20 @@ export class LangchainAgent {
|
|
|
870
848
|
}
|
|
871
849
|
// Start listening for subagent events
|
|
872
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
|
+
}));
|
|
873
857
|
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
858
|
const result = await Promise.race([streamPromise, subagentPromise]);
|
|
881
859
|
if (result.source === "stream") {
|
|
882
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
|
+
}
|
|
883
865
|
streamDone = true;
|
|
884
866
|
// Continue to drain remaining subagent events
|
|
885
867
|
subagentListenerActive = false;
|
|
@@ -890,6 +872,12 @@ export class LangchainAgent {
|
|
|
890
872
|
break;
|
|
891
873
|
}
|
|
892
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
|
+
}));
|
|
893
881
|
}
|
|
894
882
|
else if (result.source === "subagent") {
|
|
895
883
|
// Subagent event arrived - it's already in the queue
|
|
@@ -900,6 +888,7 @@ export class LangchainAgent {
|
|
|
900
888
|
}
|
|
901
889
|
}
|
|
902
890
|
// Iterate through the merged stream
|
|
891
|
+
let messageCount = 0;
|
|
903
892
|
for await (const item of mergeStreams()) {
|
|
904
893
|
if (item.source === "subagent") {
|
|
905
894
|
// Yield any queued subagent messages
|
|
@@ -908,6 +897,7 @@ export class LangchainAgent {
|
|
|
908
897
|
}
|
|
909
898
|
// Process the stream item
|
|
910
899
|
const streamItem = item.value;
|
|
900
|
+
messageCount++;
|
|
911
901
|
// biome-ignore lint/suspicious/noExplicitAny: LangChain stream items are tuples with dynamic types
|
|
912
902
|
const [streamMode, chunk] = streamItem;
|
|
913
903
|
if (streamMode === "updates") {
|
|
@@ -1011,11 +1001,10 @@ export class LangchainAgent {
|
|
|
1011
1001
|
const qHash = hashQuery(toolCall.args.query);
|
|
1012
1002
|
queryToToolCallId.set(qHash, toolCall.id);
|
|
1013
1003
|
activeSubagentToolCalls.add(toolCall.id);
|
|
1014
|
-
telemetry.log("info", "
|
|
1015
|
-
queryHash: qHash,
|
|
1004
|
+
telemetry.log("info", "Subagent invoked", {
|
|
1016
1005
|
toolCallId: toolCall.id,
|
|
1017
|
-
|
|
1018
|
-
|
|
1006
|
+
agentName: agentName,
|
|
1007
|
+
sessionId: req.sessionId,
|
|
1019
1008
|
});
|
|
1020
1009
|
}
|
|
1021
1010
|
else {
|
|
@@ -1101,30 +1090,12 @@ export class LangchainAgent {
|
|
|
1101
1090
|
turnTokenUsage.totalTokens += messageTokenUsage.totalTokens ?? 0;
|
|
1102
1091
|
countedMessageIds.add(aiMessage.id);
|
|
1103
1092
|
}
|
|
1104
|
-
if (messageTokenUsage) {
|
|
1105
|
-
const contentType = typeof aiMessage.content;
|
|
1106
|
-
const contentIsArray = Array.isArray(aiMessage.content);
|
|
1107
|
-
const contentLength = contentIsArray
|
|
1108
|
-
? aiMessage.content.length
|
|
1109
|
-
: typeof aiMessage.content === "string"
|
|
1110
|
-
? aiMessage.content.length
|
|
1111
|
-
: -1;
|
|
1112
|
-
_logger.debug("messageTokenUsage", {
|
|
1113
|
-
messageTokenUsage,
|
|
1114
|
-
contentType,
|
|
1115
|
-
isArray: contentIsArray,
|
|
1116
|
-
length: contentLength,
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
1093
|
// If we have tokenUsage but no content, send a token-only chunk
|
|
1120
1094
|
if (messageTokenUsage &&
|
|
1121
1095
|
(typeof aiMessage.content === "string"
|
|
1122
1096
|
? aiMessage.content === ""
|
|
1123
1097
|
: Array.isArray(aiMessage.content) &&
|
|
1124
1098
|
aiMessage.content.length === 0)) {
|
|
1125
|
-
_logger.debug("sending token-only chunk", {
|
|
1126
|
-
messageTokenUsage,
|
|
1127
|
-
});
|
|
1128
1099
|
const msgToYield = {
|
|
1129
1100
|
sessionUpdate: "agent_message_chunk",
|
|
1130
1101
|
content: {
|
|
@@ -1318,12 +1289,9 @@ export class LangchainAgent {
|
|
|
1318
1289
|
const maxWaitTime = 300000; // Absolute max 5 minutes
|
|
1319
1290
|
const startTime = Date.now();
|
|
1320
1291
|
let lastMessageTime = Date.now();
|
|
1292
|
+
let iterations = 0;
|
|
1321
1293
|
while (Date.now() - startTime < maxWaitTime) {
|
|
1322
|
-
|
|
1323
|
-
if (activeSubagentToolCalls.size > 0 &&
|
|
1324
|
-
completedSubagentToolCalls.size >= activeSubagentToolCalls.size) {
|
|
1325
|
-
break;
|
|
1326
|
-
}
|
|
1294
|
+
iterations++;
|
|
1327
1295
|
// Check if there are pending messages
|
|
1328
1296
|
if (subagentMessagesQueue.length > 0) {
|
|
1329
1297
|
yield* yieldPendingSubagentUpdates();
|
|
@@ -1336,11 +1304,6 @@ export class LangchainAgent {
|
|
|
1336
1304
|
// Wait a bit before checking again
|
|
1337
1305
|
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1338
1306
|
}
|
|
1339
|
-
if (Date.now() - startTime >= maxWaitTime) {
|
|
1340
|
-
_logger.warn("[SUBAGENT] Timeout waiting for subagents", {
|
|
1341
|
-
sessionId: req.sessionId,
|
|
1342
|
-
});
|
|
1343
|
-
}
|
|
1344
1307
|
// Final yield of any remaining messages
|
|
1345
1308
|
yield* yieldPendingSubagentUpdates();
|
|
1346
1309
|
// Now that content streaming is complete, yield all buffered tool call notifications
|
|
@@ -1398,7 +1361,7 @@ const makeMcpToolsClient = async (mcpConfigs) => {
|
|
|
1398
1361
|
const mcpServers = await Promise.all((mcpConfigs ?? []).map(async (config) => {
|
|
1399
1362
|
if (typeof config === "string") {
|
|
1400
1363
|
// String configs use the centralized MCP proxy with auth
|
|
1401
|
-
const shedAuth = await
|
|
1364
|
+
const shedAuth = await getShedAuth();
|
|
1402
1365
|
if (!shedAuth) {
|
|
1403
1366
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use cloud MCP servers.");
|
|
1404
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
|
}
|
|
@@ -22,7 +22,7 @@ export async function getTownE2BApiKey() {
|
|
|
22
22
|
return _apiKeyFetchPromise;
|
|
23
23
|
}
|
|
24
24
|
_apiKeyFetchPromise = (async () => {
|
|
25
|
-
const shedAuth = getShedAuth();
|
|
25
|
+
const shedAuth = await getShedAuth();
|
|
26
26
|
if (!shedAuth) {
|
|
27
27
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use the code_sandbox tools.");
|
|
28
28
|
}
|
|
@@ -329,7 +329,7 @@ function makeE2BToolsInternal(getSandbox) {
|
|
|
329
329
|
const fileBuffer = Buffer.from(result.stdout.trim(), "base64");
|
|
330
330
|
await fs.writeFile(outputPath, fileBuffer);
|
|
331
331
|
// Step 2: Upload to Supabase Storage
|
|
332
|
-
const shedAuth = getShedAuth();
|
|
332
|
+
const shedAuth = await getShedAuth();
|
|
333
333
|
if (!shedAuth) {
|
|
334
334
|
// Fallback to local URL if not authenticated
|
|
335
335
|
const port = process.env.PORT || "3100";
|
|
@@ -37,16 +37,6 @@ export function emitSubagentMessages(queryHash, messages, completed = false) {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
if (toolCallId) {
|
|
40
|
-
const firstMessage = messages[0];
|
|
41
|
-
logger.info("✓ Emitting subagent messages for live streaming", {
|
|
42
|
-
queryHash,
|
|
43
|
-
toolCallId,
|
|
44
|
-
messageCount: messages.length,
|
|
45
|
-
hasContent: firstMessage ? firstMessage.content.length > 0 : false,
|
|
46
|
-
hasToolCalls: firstMessage ? firstMessage.toolCalls.length > 0 : false,
|
|
47
|
-
completed,
|
|
48
|
-
invocationId: invocationCtx.invocationId,
|
|
49
|
-
});
|
|
50
40
|
// Emit to the parent's invocation-scoped EventEmitter
|
|
51
41
|
invocationCtx.subagentEventEmitter.emit("messages", {
|
|
52
42
|
toolCallId,
|
|
@@ -3,11 +3,13 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
6
|
+
import { createLogger } from "@townco/core";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { SUBAGENT_MODE_KEY, } from "../../../acp-server/adapter.js";
|
|
8
9
|
import { makeRunnerFromDefinition } from "../../index.js";
|
|
9
10
|
import { bindGeneratorToSessionContext, getAbortSignal, } from "../../session-context.js";
|
|
10
11
|
import { emitSubagentMessages, hashQuery, } from "./subagent-connections.js";
|
|
12
|
+
const logger = createLogger("subagent-tool", "debug");
|
|
11
13
|
/**
|
|
12
14
|
* Name of the Task tool created by makeSubagentsTool
|
|
13
15
|
*/
|
|
@@ -217,6 +219,11 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
|
|
|
217
219
|
};
|
|
218
220
|
const toolCallMap = new Map();
|
|
219
221
|
const queryHash = hashQuery(query);
|
|
222
|
+
logger.info("[DEBUG] Starting subagent generator loop", {
|
|
223
|
+
agentName,
|
|
224
|
+
queryHash,
|
|
225
|
+
sessionId: subagentSessionId,
|
|
226
|
+
});
|
|
220
227
|
try {
|
|
221
228
|
for await (const update of generator) {
|
|
222
229
|
let shouldEmit = false;
|
|
@@ -291,23 +298,60 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
|
|
|
291
298
|
}
|
|
292
299
|
// Emit incremental update to parent (for live streaming)
|
|
293
300
|
if (shouldEmit) {
|
|
301
|
+
logger.debug("[SUBAGENT-ACCUMULATION] Emitting incremental update", {
|
|
302
|
+
agentName,
|
|
303
|
+
queryHash,
|
|
304
|
+
contentLength: currentMessage.content.length,
|
|
305
|
+
contentBlocksCount: currentMessage.contentBlocks.length,
|
|
306
|
+
toolCallsCount: currentMessage.toolCalls.length,
|
|
307
|
+
});
|
|
294
308
|
emitSubagentMessages(queryHash, [{ ...currentMessage }]);
|
|
295
309
|
}
|
|
296
310
|
}
|
|
311
|
+
logger.info("[DEBUG] Subagent generator loop finished", {
|
|
312
|
+
agentName,
|
|
313
|
+
queryHash,
|
|
314
|
+
sessionId: subagentSessionId,
|
|
315
|
+
contentLength: currentMessage.content.length,
|
|
316
|
+
toolCallCount: currentMessage.toolCalls.length,
|
|
317
|
+
});
|
|
297
318
|
// Final emit to ensure everything is captured, with completion flag
|
|
298
319
|
if (currentMessage.content || currentMessage.toolCalls.length > 0) {
|
|
320
|
+
logger.info("[DEBUG] Emitting final completion flag", {
|
|
321
|
+
agentName,
|
|
322
|
+
queryHash,
|
|
323
|
+
sessionId: subagentSessionId,
|
|
324
|
+
hasContent: true,
|
|
325
|
+
});
|
|
299
326
|
emitSubagentMessages(queryHash, [currentMessage], true);
|
|
300
327
|
}
|
|
301
328
|
else {
|
|
302
329
|
// Even if no messages, emit completion sentinel
|
|
330
|
+
logger.info("[DEBUG] Emitting empty completion flag", {
|
|
331
|
+
agentName,
|
|
332
|
+
queryHash,
|
|
333
|
+
sessionId: subagentSessionId,
|
|
334
|
+
hasContent: false,
|
|
335
|
+
});
|
|
303
336
|
emitSubagentMessages(queryHash, [], true);
|
|
304
337
|
}
|
|
338
|
+
logger.info("[DEBUG] Subagent querySubagent() returning result", {
|
|
339
|
+
agentName,
|
|
340
|
+
queryHash,
|
|
341
|
+
sessionId: subagentSessionId,
|
|
342
|
+
});
|
|
305
343
|
return {
|
|
306
344
|
text: responseText,
|
|
307
345
|
sources: collectedSources,
|
|
308
346
|
};
|
|
309
347
|
}
|
|
310
348
|
catch (error) {
|
|
349
|
+
logger.info("[DEBUG] Subagent querySubagent() caught error", {
|
|
350
|
+
agentName,
|
|
351
|
+
queryHash,
|
|
352
|
+
sessionId: subagentSessionId,
|
|
353
|
+
error: error instanceof Error ? error.message : String(error),
|
|
354
|
+
});
|
|
311
355
|
// Emit completion sentinel even on error to prevent parent from hanging
|
|
312
356
|
emitSubagentMessages(queryHash, [], true);
|
|
313
357
|
if (parentAbortSignal?.aborted) {
|
|
@@ -19,11 +19,11 @@ function getDirectExaClient() {
|
|
|
19
19
|
return _directExaClient;
|
|
20
20
|
}
|
|
21
21
|
/** Get Exa client using Town proxy with authenticated credentials */
|
|
22
|
-
function getTownExaClient() {
|
|
22
|
+
async function getTownExaClient() {
|
|
23
23
|
if (_townExaClient) {
|
|
24
24
|
return _townExaClient;
|
|
25
25
|
}
|
|
26
|
-
const shedAuth = getShedAuth();
|
|
26
|
+
const shedAuth = await getShedAuth();
|
|
27
27
|
if (!shedAuth) {
|
|
28
28
|
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use the town_web_search tool.");
|
|
29
29
|
}
|
|
@@ -43,7 +43,7 @@ export function getWebSearchCitationCounter() {
|
|
|
43
43
|
}
|
|
44
44
|
function makeWebSearchToolsInternal(getClient) {
|
|
45
45
|
const webSearch = tool(async ({ query }) => {
|
|
46
|
-
const client = getClient();
|
|
46
|
+
const client = await getClient();
|
|
47
47
|
const result = await client.searchAndContents(query, {
|
|
48
48
|
numResults: 5,
|
|
49
49
|
type: "auto",
|
|
@@ -102,7 +102,7 @@ function makeWebSearchToolsInternal(getClient) {
|
|
|
102
102
|
paramKey: "query",
|
|
103
103
|
};
|
|
104
104
|
const webFetch = tool(async ({ url, prompt }) => {
|
|
105
|
-
const client = getClient();
|
|
105
|
+
const client = await getClient();
|
|
106
106
|
try {
|
|
107
107
|
const result = await client.getContents([url], {
|
|
108
108
|
text: true,
|