@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.
@@ -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
- let citationId;
277
- if (typeof result.citationId === "number") {
278
- citationId = String(result.citationId);
279
- }
280
- else {
281
- session.sourceCounter++;
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
- let citationId;
424
- if (docId) {
425
- citationId = docId;
426
- }
427
- else {
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
- messagePreview: userMessageText.slice(0, 100),
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] Storing subagent messages for session replay", {
1495
+ logger.info("[SUBAGENT-ADAPTER] Updating toolCallBlock with subagent messages", {
1463
1496
  sessionId: params.sessionId,
1464
- toolCallId: updateMsg.toolCallId,
1465
- messageCount: meta.subagentMessages.length,
1466
- contentPreview: meta.subagentMessages[0]?.content?.substring(0, 100),
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] Successfully stored messages", {
1504
+ logger.info("[SUBAGENT-ADAPTER] ToolCallBlock updated", {
1470
1505
  sessionId: params.sessionId,
1471
- toolCallId: updateMsg.toolCallId,
1472
- storedCount: toolCallBlock.subagentMessages?.length,
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
- logger.info("[SUBAGENT] Forwarding tool_call_update with _meta to client", {
1479
- sessionId: params.sessionId,
1517
+ const subagentCompletedValue = updateMsg._meta
1518
+ ?.subagentCompleted;
1519
+ logger.info("[DEBUG] Adapter forwarding tool_call_update to client", {
1480
1520
  toolCallId: updateMsg.toolCallId,
1481
- status: updateMsg.status,
1482
- hasSubagentMessages: !!updateMsg._meta
1483
- ?.subagentMessages,
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
- 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
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 { 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,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", " Registered subagent query hash mapping", {
1032
- queryHash: qHash,
1004
+ telemetry.log("info", "Subagent invoked", {
1033
1005
  toolCallId: toolCall.id,
1034
- queryPreview: toolCall.args.query.slice(0, 50),
1035
- timestamp: new Date().toISOString(),
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
- // 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
- }
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 ensureAuthenticated();
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
  }