@townco/agent 0.1.54 → 0.1.56

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 (37) hide show
  1. package/dist/acp-server/adapter.d.ts +1 -0
  2. package/dist/acp-server/adapter.js +9 -1
  3. package/dist/acp-server/http.js +75 -6
  4. package/dist/acp-server/session-storage.d.ts +14 -0
  5. package/dist/acp-server/session-storage.js +47 -0
  6. package/dist/definition/index.d.ts +9 -0
  7. package/dist/definition/index.js +9 -0
  8. package/dist/index.js +1 -1
  9. package/dist/logger.d.ts +26 -0
  10. package/dist/logger.js +43 -0
  11. package/dist/runner/agent-runner.d.ts +5 -1
  12. package/dist/runner/agent-runner.js +2 -1
  13. package/dist/runner/hooks/executor.js +1 -1
  14. package/dist/runner/hooks/loader.js +1 -1
  15. package/dist/runner/hooks/predefined/compaction-tool.js +1 -1
  16. package/dist/runner/hooks/predefined/tool-response-compactor.js +1 -1
  17. package/dist/runner/langchain/index.js +19 -7
  18. package/dist/runner/langchain/model-factory.d.ts +2 -0
  19. package/dist/runner/langchain/model-factory.js +20 -1
  20. package/dist/runner/langchain/tools/browser.d.ts +100 -0
  21. package/dist/runner/langchain/tools/browser.js +412 -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.js +230 -127
  25. package/dist/runner/tools.d.ts +2 -2
  26. package/dist/runner/tools.js +1 -0
  27. package/dist/scaffold/index.js +7 -1
  28. package/dist/scaffold/templates/dot-claude/CLAUDE-append.md +2 -0
  29. package/dist/storage/index.js +1 -1
  30. package/dist/telemetry/index.d.ts +5 -0
  31. package/dist/telemetry/index.js +10 -0
  32. package/dist/templates/index.d.ts +2 -0
  33. package/dist/templates/index.js +30 -8
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/index.ts +1 -1
  36. package/package.json +11 -6
  37. package/templates/index.ts +40 -8
