@townco/agent 0.1.53 → 0.1.55

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 (52) hide show
  1. package/dist/acp-server/adapter.d.ts +16 -0
  2. package/dist/acp-server/adapter.js +231 -17
  3. package/dist/acp-server/cli.d.ts +1 -3
  4. package/dist/acp-server/http.js +51 -7
  5. package/dist/acp-server/session-storage.d.ts +16 -1
  6. package/dist/acp-server/session-storage.js +23 -0
  7. package/dist/bin.js +0 -0
  8. package/dist/definition/index.d.ts +2 -2
  9. package/dist/definition/index.js +1 -0
  10. package/dist/index.js +1 -1
  11. package/dist/logger.d.ts +26 -0
  12. package/dist/logger.js +43 -0
  13. package/dist/runner/agent-runner.d.ts +7 -2
  14. package/dist/runner/hooks/executor.js +1 -1
  15. package/dist/runner/hooks/loader.js +1 -1
  16. package/dist/runner/hooks/predefined/compaction-tool.js +1 -1
  17. package/dist/runner/hooks/predefined/tool-response-compactor.js +1 -1
  18. package/dist/runner/index.d.ts +1 -3
  19. package/dist/runner/langchain/index.js +179 -39
  20. package/dist/runner/langchain/model-factory.js +1 -1
  21. package/dist/runner/langchain/tools/generate_image.d.ts +28 -0
  22. package/dist/runner/langchain/tools/generate_image.js +135 -0
  23. package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
  24. package/dist/runner/langchain/tools/port-utils.js +35 -0
  25. package/dist/runner/langchain/tools/subagent.d.ts +6 -1
  26. package/dist/runner/langchain/tools/subagent.js +242 -129
  27. package/dist/runner/tools.d.ts +19 -2
  28. package/dist/runner/tools.js +9 -0
  29. package/dist/storage/index.js +1 -1
  30. package/dist/telemetry/index.js +7 -1
  31. package/dist/templates/index.d.ts +3 -0
  32. package/dist/templates/index.js +27 -5
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/index.ts +1 -1
  35. package/package.json +11 -6
  36. package/templates/index.ts +37 -6
  37. package/dist/definition/mcp.d.ts +0 -0
  38. package/dist/definition/mcp.js +0 -0
  39. package/dist/definition/tools/todo.d.ts +0 -49
  40. package/dist/definition/tools/todo.js +0 -80
  41. package/dist/definition/tools/web_search.d.ts +0 -4
  42. package/dist/definition/tools/web_search.js +0 -26
  43. package/dist/dev-agent/index.d.ts +0 -2
  44. package/dist/dev-agent/index.js +0 -18
  45. package/dist/example.d.ts +0 -2
  46. package/dist/example.js +0 -19
  47. package/dist/scaffold/link-local.d.ts +0 -1
  48. package/dist/scaffold/link-local.js +0 -54
  49. package/dist/utils/__tests__/tool-overhead-calculator.test.d.ts +0 -1
  50. package/dist/utils/__tests__/tool-overhead-calculator.test.js +0 -153
  51. package/dist/utils/logger.d.ts +0 -39
  52. package/dist/utils/logger.js +0 -175
@@ -27,6 +27,20 @@ export declare class AgentAcpAdapter implements acp.Agent {
27
27
  private currentToolOverheadTokens;
28
28
  private currentMcpOverheadTokens;
29
29
  constructor(agent: AgentRunner, connection: acp.AgentSideConnection, agentDir?: string, agentName?: string);
30
+ /**
31
+ * Extract tool metadata from the agent definition for exposing to clients.
32
+ * This provides basic info about available tools without loading them fully.
33
+ */
34
+ private getToolsMetadata;
35
+ /**
36
+ * Extract MCP metadata from the agent definition for exposing to clients.
37
+ */
38
+ private getMcpsMetadata;
39
+ /**
40
+ * Extract subagent metadata from the Task tool if present.
41
+ * This provides info about subagents configured via makeSubagentsTool.
42
+ */
43
+ private getSubagentsMetadata;
30
44
  /**
31
45
  * Helper to save session to disk
32
46
  * Call this after any modification to session.messages or session.context
@@ -38,10 +52,12 @@ export declare class AgentAcpAdapter implements acp.Agent {
38
52
  authenticate(_params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse | undefined>;
39
53
  setSessionMode(_params: acp.SetSessionModeRequest): Promise<acp.SetSessionModeResponse>;
40
54
  prompt(params: acp.PromptRequest): Promise<acp.PromptResponse>;
55
+ private _promptImpl;
41
56
  /**
42
57
  * Execute hooks if configured for this agent
43
58
  * Returns new context entries that should be appended to session.context
44
59
  */
45
60
  private executeHooksIfConfigured;
61
+ private _executeHooksImpl;
46
62
  cancel(params: acp.CancelNotification): Promise<void>;
47
63
  }
