@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
@@ -1,15 +1,44 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { Readable, Writable } from "node:stream";
5
- import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
4
+ import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
6
5
  import { context, propagation, trace } from "@opentelemetry/api";
6
+ import { createLogger as coreCreateLogger } from "@townco/core";
7
7
  import { z } from "zod";
8
8
  import { SUBAGENT_MODE_KEY } from "../../../acp-server/adapter.js";
9
+ import { findAvailablePort } from "./port-utils.js";
9
10
  /**
10
11
  * Name of the Task tool created by makeSubagentsTool
11
12
  */
12
- export const TASK_TOOL_NAME = "Task";
13
+ export const SUBAGENT_TOOL_NAME = "subagent";
14
+ /**
15
+ * Base port for subagent HTTP servers (avoid conflict with main agents at 3100+)
16
+ */
17
+ const SUBAGENT_BASE_PORT = 4000;
18
+ /**
19
+ * Wait for HTTP server to be ready by polling health endpoint
20
+ */
21
+ async function waitForServerReady(port, timeoutMs = 30000) {
22
+ const startTime = Date.now();
23
+ const baseDelay = 50;
24
+ let attempt = 0;
25
+ while (Date.now() - startTime < timeoutMs) {
26
+ try {
27
+ const response = await fetch(`http://localhost:${port}/health`, {
28
+ signal: AbortSignal.timeout(1000),
29
+ });
30
+ if (response.ok) {
31
+ return;
32
+ }
33
+ }
34
+ catch {
35
+ // Server not ready yet
36
+ }
37
+ await new Promise((r) => setTimeout(r, baseDelay * Math.pow(1.5, attempt)));
38
+ attempt++;
39
+ }
40
+ throw new Error(`Subagent server at port ${port} did not become ready within ${timeoutMs}ms`);
41
+ }
13
42
  /**
14
43
  * Creates a DirectTool that delegates work to one of multiple configured subagents.
15
44
  *
@@ -61,9 +90,17 @@ export function makeSubagentsTool(configs) {
61
90
  .map((config) => `"${config.agentName}": ${config.description}`)
62
91
  .join("\n");
63
92
  const agentNames = configs.map((c) => c.agentName);
93
+ // Extract subagent configs for metadata (agentName, description, displayName)
94
+ const subagentConfigs = configs.map((config) => ({
95
+ agentName: config.agentName,
96
+ description: config.description,
97
+ displayName: config.displayName,
98
+ }));
64
99
  return {
65
100
  type: "direct",
66
- name: TASK_TOOL_NAME,
101
+ name: SUBAGENT_TOOL_NAME,
102
+ prettyName: "Subagent",
103
+ icon: "BrainCircuit",
67
104
  description: `Launch a new agent to handle complex, multi-step tasks autonomously.
68
105
 
69
106
  The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
@@ -129,20 +166,22 @@ assistant: "I'm going to use the Task tool to launch the greeting-responder agen
129
166
  .describe("The name of the subagent to use"),
130
167
  query: z.string().describe("The query or task to send to the subagent"),
131
168
  }),
169
+ // Expose subagent configs for metadata extraction by the adapter
170
+ subagentConfigs,
132
171
  fn: async (input) => {
133
172
  const { agentName, query } = input;
134
173
  const agent = agentMap.get(agentName);
135
174
  if (!agent) {
136
175
  throw new Error(`Unknown agent: ${agentName}`);
137
176
  }
138
- return await querySubagent(agent.agentPath, agent.agentDir, query);
177
+ return await querySubagent(agentName, agent.agentPath, agent.agentDir, query);
139
178
  },
140
179
  };
141
180
  }
142
181
  /**
143
- * Internal function that spawns a subagent process and collects its response.
182
+ * Internal function that spawns a subagent HTTP server and queries it.
144
183
  */
