@townco/agent 0.1.82 → 0.1.83

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,3 +1,5 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import * as path from "node:path";
1
3
  import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
4
  import { context, propagation, trace } from "@opentelemetry/api";
3
5
  import { getShedAuth } from "@townco/core/auth";
@@ -6,12 +8,14 @@ import { z } from "zod";
6
8
  import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
7
9
  import { createLogger } from "../../logger.js";
8
10
  import { telemetry } from "../../telemetry/index.js";
11
+ import { bindGeneratorToSessionContext } from "../session-context";
9
12
  import { loadCustomToolModule, } from "../tool-loader.js";
10
13
  import { createModelFromString, detectProvider } from "./model-factory.js";
11
14
  import { makeOtelCallbacks } from "./otel-callbacks.js";
15
+ import { makeArtifactsTools } from "./tools/artifacts";
12
16
  import { makeBrowserTools } from "./tools/browser";
13
17
  import { makeFilesystemTools } from "./tools/filesystem";
14
- import { makeGenerateImageTool } from "./tools/generate_image";
18
+ import { makeGenerateImageTool, makeTownGenerateImageTool, } from "./tools/generate_image";
15
19
  import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
16
20
  import { hashQuery, queryToToolCallId, subagentEvents, } from "./tools/subagent-connections";
17
21
  import { makeTodoWriteTool, TODO_WRITE_TOOL_NAME } from "./tools/todo";
@@ -24,15 +28,19 @@ const getWeather = tool(({ city }) => `It's always sunny in ${city}!`, {
24
28
  city: z.string(),
25
29
  }),
26
30
  });
31
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
27
32
  getWeather.prettyName = "Get Weather";
33
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
28
34
  getWeather.icon = "Cloud";
29
35
  export const TOOL_REGISTRY = {
36
+ artifacts: () => makeArtifactsTools(),
30
37
  todo_write: () => makeTodoWriteTool(), // Factory function to create fresh instance per invocation
31
38
  get_weather: getWeather, // TODO: Convert to factory function for full concurrency safety
32
39
  web_search: () => makeWebSearchTools(),
33
40
  town_web_search: () => makeTownWebSearchTools(),
34
- filesystem: () => makeFilesystemTools(process.cwd()),
41
+ filesystem: () => makeFilesystemTools(),
35
42
  generate_image: () => makeGenerateImageTool(),
43
+ town_generate_image: () => makeTownGenerateImageTool(),
36
44
  browser: () => makeBrowserTools(),
37
45
  };
38
46
  // ============================================================================
@@ -44,7 +52,9 @@ function toLangchainTool(resolved) {
44
52
  description: resolved.description,
45
53
  schema: resolved.schema,
46
54
  });
55
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
47
56
  t.prettyName = resolved.prettyName;
57
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
48
58
  t.icon = resolved.icon;
49
59
  return t;
50
60
  }
@@ -70,6 +80,26 @@ export class LangchainAgent {
70
80
  return telemetry.bindGeneratorToContext(sessionAttributes, generator);
71
81
  }
