@townco/agent 0.1.72 → 0.1.74

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.
@@ -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 {
@@ -78,3 +78,7 @@ When in doubt, use this tool. Being proactive with task management demonstrates
78
78
  });
79
79
  todoWrite.prettyName = "Todo List";
80
80
  todoWrite.icon = "CheckSquare";
81
+ todoWrite.verbiage = {
82
+ active: "Updating to-do's",
83
+ past: "Updated to-do's",
84
+ };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ /** Create web search tools using direct EXA_API_KEY */
2
3
  export declare function makeWebSearchTools(): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
3
4
  query: z.ZodString;
4
5
  }, z.core.$strip>, {
@@ -21,3 +22,26 @@ export declare function makeWebSearchTools(): readonly [import("langchain").Dyna
21
22
  url: string;
22
23
  prompt: string;
23
24
  }, string>];
25
+ /** Create web search tools using Town proxy */
26
+ export declare function makeTownWebSearchTools(): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
27
+ query: z.ZodString;
28
+ }, z.core.$strip>, {
29
+ query: string;
30
+ }, {
31
+ query: string;
32
+ }, string | import("exa-js").SearchResponse<{
33
+ numResults: number;
34
+ type: "auto";
35
+ text: {
36
+ maxCharacters: number;
37
+ };
38
+ }>>, import("langchain").DynamicStructuredTool<z.ZodObject<{
39
+ url: z.ZodString;
40
+ prompt: z.ZodString;
41
+ }, z.core.$strip>, {
42
+ url: string;
43
+ prompt: string;
44
+ }, {
45
+ url: string;
46
+ prompt: string;
47
+ }, string>];
@@ -1,24 +1,38 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
+ import { getShedAuth } from "@townco/core/auth";
2
3
  import Exa from "exa-js";
3
4
  import { tool } from "langchain";
4
5
  import { z } from "zod";