@@ -24,6 +24,7 @@ export declare class AgentAcpAdapter implements acp.Agent {
24
24
  private agentVersion;
25
25
  private agentDescription;
26
26
  private agentSuggestedPrompts;
27
+ private agentInitialMessage;
27
28
  private currentToolOverheadTokens;
28
29
  private currentMcpOverheadTokens;
29
30
  constructor(agent: AgentRunner, connection: acp.AgentSideConnection, agentDir?: string, agentName?: string);
@@ -1,6 +1,6 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import { context, trace } from "@opentelemetry/api";
3
- import { createLogger } from "@townco/core";
3
+ import { createLogger } from "../logger.js";
4
4
  import { HookExecutor, loadHookCallback } from "../runner/hooks";
5
5
  import { telemetry } from "../telemetry/index.js";
6
6
  import { calculateContextSize, } from "../utils/context-size-calculator.js";
@@ -105,6 +105,7 @@ export class AgentAcpAdapter {
105
105
  agentVersion;
106
106
  agentDescription;
107
107
  agentSuggestedPrompts;
108
+ agentInitialMessage;
108
109
  currentToolOverheadTokens = 0; // Track tool overhead for current turn
109
110
  currentMcpOverheadTokens = 0; // Track MCP overhead for current turn
110
111
  constructor(agent, connection, agentDir, agentName) {
@@ -117,6 +118,7 @@ export class AgentAcpAdapter {
117
118
  this.agentVersion = agent.definition.version;
118
119
  this.agentDescription = agent.definition.description;
119
120
  this.agentSuggestedPrompts = agent.definition.suggestedPrompts;
121
+ this.agentInitialMessage = agent.definition.initialMessage;
120
122
  this.noSession = process.env.TOWN_NO_SESSION === "true";
121
123
  this.storage =
122
124
  agentDir && agentName && !this.noSession
@@ -129,6 +131,7 @@ export class AgentAcpAdapter {
129
131
  agentVersion: this.agentVersion,
130
132
  agentDescription: this.agentDescription,
131
133
  suggestedPrompts: this.agentSuggestedPrompts,
134
+ initialMessage: this.agentInitialMessage,
132
135
  noSession: this.noSession,
133
136
  hasStorage: this.storage !== null,
134
137
  sessionStoragePath: this.storage ? `${agentDir}/.sessions` : null,
@@ -259,6 +262,9 @@ export class AgentAcpAdapter {
259
262
  ...(this.agentSuggestedPrompts
260
263
  ? { suggestedPrompts: this.agentSuggestedPrompts }
261
264
  : {}),
265
+ ...(this.agentInitialMessage
266
+ ? { initialMessage: this.agentInitialMessage }
267
+ : {}),
262
268
  ...(toolsMetadata.length > 0 ? { tools: toolsMetadata } : {}),
263
269
  ...(mcpsMetadata.length > 0 ? { mcps: mcpsMetadata } : {}),
264
270
  ...(subagentsMetadata.length > 0 ? { subagents: subagentsMetadata } : {}),
@@ -273,6 +279,8 @@ export class AgentAcpAdapter {
273
279
  context: [],
274
280
  requestParams: params,
275
281
  });
282
+ // Note: Initial message is sent by the HTTP transport when SSE connection is established
283
+ // This ensures the message is delivered after the client is ready to receive it
276
284
  return {
277
285
  sessionId,
278
286
  };
@@ -3,13 +3,15 @@ import { join, resolve } from "node:path";
3
3
  import { gzipSync } from "node:zlib";
4
4
  import * as acp from "@agentclientprotocol/sdk";
5
5
  import { PGlite } from "@electric-sql/pglite";
6
- import { configureLogsDir, createLogger } from "@townco/core";
6
+ import { configureLogsDir } from "@townco/core";
7
7
  import { Hono } from "hono";
8
8
  import { cors } from "hono/cors";
9
9
  import { streamSSE } from "hono/streaming";
10
+ import { createLogger, isSubagent } from "../logger.js";
10
11
  import { makeRunnerFromDefinition } from "../runner";
11
12
  import { AgentAcpAdapter } from "./adapter";
12
- const logger = createLogger("agent");
13
+ import { SessionStorage } from "./session-storage";
14
+ const logger = createLogger("http");
13
15
  /**
14
16
  * Compress a payload using gzip if it's too large for PostgreSQL NOTIFY
15
17
  * Returns an object with the payload and metadata about compression
@@ -49,11 +51,16 @@ function safeChannelName(prefix, id) {
49
51
  return `${prefix}_${hash}`;
50
52
  }
51
53
  export function makeHttpTransport(agent, agentDir, agentName) {
52
- // Configure logger to write to .logs/ directory if agentDir is provided
53
- if (agentDir) {
54
- const logsDir = join(agentDir, ".logs");
54
+ // Configure logger to write to .logs/ directory
55
+ // Use TOWN_LOGS_DIR env var if set (for subagents), otherwise use agentDir
56
+ const logsDir = process.env.TOWN_LOGS_DIR ||
57
+ (agentDir ? join(agentDir, ".logs") : undefined);
58
+ if (logsDir) {
55
59
  configureLogsDir(logsDir);
56
- logger.info("Configured logs directory", { logsDir });
60
+ logger.info("Configured logs directory", {
61
+ logsDir,
62
+ isSubagent: isSubagent(),
63
+ });
57
64
  }
58
65
  const inbound = new TransformStream();
59
66
  const outbound = new TransformStream();
@@ -63,6 +70,10 @@ export function makeHttpTransport(agent, agentDir, agentName) {
63
70
  const app = new Hono();
64
71
  // Track active SSE streams by sessionId for direct output delivery
65
72
  const sseStreams = new Map();
73
+ // Track sessions that have already received initial message
74
+ const initialMessageSentSessions = new Set();
75
+ // Get initial message config from agent definition
76
+ const initialMessageConfig = agentRunner.definition.initialMessage;
66
77
  const decoder = new TextDecoder();
67
78
  const encoder = new TextEncoder();
68
79
  (async () => {
@@ -264,6 +275,28 @@ export function makeHttpTransport(agent, agentDir, agentName) {
264
275
  allowMethods: ["GET", "POST", "OPTIONS"],
265
276
  }));
266
277
  app.get("/health", (c) => c.json({ ok: true }));
278
+ // List available sessions
279
+ app.get("/sessions", async (c) => {
280
+ if (!agentDir || !agentName) {
281
+ return c.json({ sessions: [], error: "Session storage not configured" });
282
+ }
283
+ const noSession = process.env.TOWN_NO_SESSION === "true";
284
+ if (noSession) {
285
+ return c.json({ sessions: [], error: "Sessions disabled" });
286
+ }
287
+ try {
288
+ const storage = new SessionStorage(agentDir, agentName);
289
+ const sessions = await storage.listSessionsWithMetadata();
290
+ return c.json({ sessions });
291
+ }
292
+ catch (error) {
293
+ logger.error("Failed to list sessions", { error });
294
+ return c.json({
295
+ sessions: [],
296
+ error: error instanceof Error ? error.message : String(error),
297
+ }, 500);
298
+ }
299
+ });
267
300
  // Serve static files from agent directory (for generated images, etc.)
268
301
  if (agentDir) {
269
302
  app.get("/static/*", async (c) => {
@@ -313,6 +346,42 @@ export function makeHttpTransport(agent, agentDir, agentName) {
313
346
  // Register this stream for direct tool output delivery
314
347
  sseStreams.set(sessionId, stream);
315
348
  await stream.writeSSE({ event: "ping", data: "{}" });
349
+ // Send initial message if configured and not already sent for this session
350
+ if (initialMessageConfig?.enabled &&
351
+ initialMessageConfig.content &&
352
+ !initialMessageSentSessions.has(sessionId)) {
353
+ initialMessageSentSessions.add(sessionId);
354
+ // Process template variables in the content
355
+ let content = initialMessageConfig.content;
356
+ content = content.replace(/\{\{\.AgentName\}\}/g, agentName ?? "Agent");
357
+ const displayName = agentRunner.definition.displayName;
358
+ content = content.replace(/\{\{\.DisplayName\}\}/g, displayName ?? agentName ?? "Agent");
359
+ const initialMessage = {
360
+ jsonrpc: "2.0",
361
+ method: "session/update",
362
+ params: {
363
+ sessionId,
364
+ update: {
365
+ sessionUpdate: "agent_message_chunk",
366
+ content: {
367
+ type: "text",
368
+ text: content,
369
+ },
370
+ _meta: {
371
+ isInitialMessage: true,
372
+ },
373
+ },
374
+ },
375
+ };
376
+ await stream.writeSSE({
377
+ event: "message",
378
+ data: JSON.stringify(initialMessage),
379
+ });
380
+ logger.info("Sent initial message via SSE", {
381
+ sessionId,
382
+ contentPreview: content.slice(0, 100),
383
+ });
384
+ }
316
385
  const hb = setInterval(() => {
317
386
  // Heartbeat to keep proxies from terminating idle connections
318
387
  void stream.writeSSE({ event: "ping", data: "{}" });
@@ -150,4 +150,18 @@ export declare class SessionStorage {
150
150
  * List all session IDs
151
151
  */
152
152
  listSessions(): Promise<string[]>;
153
+ /**
154
+ * Session summary for listing
155
+ */
156
+ /**
157
+ * List all sessions with metadata
158
+ * Returns sessions sorted by updatedAt (most recent first)
159
+ */
160
+ listSessionsWithMetadata(): Promise<Array<{
161
+ sessionId: string;
162
+ createdAt: string;
163
+ updatedAt: string;
164
+ messageCount: number;
165
+ firstUserMessage?: string;
166
+ }>>;
153
167
  }
@@ -236,4 +236,51 @@ export class SessionStorage {
236
236
  throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
237
237
  }
238
238
  }
239
+ /**
240
+ * Session summary for listing
241
+ */
242
+ /**
243
+ * List all sessions with metadata
244
+ * Returns sessions sorted by updatedAt (most recent first)
245
+ */
246
+ async listSessionsWithMetadata() {
247
+ if (!existsSync(this.sessionsDir)) {
248
+ return [];
249
+ }
250
+ try {
251
+ const files = readdirSync(this.sessionsDir);
252
+ const sessionFiles = files.filter((file) => file.endsWith(".json") && !file.endsWith(".tmp"));
253
+ const sessions = [];
254
+ for (const file of sessionFiles) {
255
+ const sessionId = file.replace(".json", "");
256
+ try {
257
+ const session = this.loadSessionSync(sessionId);
258
+ if (session) {
259
+ // Find the first user message for preview
260
+ const firstUserMsg = session.messages.find((m) => m.role === "user");
261
+ const firstUserText = firstUserMsg?.content.find((c) => c.type === "text");
262
+ const entry = {
263
+ sessionId: session.sessionId,
264
+ createdAt: session.metadata.createdAt,
265
+ updatedAt: session.metadata.updatedAt,
266
+ messageCount: session.messages.length,
267
+ };
268
+ if (firstUserText && "text" in firstUserText) {
269
+ entry.firstUserMessage = firstUserText.text.slice(0, 100);
270
+ }
271
+ sessions.push(entry);
272
+ }
273
+ }
274
+ catch {
275
+ // Skip invalid sessions
276
+ }
277
+ }
278
+ // Sort by updatedAt, most recent first
279
+ sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
280
+ return sessions;
281
+ }
282
+ catch (error) {
283
+ throw new Error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
284
+ }
285
+ }
239
286
  }
@@ -26,6 +26,11 @@ export declare const HookConfigSchema: z.ZodObject<{
26
26
  }, z.core.$strip>]>>;
27
27
  callback: z.ZodString;
28
28
  }, z.core.$strip>;