72
82
  async *invokeInternal(req) {
83
+ // Set up session context for session-scoped file storage
84
+ // This allows tools to access getSessionContext() / getToolOutputDir()
85
+ if (req.agentDir && req.sessionId) {
86
+ const sessionDir = path.join(req.agentDir, ".sessions", req.sessionId);
87
+ const artifactsDir = path.join(sessionDir, "artifacts");
88
+ // Ensure artifacts directory exists
89
+ await mkdir(artifactsDir, { recursive: true });
90
+ // Bind the generator to session context so every iteration has access
91
+ const sessionContext = {
92
+ sessionId: req.sessionId,
93
+ sessionDir,
94
+ artifactsDir,
95
+ };
96
+ const boundGenerator = bindGeneratorToSessionContext(sessionContext, this.invokeWithContext(req));
97
+ return yield* boundGenerator;
98
+ }
99
+ // Fallback: no session context (e.g., missing agentDir)
100
+ return yield* this.invokeWithContext(req);
101
+ }
102
+ async *invokeWithContext(req) {
73
103
  // Derive the parent OTEL context for this invocation.
74
104
  // If this is a subagent and the parent process propagated an OTEL trace
75
105
  // context via sessionMeta.otelTraceContext, use that as the parent;
@@ -136,6 +166,7 @@ export class LangchainAgent {
136
166
  // Helper to get next subagent update (returns immediately if queued, otherwise waits)
137
167
  const waitForSubagentUpdate = () => {
138
168
  if (subagentUpdateQueue.length > 0) {
169
+ // biome-ignore lint/style/noNonNullAssertion: We check length > 0, so shift() will return a value
139
170
  return Promise.resolve(subagentUpdateQueue.shift());
140
171
  }
141
172
  return new Promise((resolve) => {
@@ -146,6 +177,8 @@ export class LangchainAgent {
146
177
  async function* yieldPendingSubagentUpdates() {
147
178
  while (subagentUpdateQueue.length > 0) {
148
179
  const update = subagentUpdateQueue.shift();
180
+ if (!update)
181
+ continue;
149
182
  _logger.info("Yielding queued subagent connection update", {
150
183
  toolCallId: update.toolCallId,
151
184
  subagentPort: update.port,
@@ -164,6 +197,8 @@ export class LangchainAgent {
164
197
  // Also yield any pending messages updates
165
198
  while (subagentMessagesQueue.length > 0) {
166
199
  const messagesUpdate = subagentMessagesQueue.shift();
200
+ if (!messagesUpdate)
201
+ continue;
167
202
  _logger.info("Yielding queued subagent messages update", {
168
203
  toolCallId: messagesUpdate.toolCallId,
169
204
  messageCount: messagesUpdate.messages.length,
@@ -246,11 +281,11 @@ export class LangchainAgent {
246
281
  customToolPaths.push(t.modulePath);
247
282
  }
248
283
  else if (type === "filesystem") {
249
- const wd = t.working_directory ??
250
- process.cwd();
251
- const fsTools = makeFilesystemTools(wd);
284
+ // Note: working_directory is ignored - filesystem tools now use session context
285
+ const fsTools = makeFilesystemTools();
252
286
  // Tag filesystem tools with their group name for tool override filtering
253
287
  for (const fsTool of fsTools) {
288
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom property for tool grouping
254
289
  fsTool.__groupName = "filesystem";
255
290
  }
256
291
  enabledTools.push(...fsTools);
@@ -263,7 +298,9 @@ export class LangchainAgent {
263
298
  description: t.description,
264
299
  schema: t.schema,
265
300
  });
301
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
266
302
  addedTool.prettyName = t.prettyName;
303
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
267
304
  addedTool.icon = t.icon;
268
305
  enabledTools.push(addedTool);
269
306
  }
@@ -279,6 +316,7 @@ export class LangchainAgent {
279
316
  // Track which built-in group produced this tool so overrides can
280
317
  // filter by the original config name (e.g. "web_search" filters
281
318
  // both WebSearch and WebFetch helpers).
319
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom property for tool grouping
282
320
  tool.__groupName = name;
283
321
  };
284
322
  if (typeof entry === "function") {
@@ -418,13 +456,17 @@ export class LangchainAgent {
418
456
  return result;
419
457
  };
420
458
  // Create new tool with wrapped function
459
+ // biome-ignore lint/suspicious/noExplicitAny: Need to pass function with dynamic signature
421
460
  const wrappedTool = tool(wrappedFunc, {
422
461
  name: originalTool.name,
423
462
  description: originalTool.description,
463
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing internal schema property
424
464
  schema: originalTool.schema,
425
465
  });
426
466
  // Preserve metadata
467
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
427
468
  wrappedTool.prettyName = originalTool.prettyName;
469
+ // biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
428
470
  wrappedTool.icon = originalTool.icon;
429
471
  return wrappedTool;
430
472
  });
@@ -434,15 +476,16 @@ export class LangchainAgent {
434
476
  const filteredTools = isSubagent
435
477
  ? wrappedTools.filter((t) => t.name !== TODO_WRITE_TOOL_NAME && t.name !== SUBAGENT_TOOL_NAME)
436
478
  : wrappedTools;
437
- // Wrap tools with tracing so each tool executes within its own span context.
479
+ // Wrap tools with tracing and session_id injection (combined in one wrapper)
438
480
  // This ensures subagent spans are children of the Task tool span.
439
481
  // Pass the context getter so tools can nest under the current iteration span.
440
482
  let finalTools = filteredTools.map((t) => wrapToolWithTracing(t, otelCallbacks?.getCurrentIterationContext ??
441
- (() => invocationContext)));
483
+ (() => invocationContext), req.sessionId));
442
484
  // Apply tool overrides if provided (Town Hall comparison feature)
443
485
  if (req.configOverrides?.tools && req.configOverrides.tools.length > 0) {
444
486
  const allowedToolNames = new Set(req.configOverrides.tools);
445
487
  finalTools = finalTools.filter((t) => {
488
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing custom property for tool grouping
446
489
  const groupName = t.__groupName;
447
490
  return (allowedToolNames.has(groupName ?? "") ||
448
491
  allowedToolNames.has(t.name));
@@ -490,26 +533,28 @@ export class LangchainAgent {
490
533
  // LangChain uses image_url type with data URL format
491
534
  return blocks
492
535
  .map((block) => {
493
- if (block.type === "text") {
536
+ const typedBlock = block;
537
+ if (typedBlock.type === "text") {
494
538
  return {
495
539
  type: "text",
496
- text: block.text,
540
+ text: typedBlock.text,
497
541
  };
498
542
  }
499
- else if (block.type === "image") {
543
+ else if (typedBlock.type === "image") {
500
544
  // Extract base64 data and media type from various formats
501
545
  let base64Data;
502
546
  let mediaType = "image/png";
503
547
  // Check if it has the source format (Claude API format)
504
- if ("source" in block && block.source) {
505
- base64Data = block.source.data;
506
- mediaType = block.source.media_type || "image/png";
548
+ if ("source" in typedBlock && typedBlock.source) {
549
+ const source = typedBlock.source;
550
+ base64Data = source.data;
551
+ mediaType = source.media_type || "image/png";
507
552
  }
508
553
  // ACP format: { type: "image", data: "...", mimeType: "..." }
509
- else if ("data" in block && block.data) {
510
- base64Data = block.data;
511
- if (block.mimeType) {
512
- const mt = block.mimeType.toLowerCase();
554
+ else if ("data" in typedBlock && typedBlock.data) {
555
+ base64Data = typedBlock.data;
556
+ if (typedBlock.mimeType) {
557
+ const mt = typedBlock.mimeType.toLowerCase();
513
558
  if (mt === "image/jpeg" || mt === "image/jpg") {
514
559
  mediaType = "image/jpeg";
515
560
  }
@@ -536,7 +581,7 @@ export class LangchainAgent {
536
581
  }
537
582
  return null;
538
583
  })
539
- .filter(Boolean);
584
+ .filter((item) => item !== null);
540
585
  };
541
586
  if (req.contextMessages && req.contextMessages.length > 0) {
542
587
  // Use context messages (already resolved from context entries)
@@ -667,6 +712,7 @@ export class LangchainAgent {
667
712
  }
668
713
  // Also yield any queued subagent updates before processing stream item
669
714
  yield* yieldPendingSubagentUpdates();
715
+ // biome-ignore lint/suspicious/noExplicitAny: LangChain stream items are tuples with dynamic types
670
716
  const [streamMode, chunk] = streamItem;
671
717
  if (streamMode === "updates") {
672
718
  const updatesChunk = modelRequestSchema.safeParse(chunk);
@@ -741,8 +787,11 @@ export class LangchainAgent {
741
787
  // continue;
742
788
  //}
743
789
  const matchingTool = finalTools.find((t) => t.name === toolCall.name);
790
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing custom property on LangChain tool
744
791
  let prettyName = matchingTool?.prettyName;
792
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing custom property on LangChain tool
745
793
  const icon = matchingTool?.icon;
794
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing custom property on LangChain tool
746
795
  const verbiage = matchingTool?.verbiage;
747
796
  // For the Task tool, use the displayName (or agentName as fallback) as the prettyName
748
797
  if (toolCall.name === SUBAGENT_TOOL_NAME &&
@@ -756,6 +805,7 @@ export class LangchainAgent {
756
805
  const taskTool = this.definition.tools?.find((t) => typeof t === "object" &&
757
806
  t.type === "direct" &&
758
807
  t.name === SUBAGENT_TOOL_NAME);
808
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing custom property on tool definition
759
809
  const subagentConfigs = taskTool?.subagentConfigs;
760
810
  const subagentConfig = subagentConfigs?.find((config) => config.agentName === agentName);
761
811
  prettyName = subagentConfig?.displayName ?? agentName;
@@ -1172,16 +1222,33 @@ I've found some existing telemetry code. Let me mark the first todo as in_progre
1172
1222
  // Re-export subagent tool utility
1173
1223
  export { makeSubagentsTool } from "./tools/subagent.js";
1174
1224
  /**
1175
- * Wraps a LangChain tool with OpenTelemetry tracing.
1225
+ * Wraps a LangChain tool with OpenTelemetry tracing and session_id injection.
1176
1226
  * This ensures the tool executes within its own span context,
1177
1227
  * so any child operations (like subagent spawning) become children
1178
1228
  * of the tool span rather than the parent invocation span.
1179
1229
  * @param originalTool The tool to wrap
1180
1230
  * @param getIterationContext Function that returns the current iteration context
1231
+ * @param sessionId Optional session ID to inject for artifact tools
1181
1232
  */
1182
- function wrapToolWithTracing(originalTool, getIterationContext) {
1233
+ function wrapToolWithTracing(originalTool, getIterationContext, sessionId) {
1234
+ // Check if this tool needs session_id injection
1235
+ const TOOLS_NEEDING_SESSION_ID = [
1236
+ "artifacts_cp",
1237
+ "artifacts_del",
1238
+ "artifacts_ls",
1239
+ "artifacts_url",
1240
+ ];
1241
+ const needsSessionId = TOOLS_NEEDING_SESSION_ID.includes(originalTool.name);
1183
1242
  const wrappedFunc = async (input) => {
1184
- const toolInputJson = JSON.stringify(input);
1243
+ // Inject session_id if needed
1244
+ let finalInput = input;
1245
+ if (needsSessionId && sessionId) {
1246
+ finalInput = {
1247
+ ...input,
1248
+ session_id: sessionId,
1249
+ };
1250
+ }
1251
+ const toolInputJson = JSON.stringify(finalInput);
1185
1252
  // CRITICAL: Get the iteration context synchronously when the tool is invoked.
1186
1253
  // We must capture both the context AND the parent span at this moment.
1187
1254
  const iterationContext = getIterationContext();
@@ -1203,7 +1270,7 @@ function wrapToolWithTracing(originalTool, getIterationContext) {
1203
1270
  : iterationContext;
1204
1271
  try {
1205
1272
  // Execute within the tool span's context
1206
- const result = await context.with(spanContext, () => originalTool.invoke(input));
1273
+ const result = await context.with(spanContext, () => originalTool.invoke(finalInput));
1207
1274
  const resultStr = typeof result === "string" ? result : JSON.stringify(result);
1208
1275
  if (toolSpan) {
1209
1276
  telemetry.setSpanAttributes(toolSpan, {
@@ -1222,14 +1289,19 @@ function wrapToolWithTracing(originalTool, getIterationContext) {
1222
1289
  });
1223
1290
  };
1224
1291
  // Create new tool with wrapped function
1292
+ // biome-ignore lint/suspicious/noExplicitAny: Need to pass function with dynamic signature
1225
1293
  const wrappedTool = tool(wrappedFunc, {
1226
1294
  name: originalTool.name,
1227
1295
  description: originalTool.description,
1296
+ // biome-ignore lint/suspicious/noExplicitAny: Accessing internal schema property
1228
1297
  schema: originalTool.schema,
1229
1298
  });
1230
1299
  // Preserve metadata
1300
+ // biome-ignore lint/suspicious/noExplicitAny: Need to copy custom properties between LangChain tools
1231
1301
  wrappedTool.prettyName = originalTool.prettyName;
1302
+ // biome-ignore lint/suspicious/noExplicitAny: Need to copy custom properties between LangChain tools
1232
1303
  wrappedTool.icon = originalTool.icon;
1304
+ // biome-ignore lint/suspicious/noExplicitAny: Need to copy custom properties between LangChain tools
1233
1305
  wrappedTool.__groupName = originalTool.__groupName;
1234
1306
  return wrappedTool;
1235
1307
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Supabase Storage-backed artifacts tool for agent backend
3
+ *
4
+ * Provides file storage capabilities using Supabase Storage with the following operations:
5
+ * - artifacts_cp: Copy files to/from Supabase Storage
6
+ * - artifacts_del: Delete files from Supabase Storage
7
+ * - artifacts_ls: List files in Supabase Storage
8
+ * - artifacts_url: Generate signed URLs
9
+ *
10
+ * Storage keys are scoped by: <deploying_user>/<agent_name>/<session_id>/<file_path>
11
+ */
12
+ import { z } from "zod";
13
+ /**
14
+ * Factory function to create the artifacts tools
15
+ * Returns an array of all four artifact management tools
16
+ */
17
+ export declare function makeArtifactsTools(): (import("langchain").DynamicStructuredTool<z.ZodObject<{
18
+ session_id: z.ZodOptional<z.ZodString>;
19
+ source: z.ZodString;
20
+ destination: z.ZodString;
21
+ direction: z.ZodEnum<{
22
+ upload: "upload";
23
+ download: "download";
24
+ }>;
25
+ }, z.core.$strip>, {
26
+ session_id: string;
27
+ source: string;
28
+ destination: string;
29
+ direction: "upload" | "download";
30
+ }, {
31
+ source: string;
32
+ destination: string;
33
+ direction: "upload" | "download";
34
+ session_id?: string | undefined;
35
+ }, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
36
+ session_id: z.ZodOptional<z.ZodString>;
37
+ path: z.ZodString;
38
+ }, z.core.$strip>, {
39
+ session_id: string;
40
+ path: string;
41
+ }, {
42
+ path: string;
43
+ session_id?: string | undefined;
44
+ }, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
45
+ session_id: z.ZodOptional<z.ZodString>;
46
+ path: z.ZodOptional<z.ZodString>;
47
+ recursive: z.ZodOptional<z.ZodBoolean>;
48
+ }, z.core.$strip>, {
49
+ session_id: string;
50
+ path?: string;
51
+ recursive?: boolean;
52
+ }, {
53
+ session_id?: string | undefined;
54
+ path?: string | undefined;
55
+ recursive?: boolean | undefined;
56
+ }, string> | import("langchain").DynamicStructuredTool<z.ZodObject<{
57
+ session_id: z.ZodOptional<z.ZodString>;
58
+ path: z.ZodString;
59
+ expires_in: z.ZodOptional<z.ZodNumber>;
60
+ }, z.core.$strip>, {
61
+ session_id: string;
62
+ path: string;
63
+ expires_in?: number;
64
+ }, {
65
+ path: string;
66
+ session_id?: string | undefined;
67
+ expires_in?: number | undefined;
68
+ }, string>)[];