5
- let _exaClient = null;
6
- function getExaClient() {
7
- if (_exaClient) {
8
- return _exaClient;
6
+ let _directExaClient = null;
7
+ let _townExaClient = null;
8
+ /** Get Exa client using direct EXA_API_KEY environment variable */
9
+ function getDirectExaClient() {
10
+ if (_directExaClient) {
11
+ return _directExaClient;
9
12
  }
10
13
  const apiKey = process.env.EXA_API_KEY;
11
14
  if (!apiKey) {
12
15
  throw new Error("EXA_API_KEY environment variable is required to use the web_search tool. " +
13
16
  "Please set it to your Exa API key from https://exa.ai");
14
17
  }
15
- _exaClient = new Exa(apiKey);
16
- return _exaClient;
18
+ _directExaClient = new Exa(apiKey);
19
+ return _directExaClient;
17
20
  }
18
- export function makeWebSearchTools() {
19
- // WebSearch tool - search the web
21
+ /** Get Exa client using Town proxy with authenticated credentials */
22
+ function getTownExaClient() {
23
+ if (_townExaClient) {
24
+ return _townExaClient;
25
+ }
26
+ const shedAuth = getShedAuth();
27
+ if (!shedAuth) {
28
+ throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use the town_web_search tool.");
29
+ }
30
+ _townExaClient = new Exa(shedAuth.accessToken, `${shedAuth.shedUrl}/api/exa`);
31
+ return _townExaClient;
32
+ }
33
+ function makeWebSearchToolsInternal(getClient) {
20
34
  const webSearch = tool(async ({ query }) => {
21
- const client = getExaClient();
35
+ const client = getClient();
22
36
  const result = await client.searchAndContents(query, {
23
37
  numResults: 5,
24
38
  type: "auto",
@@ -48,9 +62,13 @@ export function makeWebSearchTools() {
48
62
  });
49
63
  webSearch.prettyName = "Web Search";
50
64
  webSearch.icon = "Globe";
51
- // WebFetch tool - get contents of specific URLs
65
+ webSearch.verbiage = {
66
+ active: "Searching the web for {query}",
67
+ past: "Searched the web for {query}",
68
+ paramKey: "query",
69
+ };
52
70
  const webFetch = tool(async ({ url, prompt }) => {
53
- const client = getExaClient();
71
+ const client = getClient();
54
72
  try {
55
73
  const result = await client.getContents([url], {
56
74
  text: true,
@@ -122,8 +140,21 @@ export function makeWebSearchTools() {
122
140
  });
123
141
  webFetch.prettyName = "Web Fetch";
124
142
  webFetch.icon = "Link";
143
+ webFetch.verbiage = {
144
+ active: "Fetching {url}",
145
+ past: "Fetched {url}",
146
+ paramKey: "url",
147
+ };
125
148
  return [webSearch, webFetch];
126
149
  }
150
+ /** Create web search tools using direct EXA_API_KEY */
151
+ export function makeWebSearchTools() {
152
+ return makeWebSearchToolsInternal(getDirectExaClient);
153
+ }
154
+ /** Create web search tools using Town proxy */
155
+ export function makeTownWebSearchTools() {
156
+ return makeWebSearchToolsInternal(getTownExaClient);
157
+ }
127
158
  function buildWebFetchUserMessage(pageContent, prompt) {
128
159
  return `Web page content:
129
160
  ---
@@ -6,6 +6,11 @@ export type CustomToolModule = {
6
6
  description: string;
7
7
  prettyName?: string;
8
8
  icon?: string;
9
+ verbiage?: {
10
+ active: string;
11
+ past: string;
12
+ paramKey?: string;
13
+ };
9
14
  };
10
15
  export type ResolvedCustomTool = {
11
16
  fn: (input: unknown) => unknown | Promise<unknown>;
@@ -14,5 +19,10 @@ export type ResolvedCustomTool = {
14
19
  description: string;
15
20
  prettyName?: string;
16
21
  icon?: string;
22
+ verbiage?: {
23
+ active: string;
24
+ past: string;
25
+ paramKey?: string;
26
+ };
17
27
  };
18
28
  export declare function loadCustomToolModule(modulePath: string): Promise<ResolvedCustomTool>;
@@ -37,6 +37,7 @@ export async function loadCustomToolModule(modulePath) {
37
37
  description: mod.description,
38
38
  ...(mod.prettyName && { prettyName: mod.prettyName }),
39
39
  ...(mod.icon && { icon: mod.icon }),
40
+ ...(mod.verbiage && { verbiage: mod.verbiage }),
40
41
  };
41
42
  // Cache the resolved tool
42
43
  customToolCache.set(modulePath, resolved);
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  /** Built-in tool types. */
3
- export declare const zBuiltInToolType: z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>;
3
+ export declare const zBuiltInToolType: z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>;
4
4
  /** Subagent configuration schema for Task tools. */
5
5
  export declare const zSubagentConfig: z.ZodObject<{
6
6
  agentName: z.ZodString;
@@ -23,7 +23,7 @@ declare const zDirectTool: z.ZodObject<{
23
23
  }, z.core.$strip>>>;
24
24
  }, z.core.$strip>;
25
25
  /** Tool type - can be a built-in tool string or custom tool object. */
26
- export declare const zToolType: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
26
+ export declare const zToolType: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"town_web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">, z.ZodLiteral<"browser">]>, z.ZodObject<{
27
27
  type: z.ZodLiteral<"custom">;
28
28
  modulePath: z.ZodString;
29
29
  }, z.core.$strip>, z.ZodObject<{
@@ -4,6 +4,7 @@ export const zBuiltInToolType = z.union([
4
4
  z.literal("todo_write"),
5
5
  z.literal("get_weather"),
6
6
  z.literal("web_search"),
7
+ z.literal("town_web_search"),
7
8
  z.literal("filesystem"),
8
9
  z.literal("generate_image"),
9
10
  z.literal("browser"),
@@ -28,6 +28,11 @@ declare class AgentTelemetry {
28
28
  * @param attributes - Attributes to merge with existing base attributes
29
29
  */
30
30
  setBaseAttributes(attributes: Record<string, string | number>): void;
31
+ /**
32
+ * Remove a base attribute
33
+ * @param key - Attribute key to remove
34
+ */
35
+ clearBaseAttribute(key: string): void;
31
36
  /**
32
37
  * Start a new span
33
38
  * @param name - Span name
@@ -40,6 +40,14 @@ class AgentTelemetry {
40
40
  ...attributes,
41
41
  };
42
42
  }
43
+ /**
44
+ * Remove a base attribute
45
+ * @param key - Attribute key to remove
46
+ */
47
+ clearBaseAttribute(key) {
48
+ const { [key]: _, ...rest } = this.baseAttributes;
49
+ this.baseAttributes = rest;
50
+ }
43
51
  /**
44
52
  * Start a new span
45
53
  * @param name - Span name
@@ -68,10 +68,12 @@ export function initializeOpenTelemetry(options = {}) {
68
68
  // false "Request timed out" errors. The export usually succeeds anyway.
69
69
  // Only log non-timeout errors as actual errors.
70
70
  const errorMsg = result.error?.message ?? "";
71
- if (errorMsg.includes("timed out")) {
71
+ if (errorMsg.includes("timed out") ||
72
+ errorMsg.includes("Request timed out")) {
72
73
  if (debug) {
73
- console.log(`⚠️ Export reported timeout (Bun http bug - data likely sent successfully)`);
74
+ console.log(`⚠️ Export reported timeout (Bun http bug - data successfully sent)`);
74
75
  }
76
+ // Don't log timeout errors at all - they're false positives
75
77
  }
76
78
  else {
77
79
  console.error(`❌ Failed to export spans:`, result.error);
@@ -141,7 +143,12 @@ export function initializeOpenTelemetry(options = {}) {
141
143
  }
142
144
  }
143
145
  catch (error) {
144
- console.error("Error flushing telemetry:", error);
146
+ // Filter out Bun HTTP timeout errors - they're false positives
147
+ const errorMsg = error instanceof Error ? error.message : String(error);
148
+ if (!errorMsg.includes("timed out") &&
149
+ !errorMsg.includes("Request timed out")) {
150
+ console.error("Error flushing telemetry:", error);
151
+ }
145
152
  }
146
153
  };
147
154
  return { provider, loggerProvider, shutdown };