@townco/agent 0.1.116 → 0.1.118

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.
@@ -1,68 +1,4 @@
1
+ import type { AgentDefinition } from "../definition";
1
2
  import { type AgentRunner, type SessionUpdateNotification } from "./agent-runner";
2
3
  export type { AgentRunner, SessionUpdateNotification };
3
- export declare const makeRunnerFromDefinition: (definition: {
4
- displayName?: string | undefined;
5
- version?: string | undefined;
6
- description?: string | undefined;
7
- suggestedPrompts?: string[] | undefined;
8
- systemPrompt: string | null;
9
- model: string;
10
- tools?: (string | {
11
- type: "custom";
12
- modulePath: string;
13
- } | {
14
- type: "filesystem";
15
- working_directory?: string | undefined;
16
- } | {
17
- type: "direct";
18
- name: string;
19
- description: string;
20
- fn: import("zod/v4/core").$InferOuterFunctionType<import("zod/v4/core").$ZodFunctionArgs, import("zod/v4/core").$ZodFunctionOut>;
21
- schema: any;
22
- prettyName?: string | undefined;
23
- icon?: string | undefined;
24
- })[] | undefined;
25
- mcps?: (string | {
26
- name: string;
27
- transport: "stdio";
28
- command: string;
29
- args?: string[] | undefined;
30
- } | {
31
- name: string;
32
- transport: "http";
33
- url: string;
34
- headers?: Record<string, string> | undefined;
35
- })[] | undefined;
36
- harnessImplementation?: "langchain" | undefined;
37
- hooks?: {
38
- type: "context_size" | "tool_response";
39
- setting?: Record<string, unknown> | {
40
- threshold: number;
41
- } | {
42
- maxTokensSize?: number | undefined;
43
- } | undefined;
44
- callback?: string | undefined;
45
- callbacks?: {
46
- name: string;
47
- setting?: Record<string, unknown> | undefined;
48
- }[] | undefined;
49
- }[] | undefined;
50
- initialMessage?: {
51
- enabled: boolean;
52
- content: string;
53
- } | undefined;
54
- uiConfig?: {
55
- hideTopBar?: boolean | undefined;
56
- } | undefined;
57
- promptParameters?: {
58
- id: string;
59
- label: string;
60
- description?: string | undefined;
61
- options: {
62
- id: string;
63
- label: string;
64
- systemPromptAddendum?: string | undefined;
65
- }[];
66
- defaultOptionId?: string | undefined;
67
- }[] | undefined;
68
- }) => AgentRunner;
4
+ export declare const makeRunnerFromDefinition: (definition: AgentDefinition) => AgentRunner;
@@ -334,7 +334,9 @@ export class LangchainAgent {
334
334
  const nonMcpToolMetadata = enabledTools.map(extractToolMetadata);
335
335
  const nonMcpToolDefinitionsTokens = estimateAllToolsOverhead(nonMcpToolMetadata);
336
336
  // Calculate TODO_WRITE_INSTRUCTIONS overhead if applicable
337
- const hasTodoWriteTool = builtInNames.includes("todo_write");
337
+ // Skip for subagents since the todo_write tool is filtered out for them
338
+ const isSubagentForTokens = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true;
339
+ const hasTodoWriteTool = builtInNames.includes("todo_write") && !isSubagentForTokens;
338
340
  const todoInstructionsTokens = hasTodoWriteTool
339
341
  ? countTokens(TODO_WRITE_INSTRUCTIONS)
340
342
  : 0;
@@ -623,7 +625,9 @@ export class LangchainAgent {
623
625
  agentConfig.systemPrompt = effectiveSystemPrompt;
624
626
  }
625
627
  // Inject system prompt with optional TodoWrite instructions
626
- const hasTodoWrite = builtInNames.includes("todo_write");
628
+ // Skip for subagents since the todo_write tool is filtered out for them
629
+ const isSubagentForPrompt = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true;
630
+ const hasTodoWrite = builtInNames.includes("todo_write") && !isSubagentForPrompt;
627
631
  if (hasTodoWrite) {
628
632
  agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${TODO_WRITE_INSTRUCTIONS}`;
629
633
  }
@@ -888,7 +892,7 @@ export class LangchainAgent {
888
892
  }
889
893
  }
890
894
  // Iterate through the merged stream
891
- let messageCount = 0;
895
+ let _messageCount = 0;
892
896
  for await (const item of mergeStreams()) {
893
897
  if (item.source === "subagent") {
894
898
  // Yield any queued subagent messages
@@ -897,7 +901,7 @@ export class LangchainAgent {
897
901
  }
898
902
  // Process the stream item
899
903
  const streamItem = item.value;
900
- messageCount++;
904
+ _messageCount++;
901
905
  // biome-ignore lint/suspicious/noExplicitAny: LangChain stream items are tuples with dynamic types
902
906
  const [streamMode, chunk] = streamItem;
903
907
  if (streamMode === "updates") {
@@ -1289,9 +1293,9 @@ export class LangchainAgent {
1289
1293
  const maxWaitTime = 300000; // Absolute max 5 minutes
1290
1294
  const startTime = Date.now();
1291
1295
  let lastMessageTime = Date.now();
1292
- let iterations = 0;
1296
+ let _iterations = 0;
1293
1297
  while (Date.now() - startTime < maxWaitTime) {
1294
- iterations++;
1298
+ _iterations++;
1295
1299
  // Check if there are pending messages
1296
1300
  if (subagentMessagesQueue.length > 0) {
1297
1301
  yield* yieldPendingSubagentUpdates();
@@ -7,6 +7,8 @@ export interface SubagentToolCall {
7
7
  prettyName?: string | undefined;
8
8
  icon?: string | undefined;
9
9
  status: "pending" | "in_progress" | "completed" | "failed";
10
+ rawInput?: Record<string, unknown> | undefined;
11
+ rawOutput?: Record<string, unknown> | undefined;
10
12
  }
11
13
  /**
12
14
  * Content block for sub-agent messages
@@ -10,6 +10,175 @@ import { makeRunnerFromDefinition } from "../../index.js";
10
10
  import { bindGeneratorToSessionContext, getAbortSignal, } from "../../session-context.js";
11
11
  import { emitSubagentMessages, hashQuery, } from "./subagent-connections.js";
12
12
  const logger = createLogger("subagent-tool", "debug");
13
+ /**
14
+ * Helper to derive favicon URL from a domain
15
+ */
16
+ function getFaviconUrl(url) {
17
+ try {
18
+ const domain = new URL(url).hostname;
19
+ return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ /**
26
+ * Helper to get a human-readable source name from URL
27
+ */
28
+ function getSourceName(url) {
29
+ try {
30
+ const hostname = new URL(url).hostname;
31
+ // Remove www. prefix and return domain name
32
+ return hostname.replace(/^www\./, "");
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ /**
39
+ * Extract citation sources from a tool's raw output.
40
+ * This handles WebSearch, WebFetch, and library tools.
41
+ *
42
+ * Note: The runner wraps tool results as { content: JSON.stringify(result) },
43
+ * so we need to parse rawOutput.content if it's a string.
44
+ */
45
+ function extractSourcesFromToolOutput(toolName, rawOutput, toolCallId, sourceCounter) {
46
+ const sources = [];
47
+ // Parse the actual output from the wrapper
48
+ // The runner wraps results as { content: JSON.stringify(actualResult) }
49
+ let actualOutput = rawOutput;
50
+ if (typeof rawOutput.content === "string") {
51
+ try {
52
+ const parsed = JSON.parse(rawOutput.content);
53
+ if (typeof parsed === "object" && parsed !== null) {
54
+ actualOutput = parsed;
55
+ }
56
+ }
57
+ catch {
58
+ // Not valid JSON, use rawOutput as-is
59
+ }
60
+ }
61
+ // Handle WebSearch (Exa) results
62
+ if (toolName === "WebSearch" || toolName === "web_search") {
63
+ // Check for formatted results with citation IDs first
64
+ const formattedResults = actualOutput.formattedForCitation;
65
+ if (Array.isArray(formattedResults)) {
66
+ for (const result of formattedResults) {
67
+ if (result &&
68
+ typeof result === "object" &&
69
+ "url" in result &&
70
+ typeof result.url === "string") {
71
+ // Use the citationId from the tool output if available
72
+ const citationId = typeof result.citationId === "number"
73
+ ? String(result.citationId)
74
+ : (() => {
75
+ sourceCounter.value++;
76
+ return String(sourceCounter.value);
77
+ })();
78
+ const url = result.url;
79
+ const title = typeof result.title === "string" ? result.title : "Untitled";
80
+ const snippet = typeof result.text === "string"
81
+ ? result.text.slice(0, 200)
82
+ : undefined;
83
+ sources.push({
84
+ id: citationId,
85
+ url,
86
+ title,
87
+ snippet,
88
+ favicon: getFaviconUrl(url),
89
+ toolCallId,
90
+ sourceName: getSourceName(url),
91
+ });
92
+ }
93
+ }
94
+ }
95
+ else {
96
+ // Fallback to raw results (backwards compatibility)
97
+ const results = actualOutput.results;
98
+ if (Array.isArray(results)) {
99
+ for (const result of results) {
100
+ if (result &&
101
+ typeof result === "object" &&
102
+ "url" in result &&
103
+ typeof result.url === "string") {
104
+ sourceCounter.value++;
105
+ const url = result.url;
106
+ const title = typeof result.title === "string" ? result.title : "Untitled";
107
+ const snippet = typeof result.text === "string"
108
+ ? result.text.slice(0, 200)
109
+ : undefined;
110
+ sources.push({
111
+ id: String(sourceCounter.value),
112
+ url,
113
+ title,
114
+ snippet,
115
+ favicon: getFaviconUrl(url),
116
+ toolCallId,
117
+ sourceName: getSourceName(url),
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // Handle WebFetch - extract URLs from the fetched content
125
+ // Note: WebFetch returns plain text (the AI-processed result), not structured data
126
+ // So we skip URL extraction here as the URLs would be from the content itself
127
+ // The actual fetched URL should be tracked via tool input, not output
128
+ // Handle library/document retrieval tools
129
+ const isLibraryTool = toolName.startsWith("library__") ||
130
+ toolName.includes("get_document") ||
131
+ toolName.includes("retrieve_document") ||
132
+ toolName.includes("bibliotecha");
133
+ if (isLibraryTool) {
134
+ // Helper to extract a single document source
135
+ const extractDocSource = (doc) => {
136
+ const docUrl = typeof doc.document_url === "string"
137
+ ? doc.document_url
138
+ : typeof doc.url === "string"
139
+ ? doc.url
140
+ : typeof doc.source_url === "string"
141
+ ? doc.source_url
142
+ : null;
143
+ const docTitle = typeof doc.title === "string" ? doc.title : null;
144
+ const docId = typeof doc.document_id === "number"
145
+ ? String(doc.document_id)
146
+ : typeof doc.document_id === "string"
147
+ ? doc.document_id
148
+ : null;
149
+ if (!docUrl && !docTitle) {
150
+ return null;
151
+ }
152
+ sourceCounter.value++;
153
+ return {
154
+ id: docId || String(sourceCounter.value),
155
+ url: docUrl || "",
156
+ title: docTitle || docUrl || "Document",
157
+ toolCallId,
158
+ ...(docUrl && { favicon: getFaviconUrl(docUrl) }),
159
+ ...(docUrl && { sourceName: getSourceName(docUrl) }),
160
+ };
161
+ };
162
+ // Extract from single document fields
163
+ const singleDoc = extractDocSource(actualOutput);
164
+ if (singleDoc) {
165
+ sources.push(singleDoc);
166
+ }
167
+ // Extract from documents array
168
+ const documents = actualOutput.documents;
169
+ if (Array.isArray(documents)) {
170
+ for (const doc of documents) {
171
+ if (doc && typeof doc === "object") {
172
+ const docSource = extractDocSource(doc);
173
+ if (docSource) {
174
+ sources.push(docSource);
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ return sources;
181
+ }
13
182
  /**
14
183
  * Name of the Task tool created by makeSubagentsTool
15
184
  */
@@ -211,6 +380,7 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
211
380
  // Consume stream, accumulate results, and emit incremental updates
212
381
  let responseText = "";
213
382
  const collectedSources = [];
383
+ const sourceCounter = { value: 0 }; // Mutable counter for source ID generation
214
384
  const currentMessage = {
215
385
  id: `subagent-${Date.now()}`,
216
386
  content: "",
@@ -218,6 +388,7 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
218
388
  toolCalls: [],
219
389
  };
220
390
  const toolCallMap = new Map();
391
+ const toolNameMap = new Map(); // Map toolCallId -> toolName
221
392
  const queryHash = hashQuery(query);
222
393
  logger.info("[DEBUG] Starting subagent generator loop", {
223
394
  agentName,
@@ -249,15 +420,23 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
249
420
  // Handle tool_call
250
421
  if (update.sessionUpdate === "tool_call" && update.toolCallId) {
251
422
  const meta = update._meta;
423
+ // Extract rawInput from the update
424
+ const rawInput = update
425
+ .rawInput;
252
426
  const toolCall = {
253
427
  id: update.toolCallId,
254
428
  title: update.title || "Tool call",
255
429
  prettyName: meta?.prettyName,
256
430
  icon: meta?.icon,
257
431
  status: update.status || "pending",
432
+ rawInput,
258
433
  };
259
434
  currentMessage.toolCalls.push(toolCall);
260
435
  toolCallMap.set(update.toolCallId, currentMessage.toolCalls.length - 1);
436
+ // Store tool name for source extraction when output arrives
437
+ if (update.title) {
438
+ toolNameMap.set(update.toolCallId, update.title);
439
+ }
261
440
  currentMessage.contentBlocks.push({ type: "tool_call", toolCall });
262
441
  shouldEmit = true; // Emit when new tool call appears
263
442
  }
@@ -276,9 +455,41 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
276
455
  shouldEmit = true; // Emit when tool status changes
277
456
  }
278
457
  }
458
+ // Handle tool_output - capture rawOutput and extract sources
459
+ // (rawOutput will be included in final emit when subagent completes)
460
+ if (update.sessionUpdate === "tool_output" && update.toolCallId) {
461
+ const outputUpdate = update;
462
+ const idx = toolCallMap.get(update.toolCallId);
463
+ if (idx !== undefined && currentMessage.toolCalls[idx]) {
464
+ if (outputUpdate.rawOutput) {
465
+ currentMessage.toolCalls[idx].rawOutput = outputUpdate.rawOutput;
466
+ // Also update the content block
467
+ const block = currentMessage.contentBlocks.find((b) => b.type === "tool_call" && b.toolCall.id === update.toolCallId);
468
+ if (block) {
469
+ block.toolCall.rawOutput = outputUpdate.rawOutput;
470
+ }
471
+ // Extract citation sources from tool output (WebSearch, WebFetch, library tools)
472
+ const toolName = toolNameMap.get(update.toolCallId) || "";
473
+ const extractedSources = extractSourcesFromToolOutput(toolName, outputUpdate.rawOutput, update.toolCallId, sourceCounter);
474
+ if (extractedSources.length > 0) {
475
+ collectedSources.push(...extractedSources);
476
+ logger.info("Extracted sources from subagent tool output", {
477
+ toolName,
478
+ toolCallId: update.toolCallId,
479
+ sourcesCount: extractedSources.length,
480
+ });
481
+ shouldEmit = true; // Emit when sources are extracted
482
+ }
483
+ }
484
+ // NOTE: Don't set shouldEmit for rawOutput alone - rawOutput is large and will be
485
+ // included in the final emit when the subagent completes
486
+ }
487
+ }
279
488
  // Handle sources (from ACP protocol)
280
- if ("sources" in update && Array.isArray(update.sources)) {
281
- const sources = update.sources;
489
+ if ("sources" in update &&
490
+ Array.isArray(update.sources)) {
491
+ const sources = update
492
+ .sources;
282
493
  for (const source of sources) {
283
494
  const citationSource = {
284
495
  id: source.id,
@@ -305,7 +516,23 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
305
516
  contentBlocksCount: currentMessage.contentBlocks.length,
306
517
  toolCallsCount: currentMessage.toolCalls.length,
307
518
  });
308
- emitSubagentMessages(queryHash, [{ ...currentMessage }]);
519
+ // Strip rawOutput from streamed messages to avoid OOM in browser
520
+ // rawOutput is preserved in currentMessage for final session save
521
+ const streamMessage = {
522
+ ...currentMessage,
523
+ toolCalls: currentMessage.toolCalls.map((tc) => {
524
+ const { rawOutput: _rawOutput, ...rest } = tc;
525
+ return rest;
526
+ }),
527
+ contentBlocks: currentMessage.contentBlocks.map((block) => {
528
+ if (block.type === "tool_call") {
529
+ const { rawOutput: _rawOutput, ...rest } = block.toolCall;
530
+ return { type: "tool_call", toolCall: rest };
531
+ }
532
+ return block;
533
+ }),
534
+ };
535
+ emitSubagentMessages(queryHash, [streamMessage]);
309
536
  }
310
537
  }
311
538
  logger.info("[DEBUG] Subagent generator loop finished", {
@@ -74,16 +74,21 @@ export async function generateIndexTs(vars) {
74
74
  if (vars.hooks) {
75
75
  agentDef.hooks = vars.hooks;
76
76
  }
77
- return prettier.format(`import { makeHttpTransport, makeStdioTransport } from "@townco/agent/acp-server";
78
- import type { AgentDefinition } from "@townco/agent/definition";
79
- import { createLogger } from "@townco/agent/logger";
80
- import { basename } from "node:path";
81
-
82
- const logger = createLogger("agent-index");
77
+ return prettier.format(`import type { AgentDefinition } from "@townco/agent/definition";
83
78
 
84
- // Load agent definition from JSON file
85
79
  const agent: AgentDefinition = ${JSON.stringify(agentDef)};
86
80
 
81
+ export default agent;
82
+ `, { parser: "typescript" });
83
+ }
84
+ export function generateBinTs() {
85
+ return `#!/usr/bin/env bun
86
+ import { basename } from "node:path";
87
+ import { makeHttpTransport, makeStdioTransport } from "@townco/agent/acp-server";
88
+ import { createLogger } from "@townco/agent/logger";
89
+ import agent from "./index";
90
+
91
+ const logger = createLogger("agent-index");
87
92
  const transport = process.argv[2] || "stdio";
88
93
 
89
94
  // Get agent directory and name for session storage
@@ -93,18 +98,13 @@ const agentName = basename(agentDir);
93
98
  logger.info("Configuration", { transport, agentDir, agentName });
94
99
 
95
100
  if (transport === "http") {
96
- makeHttpTransport(agent, agentDir, agentName);
101
+ makeHttpTransport(agent, agentDir, agentName);
97
102
  } else if (transport === "stdio") {
98
- makeStdioTransport(agent);
103
+ makeStdioTransport(agent);
99
104
  } else {
100
- logger.error(\`Invalid transport: \${transport}\`);
101
- process.exit(1);
105
+ logger.error(\`Invalid transport: \${transport}\`);
106
+ process.exit(1);
102
107
  }
103
- `, { parser: "typescript" });
104
- }
105
- export function generateBinTs() {
106
- return `#!/usr/bin/env bun
107
- import "./index.ts";
108
108
  `;
109
109
  }
110
110
  export function generateGitignore() {