145
- async function querySubagent(agentPath, agentWorkingDirectory, query) {
184
+ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query) {
146
185
  // Validate that the agent exists
147
186
  try {
148
187
  await fs.access(agentPath);
@@ -150,153 +189,227 @@ async function querySubagent(agentPath, agentWorkingDirectory, query) {
150
189
  catch (_error) {
151
190
  throw new Error(`Agent not found at ${agentPath}. Make sure the agent exists and has an index.ts file.`);
152
191
  }
192
+ // Find an available port for this subagent
193
+ const port = await findAvailablePort(SUBAGENT_BASE_PORT, 100);
194
+ // Create a logger for this subagent instance with port prefix
195
+ // Use core logger directly since service name already identifies the subagent
196
+ const logger = coreCreateLogger(`subagent:${port}:${agentName}`);
153
197
  let agentProcess = null;
154
- let connection = null;
198
+ let sseAbortController = null;
155
199
  try {
156
- // Spawn the agent process
157
- agentProcess = spawn("bun", [agentPath], {
200
+ // Get the parent's logs directory to pass to the subagent
201
+ const parentLogsDir = process.env.TOWN_LOGS_DIR || path.join(process.cwd(), ".logs");
202
+ // Spawn the agent process in HTTP mode
203
+ agentProcess = spawn("bun", [agentPath, "http"], {
158
204
  cwd: agentWorkingDirectory,
159
- env: { ...process.env },
205
+ env: {
206
+ ...process.env,
207
+ PORT: String(port),
208
+ TOWN_LOGS_DIR: parentLogsDir,
209
+ TOWN_SUBAGENT_NAME: agentName,
210
+ },
160
211
  stdio: ["pipe", "pipe", "pipe"],
161
212
  });
162
- if (!agentProcess.stdin || !agentProcess.stdout || !agentProcess.stderr) {
163
- throw new Error("Failed to create stdio pipes for agent process");
213
+ if (!agentProcess.stderr) {
214
+ throw new Error("Failed to create stderr pipe for agent process");
164
215
  }
165
- // Convert Node.js streams to Web streams
166
- const outputStream = Writable.toWeb(agentProcess.stdin);
167
- const inputStream = Readable.toWeb(agentProcess.stdout);
168
- // Create the bidirectional stream using ndJsonStream
169
- const stream = ndJsonStream(outputStream, inputStream);
170
- // Track accumulated response text
171
- let responseText = "";
172
- // Create ACP client implementation factory
173
- const clientFactory = (_agent) => {
174
- return {
175
- async requestPermission(_params) {
176
- // Deny all permission requests from the subagent
177
- return { outcome: { outcome: "cancelled" } };
178
- },
179
- async sessionUpdate(params) {
180
- // Handle session updates from the agent
181
- const paramsExtended = params;
182
- const update = paramsExtended.update;
183
- // Reset accumulated text when a tool call starts (marks a new message boundary)
184
- if (update?.sessionUpdate === "tool_call") {
185
- responseText = "";
186
- }
187
- // Accumulate agent_message_chunk text content
188
- if (update?.sessionUpdate === "agent_message_chunk") {
189
- const content = update.content;
190
- if (content &&
191
- content.type === "text" &&
192
- typeof content.text === "string") {
193
- responseText += content.text;
194
- }
195
- }
196
- },
197
- async writeTextFile() {
198
- // Subagents should not write files outside their scope
199
- throw new Error("Subagent attempted to write files, which is not allowed");
200
- },
201
- async readTextFile() {
202
- // Subagents should not read files outside their scope
203
- throw new Error("Subagent attempted to read files, which is not allowed");
204
- },
205
- };
206
- };
207
- // Create the client-side connection
208
- connection = new ClientSideConnection(clientFactory, stream);
209
- // Set up timeout for the entire operation
210
- const timeoutMs = 5 * 60 * 1000; // 5 minutes
211
- const timeoutPromise = new Promise((_resolve, reject) => {
212
- setTimeout(() => {
213
- reject(new Error(`Subagent query timed out after ${timeoutMs / 1000} seconds`));
214
- }, timeoutMs);
215
- });
216
- // Handle process errors and exit
217
- const processExitPromise = new Promise((_resolve, reject) => {
218
- agentProcess?.on("exit", (code, signal) => {
219
- if (code !== 0 && code !== null) {
220
- reject(new Error(`Agent process exited with code ${code} and signal ${signal}`));
216
+ // Capture stdout and forward to logger
217
+ if (agentProcess.stdout) {
218
+ agentProcess.stdout.on("data", (data) => {
219
+ const lines = data
220
+ .toString()
221
+ .split("\n")
222
+ .filter((line) => line.trim());
223
+ for (const line of lines) {
224
+ logger.info(line);
221
225
  }
222
226
  });
227
+ }
228
+ // Capture stderr and forward to logger as errors
229
+ agentProcess.stderr.on("data", (data) => {
230
+ const lines = data
231
+ .toString()
232
+ .split("\n")
233
+ .filter((line) => line.trim());
234
+ for (const line of lines) {
235
+ logger.error(line);
236
+ }
237
+ });
238
+ // Handle process errors
239
+ const processErrorPromise = new Promise((_, reject) => {
223
240
  agentProcess?.on("error", (error) => {
241
+ logger.error(`Process error: ${error.message}`);
224
242
  reject(new Error(`Agent process error: ${error.message}`));
225
243
  });
244
+ agentProcess?.on("exit", (code, signal) => {
245
+ if (code !== 0 && code !== null) {
246
+ logger.error(`Process exited with code ${code}, signal ${signal}`);
247
+ reject(new Error(`Agent process exited unexpectedly with code ${code}, signal ${signal}`));
248
+ }
249
+ });
226
250
  });
227
- // Run the query with timeout and error handling
228
- const queryPromise = (async () => {
229
- // Initialize the connection
230
- await connection?.initialize({
231
- protocolVersion: PROTOCOL_VERSION,
232
- clientCapabilities: {
233
- fs: {
234
- readTextFile: false,
235
- writeTextFile: false,
251
+ logger.info(`Starting subagent HTTP server on port ${port}`);
252
+ // Wait for server to be ready
253
+ await Promise.race([waitForServerReady(port), processErrorPromise]);
254
+ logger.info(`Subagent server ready on port ${port}`);
255
+ const baseUrl = `http://localhost:${port}`;
256
+ // Step 1: Initialize ACP connection
257
+ const initResponse = await fetch(`${baseUrl}/rpc`, {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json" },
260
+ body: JSON.stringify({
261
+ jsonrpc: "2.0",
262
+ id: "init-1",
263
+ method: "initialize",
264
+ params: {
265
+ protocolVersion: PROTOCOL_VERSION,
266
+ clientCapabilities: {
267
+ fs: { readTextFile: false, writeTextFile: false },
236
268
  },
237
269
  },
238
- });
239
- // Prepare OpenTelemetry trace context to propagate to the subagent.
240
- // We inject from the current active context so the subagent's root
241
- // invocation span can be a child of whatever span is active when
242
- // this Task tool runs (ideally the agent.tool_call span).
243
- if (process.env.DEBUG_TELEMETRY === "true") {
244
- console.log(`[querySubagent] Tool function executing for agent: ${agentPath}`);
245
- }
246
- const otelCarrier = {};
247
- const activeCtx = context.active();
248
- const activeSpan = trace.getSpan(activeCtx);
249
- if (process.env.DEBUG_TELEMETRY === "true") {
250
- console.log(`[querySubagent] Active span when tool executes:`, activeSpan?.spanContext());
251
- }
252
- const ctxForInjection = activeSpan
253
- ? trace.setSpan(activeCtx, activeSpan)
254
- : activeCtx;
270
+ }),
271
+ });
272
+ if (!initResponse.ok) {
273
+ throw new Error(`Initialize failed: HTTP ${initResponse.status}`);
274
+ }
275
+ // Step 2: Create new session with subagent mode and OTEL context
276
+ // Prepare OpenTelemetry trace context to propagate to the subagent
277
+ const otelCarrier = {};
278
+ const activeCtx = context.active();
279
+ const activeSpan = trace.getSpan(activeCtx);
280
+ if (process.env.DEBUG_TELEMETRY === "true") {
281
+ console.log(`[querySubagent] Active span when tool executes:`, activeSpan?.spanContext());
282
+ }
283
+ if (activeSpan) {
284
+ const ctxForInjection = trace.setSpan(activeCtx, activeSpan);
255
285
  propagation.inject(ctxForInjection, otelCarrier);
256
- const hasOtelContext = Object.keys(otelCarrier).length > 0;
257
- // Create a new session with subagent mode flag and OTEL trace context
258
- const sessionResponse = await connection?.newSession({
259
- cwd: agentWorkingDirectory,
260
- mcpServers: [],
261
- _meta: {
262
- [SUBAGENT_MODE_KEY]: true,
263
- ...(hasOtelContext ? { otelTraceContext: otelCarrier } : {}),
286
+ }
287
+ const hasOtelContext = Object.keys(otelCarrier).length > 0;
288
+ const sessionResponse = await fetch(`${baseUrl}/rpc`, {
289
+ method: "POST",
290
+ headers: { "Content-Type": "application/json" },
291
+ body: JSON.stringify({
292
+ jsonrpc: "2.0",
293
+ id: "session-1",
294
+ method: "session/new",
295
+ params: {
296
+ cwd: agentWorkingDirectory,
297
+ mcpServers: [],
298
+ _meta: {
299
+ [SUBAGENT_MODE_KEY]: true,
300
+ ...(hasOtelContext ? { otelTraceContext: otelCarrier } : {}),
301
+ },
264
302
  },
303
+ }),
304
+ });
305
+ if (!sessionResponse.ok) {
306
+ throw new Error(`Session creation failed: HTTP ${sessionResponse.status}`);
307
+ }
308
+ const sessionResult = (await sessionResponse.json());
309
+ const sessionId = sessionResult.result?.sessionId;
310
+ if (!sessionId) {
311
+ throw new Error("No sessionId in session/new response");
312
+ }
313
+ // Step 3: Connect to SSE for receiving streaming responses
314
+ sseAbortController = new AbortController();
315
+ let responseText = "";
316
+ const ssePromise = (async () => {
317
+ const sseResponse = await fetch(`${baseUrl}/events`, {
318
+ headers: { "X-Session-ID": sessionId },
319
+ signal: sseAbortController.signal,
265
320
  });
266
- // Send the prompt
267
- await connection?.prompt({
268
- sessionId: sessionResponse.sessionId,
269
- prompt: [
270
- {
271
- type: "text",
272
- text: query,
321
+ if (!sseResponse.ok || !sseResponse.body) {
322
+ throw new Error(`SSE connection failed: HTTP ${sseResponse.status}`);
323
+ }
324
+ const reader = sseResponse.body.getReader();
325
+ const decoder = new TextDecoder();
326
+ let buffer = "";
327
+ while (true) {
328
+ const { done, value } = await reader.read();
329
+ if (done)
330
+ break;
331
+ buffer += decoder.decode(value, { stream: true });
332
+ const lines = buffer.split("\n");
333
+ buffer = lines.pop() || "";
334
+ for (const line of lines) {
335
+ if (line.startsWith("data:")) {
336
+ const data = line.substring(5).trim();
337
+ if (!data)
338
+ continue;
339
+ try {
340
+ const message = JSON.parse(data);
341
+ // Handle session/update notifications for agent_message_chunk
342
+ if (message.method === "session/update" &&
343
+ message.params?.update?.sessionUpdate === "agent_message_chunk") {
344
+ const content = message.params.update.content;
345
+ if (content?.type === "text" &&
346
+ typeof content.text === "string") {
347
+ responseText += content.text;
348
+ }
349
+ }
350
+ // Reset on tool_call (marks new message boundary)
351
+ if (message.params?.update?.sessionUpdate === "tool_call") {
352
+ responseText = "";
353
+ }
354
+ }
355
+ catch {
356
+ // Ignore malformed SSE data
357
+ }
358
+ }
359
+ }
360
+ }
361
+ })();
362
+ // Step 4: Send the prompt with timeout
363
+ const timeoutMs = 5 * 60 * 1000; // 5 minutes
364
+ const promptPromise = (async () => {
365
+ const promptResponse = await fetch(`${baseUrl}/rpc`, {
366
+ method: "POST",
367
+ headers: { "Content-Type": "application/json" },
368
+ body: JSON.stringify({
369
+ jsonrpc: "2.0",
370
+ id: "prompt-1",
371
+ method: "session/prompt",
372
+ params: {
373
+ sessionId,
374
+ prompt: [{ type: "text", text: query }],
273
375
  },
274
- ],
376
+ }),
275
377
  });
276
- return responseText;
378
+ if (!promptResponse.ok) {
379
+ throw new Error(`Prompt failed: HTTP ${promptResponse.status}`);
380
+ }
381
+ // Wait for prompt to complete (this blocks until agent finishes processing)
382
+ const promptResult = (await promptResponse.json());
383
+ if (promptResult.error) {
384
+ throw new Error(`Prompt error: ${promptResult.error.message || JSON.stringify(promptResult.error)}`);
385
+ }
277
386
  })();
278
- // Race between query execution, timeout, and process exit
279
- return await Promise.race([
280
- queryPromise,
281
- timeoutPromise,
282
- processExitPromise,
387
+ const timeoutPromise = new Promise((_, reject) => {
388
+ setTimeout(() => {
389
+ reject(new Error(`Subagent query timed out after ${timeoutMs / 1000} seconds`));
390
+ }, timeoutMs);
391
+ });
392
+ // Wait for prompt to complete with timeout
393
+ await Promise.race([promptPromise, timeoutPromise, processErrorPromise]);
394
+ // Give SSE a moment to flush remaining messages
395
+ await new Promise((r) => setTimeout(r, 100));
396
+ // Abort SSE connection
397
+ sseAbortController.abort();
398
+ // Wait for SSE to finish (with timeout)
399
+ await Promise.race([
400
+ ssePromise.catch(() => { }), // Ignore abort errors
401
+ new Promise((r) => setTimeout(r, 1000)),
283
402
  ]);
403
+ return responseText;
284
404
  }
285
405
  finally {
286
- // Cleanup: kill process and close connection
406
+ // Cleanup: abort SSE and kill process
407
+ logger.info(`Shutting down subagent on port ${port}`);
408
+ if (sseAbortController) {
409
+ sseAbortController.abort();
410
+ }
287
411
  if (agentProcess) {
288
412
  agentProcess.kill();
289
413
  }
290
- if (connection) {
291
- try {
292
- await Promise.race([
293
- connection.closed,
294
- new Promise((resolve) => setTimeout(resolve, 1000)),
295
- ]);
296
- }
297
- catch {
298
- // Ignore cleanup errors
299
- }
300
- }
301
414
  }
302
415
  }
@@ -1,6 +1,12 @@
1
1
  import { z } from "zod";
2
2
  /** Built-in tool types. */
3
- export declare const zBuiltInToolType: z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">]>;
3
+ export declare const zBuiltInToolType: z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">, z.ZodLiteral<"generate_image">]>;
4
+ /** Subagent configuration schema for Task tools. */
5
+ export declare const zSubagentConfig: z.ZodObject<{
6
+ agentName: z.ZodString;
7
+ description: z.ZodString;
8
+ displayName: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>;
4
10
  /** Direct tool object schema (for tools imported directly in code). */
5
11
  declare const zDirectTool: z.ZodObject<{
6
12
  type: z.ZodLiteral<"direct">;
@@ -10,9 +16,14 @@ declare const zDirectTool: z.ZodObject<{
10
16
  schema: z.ZodAny;
11
17
  prettyName: z.ZodOptional<z.ZodString>;
12
18
  icon: z.ZodOptional<z.ZodString>;
19
+ subagentConfigs: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ agentName: z.ZodString;
21
+ description: z.ZodString;
22
+ displayName: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strip>>>;
13
24
  }, z.core.$strip>;
14
25
  /** Tool type - can be a built-in tool string or custom tool object. */
15
- export declare const zToolType: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodLiteral<"todo_write">, z.ZodLiteral<"get_weather">, z.ZodLiteral<"web_search">, z.ZodLiteral<"filesystem">]>, z.ZodObject<{
26
+ export declare const zToolType: 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<{
16
27
  type: z.ZodLiteral<"custom">;
17
28
  modulePath: z.ZodString;
18
29
  }, z.core.$strip>, z.ZodObject<{
@@ -26,8 +37,14 @@ export declare const zToolType: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodL
26
37
  schema: z.ZodAny;
27
38
  prettyName: z.ZodOptional<z.ZodString>;
28
39
  icon: z.ZodOptional<z.ZodString>;
40
+ subagentConfigs: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
+ agentName: z.ZodString;
42
+ description: z.ZodString;
43
+ displayName: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strip>>>;
29
45
  }, z.core.$strip>]>;
30
46
  export type ToolType = z.infer<typeof zToolType>;
31
47
  export type BuiltInToolType = z.infer<typeof zBuiltInToolType>;
32
48
  export type DirectTool = z.infer<typeof zDirectTool>;
49
+ export type SubagentConfig = z.infer<typeof zSubagentConfig>;
33
50
  export {};
@@ -5,6 +5,7 @@ export const zBuiltInToolType = z.union([
5
5
  z.literal("get_weather"),
6
6
  z.literal("web_search"),
7
7
  z.literal("filesystem"),
8
+ z.literal("generate_image"),
8
9
  ]);
9
10
  /** Custom tool schema (loaded from module path). */
10
11
  const zCustomTool = z.object({
@@ -16,6 +17,12 @@ const zFilesystemTool = z.object({
16
17
  type: z.literal("filesystem"),
17
18
  working_directory: z.string().optional(),
18
19
  });
20
+ /** Subagent configuration schema for Task tools. */
21
+ export const zSubagentConfig = z.object({
22
+ agentName: z.string(),
23
+ description: z.string(),
24
+ displayName: z.string().optional(),
25
+ });
19
26
  /** Direct tool object schema (for tools imported directly in code). */
20
27
  const zDirectTool = z.object({
21
28
  type: z.literal("direct"),
@@ -25,6 +32,8 @@ const zDirectTool = z.object({
25
32
  schema: z.any(), // Accept any Zod schema
26
33
  prettyName: z.string().optional(),
27
34
  icon: z.string().optional(),
35
+ /** Subagent configurations (only present for Task tools created by makeSubagentsTool). */
36
+ subagentConfigs: z.array(zSubagentConfig).optional(),
28
37
  });
29
38
  /** Tool type - can be a built-in tool string or custom tool object. */
30
39
  export const zToolType = z.union([
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readdir, rm, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { createLogger } from "@townco/core";
4
+ import { createLogger } from "../logger.js";
5
5
  const AGENTS_DIR = join(homedir(), ".config", "town", "agents");
6
6
  const logger = createLogger("storage");
7
7
  /**
@@ -3,7 +3,7 @@
3
3
  * Provides tracing and logging capabilities for agent operations
4
4
  */
5
5
  import { context, SpanStatusCode, trace, } from "@opentelemetry/api";
6
- import { logs } from "@opentelemetry/api-logs";
6
+ import { logs, SeverityNumber, } from "@opentelemetry/api-logs";
7
7
  class AgentTelemetry {
8
8
  tracer = null;
9
9
  logger = null;
@@ -115,7 +115,13 @@ class AgentTelemetry {
115
115
  if (!this.enabled || !this.logger) {
116
116
  return;
117
117
  }
118
+ const severityNumber = {
119
+ info: SeverityNumber.INFO,
120
+ warn: SeverityNumber.WARN,
121
+ error: SeverityNumber.ERROR,
122
+ }[level];
118
123
  this.logger.emit({
124
+ severityNumber,
119
125
  severityText: level.toUpperCase(),
120
126
  body: message,
121
127
  attributes: {
@@ -2,6 +2,9 @@ import type { AgentDefinition } from "../definition";
2
2
  export interface TemplateVars {
3
3
  name: string;
4
4
  model: string;
5
+ displayName?: string;
6
+ description?: string;
7
+ suggestedPrompts?: string[];
5
8
  tools: Array<string | {
6
9
  type: "custom";
7
10
  modulePath: string;
@@ -1,7 +1,7 @@
1
1
  import * as prettier from "prettier";
2
2
  export function getTemplateVars(name, definition) {
3
3
  const tools = definition.tools ?? [];
4
- return {
4
+ const result = {
5
5
  name,
6
6
  model: definition.model,
7
7
  tools,
@@ -9,6 +9,16 @@ export function getTemplateVars(name, definition) {
9
9
  hasWebSearch: tools.some((tool) => typeof tool === "string" && tool === "web_search"),
10
10
  hooks: definition.hooks,
11
11
  };
12
+ if (definition.displayName) {
13
+ result.displayName = definition.displayName;
14
+ }
15
+ if (definition.description) {
16
+ result.description = definition.description;
17
+ }
18
+ if (definition.suggestedPrompts) {
19
+ result.suggestedPrompts = definition.suggestedPrompts;
20
+ }
21
+ return result;
12
22
  }
13
23
  export function generatePackageJson(vars) {
14
24
  // Include @townco/agent as a dependency instead of bundling
@@ -44,16 +54,28 @@ export function generatePackageJson(vars) {
44
54
  return JSON.stringify(pkg, null, 2);
45
55
  }
46
56
  export async function generateIndexTs(vars) {
57
+ // Build agent definition with fields in a logical order
47
58
  const agentDef = {
48
59
  model: vars.model,
49
- systemPrompt: vars.systemPrompt,
50
- tools: vars.tools,
51
- hooks: vars.hooks,
52
60
  };
61
+ if (vars.displayName) {
62
+ agentDef.displayName = vars.displayName;
63
+ }
64
+ if (vars.description) {
65
+ agentDef.description = vars.description;
66
+ }
67
+ if (vars.suggestedPrompts) {
68
+ agentDef.suggestedPrompts = vars.suggestedPrompts;
69
+ }
70
+ agentDef.systemPrompt = vars.systemPrompt;
71
+ agentDef.tools = vars.tools;
72
+ if (vars.hooks) {
73
+ agentDef.hooks = vars.hooks;
74
+ }
53
75
  return prettier.format(`import { makeHttpTransport, makeStdioTransport } from "@townco/agent/acp-server";
54
76
  import type { AgentDefinition } from "@townco/agent/definition";
77
+ import { createLogger } from "@townco/agent/logger";
55
78
  import { basename } from "node:path";
56
- import { createLogger } from "@townco/core";
57
79
 
58
80
  const logger = createLogger("agent-index");
59
81