29
+ /** Initial message configuration schema. */
30
+ export declare const InitialMessageSchema: z.ZodObject<{
31
+ enabled: z.ZodBoolean;
32
+ content: z.ZodString;
33
+ }, z.core.$strip>;
29
34
  /** Agent definition schema. */
30
35
  export declare const AgentDefinitionSchema: z.ZodObject<{
31
36
  displayName: z.ZodOptional<z.ZodString>;
@@ -74,4 +79,8 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
74
79
  }, z.core.$strip>]>>;
75
80
  callback: z.ZodString;
76
81
  }, z.core.$strip>>>;
82
+ initialMessage: z.ZodOptional<z.ZodObject<{
83
+ enabled: z.ZodBoolean;
84
+ content: z.ZodString;
85
+ }, z.core.$strip>>;
77
86
  }, z.core.$strip>;
@@ -71,6 +71,13 @@ export const HookConfigSchema = z.object({
71
71
  .optional(),
72
72
  callback: z.string(),
73
73
  });
74
+ /** Initial message configuration schema. */
75
+ export const InitialMessageSchema = z.object({
76
+ /** Whether the agent should send an initial message when a session starts. */
77
+ enabled: z.boolean(),
78
+ /** The content of the initial message to send. Supports template variables like {{.AgentName}}. */
79
+ content: z.string(),
80
+ });
74
81
  /** Agent definition schema. */
