@townco/agent 0.1.114 → 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.
@@ -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
  });
@@ -1490,17 +1492,43 @@ export class AgentAcpAdapter {
1490
1492
  toolCallBlock.subagentSessionId = meta.subagentSessionId;
1491
1493
  }
1492
1494
  if (meta?.subagentMessages) {
1495
+ logger.info("[SUBAGENT-ADAPTER] Updating toolCallBlock with subagent messages", {
1496
+ sessionId: params.sessionId,
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,
1502
+ });
1493
1503
  toolCallBlock.subagentMessages = meta.subagentMessages;
1504
+ logger.info("[SUBAGENT-ADAPTER] ToolCallBlock updated", {
1505
+ sessionId: params.sessionId,
1506
+ toolCallId: toolCallBlock.id,
1507
+ toolCallBlockMessagesCount: toolCallBlock.subagentMessages?.length,
1508
+ toolCallBlockFirstMessageLength: toolCallBlock.subagentMessages?.[0]?.content?.length,
1509
+ });
1510
+ }
1511
+ if (meta?.subagentCompleted !== undefined) {
1512
+ toolCallBlock.subagentCompleted = meta.subagentCompleted;
1494
1513
  }
1495
1514
  }
1496
1515
  // Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
1497
1516
  if (updateMsg._meta) {
1517
+ const subagentCompletedValue = updateMsg._meta
1518
+ ?.subagentCompleted;
1519
+ logger.info("[DEBUG] Adapter forwarding tool_call_update to client", {
1520
+ toolCallId: updateMsg.toolCallId,
1521
+ hasSubagentCompleted: subagentCompletedValue !== undefined,
1522
+ subagentCompleted: subagentCompletedValue,
1523
+ metaKeys: Object.keys(updateMsg._meta),
1524
+ });
1498
1525
  this.connection.sessionUpdate({
1499
1526
  sessionId: params.sessionId,
1500
1527
  update: {
1501
1528
  sessionUpdate: "tool_call_update",
1502
1529
  toolCallId: updateMsg.toolCallId,
1503
1530
  status: updateMsg.status,
1531
+ subagentCompleted: subagentCompletedValue,
1504
1532
  _meta: updateMsg._meta,
1505
1533
  },
1506
1534
  });
@@ -1823,7 +1851,9 @@ export class AgentAcpAdapter {
1823
1851
  // We store the raw output here for session persistence
1824
1852
  // Create mid-turn context snapshot after tool completes
1825
1853
  if (!this.noSession) {
1826
- flushPendingText(); // Ensure all text is captured
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
1827
1857
  // Update or create the partial assistant message in the messages array
1828
1858
  const partialAssistantMessage = {
1829
1859
  role: "assistant",
@@ -1990,6 +2020,13 @@ export class AgentAcpAdapter {
1990
2020
  }
1991
2021
  // Capture the return value (PromptResponse with tokenUsage)
1992
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
+ });
1993
2030
  // Flush any remaining pending text
1994
2031
  flushPendingText();
1995
2032
  }
@@ -2067,6 +2104,45 @@ export class AgentAcpAdapter {
2067
2104
  // Store the complete assistant response in session messages
2068
2105
  // Only store if session persistence is enabled
2069
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
+ }
2070
2146
  const assistantMessage = {
2071
2147
  role: "assistant",
2072
2148
  content: contentBlocks,
@@ -2076,10 +2152,19 @@ export class AgentAcpAdapter {
2076
2152
  const lastMessage = session.messages[session.messages.length - 1];
2077
2153
  if (lastMessage && lastMessage.role === "assistant") {
2078
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
+ });
2079
2160
  session.messages[session.messages.length - 1] = assistantMessage;
2080
2161
  }
2081
2162
  else {
2082
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
+ });
2083
2168
  session.messages.push(assistantMessage);
2084
2169
  }
2085
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 { ensureAuthenticated } from "@townco/core/auth";
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", " Registered subagent query hash mapping", {
1015
- queryHash: qHash,
1004
+ telemetry.log("info", "Subagent invoked", {
1016
1005
  toolCallId: toolCall.id,
1017
- queryPreview: toolCall.args.query.slice(0, 50),
1018
- timestamp: new Date().toISOString(),
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
- // Check if all subagent tool calls have completed
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 ensureAuthenticated();
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,