@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.
- package/dist/acp-server/adapter.d.ts +1 -0
- package/dist/acp-server/adapter.js +9 -1
- package/dist/acp-server/http.js +75 -6
- package/dist/acp-server/session-storage.d.ts +14 -0
- package/dist/acp-server/session-storage.js +47 -0
- package/dist/definition/index.d.ts +9 -0
- package/dist/definition/index.js +9 -0
- package/dist/index.js +1 -1
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +43 -0
- package/dist/runner/agent-runner.d.ts +5 -1
- package/dist/runner/agent-runner.js +2 -1
- package/dist/runner/hooks/executor.js +1 -1
- package/dist/runner/hooks/loader.js +1 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +1 -1
- package/dist/runner/hooks/predefined/tool-response-compactor.js +1 -1
- package/dist/runner/langchain/index.js +19 -7
- package/dist/runner/langchain/model-factory.d.ts +2 -0
- package/dist/runner/langchain/model-factory.js +20 -1
- package/dist/runner/langchain/tools/browser.d.ts +100 -0
- package/dist/runner/langchain/tools/browser.js +412 -0
- package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
- package/dist/runner/langchain/tools/port-utils.js +35 -0
- package/dist/runner/langchain/tools/subagent.js +230 -127
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +1 -0
- package/dist/scaffold/index.js +7 -1
- package/dist/scaffold/templates/dot-claude/CLAUDE-append.md +2 -0
- package/dist/storage/index.js +1 -1
- package/dist/telemetry/index.d.ts +5 -0
- package/dist/telemetry/index.js +10 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.js +30 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -1
- package/package.json +11 -6
- package/templates/index.ts +40 -8
|
@@ -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 {
|
|
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
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
|
*
|
|
@@ -145,14 +174,14 @@ assistant: "I'm going to use the Task tool to launch the greeting-responder agen
|
|
|
145
174
|
if (!agent) {
|
|
146
175
|
throw new Error(`Unknown agent: ${agentName}`);
|
|
147
176
|
}
|
|
148
|
-
return await querySubagent(agent.agentPath, agent.agentDir, query);
|
|
177
|
+
return await querySubagent(agentName, agent.agentPath, agent.agentDir, query);
|
|
149
178
|
},
|
|
150
179
|
};
|
|
151
180
|
}
|
|
152
181
|
/**
|
|
153
|
-
* Internal function that spawns a subagent
|
|
182
|
+
* Internal function that spawns a subagent HTTP server and queries it.
|
|
154
183
|
*/
|
|
155
|
-
async function querySubagent(agentPath, agentWorkingDirectory, query) {
|
|
184
|
+
async function querySubagent(agentName, agentPath, agentWorkingDirectory, query) {
|
|
156
185
|
// Validate that the agent exists
|
|
157
186
|
try {
|
|
158
187
|
await fs.access(agentPath);
|
|
@@ -160,153 +189,227 @@ async function querySubagent(agentPath, agentWorkingDirectory, query) {
|
|
|
160
189
|
catch (_error) {
|
|
161
190
|
throw new Error(`Agent not found at ${agentPath}. Make sure the agent exists and has an index.ts file.`);
|
|
162
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}`);
|
|
163
197
|
let agentProcess = null;
|
|
164
|
-
let
|
|
198
|
+
let sseAbortController = null;
|
|
165
199
|
try {
|
|
166
|
-
//
|
|
167
|
-
|
|
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"], {
|
|
168
204
|
cwd: agentWorkingDirectory,
|
|
169
|
-
env: {
|
|
205
|
+
env: {
|
|
206
|
+
...process.env,
|
|
207
|
+
PORT: String(port),
|
|
208
|
+
TOWN_LOGS_DIR: parentLogsDir,
|
|
209
|
+
TOWN_SUBAGENT_NAME: agentName,
|
|
210
|
+
},
|
|
170
211
|
stdio: ["pipe", "pipe", "pipe"],
|
|
171
212
|
});
|
|
172
|
-
if (!agentProcess.
|
|
173
|
-
throw new Error("Failed to create
|
|
213
|
+
if (!agentProcess.stderr) {
|
|
214
|
+
throw new Error("Failed to create stderr pipe for agent process");
|
|
174
215
|
}
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
async requestPermission(_params) {
|
|
186
|
-
// Deny all permission requests from the subagent
|
|
187
|
-
return { outcome: { outcome: "cancelled" } };
|
|
188
|
-
},
|
|
189
|
-
async sessionUpdate(params) {
|
|
190
|
-
// Handle session updates from the agent
|
|
191
|
-
const paramsExtended = params;
|
|
192
|
-
const update = paramsExtended.update;
|
|
193
|
-
// Reset accumulated text when a tool call starts (marks a new message boundary)
|
|
194
|
-
if (update?.sessionUpdate === "tool_call") {
|
|
195
|
-
responseText = "";
|
|
196
|
-
}
|
|
197
|
-
// Accumulate agent_message_chunk text content
|
|
198
|
-
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
199
|
-
const content = update.content;
|
|
200
|
-
if (content &&
|
|
201
|
-
content.type === "text" &&
|
|
202
|
-
typeof content.text === "string") {
|
|
203
|
-
responseText += content.text;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
async writeTextFile() {
|
|
208
|
-
// Subagents should not write files outside their scope
|
|
209
|
-
throw new Error("Subagent attempted to write files, which is not allowed");
|
|
210
|
-
},
|
|
211
|
-
async readTextFile() {
|
|
212
|
-
// Subagents should not read files outside their scope
|
|
213
|
-
throw new Error("Subagent attempted to read files, which is not allowed");
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
};
|
|
217
|
-
// Create the client-side connection
|
|
218
|
-
connection = new ClientSideConnection(clientFactory, stream);
|
|
219
|
-
// Set up timeout for the entire operation
|
|
220
|
-
const timeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
221
|
-
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
222
|
-
setTimeout(() => {
|
|
223
|
-
reject(new Error(`Subagent query timed out after ${timeoutMs / 1000} seconds`));
|
|
224
|
-
}, timeoutMs);
|
|
225
|
-
});
|
|
226
|
-
// Handle process errors and exit
|
|
227
|
-
const processExitPromise = new Promise((_resolve, reject) => {
|
|
228
|
-
agentProcess?.on("exit", (code, signal) => {
|
|
229
|
-
if (code !== 0 && code !== null) {
|
|
230
|
-
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);
|
|
231
225
|
}
|
|
232
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) => {
|
|
233
240
|
agentProcess?.on("error", (error) => {
|
|
241
|
+
logger.error(`Process error: ${error.message}`);
|
|
234
242
|
reject(new Error(`Agent process error: ${error.message}`));
|
|
235
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
|
+
});
|
|
236
250
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 },
|
|
246
268
|
},
|
|
247
269
|
},
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const ctxForInjection = activeSpan
|
|
263
|
-
? trace.setSpan(activeCtx, activeSpan)
|
|
264
|
-
: 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);
|
|
265
285
|
propagation.inject(ctxForInjection, otelCarrier);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
},
|
|
274
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,
|
|
275
320
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 }],
|
|
283
375
|
},
|
|
284
|
-
|
|
376
|
+
}),
|
|
285
377
|
});
|
|
286
|
-
|
|
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
|
+
}
|
|
287
386
|
})();
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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)),
|
|
293
402
|
]);
|
|
403
|
+
return responseText;
|
|
294
404
|
}
|
|
295
405
|
finally {
|
|
296
|
-
// Cleanup:
|
|
406
|
+
// Cleanup: abort SSE and kill process
|
|
407
|
+
logger.info(`Shutting down subagent on port ${port}`);
|
|
408
|
+
if (sseAbortController) {
|
|
409
|
+
sseAbortController.abort();
|
|
410
|
+
}
|
|
297
411
|
if (agentProcess) {
|
|
298
412
|
agentProcess.kill();
|
|
299
413
|
}
|
|
300
|
-
if (connection) {
|
|
301
|
-
try {
|
|
302
|
-
await Promise.race([
|
|
303
|
-
connection.closed,
|
|
304
|
-
new Promise((resolve) => setTimeout(resolve, 1000)),
|
|
305
|
-
]);
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
// Ignore cleanup errors
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
414
|
}
|
|
312
415
|
}
|
package/dist/runner/tools.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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">, z.ZodLiteral<"generate_image">]>;
|
|
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">, z.ZodLiteral<"browser">]>;
|
|
4
4
|
/** Subagent configuration schema for Task tools. */
|
|
5
5
|
export declare const zSubagentConfig: z.ZodObject<{
|
|
6
6
|
agentName: z.ZodString;
|
|
@@ -23,7 +23,7 @@ declare const zDirectTool: z.ZodObject<{
|
|
|
23
23
|
}, z.core.$strip>>>;
|
|
24
24
|
}, z.core.$strip>;
|
|
25
25
|
/** Tool type - can be a built-in tool string or custom tool object. */
|
|
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<{
|
|
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.ZodLiteral<"browser">]>, z.ZodObject<{
|
|
27
27
|
type: z.ZodLiteral<"custom">;
|
|
28
28
|
modulePath: z.ZodString;
|
|
29
29
|
}, z.core.$strip>, z.ZodObject<{
|
package/dist/runner/tools.js
CHANGED
package/dist/scaffold/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { generateBinTs, generateIndexTs, getTemplateVars } from "../templates";
|
|
4
|
+
import { generateBinTs, generateEnvExample, generateIndexTs, generateReadme, getTemplateVars, } from "../templates";
|
|
5
5
|
import { copyGuiApp } from "./copy-gui";
|
|
6
6
|
import { copyTuiApp } from "./copy-tui";
|
|
7
7
|
/**
|
|
@@ -34,7 +34,13 @@ export async function scaffoldAgent(options) {
|
|
|
34
34
|
const files = [
|
|
35
35
|
{ path: "index.ts", content: await generateIndexTs(vars) },
|
|
36
36
|
{ path: "bin.ts", content: generateBinTs(), executable: true },
|
|
37
|
+
{ path: "README.md", content: generateReadme(vars) },
|
|
37
38
|
];
|
|
39
|
+
// Add .env.example if needed
|
|
40
|
+
const envExample = generateEnvExample(vars);
|
|
41
|
+
if (envExample) {
|
|
42
|
+
files.push({ path: ".env.example", content: envExample });
|
|
43
|
+
}
|
|
38
44
|
// Write all files
|
|
39
45
|
for (const file of files) {
|
|
40
46
|
const filePath = join(agentPath, file.path);
|
|
@@ -9,6 +9,8 @@ The following built-in tools are available:
|
|
|
9
9
|
- `todo_write`: Task management and planning tool
|
|
10
10
|
- `web_search`: Exa-powered web search (requires EXA_API_KEY)
|
|
11
11
|
- `filesystem`: Read, write, and search files in the project directory
|
|
12
|
+
- `generate_image`: Image generation using Google Gemini (requires GEMINI_API_KEY or GOOGLE_API_KEY)
|
|
13
|
+
- `browser`: Cloud browser automation using Kernel (requires KERNEL_API_KEY)
|
|
12
14
|
|
|
13
15
|
To use built-in tools, simply add them to the `tools` array in your agent definition:
|
|
14
16
|
```
|
package/dist/storage/index.js
CHANGED
|
@@ -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 "
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
5
|
const AGENTS_DIR = join(homedir(), ".config", "town", "agents");
|
|
6
6
|
const logger = createLogger("storage");
|
|
7
7
|
/**
|
|
@@ -23,6 +23,11 @@ declare class AgentTelemetry {
|
|
|
23
23
|
private serviceName;
|
|
24
24
|
private baseAttributes;
|
|
25
25
|
configure(config: TelemetryConfig): void;
|
|
26
|
+
/**
|
|
27
|
+
* Update base attributes that will be added to all future spans
|
|
28
|
+
* @param attributes - Attributes to merge with existing base attributes
|
|
29
|
+
*/
|
|
30
|
+
setBaseAttributes(attributes: Record<string, string | number>): void;
|
|
26
31
|
/**
|
|
27
32
|
* Start a new span
|
|
28
33
|
* @param name - Span name
|
package/dist/telemetry/index.js
CHANGED
|
@@ -30,6 +30,16 @@ class AgentTelemetry {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Update base attributes that will be added to all future spans
|
|
35
|
+
* @param attributes - Attributes to merge with existing base attributes
|
|
36
|
+
*/
|
|
37
|
+
setBaseAttributes(attributes) {
|
|
38
|
+
this.baseAttributes = {
|
|
39
|
+
...this.baseAttributes,
|
|
40
|
+
...attributes,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
33
43
|
/**
|
|
34
44
|
* Start a new span
|
|
35
45
|
* @param name - Span name
|
package/dist/templates/index.js
CHANGED
|
@@ -7,6 +7,8 @@ export function getTemplateVars(name, definition) {
|
|
|
7
7
|
tools,
|
|
8
8
|
systemPrompt: definition.systemPrompt,
|
|
9
9
|
hasWebSearch: tools.some((tool) => typeof tool === "string" && tool === "web_search"),
|
|
10
|
+
hasGenerateImage: tools.some((tool) => typeof tool === "string" && tool === "generate_image"),
|
|
11
|
+
hasBrowser: tools.some((tool) => typeof tool === "string" && tool === "browser"),
|
|
10
12
|
hooks: definition.hooks,
|
|
11
13
|
};
|
|
12
14
|
if (definition.displayName) {
|
|
@@ -74,8 +76,8 @@ export async function generateIndexTs(vars) {
|
|
|
74
76
|
}
|
|
75
77
|
return prettier.format(`import { makeHttpTransport, makeStdioTransport } from "@townco/agent/acp-server";
|
|
76
78
|
import type { AgentDefinition } from "@townco/agent/definition";
|
|
79
|
+
import { createLogger } from "@townco/agent/logger";
|
|
77
80
|
import { basename } from "node:path";
|
|
78
|
-
import { createLogger } from "@townco/core";
|
|
79
81
|
|
|
80
82
|
const logger = createLogger("agent-index");
|
|
81
83
|
|
|
@@ -148,9 +150,15 @@ export function generateReadme(vars) {
|
|
|
148
150
|
})
|
|
149
151
|
.join(", ")
|
|
150
152
|
: "None";
|
|
151
|
-
const envVars =
|
|
152
|
-
? "
|
|
153
|
-
|
|
153
|
+
const envVars = [
|
|
154
|
+
vars.hasWebSearch ? "- `EXA_API_KEY`: Required for web_search tool" : "",
|
|
155
|
+
vars.hasGenerateImage
|
|
156
|
+
? "- `GEMINI_API_KEY` (or `GOOGLE_API_KEY`): Required for generate_image tool"
|
|
157
|
+
: "",
|
|
158
|
+
vars.hasBrowser ? "- `KERNEL_API_KEY`: Required for browser tool" : "",
|
|
159
|
+
]
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.join("\n");
|
|
154
162
|
return `# ${vars.name}
|
|
155
163
|
|
|
156
164
|
Agent created with \`town create\`.
|
|
@@ -211,10 +219,24 @@ bun run build
|
|
|
211
219
|
`;
|
|
212
220
|
}
|
|
213
221
|
export function generateEnvExample(vars) {
|
|
214
|
-
|
|
222
|
+
const envs = [];
|
|
223
|
+
if (vars.hasWebSearch) {
|
|
224
|
+
envs.push("# Required for web_search tool");
|
|
225
|
+
envs.push("EXA_API_KEY=your_exa_api_key_here");
|
|
226
|
+
envs.push("");
|
|
227
|
+
}
|
|
228
|
+
if (vars.hasGenerateImage) {
|
|
229
|
+
envs.push("# Required for generate_image tool");
|
|
230
|
+
envs.push("GEMINI_API_KEY=your_gemini_api_key_here");
|
|
231
|
+
envs.push("");
|
|
232
|
+
}
|
|
233
|
+
if (vars.hasBrowser) {
|
|
234
|
+
envs.push("# Required for browser tool");
|
|
235
|
+
envs.push("KERNEL_API_KEY=your_kernel_api_key_here");
|
|
236
|
+
envs.push("");
|
|
237
|
+
}
|
|
238
|
+
if (envs.length === 0) {
|
|
215
239
|
return null;
|
|
216
240
|
}
|
|
217
|
-
return
|
|
218
|
-
EXA_API_KEY=your_exa_api_key_here
|
|
219
|
-
`;
|
|
241
|
+
return envs.join("\n");
|
|
220
242
|
}
|