75
82
  export const AgentDefinitionSchema = z.object({
76
83
  /** Human-readable display name for the agent (shown in UI). */
@@ -84,4 +91,6 @@ export const AgentDefinitionSchema = z.object({
84
91
  mcps: z.array(McpConfigSchema).optional(),
85
92
  harnessImplementation: z.literal("langchain").optional(),
86
93
  hooks: z.array(HookConfigSchema).optional(),
94
+ /** Configuration for an initial message the agent sends when a session starts. */
95
+ initialMessage: InitialMessageSchema.optional(),
87
96
  });
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { basename } from "node:path";
2
- import { createLogger } from "@townco/core";
3
2
  import { makeHttpTransport, makeStdioTransport } from "./acp-server";
3
+ import { createLogger } from "./logger.js";
4
4
  import { initializeOpenTelemetryFromEnv } from "./telemetry/setup.js";
5
5
  import { makeSubagentsTool } from "./utils";
6
6
  // Re-export telemetry configuration for library users
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Logger utilities for the agent package.
3
+ * Provides subagent-aware service name generation.
4
+ */
5
+ import { createLogger as coreCreateLogger } from "@townco/core";
6
+ /**
7
+ * Check if running as a subagent (detected via TOWN_LOGS_DIR env var)
8
+ */
9
+ export declare function isSubagent(): boolean;
10
+ /**
11
+ * Create a logger with subagent-aware service name.
12
+ * When running as a subagent, the service name is prefixed with "subagent:{port}:{name}:".
13
+ *
14
+ * @param service - The service name (e.g., "adapter", "hook-executor")
15
+ * @returns A logger instance with the appropriate service name
16
+ *
17
+ * @example
18
+ * // In main agent: creates logger with service "adapter"
19
+ * // In subagent on port 4001 named "researcher": creates logger with service "subagent:4001:researcher:adapter"
20
+ * const logger = createLogger("adapter");
21
+ */
22
+ export declare function createLogger(service: string): import("@townco/core").Logger;
23
+ /**
24
+ * Re-export the core createLogger for cases where subagent prefix is not wanted
25
+ */
26
+ export { coreCreateLogger as createCoreLogger };
package/dist/logger.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Logger utilities for the agent package.
3
+ * Provides subagent-aware service name generation.
4
+ */
5
+ import { createLogger as coreCreateLogger } from "@townco/core";
6
+ /**
7
+ * Check if running as a subagent (detected via TOWN_LOGS_DIR env var)
8
+ */
9
+ export function isSubagent() {
10
+ return !!process.env.TOWN_LOGS_DIR;
11
+ }
12
+ /**
13
+ * Get the subagent prefix if running as a subagent.
14
+ * Returns format: "subagent:{port}:{name}:" or empty string if not a subagent.
15
+ */
16
+ function getSubagentPrefix() {
17
+ if (!isSubagent()) {
18
+ return "";
19
+ }
20
+ const port = process.env.PORT || "unknown";
21
+ const name = process.env.TOWN_SUBAGENT_NAME || "agent";
22
+ return `subagent:${port}:${name}:`;
23
+ }
24
+ /**
25
+ * Create a logger with subagent-aware service name.
26
+ * When running as a subagent, the service name is prefixed with "subagent:{port}:{name}:".
27
+ *
28
+ * @param service - The service name (e.g., "adapter", "hook-executor")
29
+ * @returns A logger instance with the appropriate service name
30
+ *
31
+ * @example
32
+ * // In main agent: creates logger with service "adapter"
33
+ * // In subagent on port 4001 named "researcher": creates logger with service "subagent:4001:researcher:adapter"
34
+ * const logger = createLogger("adapter");
35
+ */
36
+ export function createLogger(service) {
37
+ const prefix = getSubagentPrefix();
38
+ return coreCreateLogger(`${prefix}${service}`);
39
+ }
40
+ /**
41
+ * Re-export the core createLogger for cases where subagent prefix is not wanted
42
+ */
43
+ export { coreCreateLogger as createCoreLogger };
@@ -8,7 +8,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
8
8
  suggestedPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
9
9
  systemPrompt: z.ZodNullable<z.ZodString>;
10
10
  model: z.ZodString;
11
- tools: z.ZodOptional<z.ZodArray<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.ZodObject<{
11
+ tools: z.ZodOptional<z.ZodArray<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<{
12
12
  type: z.ZodLiteral<"custom">;
13
13
  modulePath: z.ZodString;
14
14
  }, z.core.$strip>, z.ZodObject<{
@@ -52,6 +52,10 @@ export declare const zAgentRunnerParams: z.ZodObject<{
52
52
  }, z.core.$strip>]>>;
53
53
  callback: z.ZodString;
54
54
  }, z.core.$strip>>>;
55
+ initialMessage: z.ZodOptional<z.ZodObject<{
56
+ enabled: z.ZodBoolean;
57
+ content: z.ZodString;
58
+ }, z.core.$strip>>;
55
59
  }, z.core.$strip>;
56
60
  export type CreateAgentRunnerParams = z.infer<typeof zAgentRunnerParams>;
57
61
  export interface SessionMessage {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { HookConfigSchema, McpConfigSchema } from "../definition";
2
+ import { HookConfigSchema, InitialMessageSchema, McpConfigSchema, } from "../definition";
3
3
  import { zToolType } from "./tools";
4
4
  export const zAgentRunnerParams = z.object({
5
5
  displayName: z.string().optional(),
@@ -11,4 +11,5 @@ export const zAgentRunnerParams = z.object({
11
11
  tools: z.array(zToolType).optional(),
12
12
  mcps: z.array(McpConfigSchema).optional(),
13
13
  hooks: z.array(HookConfigSchema).optional(),
14
+ initialMessage: InitialMessageSchema.optional(),
14
15
  });
@@ -1,4 +1,4 @@
1
- import { createLogger } from "@townco/core";
1
+ import { createLogger } from "../../logger.js";
2
2
  import { DEFAULT_CONTEXT_SIZE, MODEL_CONTEXT_WINDOWS } from "./constants";
3
3
  const logger = createLogger("hook-executor");
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import { resolve } from "node:path";
2
- import { createLogger } from "@townco/core";
2
+ import { createLogger } from "../../logger.js";
3
3
  import { getPredefinedHook, isPredefinedHook } from "./registry";
4
4
  const logger = createLogger("hook-loader");
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import { ChatAnthropic } from "@langchain/anthropic";
2
2
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
3
- import { createLogger } from "@townco/core";
3
+ import { createLogger } from "../../../logger.js";
4
4
  import { createContextEntry, createFullMessageEntry, } from "../types";
5
5
  const logger = createLogger("compaction-tool");
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { ChatAnthropic } from "@langchain/anthropic";
2
2
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
3
- import { createLogger } from "@townco/core";
3
+ import { createLogger } from "../../../logger.js";
4
4
  import { countToolResultTokens } from "../../../utils/token-counter.js";
5
5
  const logger = createLogger("tool-response-compactor");
6
6
  // Haiku 4.5 for compaction (fast and cost-effective)
@@ -1,13 +1,15 @@
1
1
  import { MultiServerMCPClient } from "@langchain/mcp-adapters";
2
2
  import { context, propagation, trace } from "@opentelemetry/api";
3
- import { createLogger } from "@townco/core";
3
+ import { loadAuthCredentials } from "@townco/core/auth";
4
4
  import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
5
5
  import { z } from "zod";
6
6
  import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
7
+ import { createLogger } from "../../logger.js";
7
8
  import { telemetry } from "../../telemetry/index.js";
8
9
  import { loadCustomToolModule, } from "../tool-loader.js";
9
10
  import { createModelFromString, detectProvider } from "./model-factory.js";
10
11
  import { makeOtelCallbacks } from "./otel-callbacks.js";
12
+ import { makeBrowserTools } from "./tools/browser";
11
13
  import { makeFilesystemTools } from "./tools/filesystem";
12
14
  import { makeGenerateImageTool } from "./tools/generate_image";
13
15
  import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
@@ -29,6 +31,7 @@ export const TOOL_REGISTRY = {
29
31
  web_search: () => makeWebSearchTools(),
30
32
  filesystem: () => makeFilesystemTools(process.cwd()),
31
33
  generate_image: () => makeGenerateImageTool(),
34
+ browser: () => makeBrowserTools(),
32
35
  };
33
36
  // ============================================================================
34
37
  // Custom tool loading
@@ -78,11 +81,14 @@ export class LangchainAgent {
78
81
  const countedMessageIds = new Set();
79
82
  // Track tool calls for which we've emitted preliminary notifications (from early tool_use blocks)
80
83
  const preliminaryToolCallIds = new Set();
84
+ // Set session_id as a base attribute so all spans in this invocation include it
85
+ telemetry.setBaseAttributes({
86
+ "agent.session_id": req.sessionId,
87
+ });
81
88
  // Start telemetry span for entire invocation
82
89
  const invocationSpan = telemetry.startSpan("agent.invoke", {
83
90
  "agent.model": this.definition.model,
84
91
  "agent.subagent": meta?.[SUBAGENT_MODE_KEY] === true,
85
- "agent.session_id": req.sessionId,
86
92
  "agent.message_id": req.messageId,
87
93
  }, parentContext);
88
94
  // Create a context with the invocation span as active
@@ -294,7 +300,7 @@ export class LangchainAgent {
294
300
  : wrappedTools;
295
301
  // Wrap tools with tracing so each tool executes within its own span context.
296
302
  // This ensures subagent spans are children of the Task tool span.
297
- const finalTools = filteredTools.map((t) => wrapToolWithTracing(t, req.sessionId));
303
+ const finalTools = filteredTools.map((t) => wrapToolWithTracing(t));
298
304
  // Create the model instance using the factory
299
305
  // This detects the provider from the model string:
300
306
  // - "gemini-2.0-flash" → Google Generative AI
@@ -776,12 +782,19 @@ const modelRequestSchema = z.object({
776
782
  const makeMcpToolsClient = (mcpConfigs) => {
777
783
  const mcpServers = mcpConfigs?.map((config) => {
778
784
  if (typeof config === "string") {
779
- // Default to localhost:3000/mcp_proxy if not specified
780
- const proxyUrl = process.env.MCP_PROXY_URL || "http://localhost:3000/mcp_proxy";
785
+ // String configs use the centralized MCP proxy with auth
786
+ const credentials = loadAuthCredentials();
787
+ if (!credentials) {
788
+ throw new Error("Not logged in. Run 'town login' first to use cloud MCP servers.");
789
+ }
790
+ const proxyUrl = process.env.MCP_PROXY_URL ?? `${credentials.shed_url}/mcp_proxy`;
781
791
  return [
782
792
  config,
783
793
  {
784
794
  url: `${proxyUrl}?server=${config}`,
795
+ headers: {
796
+ Authorization: `Bearer ${credentials.access_token}`,
797
+ },
785
798
  },
786
799
  ];
787
800
  }
@@ -874,13 +887,12 @@ export { makeSubagentsTool } from "./tools/subagent.js";
874
887
  * so any child operations (like subagent spawning) become children
875
888
  * of the tool span rather than the parent invocation span.
876
889
  */
877
- function wrapToolWithTracing(originalTool, sessionId) {
890
+ function wrapToolWithTracing(originalTool) {
878
891
  const wrappedFunc = async (input) => {
879
892
  const toolInputJson = JSON.stringify(input);
880
893
  const toolSpan = telemetry.startSpan("agent.tool_call", {
881
894
  "tool.name": originalTool.name,
882
895
  "tool.input": toolInputJson,
883
- "agent.session_id": sessionId,
884
896
  });
885
897
  // Create a context with the tool span as active
886
898
  const spanContext = toolSpan
@@ -4,6 +4,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
4
4
  * LangChain chat model instance.
5
5
  *
6
6
  * Detection logic:
7
+ * - If model starts with "town-" → Proxied via shed (strips prefix, uses TOWN_SHED_URL)
7
8
  * - If model starts with "vertex-" → Google Vertex AI (strips prefix)
8
9
  * - If model contains "gemini" (unprefixed) → Google Generative AI
9
10
  * - If model contains "gpt" → OpenAI (future support)
@@ -12,6 +13,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
12
13
  * Supported formats:
13
14
  * - Direct model name: "gemini-2.0-flash", "vertex-gemini-2.0-flash", "claude-sonnet-4-5-20250929"
14
15
  * - Provider prefix: "google_vertexai:gemini-2.0-flash", "google_genai:gemini-2.0-flash", "anthropic:claude-3-5-sonnet"
16
+ * - Proxied: "town-claude-sonnet-4-5-20250929" (uses TOWN_SHED_URL or defaults to localhost:3000)
15
17
  */
16
18
  export declare function createModelFromString(modelString: string): BaseChatModel;
17
19
  /**
@@ -1,13 +1,15 @@
1
1
  import { ChatAnthropic } from "@langchain/anthropic";
2
2
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
3
3
  import { ChatVertexAI } from "@langchain/google-vertexai";
4
- import { createLogger } from "@townco/core";
4
+ import { loadAuthCredentials } from "@townco/core/auth";
5
+ import { createLogger } from "../../logger.js";
5
6
  const logger = createLogger("model-factory");
6
7
  /**
7
8
  * Detects the provider from a model string and returns the appropriate
8
9
  * LangChain chat model instance.
9
10
  *
10
11
  * Detection logic:
12
+ * - If model starts with "town-" → Proxied via shed (strips prefix, uses TOWN_SHED_URL)
11
13
  * - If model starts with "vertex-" → Google Vertex AI (strips prefix)
12
14
  * - If model contains "gemini" (unprefixed) → Google Generative AI
13
15
  * - If model contains "gpt" → OpenAI (future support)
@@ -16,8 +18,25 @@ const logger = createLogger("model-factory");
16
18
  * Supported formats:
17
19
  * - Direct model name: "gemini-2.0-flash", "vertex-gemini-2.0-flash", "claude-sonnet-4-5-20250929"
18
20
  * - Provider prefix: "google_vertexai:gemini-2.0-flash", "google_genai:gemini-2.0-flash", "anthropic:claude-3-5-sonnet"
21
+ * - Proxied: "town-claude-sonnet-4-5-20250929" (uses TOWN_SHED_URL or defaults to localhost:3000)
19
22
  */
20
23
  export function createModelFromString(modelString) {
24
+ // Check for town- prefix for proxied models via shed
25
+ if (modelString.startsWith("town-")) {
26
+ const actualModel = modelString.slice(5); // strip "town-"
27
+ const credentials = loadAuthCredentials();
28
+ if (!credentials) {
29
+ throw new Error("Not logged in. Run 'town login' first.");
30
+ }
31
+ const shedUrl = credentials.shed_url ??
32
+ process.env.TOWN_SHED_URL ??
33
+ "http://localhost:3000";
34
+ return new ChatAnthropic({
35
+ model: actualModel,
36
+ anthropicApiUrl: `${shedUrl}/api/anthropic`,
37
+ apiKey: credentials.access_token,
38
+ });
39
+ }
21
40
  // Check if the model string uses provider prefix format
22
41
  const parts = modelString.split(":", 2);
23
42
  const maybeProvider = parts[0];