@@ -1,6 +1,8 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
- import { createLogger } from "@townco/core";
2
+ import { context, trace } from "@opentelemetry/api";
3
+ import { createLogger } from "../logger.js";
3
4
  import { HookExecutor, loadHookCallback } from "../runner/hooks";
5
+ import { telemetry } from "../telemetry/index.js";
4
6
  import { calculateContextSize, } from "../utils/context-size-calculator.js";
5
7
  import { countToolResultTokens } from "../utils/token-counter.js";
6
8
  import { SessionStorage, } from "./session-storage.js";
@@ -132,6 +134,82 @@ export class AgentAcpAdapter {
132
134
  sessionStoragePath: this.storage ? `${agentDir}/.sessions` : null,
133
135
  });
134
136
  }
137
+ /**
138
+ * Extract tool metadata from the agent definition for exposing to clients.
139
+ * This provides basic info about available tools without loading them fully.
140
+ */
141
+ getToolsMetadata() {
142
+ const tools = this.agent.definition.tools ?? [];
143
+ return tools.map((tool) => {
144
+ if (typeof tool === "string") {
145
+ // Built-in tool - return basic info
146
+ return { name: tool, description: `Built-in tool: ${tool}` };
147
+ }
148
+ else if (tool.type === "direct") {
149
+ return {
150
+ name: tool.name,
151
+ description: tool.description,
152
+ ...(tool.prettyName ? { prettyName: tool.prettyName } : {}),
153
+ ...(tool.icon ? { icon: tool.icon } : {}),
154
+ };
155
+ }
156
+ else if (tool.type === "filesystem") {
157
+ return {
158
+ name: "filesystem",
159
+ description: "File system access tools",
160
+ };
161
+ }
162
+ else if (tool.type === "custom") {
163
+ // Custom tools from module paths - extract name from path
164
+ const pathParts = tool.modulePath.split("/");
165
+ const fileName = pathParts[pathParts.length - 1] ?? "custom";
166
+ const name = fileName.replace(/\.(ts|js)$/, "");
167
+ return {
168
+ name,
169
+ description: `Custom tool from ${tool.modulePath}`,
170
+ };
171
+ }
172
+ return { name: "unknown", description: "Unknown tool type" };
173
+ });
174
+ }
175
+ /**
176
+ * Extract MCP metadata from the agent definition for exposing to clients.
177
+ */
178
+ getMcpsMetadata() {
179
+ const mcps = this.agent.definition.mcps ?? [];
180
+ return mcps.map((mcp) => {
181
+ if (typeof mcp === "string") {
182
+ return {
183
+ name: mcp,
184
+ transport: "http", // String configs are resolved to HTTP proxy
185
+ };
186
+ }
187
+ return {
188
+ name: mcp.name,
189
+ transport: mcp.transport,
190
+ };
191
+ });
192
+ }
193
+ /**
194
+ * Extract subagent metadata from the Task tool if present.
195
+ * This provides info about subagents configured via makeSubagentsTool.
196
+ */
197
+ getSubagentsMetadata() {
198
+ const tools = this.agent.definition.tools ?? [];
199
+ for (const tool of tools) {
200
+ // Look for direct tools with subagentConfigs (Task tools created by makeSubagentsTool)
201
+ if (typeof tool === "object" &&
202
+ tool.type === "direct" &&
203
+ "subagentConfigs" in tool &&
204
+ Array.isArray(tool.subagentConfigs)) {
205
+ return tool.subagentConfigs.map((config) => ({
206
+ name: config.agentName,
207
+ description: config.description,
208
+ }));
209
+ }
210
+ }
211
+ return [];
212
+ }
135
213
  /**
136
214
  * Helper to save session to disk
137
215
  * Call this after any modification to session.messages or session.context
@@ -168,19 +246,23 @@ export class AgentAcpAdapter {
168
246
  ...(this.agentDisplayName ? { title: this.agentDisplayName } : {}),
169
247
  };
170
248
  }
171
- // Pass description and suggestedPrompts via _meta extension point
172
- // since Implementation doesn't support these fields
173
- if (this.agentDescription || this.agentSuggestedPrompts) {
174
- response._meta = {
175
- ...response._meta,
176
- ...(this.agentDescription
177
- ? { agentDescription: this.agentDescription }
178
- : {}),
179
- ...(this.agentSuggestedPrompts
180
- ? { suggestedPrompts: this.agentSuggestedPrompts }
181
- : {}),
182
- };
183
- }
249
+ // Pass extended agent info via _meta extension point
250
+ // since ACP Implementation type doesn't support these fields directly
251
+ const toolsMetadata = this.getToolsMetadata();
252
+ const mcpsMetadata = this.getMcpsMetadata();
253
+ const subagentsMetadata = this.getSubagentsMetadata();
254
+ response._meta = {
255
+ ...response._meta,
256
+ ...(this.agentDescription
257
+ ? { agentDescription: this.agentDescription }
258
+ : {}),
259
+ ...(this.agentSuggestedPrompts
260
+ ? { suggestedPrompts: this.agentSuggestedPrompts }
261
+ : {}),
262
+ ...(toolsMetadata.length > 0 ? { tools: toolsMetadata } : {}),
263
+ ...(mcpsMetadata.length > 0 ? { mcps: mcpsMetadata } : {}),
264
+ ...(subagentsMetadata.length > 0 ? { subagents: subagentsMetadata } : {}),
265
+ };
184
266
  return response;
185
267
  }
186
268
  async newSession(params) {
@@ -236,6 +318,14 @@ export class AgentAcpAdapter {
236
318
  }
237
319
  else if (block.type === "tool_call") {
238
320
  // Replay tool call directly in final state (skip pending → completed transitions)
321
+ const replayMeta = {
322
+ isReplay: true,
323
+ ...(block.prettyName ? { prettyName: block.prettyName } : {}),
324
+ ...(block.icon ? { icon: block.icon } : {}),
325
+ ...(block.subline ? { subline: block.subline } : {}),
326
+ ...(block.batchId ? { batchId: block.batchId } : {}),
327
+ ...block._meta,
328
+ };
239
329
  this.connection.sessionUpdate({
240
330
  sessionId: params.sessionId,
241
331
  update: {
@@ -245,6 +335,7 @@ export class AgentAcpAdapter {
245
335
  kind: block.kind,
246
336
  status: block.status, // Use final status directly
247
337
  rawInput: block.rawInput,
338
+ _meta: replayMeta,
248
339
  },
249
340
  });
250
341
  // If there's output, emit tool_output event for the UI to display
@@ -326,6 +417,22 @@ export class AgentAcpAdapter {
326
417
  return {};
327
418
  }
328
419
  async prompt(params) {
420
+ const promptSpan = telemetry.startSpan("adapter.prompt", {
421
+ "session.id": params.sessionId,
422
+ });
423
+ const spanContext = promptSpan
424
+ ? trace.setSpan(context.active(), promptSpan)
425
+ : context.active();
426
+ return context.with(spanContext, async () => {
427
+ try {
428
+ return await this._promptImpl(params);
429
+ }
430
+ finally {
431
+ telemetry.endSpan(promptSpan);
432
+ }
433
+ });
434
+ }
435
+ async _promptImpl(params) {
329
436
  let session = this.sessions.get(params.sessionId);
330
437
  // If session not found (e.g., after server restart), create a new one
331
438
  if (!session) {
@@ -345,7 +452,29 @@ export class AgentAcpAdapter {
345
452
  this.currentMcpOverheadTokens = 0;
346
453
  // Generate a unique messageId for this assistant response
347
454
  const messageId = Math.random().toString(36).substring(2);
348
- // Extract and store the user message
455
+ // Convert prompt content blocks to session storage format
456
+ const userContentBlocks = params.prompt.map((block) => {
457
+ if (block.type === "text") {
458
+ return { type: "text", text: block.text };
459
+ }
460
+ else if (block.type === "image") {
461
+ // Preserve image block with all its data
462
+ const imgBlock = block;
463
+ const imageBlock = { type: "image" };
464
+ if (imgBlock.source)
465
+ imageBlock.source = imgBlock.source;
466
+ if (imgBlock.url)
467
+ imageBlock.url = imgBlock.url;
468
+ if (imgBlock.data)
469
+ imageBlock.data = imgBlock.data;
470
+ if (imgBlock.mimeType)
471
+ imageBlock.mimeType = imgBlock.mimeType;
472
+ return imageBlock;
473
+ }
474
+ // Fallback for unknown types - convert to text
475
+ return { type: "text", text: JSON.stringify(block) };
476
+ });
477
+ // Extract text for logging
349
478
  const userMessageText = params.prompt
350
479
  .filter((p) => p.type === "text")
351
480
  .map((p) => p.text)
@@ -359,7 +488,7 @@ export class AgentAcpAdapter {
359
488
  if (!this.noSession) {
360
489
  const userMessage = {
361
490
  role: "user",
362
- content: [{ type: "text", text: userMessageText }],
491
+ content: userContentBlocks,
363
492
  timestamp: new Date().toISOString(),
364
493
  };
365
494
  session.messages.push(userMessage);
@@ -537,10 +666,50 @@ export class AgentAcpAdapter {
537
666
  if ("sessionUpdate" in msg && msg.sessionUpdate === "tool_call") {
538
667
  flushPendingText();
539
668
  const toolCallMsg = msg;
669
+ // Extract prettyName, icon, and batchId from _meta
670
+ const prettyName = toolCallMsg._meta &&
671
+ typeof toolCallMsg._meta === "object" &&
672
+ "prettyName" in toolCallMsg._meta
673
+ ? String(toolCallMsg._meta.prettyName)
674
+ : undefined;
675
+ const icon = toolCallMsg._meta &&
676
+ typeof toolCallMsg._meta === "object" &&
677
+ "icon" in toolCallMsg._meta
678
+ ? String(toolCallMsg._meta.icon)
679
+ : undefined;
680
+ const batchId = toolCallMsg._meta &&
681
+ typeof toolCallMsg._meta === "object" &&
682
+ "batchId" in toolCallMsg._meta
683
+ ? String(toolCallMsg._meta.batchId)
684
+ : undefined;
685
+ // Extract subline from _meta, or compute it for todo_write from rawInput
686
+ let subline = toolCallMsg._meta &&
687
+ typeof toolCallMsg._meta === "object" &&
688
+ "subline" in toolCallMsg._meta
689
+ ? String(toolCallMsg._meta.subline)
690
+ : undefined;
691
+ // For todo_write, compute subline from in_progress item's activeForm
692
+ if (!subline &&
693
+ toolCallMsg.title === "todo_write" &&
694
+ toolCallMsg.rawInput &&
695
+ "todos" in toolCallMsg.rawInput &&
696
+ Array.isArray(toolCallMsg.rawInput.todos)) {
697
+ const inProgressItem = toolCallMsg.rawInput.todos.find((todo) => todo.status === "in_progress");
698
+ if (inProgressItem &&
699
+ typeof inProgressItem === "object" &&
700
+ "activeForm" in inProgressItem &&
701
+ typeof inProgressItem.activeForm === "string") {
702
+ subline = inProgressItem.activeForm;
703
+ }
704
+ }
540
705
  const toolCall = {
541
706
  type: "tool_call",
542
707
  id: toolCallMsg.toolCallId || `tool_${Date.now()}`,
708
+ ...(batchId ? { batchId } : {}),
543
709
  title: toolCallMsg.title || "Tool",
710
+ ...(prettyName ? { prettyName } : {}),
711
+ ...(icon ? { icon } : {}),
712
+ ...(subline ? { subline } : {}),
544
713
  kind: toolCallMsg.kind || "other",
545
714
  status: toolCallMsg.status || "pending",
546
715
  startedAt: Date.now(),
@@ -714,8 +883,35 @@ export class AgentAcpAdapter {
714
883
  }
715
884
  // The agent may emit extended types (like tool_output) that aren't in ACP SDK yet
716
885
  // The http transport will handle routing these appropriately
717
- // Add context input tokens to messages with token usage metadata
886
+ // Enhance todo_write tool_call with subline showing in-progress item
718
887
  let enhancedMsg = msg;
888
+ if ("sessionUpdate" in msg &&
889
+ msg.sessionUpdate === "tool_call" &&
890
+ "title" in msg &&
891
+ msg.title === "todo_write" &&
892
+ "rawInput" in msg &&
893
+ msg.rawInput &&
894
+ typeof msg.rawInput === "object" &&
895
+ "todos" in msg.rawInput &&
896
+ Array.isArray(msg.rawInput.todos)) {
897
+ // Find the in_progress item and extract its activeForm
898
+ const inProgressItem = msg.rawInput.todos.find((todo) => todo.status === "in_progress");
899
+ if (inProgressItem &&
900
+ typeof inProgressItem === "object" &&
901
+ "activeForm" in inProgressItem &&
902
+ typeof inProgressItem.activeForm === "string") {
903
+ enhancedMsg = {
904
+ ...msg,
905
+ _meta: {
906
+ ...(typeof msg._meta === "object" && msg._meta !== null
907
+ ? msg._meta
908
+ : {}),
909
+ subline: inProgressItem.activeForm,
910
+ },
911
+ };
912
+ }
913
+ }
914
+ // Add context input tokens to messages with token usage metadata
719
915
  if (!this.noSession &&
720
916
  "_meta" in msg &&
721
917
  msg._meta &&
@@ -824,6 +1020,24 @@ export class AgentAcpAdapter {
824
1020
  if (this.noSession || !hooks || hooks.length === 0) {
825
1021
  return [];
826
1022
  }
1023
+ const hookSpan = telemetry.startSpan("adapter.executeHooks", {
1024
+ "hooks.executionPoint": executionPoint,
1025
+ "hooks.count": hooks.length,
1026
+ "session.id": sessionId,
1027
+ });
1028
+ const spanContext = hookSpan
1029
+ ? trace.setSpan(context.active(), hookSpan)
1030
+ : context.active();
1031
+ return context.with(spanContext, async () => {
1032
+ try {
1033
+ return await this._executeHooksImpl(session, sessionId, executionPoint, hooks);
1034
+ }
1035
+ finally {
1036
+ telemetry.endSpan(hookSpan);
1037
+ }
1038
+ });
1039
+ }
1040
+ async _executeHooksImpl(session, sessionId, executionPoint, hooks) {
827
1041
  logger.info(`Executing hooks at ${executionPoint}`, {
828
1042
  hooksLength: hooks.length,
829
1043
  contextEntries: session.context.length,
@@ -1,5 +1,3 @@
1
1
  import type { AgentDefinition } from "../definition";
2
2
  import { type AgentRunner } from "../runner";
3
- export declare function makeStdioTransport(
4
- agent: AgentRunner | AgentDefinition,
5
- ): void;
3
+ export declare function makeStdioTransport(agent: AgentRunner | AgentDefinition): void;
@@ -1,15 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
- import { join } from "node:path";
2
+ 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
+ const logger = createLogger("http");
13
14
  /**
14
15
  * Compress a payload using gzip if it's too large for PostgreSQL NOTIFY
15
16
  * Returns an object with the payload and metadata about compression
@@ -49,11 +50,16 @@ function safeChannelName(prefix, id) {
49
50
  return `${prefix}_${hash}`;
50
51
  }
51
52
  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");
53
+ // Configure logger to write to .logs/ directory
54
+ // Use TOWN_LOGS_DIR env var if set (for subagents), otherwise use agentDir
55
+ const logsDir = process.env.TOWN_LOGS_DIR ||
56
+ (agentDir ? join(agentDir, ".logs") : undefined);
57
+ if (logsDir) {
55
58
  configureLogsDir(logsDir);
56
- logger.info("Configured logs directory", { logsDir });
59
+ logger.info("Configured logs directory", {
60
+ logsDir,
61
+ isSubagent: isSubagent(),
62
+ });
57
63
  }
58
64
  const inbound = new TransformStream();
59
65
  const outbound = new TransformStream();
@@ -264,6 +270,44 @@ export function makeHttpTransport(agent, agentDir, agentName) {
264
270
  allowMethods: ["GET", "POST", "OPTIONS"],
265
271
  }));
266
272
  app.get("/health", (c) => c.json({ ok: true }));
273
+ // Serve static files from agent directory (for generated images, etc.)
274
+ if (agentDir) {
275
+ app.get("/static/*", async (c) => {
276
+ const path = c.req.path.replace(/^\/static\//, "");
277
+ const filePath = join(agentDir, path);
278
+ // Security check: ensure the file is within the agent directory
279
+ const normalizedPath = resolve(filePath);
280
+ const normalizedAgentDir = resolve(agentDir);
281
+ if (!normalizedPath.startsWith(normalizedAgentDir)) {
282
+ logger.warn("Attempted to access file outside agent directory", {
283
+ path,
284
+ filePath,
285
+ agentDir,
286
+ });
287
+ return c.json({ error: "Invalid path" }, 403);
288
+ }
289
+ try {
290
+ const file = Bun.file(filePath);
291
+ const exists = await file.exists();
292
+ if (!exists) {
293
+ logger.warn("Static file not found", { path, filePath });
294
+ return c.json({ error: "File not found" }, 404);
295
+ }
296
+ const buffer = await file.arrayBuffer();
297
+ const contentType = file.type || "application/octet-stream";
298
+ return new Response(buffer, {
299
+ headers: {
300
+ "Content-Type": contentType,
301
+ "Cache-Control": "public, max-age=31536000",
302
+ },
303
+ });
304
+ }
305
+ catch (error) {
306
+ logger.error("Error serving static file", { error, path, filePath });
307
+ return c.json({ error: "Internal server error" }, 500);
308
+ }
309
+ });
310
+ }
267
311
  app.get("/events", (c) => {
268
312
  const sessionId = c.req.header("X-Session-ID");
269
313
  if (!sessionId) {
@@ -5,10 +5,25 @@ export interface TextBlock {
5
5
  type: "text";
6
6
  text: string;
7
7
  }
8
+ export interface ImageBlock {
9
+ type: "image";
10
+ source?: {
11
+ type: "base64";
12
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
13
+ data: string;
14
+ } | undefined;
15
+ url?: string | undefined;
16
+ data?: string | undefined;
17
+ mimeType?: string | undefined;
18
+ }
8
19
  export interface ToolCallBlock {
9
20
  type: "tool_call";
10
21
  id: string;
22
+ batchId?: string | undefined;
11
23
  title: string;
24
+ prettyName?: string | undefined;
25
+ icon?: string | undefined;
26
+ subline?: string | undefined;
12
27
  kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "switch_mode" | "other";
13
28
  status: "pending" | "in_progress" | "completed" | "failed";
14
29
  rawInput?: Record<string, unknown> | undefined;
@@ -23,7 +38,7 @@ export interface ToolCallBlock {
23
38
  finalTokens?: number;
24
39
  };
25
40
  }
26
- export type ContentBlock = TextBlock | ToolCallBlock;
41
+ export type ContentBlock = TextBlock | ImageBlock | ToolCallBlock;
27
42
  /**
28
43
  * Session message format stored in files
29
44
  */
