@townco/agent 0.1.117 → 0.1.119

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.
Files changed (30) hide show
  1. package/dist/acp-server/adapter.d.ts +3 -3
  2. package/dist/acp-server/adapter.js +101 -79
  3. package/dist/acp-server/http.js +28 -1
  4. package/dist/acp-server/session-storage.d.ts +27 -1
  5. package/dist/acp-server/session-storage.js +19 -1
  6. package/dist/definition/index.d.ts +4 -1
  7. package/dist/definition/models.d.ts +6 -0
  8. package/dist/definition/models.js +1 -0
  9. package/dist/mcp/index.d.ts +12 -0
  10. package/dist/mcp/index.js +83 -0
  11. package/dist/runner/hooks/predefined/compaction-tool.js +13 -1
  12. package/dist/runner/hooks/predefined/mid-turn-compaction.js +13 -1
  13. package/dist/runner/hooks/types.d.ts +6 -0
  14. package/dist/runner/index.d.ts +2 -66
  15. package/dist/runner/langchain/index.js +4 -4
  16. package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
  17. package/dist/runner/langchain/tools/artifacts.js +474 -0
  18. package/dist/runner/langchain/tools/conversation_search.d.ts +22 -0
  19. package/dist/runner/langchain/tools/conversation_search.js +137 -0
  20. package/dist/runner/langchain/tools/generate_image.d.ts +47 -0
  21. package/dist/runner/langchain/tools/generate_image.js +175 -0
  22. package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
  23. package/dist/runner/langchain/tools/port-utils.js +35 -0
  24. package/dist/runner/langchain/tools/subagent-connections.d.ts +2 -0
  25. package/dist/runner/langchain/tools/subagent.js +230 -3
  26. package/dist/templates/index.d.ts +5 -1
  27. package/dist/templates/index.js +22 -16
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +11 -7
  30. package/templates/index.ts +31 -20
@@ -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", {
@@ -1,4 +1,6 @@
1
- import type { AgentDefinition } from "../definition";
1
+ import type { z } from "zod";
2
+ import type { AgentDefinition, McpConfigSchema } from "../definition";
3
+ type McpConfig = z.infer<typeof McpConfigSchema>;
2
4
  export interface TemplateVars {
3
5
  name: string;
4
6
  model: string;
@@ -18,6 +20,7 @@ export interface TemplateVars {
18
20
  fn: any;
19
21
  schema: unknown;
20
22
  }>;
23
+ mcps?: McpConfig[];
21
24
  systemPrompt: string | null;
22
25
  hasWebSearch: boolean;
23
26
  hasGenerateImage: boolean;
@@ -44,3 +47,4 @@ export declare function generateGitignore(): string;
44
47
  export declare function generateTsConfig(): string;
45
48
  export declare function generateReadme(vars: TemplateVars): string;
46
49
  export declare function generateEnvExample(vars: TemplateVars): string | null;
50
+ export {};
@@ -20,6 +20,9 @@ export function getTemplateVars(name, definition) {
20
20
  if (definition.suggestedPrompts) {
21
21
  result.suggestedPrompts = definition.suggestedPrompts;
22
22
  }
23
+ if (definition.mcps && definition.mcps.length > 0) {
24
+ result.mcps = definition.mcps;
25
+ }
23
26
  return result;
24
27
  }
25
28
  export function generatePackageJson(vars) {
@@ -71,19 +74,27 @@ export async function generateIndexTs(vars) {
71
74
  }
72
75
  agentDef.systemPrompt = vars.systemPrompt;
73
76
  agentDef.tools = vars.tools;
77
+ if (vars.mcps && vars.mcps.length > 0) {
78
+ agentDef.mcps = vars.mcps;
79
+ }
74
80
  if (vars.hooks) {
75
81
  agentDef.hooks = vars.hooks;
76
82
  }
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";
83
+ return prettier.format(`import type { AgentDefinition } from "@townco/agent/definition";
81
84
 
82
- const logger = createLogger("agent-index");
83
-
84
- // Load agent definition from JSON file
85
85
  const agent: AgentDefinition = ${JSON.stringify(agentDef)};
86
86
 
87
+ export default agent;
88
+ `, { parser: "typescript" });
89
+ }
90
+ export function generateBinTs() {
91
+ return `#!/usr/bin/env bun
92
+ import { basename } from "node:path";
93
+ import { makeHttpTransport, makeStdioTransport } from "@townco/agent/acp-server";
94
+ import { createLogger } from "@townco/agent/logger";
95
+ import agent from "./index";
96
+
97
+ const logger = createLogger("agent-index");
87
98
  const transport = process.argv[2] || "stdio";
88
99
 
89
100
  // Get agent directory and name for session storage
@@ -93,18 +104,13 @@ const agentName = basename(agentDir);
93
104
  logger.info("Configuration", { transport, agentDir, agentName });
94
105
 
95
106
  if (transport === "http") {
96
- makeHttpTransport(agent, agentDir, agentName);
107
+ makeHttpTransport(agent, agentDir, agentName);
97
108
  } else if (transport === "stdio") {
98
- makeStdioTransport(agent);
109
+ makeStdioTransport(agent);
99
110
  } else {
100
- logger.error(\`Invalid transport: \${transport}\`);
101
- process.exit(1);
111
+ logger.error(\`Invalid transport: \${transport}\`);
112
+ process.exit(1);
102
113
  }
103
- `, { parser: "typescript" });
104
- }
105
- export function generateBinTs() {
106
- return `#!/usr/bin/env bun
107
- import "./index.ts";
108
114
  `;
109
115
  }
110
116
  export function generateGitignore() {