@townco/agent 0.1.71 → 0.1.73

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.
@@ -361,8 +361,27 @@ export class AgentAcpAdapter {
361
361
  ...(block.icon ? { icon: block.icon } : {}),
362
362
  ...(block.subline ? { subline: block.subline } : {}),
363
363
  ...(block.batchId ? { batchId: block.batchId } : {}),
364
+ // Include subagent data for replay - full content is sent via direct SSE
365
+ // bypassing PostgreSQL NOTIFY size limits
366
+ ...(block.subagentPort ? { subagentPort: block.subagentPort } : {}),
367
+ ...(block.subagentSessionId
368
+ ? { subagentSessionId: block.subagentSessionId }
369
+ : {}),
370
+ ...(block.subagentMessages
371
+ ? { subagentMessages: block.subagentMessages }
372
+ : {}),
364
373
  ...block._meta,
365
374
  };
375
+ // Debug: log subagent data being replayed
376
+ logger.info("Replaying tool_call", {
377
+ toolCallId: block.id,
378
+ title: block.title,
379
+ batchId: block.batchId,
380
+ hasSubagentPort: !!block.subagentPort,
381
+ hasSubagentSessionId: !!block.subagentSessionId,
382
+ hasSubagentMessages: !!block.subagentMessages,
383
+ subagentMessagesCount: block.subagentMessages?.length,
384
+ });
366
385
  this.connection.sessionUpdate({
367
386
  sessionId: params.sessionId,
368
387
  update: {
@@ -783,6 +802,41 @@ export class AgentAcpAdapter {
783
802
  toolCallBlock.status === "failed") {
784
803
  toolCallBlock.completedAt = Date.now();
785
804
  }
805
+ const meta = updateMsg._meta;
806
+ // Update batchId from _meta (comes from tool_call_update after preliminary tool_call)
807
+ if (meta?.batchId && !toolCallBlock.batchId) {
808
+ toolCallBlock.batchId = meta.batchId;
809
+ }
810
+ if (meta?.subagentPort) {
811
+ toolCallBlock.subagentPort = meta.subagentPort;
812
+ }
813
+ if (meta?.subagentSessionId) {
814
+ toolCallBlock.subagentSessionId = meta.subagentSessionId;
815
+ }
816
+ if (meta?.subagentMessages) {
817
+ logger.info("Storing subagent messages for session replay", {
818
+ toolCallId: updateMsg.toolCallId,
819
+ messageCount: meta.subagentMessages.length,
820
+ });
821
+ toolCallBlock.subagentMessages = meta.subagentMessages;
822
+ }
823
+ }
824
+ // Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
825
+ if (updateMsg._meta) {
826
+ logger.info("Forwarding tool_call_update with _meta to client", {
827
+ toolCallId: updateMsg.toolCallId,
828
+ status: updateMsg.status,
829
+ _meta: updateMsg._meta,
830
+ });
831
+ this.connection.sessionUpdate({
832
+ sessionId: params.sessionId,
833
+ update: {
834
+ sessionUpdate: "tool_call_update",
835
+ toolCallId: updateMsg.toolCallId,
836
+ status: updateMsg.status,
837
+ _meta: updateMsg._meta,
838
+ },
839
+ });
786
840
  }
787
841
  // Forward tool_call_update with _meta to the client (for subagent connection info, etc.)
788
842
  if (updateMsg._meta) {
@@ -227,6 +227,48 @@ export function makeHttpTransport(agent, agentDir, agentName) {
227
227
  }
228
228
  continue;
229
229
  }
230
+ // Check if this is a tool_call with subagentMessages - send directly via SSE
231
+ // to bypass PostgreSQL NOTIFY size limits (7500 bytes)
232
+ if (messageType === "session/update" &&
233
+ "params" in rawMsg &&
234
+ rawMsg.params != null &&
235
+ typeof rawMsg.params === "object" &&
236
+ "update" in rawMsg.params &&
237
+ rawMsg.params.update != null &&
238
+ typeof rawMsg.params.update === "object" &&
239
+ "sessionUpdate" in rawMsg.params.update &&
240
+ rawMsg.params.update.sessionUpdate === "tool_call" &&
241
+ "_meta" in rawMsg.params.update &&
242
+ rawMsg.params.update._meta != null &&
243
+ typeof rawMsg.params.update._meta === "object" &&
244
+ "subagentMessages" in rawMsg.params.update._meta) {
245
+ // Send subagent tool call directly via SSE, bypassing PostgreSQL NOTIFY
246
+ const stream = sseStreams.get(sessionId);
247
+ if (stream) {
248
+ try {
249
+ await stream.writeSSE({
250
+ event: "message",
251
+ data: JSON.stringify(rawMsg),
252
+ });
253
+ logger.debug("Sent subagent tool call directly via SSE", {
254
+ sessionId,
255
+ payloadSize: JSON.stringify(rawMsg).length,
256
+ });
257
+ }
258
+ catch (error) {
259
+ logger.error("Failed to send subagent tool call", {
260
+ error,
261
+ sessionId,
262
+ });
263
+ }
264
+ }
265
+ else {
266
+ logger.warn("No SSE stream found for subagent tool call", {
267
+ sessionId,
268
+ });
269
+ }
270
+ continue;
271
+ }
230
272
  // Other messages (notifications, requests from agent) go to
231
273
  // session-specific channel via PostgreSQL NOTIFY
232
274
  const channel = safeChannelName("notifications", sessionId);
@@ -553,7 +595,6 @@ export function makeHttpTransport(agent, agentDir, agentName) {
553
595
  logger.info("Starting HTTP server", { port });
554
596
  Bun.serve({
555
597
  fetch: app.fetch,
556
- hostname: Bun.env.BIND_HOST || "localhost",
557
598
  port,
558
599
  });
559
600
  logger.info("HTTP server listening", {
@@ -16,6 +16,40 @@ export interface ImageBlock {
16
16
  data?: string | undefined;
17
17
  mimeType?: string | undefined;
18
18
  }
19
+ /**
20
+ * Sub-agent tool call stored within a parent tool call's subagentMessages
21
+ */
22
+ export interface SubagentToolCallBlock {
23
+ id: string;
24
+ title: string;
25
+ prettyName?: string | undefined;
26
+ icon?: string | undefined;
27
+ status: "pending" | "in_progress" | "completed" | "failed";
28
+ }
29
+ /**
30
+ * Content block for sub-agent messages - either text or a tool call
31
+ */
32
+ export interface SubagentTextBlock {
33
+ type: "text";
34
+ text: string;
35
+ }
36
+ export interface SubagentToolCallContentBlock {
37
+ type: "tool_call";
38
+ toolCall: SubagentToolCallBlock;
39
+ }
40
+ export type SubagentContentBlock = SubagentTextBlock | SubagentToolCallContentBlock;
41
+ /**
42
+ * Sub-agent message stored for replay
43
+ */
44
+ export interface SubagentMessage {
45
+ id: string;
46
+ /** Accumulated text content (thinking) */
47
+ content: string;
48
+ /** Interleaved content blocks in arrival order */
49
+ contentBlocks?: SubagentContentBlock[] | undefined;
50
+ /** Tool calls made by the sub-agent */
51
+ toolCalls?: SubagentToolCallBlock[] | undefined;
52
+ }
19
53
  export interface ToolCallBlock {
20
54
  type: "tool_call";
21
55
  id: string;
@@ -37,6 +71,12 @@ export interface ToolCallBlock {
37
71
  originalTokens?: number;
38
72
  finalTokens?: number;
39
73
  };
74
+ /** Sub-agent HTTP port (for reference, not used in replay) */
75
+ subagentPort?: number | undefined;
76
+ /** Sub-agent session ID (for reference, not used in replay) */
77
+ subagentSessionId?: string | undefined;
78
+ /** Stored sub-agent messages for replay */
79
+ subagentMessages?: SubagentMessage[] | undefined;
40
80
  }
41
81
  export type ContentBlock = TextBlock | ImageBlock | ToolCallBlock;
42
82
  /**
@@ -26,6 +26,29 @@ const imageBlockSchema = z.object({
26
26
  data: z.string().optional(),
27
27
  mimeType: z.string().optional(),
28
28
  });
29
+ const subagentToolCallBlockSchema = z.object({
30
+ id: z.string(),
31
+ title: z.string(),
32
+ prettyName: z.string().optional(),
33
+ icon: z.string().optional(),
34
+ status: z.enum(["pending", "in_progress", "completed", "failed"]),
35
+ });
36
+ const subagentContentBlockSchema = z.discriminatedUnion("type", [
37
+ z.object({
38
+ type: z.literal("text"),
39
+ text: z.string(),
40
+ }),
41
+ z.object({
42
+ type: z.literal("tool_call"),
43
+ toolCall: subagentToolCallBlockSchema,
44
+ }),
45
+ ]);
46
+ const subagentMessageSchema = z.object({
47
+ id: z.string(),
48
+ content: z.string(),
49
+ contentBlocks: z.array(subagentContentBlockSchema).optional(),
50
+ toolCalls: z.array(subagentToolCallBlockSchema).optional(),
51
+ });
29
52
  const toolCallBlockSchema = z.object({
30
53
  type: z.literal("tool_call"),
31
54
  id: z.string(),
@@ -52,6 +75,9 @@ const toolCallBlockSchema = z.object({
52
75
  error: z.string().optional(),
53
76
  startedAt: z.number().optional(),
54
77
  completedAt: z.number().optional(),
78
+ subagentPort: z.number().optional(),
79
+ subagentSessionId: z.string().optional(),
80
+ subagentMessages: z.array(subagentMessageSchema).optional(),
55
81
  });
56
82
  const contentBlockSchema = z.discriminatedUnion("type", [
57
83
  textBlockSchema,
@@ -102,6 +102,7 @@ export class LangchainAgent {
102
102
  });
103
103
  const subagentUpdateQueue = [];
104
104
  let subagentUpdateResolver = null;
105
+ const subagentMessagesQueue = [];
105
106
  // Listen for subagent connection events - resolve any waiting promise immediately
106
107
  const onSubagentConnection = (event) => {
107
108
  _logger.info("Received subagent connection event", {
@@ -121,6 +122,15 @@ export class LangchainAgent {
121
122
  }
122
123
  };
123
124
  subagentEvents.on("connection", onSubagentConnection);
125
+ // Listen for subagent messages events (for session storage)
126
+ const onSubagentMessages = (event) => {
127
+ _logger.info("Received subagent messages event", {
128
+ toolCallId: event.toolCallId,
129
+ messageCount: event.messages.length,
130
+ });
131
+ subagentMessagesQueue.push(event);
132
+ };
133
+ subagentEvents.on("messages", onSubagentMessages);
124
134
  // Helper to get next subagent update (returns immediately if queued, otherwise waits)
125
135
  const waitForSubagentUpdate = () => {
126
136
  if (subagentUpdateQueue.length > 0) {
@@ -149,6 +159,22 @@ export class LangchainAgent {
149
159
  },
150
160
  };
151
161
  }
162
+ // Also yield any pending messages updates
163
+ while (subagentMessagesQueue.length > 0) {
164
+ const messagesUpdate = subagentMessagesQueue.shift();
165
+ _logger.info("Yielding queued subagent messages update", {
166
+ toolCallId: messagesUpdate.toolCallId,
167
+ messageCount: messagesUpdate.messages.length,
168
+ });
169
+ yield {
170
+ sessionUpdate: "tool_call_update",
171
+ toolCallId: messagesUpdate.toolCallId,
172
+ _meta: {
173
+ messageId: req.messageId,
174
+ subagentMessages: messagesUpdate.messages,
175
+ },
176
+ };
177
+ }
152
178
  }
153
179
  // Start telemetry span for entire invocation
154
180
  const invocationSpan = telemetry.startSpan("agent.invoke", {
@@ -888,8 +914,9 @@ export class LangchainAgent {
888
914
  yield* yieldPendingSubagentUpdates();
889
915
  // Now that content streaming is complete, yield all buffered tool call notifications
890
916
  yield* flushPendingToolCalls();
891
- // Clean up subagent connection listener
917
+ // Clean up subagent event listeners
892
918
  subagentEvents.off("connection", onSubagentConnection);
919
+ subagentEvents.off("messages", onSubagentMessages);
893
920
  // Cancel any pending wait
894
921
  if (subagentUpdateResolver) {
895
922
  subagentUpdateResolver = null;
@@ -907,8 +934,9 @@ export class LangchainAgent {
907
934
  };
908
935
  }
909
936
  catch (error) {
910
- // Clean up subagent connection listener on error
937
+ // Clean up subagent event listeners on error
911
938
  subagentEvents.off("connection", onSubagentConnection);
939
+ subagentEvents.off("messages", onSubagentMessages);
912
940
  // Log error and end span with error status
913
941
  telemetry.log("error", "Agent invocation failed", {
914
942
  error: error instanceof Error ? error.message : String(error),
@@ -7,6 +7,35 @@ export interface SubagentConnectionInfo {
7
7
  port: number;
8
8
  sessionId: string;
9
9
  }
10
+ /**
11
+ * Sub-agent tool call tracked during streaming
12
+ */
13
+ export interface SubagentToolCall {
14
+ id: string;
15
+ title: string;
16
+ prettyName?: string | undefined;
17
+ icon?: string | undefined;
18
+ status: "pending" | "in_progress" | "completed" | "failed";
19
+ }
20
+ /**
21
+ * Content block for sub-agent messages
22
+ */
23
+ export type SubagentContentBlock = {
24
+ type: "text";
25
+ text: string;
26
+ } | {
27
+ type: "tool_call";
28
+ toolCall: SubagentToolCall;
29
+ };
30
+ /**
31
+ * Sub-agent message accumulated during streaming
32
+ */
33
+ export interface SubagentMessage {
34
+ id: string;
35
+ content: string;
36
+ contentBlocks: SubagentContentBlock[];
37
+ toolCalls: SubagentToolCall[];
38
+ }
10
39
  /**
11
40
  * Event emitter for subagent connection events.
12
41
  * The runner listens to these events and emits tool_call_update.
@@ -26,3 +55,8 @@ export declare function hashQuery(query: string): string;
26
55
  * Emits an event that the runner can listen to.
27
56
  */
28
57
  export declare function emitSubagentConnection(queryHash: string, connectionInfo: SubagentConnectionInfo): void;
58
+ /**
59
+ * Called by the subagent tool when it completes with accumulated messages.
60
+ * Emits an event with the messages for session storage.
61
+ */
62
+ export declare function emitSubagentMessages(queryHash: string, messages: SubagentMessage[]): void;
@@ -12,6 +12,11 @@ export const subagentEvents = new EventEmitter();
12
12
  * Set by the runner when it sees a subagent tool_call.
13
13
  */
14
14
  export const queryToToolCallId = new Map();
15
+ /**
16
+ * Maps query hash to resolved toolCallId (preserved after connection event).
17
+ * Used to correlate messages when the tool completes.
18
+ */
19
+ const queryToResolvedToolCallId = new Map();
15
20
  /**
16
21
  * Generate a hash from the query string for correlation.
17
22
  */
@@ -46,7 +51,8 @@ export function emitSubagentConnection(queryHash, connectionInfo) {
46
51
  toolCallId,
47
52
  ...connectionInfo,
48
53
  });
49
- // Clean up the mapping
54
+ // Preserve the toolCallId for message emission, but remove from pending lookup
55
+ queryToResolvedToolCallId.set(queryHash, toolCallId);
50
56
  queryToToolCallId.delete(queryHash);
51
57
  }
52
58
  else {
@@ -56,3 +62,28 @@ export function emitSubagentConnection(queryHash, connectionInfo) {
56
62
  });
57
63
  }
58
64
  }
65
+ /**
66
+ * Called by the subagent tool when it completes with accumulated messages.
67
+ * Emits an event with the messages for session storage.
68
+ */
69
+ export function emitSubagentMessages(queryHash, messages) {
70
+ const toolCallId = queryToResolvedToolCallId.get(queryHash);
71
+ if (toolCallId) {
72
+ logger.info("Emitting subagent messages for storage", {
73
+ queryHash,
74
+ toolCallId,
75
+ messageCount: messages.length,
76
+ });
77
+ subagentEvents.emit("messages", {
78
+ toolCallId,
79
+ messages,
80
+ });
81
+ // Clean up the resolved mapping
82
+ queryToResolvedToolCallId.delete(queryHash);
83
+ }
84
+ else {
85
+ logger.warn("No resolved toolCallId for messages emission", {
86
+ queryHash,
87
+ });
88
+ }
89
+ }
@@ -7,7 +7,7 @@ import { createLogger as coreCreateLogger } from "@townco/core";
7
7
  import { z } from "zod";
8
8
  import { SUBAGENT_MODE_KEY } from "../../../acp-server/adapter.js";
9
9
  import { findAvailablePort } from "./port-utils.js";
10
- import { emitSubagentConnection, hashQuery } from "./subagent-connections.js";
10
+ import { emitSubagentConnection, emitSubagentMessages, hashQuery, } from "./subagent-connections.js";
11
11
  /**
12
12
  * Name of the Task tool created by makeSubagentsTool
13
13
  */
@@ -317,6 +317,15 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
317
317
  // Step 3: Connect to SSE for receiving streaming responses
318
318
  sseAbortController = new AbortController();
319
319
  let responseText = "";
320
+ // Track full message structure for session storage
321
+ const currentMessage = {
322
+ id: `subagent-${Date.now()}`,
323
+ content: "",
324
+ contentBlocks: [],
325
+ toolCalls: [],
326
+ };
327
+ // Map of tool call IDs to their indices in toolCalls array
328
+ const toolCallMap = new Map();
320
329
  const ssePromise = (async () => {
321
330
  const sseResponse = await fetch(`${baseUrl}/events`, {
322
331
  headers: { "X-Session-ID": sessionId },
@@ -342,18 +351,63 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
342
351
  continue;
343
352
  try {
344
353
  const message = JSON.parse(data);
345
- // Handle session/update notifications for agent_message_chunk
346
- if (message.method === "session/update" &&
347
- message.params?.update?.sessionUpdate === "agent_message_chunk") {
348
- const content = message.params.update.content;
354
+ const update = message.params?.update;
355
+ if (message.method !== "session/update" || !update)
356
+ continue;
357
+ // Handle agent_message_chunk - accumulate text
358
+ if (update.sessionUpdate === "agent_message_chunk") {
359
+ const content = update.content;
349
360
  if (content?.type === "text" &&
350
361
  typeof content.text === "string") {
351
362
  responseText += content.text;
363
+ currentMessage.content += content.text;
364
+ // Add to contentBlocks - append to last text block or create new one
365
+ const lastBlock = currentMessage.contentBlocks[currentMessage.contentBlocks.length - 1];
366
+ if (lastBlock && lastBlock.type === "text") {
367
+ lastBlock.text += content.text;
368
+ }
369
+ else {
370
+ currentMessage.contentBlocks.push({
371
+ type: "text",
372
+ text: content.text,
373
+ });
374
+ }
352
375
  }
353
376
  }
354
- // Reset on tool_call (marks new message boundary)
355
- if (message.params?.update?.sessionUpdate === "tool_call") {
356
- responseText = "";
377
+ // Handle tool_call - track new tool calls
378
+ if (update.sessionUpdate === "tool_call" && update.toolCallId) {
379
+ const toolCall = {
380
+ id: update.toolCallId,
381
+ title: update.title || "Tool call",
382
+ prettyName: update._meta?.prettyName,
383
+ icon: update._meta?.icon,
384
+ status: update.status || "pending",
385
+ };
386
+ currentMessage.toolCalls.push(toolCall);
387
+ toolCallMap.set(update.toolCallId, currentMessage.toolCalls.length - 1);
388
+ // Add to contentBlocks for interleaved display
389
+ currentMessage.contentBlocks.push({
390
+ type: "tool_call",
391
+ toolCall,
392
+ });
393
+ }
394
+ // Handle tool_call_update - update existing tool call status
395
+ if (update.sessionUpdate === "tool_call_update" &&
396
+ update.toolCallId) {
397
+ const idx = toolCallMap.get(update.toolCallId);
398
+ if (idx !== undefined && currentMessage.toolCalls[idx]) {
399
+ if (update.status) {
400
+ currentMessage.toolCalls[idx].status =
401
+ update.status;
402
+ }
403
+ // Also update in contentBlocks
404
+ const block = currentMessage.contentBlocks.find((b) => b.type === "tool_call" &&
405
+ b.toolCall.id === update.toolCallId);
406
+ if (block && update.status) {
407
+ block.toolCall.status =
408
+ update.status;
409
+ }
410
+ }
357
411
  }
358
412
  }
359
413
  catch {
@@ -404,6 +458,10 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
404
458
  ssePromise.catch(() => { }), // Ignore abort errors
405
459
  new Promise((r) => setTimeout(r, 1000)),
406
460
  ]);
461
+ // Emit accumulated messages for session storage
462
+ if (currentMessage.content || currentMessage.toolCalls.length > 0) {
463
+ emitSubagentMessages(queryHash, [currentMessage]);
464
+ }
407
465
  return responseText;
408
466
  }
409
467
  finally {