@@ -8,10 +8,32 @@ const textBlockSchema = z.object({
8
8
  type: z.literal("text"),
9
9
  text: z.string(),
10
10
  });
11
+ const imageBlockSchema = z.object({
12
+ type: z.literal("image"),
13
+ source: z
14
+ .object({
15
+ type: z.literal("base64"),
16
+ media_type: z.enum([
17
+ "image/jpeg",
18
+ "image/png",
19
+ "image/gif",
20
+ "image/webp",
21
+ ]),
22
+ data: z.string(),
23
+ })
24
+ .optional(),
25
+ url: z.string().optional(),
26
+ data: z.string().optional(),
27
+ mimeType: z.string().optional(),
28
+ });
11
29
  const toolCallBlockSchema = z.object({
12
30
  type: z.literal("tool_call"),
13
31
  id: z.string(),
32
+ batchId: z.string().optional(),
14
33
  title: z.string(),
34
+ prettyName: z.string().optional(),
35
+ icon: z.string().optional(),
36
+ subline: z.string().optional(),
15
37
  kind: z.enum([
16
38
  "read",
17
39
  "edit",
@@ -33,6 +55,7 @@ const toolCallBlockSchema = z.object({
33
55
  });
34
56
  const contentBlockSchema = z.discriminatedUnion("type", [
35
57
  textBlockSchema,
58
+ imageBlockSchema,
36
59
  toolCallBlockSchema,
37
60
  ]);
38
61
  const sessionMessageSchema = z.object({
package/dist/bin.js CHANGED
File without changes
@@ -11,7 +11,7 @@ export declare const McpConfigSchema: z.ZodUnion<readonly [z.ZodObject<{
11
11
  transport: z.ZodLiteral<"http">;
12
12
  url: z.ZodString;
13
13
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
- }, z.core.$strip>]>;
14
+ }, z.core.$strip>, z.ZodString]>;
15
15
  /** Hook configuration schema. */
16
16
  export declare const HookConfigSchema: z.ZodObject<{
17
17
  type: z.ZodEnum<{
@@ -59,7 +59,7 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
59
59
  transport: z.ZodLiteral<"http">;
60
60
  url: z.ZodString;
61
61
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
62
- }, z.core.$strip>]>>>;
62
+ }, z.core.$strip>, z.ZodString]>>>;
63
63
  harnessImplementation: z.ZodOptional<z.ZodLiteral<"langchain">>;
64
64
  hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
65
65
  type: z.ZodEnum<{
@@ -23,6 +23,7 @@ const McpStreamableHttpConfigSchema = McpBaseConfigSchema.extend({
23
23
  export const McpConfigSchema = z.union([
24
24
  McpStdioConfigSchema,
25
25
  McpStreamableHttpConfigSchema,
26
+ z.string(),
26
27
  ]);
27
28
  /** Custom tool configuration schema. */
28
29
  const CustomToolSchema = z.object({
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.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.ZodObject<{
12
12
  type: z.ZodLiteral<"custom">;
13
13
  modulePath: z.ZodString;
14
14
  }, z.core.$strip>, z.ZodObject<{
@@ -22,6 +22,11 @@ export declare const zAgentRunnerParams: z.ZodObject<{
22
22
  schema: z.ZodAny;
23
23
  prettyName: z.ZodOptional<z.ZodString>;
24
24
  icon: z.ZodOptional<z.ZodString>;
25
+ subagentConfigs: z.ZodOptional<z.ZodArray<z.ZodObject<{
26
+ agentName: z.ZodString;
27
+ description: z.ZodString;
28
+ displayName: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strip>>>;
25
30
  }, z.core.$strip>]>>>;
26
31
  mcps: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
27
32
  name: z.ZodString;
@@ -33,7 +38,7 @@ export declare const zAgentRunnerParams: z.ZodObject<{
33
38
  transport: z.ZodLiteral<"http">;
34
39
  url: z.ZodString;
35
40
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
36
- }, z.core.$strip>]>>>;
41
+ }, z.core.$strip>, z.ZodString]>>>;
37
42
  hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
38
43
  type: z.ZodEnum<{
39
44
  context_size: "context_size";