@townco/agent 0.1.55 → 0.1.57
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 +7 -0
- package/dist/acp-server/adapter.js +96 -18
- package/dist/acp-server/http.js +74 -1
- 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 +16 -0
- package/dist/definition/index.js +16 -0
- package/dist/runner/agent-runner.d.ts +8 -1
- package/dist/runner/agent-runner.js +3 -1
- package/dist/runner/langchain/index.js +139 -7
- package/dist/runner/langchain/model-factory.d.ts +2 -0
- package/dist/runner/langchain/model-factory.js +19 -0
- package/dist/runner/langchain/tools/browser.d.ts +100 -0
- package/dist/runner/langchain/tools/browser.js +412 -0
- package/dist/runner/langchain/tools/subagent-connections.d.ts +28 -0
- package/dist/runner/langchain/tools/subagent-connections.js +58 -0
- package/dist/runner/langchain/tools/subagent.js +5 -1
- 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/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 +29 -7
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/templates/index.ts +39 -7
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import Kernel from "@onkernel/sdk";
|
|
4
|
+
import { tool } from "langchain";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
let _kernelClient = null;
|
|
7
|
+
let _sessionId = null;
|
|
8
|
+
let _liveViewUrl = null;
|
|
9
|
+
function getKernelClient() {
|
|
10
|
+
if (_kernelClient) {
|
|
11
|
+
return _kernelClient;
|
|
12
|
+
}
|
|
13
|
+
const apiKey = process.env.KERNEL_API_KEY;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw new Error("KERNEL_API_KEY environment variable is required to use the browser tool. " +
|
|
16
|
+
"Please set it to your Kernel API key from https://onkernel.com");
|
|
17
|
+
}
|
|
18
|
+
_kernelClient = new Kernel({ apiKey });
|
|
19
|
+
return _kernelClient;
|
|
20
|
+
}
|
|
21
|
+
async function ensureBrowserSession() {
|
|
22
|
+
if (_sessionId && _liveViewUrl) {
|
|
23
|
+
return { sessionId: _sessionId, liveViewUrl: _liveViewUrl };
|
|
24
|
+
}
|
|
25
|
+
const kernel = getKernelClient();
|
|
26
|
+
let kernelBrowser;
|
|
27
|
+
try {
|
|
28
|
+
kernelBrowser = await kernel.browsers.create();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`Failed to create Kernel browser. Check KERNEL_API_KEY is valid. Error: ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
_sessionId = kernelBrowser.session_id;
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: browser_live_view_url not yet typed in @onkernel/sdk
|
|
36
|
+
_liveViewUrl = kernelBrowser.browser_live_view_url;
|
|
37
|
+
if (!_sessionId) {
|
|
38
|
+
throw new Error("Kernel browser created but no session_id returned.");
|
|
39
|
+
}
|
|
40
|
+
if (!_liveViewUrl) {
|
|
41
|
+
throw new Error("Kernel browser created but no browser_live_view_url returned.");
|
|
42
|
+
}
|
|
43
|
+
return { sessionId: _sessionId, liveViewUrl: _liveViewUrl };
|
|
44
|
+
}
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: Kernel SDK playwright.execute not yet typed
|
|
46
|
+
async function executePlaywright(code) {
|
|
47
|
+
const kernel = getKernelClient();
|
|
48
|
+
const { sessionId } = await ensureBrowserSession();
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: playwright.execute not yet typed in @onkernel/sdk
|
|
50
|
+
const response = await kernel.browsers.playwright.execute(sessionId, {
|
|
51
|
+
code,
|
|
52
|
+
timeout_sec: 60,
|
|
53
|
+
});
|
|
54
|
+
return response;
|
|
55
|
+
}
|
|
56
|
+
export function makeBrowserTools() {
|
|
57
|
+
// Browser Navigate tool
|
|
58
|
+
const browserNavigate = tool(async ({ url, waitUntil = "domcontentloaded", }) => {
|
|
59
|
+
try {
|
|
60
|
+
const { liveViewUrl } = await ensureBrowserSession();
|
|
61
|
+
const code = `
|
|
62
|
+
await page.goto('${url.replace(/'/g, "\\'")}', {
|
|
63
|
+
waitUntil: '${waitUntil}',
|
|
64
|
+
timeout: 30000
|
|
65
|
+
});
|
|
66
|
+
return { url: page.url(), title: await page.title() };
|
|
67
|
+
`;
|
|
68
|
+
const response = await executePlaywright(code);
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
url: response.result?.url,
|
|
72
|
+
title: response.result?.title,
|
|
73
|
+
liveViewUrl,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
error: `Navigation failed: ${errorMessage}`,
|
|
81
|
+
...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}, {
|
|
85
|
+
name: "BrowserNavigate",
|
|
86
|
+
description: "Navigate a cloud browser to a specified URL using Kernel's browser-as-a-service.\n" +
|
|
87
|
+
"- Opens web pages in a cloud-hosted browser\n" +
|
|
88
|
+
"- Returns the final URL (after redirects), page title, and a liveViewUrl\n" +
|
|
89
|
+
"- The liveViewUrl allows viewing the browser session in real-time\n" +
|
|
90
|
+
"- Browser session persists across tool calls\n" +
|
|
91
|
+
"\n" +
|
|
92
|
+
"Usage notes:\n" +
|
|
93
|
+
" - First call creates a new browser session\n" +
|
|
94
|
+
" - Subsequent calls reuse the same browser\n" +
|
|
95
|
+
" - Share the liveViewUrl with users so they can watch the browser\n" +
|
|
96
|
+
" - Use BrowserClose when done to release resources\n",
|
|
97
|
+
schema: z.object({
|
|
98
|
+
url: z.string().url().describe("The URL to navigate to"),
|
|
99
|
+
waitUntil: z
|
|
100
|
+
.enum(["load", "domcontentloaded", "networkidle"])
|
|
101
|
+
.optional()
|
|
102
|
+
.default("domcontentloaded")
|
|
103
|
+
.describe("When to consider navigation complete"),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
browserNavigate.prettyName = "Browser Navigate";
|
|
107
|
+
browserNavigate.icon = "Globe";
|
|
108
|
+
// Browser Screenshot tool
|
|
109
|
+
const browserScreenshot = tool(async ({ fullPage = false, selector, }) => {
|
|
110
|
+
try {
|
|
111
|
+
const { liveViewUrl } = await ensureBrowserSession();
|
|
112
|
+
let code;
|
|
113
|
+
if (selector) {
|
|
114
|
+
code = `
|
|
115
|
+
const element = await page.$('${selector.replace(/'/g, "\\'")}');
|
|
116
|
+
if (!element) {
|
|
117
|
+
return { error: 'Element not found: ${selector.replace(/'/g, "\\'")}' };
|
|
118
|
+
}
|
|
119
|
+
const screenshot = await element.screenshot({ type: 'png' });
|
|
120
|
+
return { screenshot: screenshot.toString('base64') };
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
code = `
|
|
125
|
+
const screenshot = await page.screenshot({ type: 'png', fullPage: ${fullPage} });
|
|
126
|
+
return { screenshot: screenshot.toString('base64') };
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
const response = await executePlaywright(code);
|
|
130
|
+
if (response.result?.error) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: response.result.error,
|
|
134
|
+
liveViewUrl,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Save screenshot to local file instead of returning base64
|
|
138
|
+
const screenshotsDir = join(process.cwd(), "screenshots");
|
|
139
|
+
await mkdir(screenshotsDir, { recursive: true });
|
|
140
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
141
|
+
const filename = `screenshot-${timestamp}.png`;
|
|
142
|
+
const filepath = join(screenshotsDir, filename);
|
|
143
|
+
const imageBuffer = Buffer.from(response.result?.screenshot, "base64");
|
|
144
|
+
await Bun.write(filepath, imageBuffer);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
screenshotPath: filepath,
|
|
148
|
+
liveViewUrl,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Screenshot failed: ${errorMessage}`,
|
|
156
|
+
...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}, {
|
|
160
|
+
name: "BrowserScreenshot",
|
|
161
|
+
description: "Take a screenshot of the current browser page or a specific element.\n" +
|
|
162
|
+
"- Captures the current viewport or full page\n" +
|
|
163
|
+
"- Can target specific elements using CSS selectors\n" +
|
|
164
|
+
"- Saves the screenshot to a local file and returns the file path\n" +
|
|
165
|
+
"\n" +
|
|
166
|
+
"Usage notes:\n" +
|
|
167
|
+
" - Use fullPage=true to capture the entire scrollable page\n" +
|
|
168
|
+
" - Use selector to capture a specific element\n" +
|
|
169
|
+
" - Requires an active browser session (call BrowserNavigate first)\n" +
|
|
170
|
+
" - Screenshots are saved in the 'screenshots' folder in the current working directory\n",
|
|
171
|
+
schema: z.object({
|
|
172
|
+
fullPage: z
|
|
173
|
+
.boolean()
|
|
174
|
+
.optional()
|
|
175
|
+
.default(false)
|
|
176
|
+
.describe("Whether to capture the full scrollable page"),
|
|
177
|
+
selector: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("CSS selector of element to screenshot (captures whole page if not provided)"),
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
browserScreenshot.prettyName = "Browser Screenshot";
|
|
184
|
+
browserScreenshot.icon = "Camera";
|
|
185
|
+
// Browser Extract Content tool
|
|
186
|
+
const browserExtract = tool(async ({ selector, extractType = "text", }) => {
|
|
187
|
+
try {
|
|
188
|
+
const { liveViewUrl } = await ensureBrowserSession();
|
|
189
|
+
let code;
|
|
190
|
+
if (selector) {
|
|
191
|
+
if (extractType === "html") {
|
|
192
|
+
code = `
|
|
193
|
+
const element = await page.$('${selector.replace(/'/g, "\\'")}');
|
|
194
|
+
if (!element) {
|
|
195
|
+
return { error: 'Element not found: ${selector.replace(/'/g, "\\'")}' };
|
|
196
|
+
}
|
|
197
|
+
const content = await element.innerHTML();
|
|
198
|
+
return { content, url: page.url(), title: await page.title() };
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
code = `
|
|
203
|
+
const element = await page.$('${selector.replace(/'/g, "\\'")}');
|
|
204
|
+
if (!element) {
|
|
205
|
+
return { error: 'Element not found: ${selector.replace(/'/g, "\\'")}' };
|
|
206
|
+
}
|
|
207
|
+
const content = await element.innerText();
|
|
208
|
+
return { content, url: page.url(), title: await page.title() };
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
if (extractType === "html") {
|
|
214
|
+
code = `
|
|
215
|
+
const content = await page.content();
|
|
216
|
+
return { content, url: page.url(), title: await page.title() };
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
code = `
|
|
221
|
+
const content = await page.innerText('body');
|
|
222
|
+
return { content, url: page.url(), title: await page.title() };
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const response = await executePlaywright(code);
|
|
227
|
+
if (response.result?.error) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: response.result.error,
|
|
231
|
+
liveViewUrl,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
success: true,
|
|
236
|
+
content: response.result?.content,
|
|
237
|
+
url: response.result?.url,
|
|
238
|
+
title: response.result?.title,
|
|
239
|
+
liveViewUrl,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
error: `Content extraction failed: ${errorMessage}`,
|
|
247
|
+
...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}, {
|
|
251
|
+
name: "BrowserExtract",
|
|
252
|
+
description: "Extract text or HTML content from the current page or a specific element.\n" +
|
|
253
|
+
"- Gets readable text content from web pages\n" +
|
|
254
|
+
"- Can extract HTML for structured data\n" +
|
|
255
|
+
"- Supports CSS selectors for targeting specific elements\n" +
|
|
256
|
+
"\n" +
|
|
257
|
+
"Usage notes:\n" +
|
|
258
|
+
" - Default extracts text from the entire page body\n" +
|
|
259
|
+
" - Use selector to target specific page sections\n" +
|
|
260
|
+
" - Use extractType='html' when you need the raw HTML structure\n",
|
|
261
|
+
schema: z.object({
|
|
262
|
+
selector: z
|
|
263
|
+
.string()
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("CSS selector of element to extract from (extracts from body if not provided)"),
|
|
266
|
+
extractType: z
|
|
267
|
+
.enum(["text", "html"])
|
|
268
|
+
.optional()
|
|
269
|
+
.default("text")
|
|
270
|
+
.describe("Type of content to extract"),
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
browserExtract.prettyName = "Browser Extract";
|
|
274
|
+
browserExtract.icon = "FileText";
|
|
275
|
+
// Browser Click tool
|
|
276
|
+
const browserClick = tool(async ({ selector, button = "left" }) => {
|
|
277
|
+
try {
|
|
278
|
+
const { liveViewUrl } = await ensureBrowserSession();
|
|
279
|
+
const code = `
|
|
280
|
+
await page.click('${selector.replace(/'/g, "\\'")}', {
|
|
281
|
+
button: '${button}',
|
|
282
|
+
timeout: 10000,
|
|
283
|
+
});
|
|
284
|
+
await page.waitForTimeout(500);
|
|
285
|
+
return { success: true };
|
|
286
|
+
`;
|
|
287
|
+
await executePlaywright(code);
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
message: `Clicked element: ${selector}`,
|
|
291
|
+
liveViewUrl,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: `Click failed: ${errorMessage}`,
|
|
299
|
+
...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}, {
|
|
303
|
+
name: "BrowserClick",
|
|
304
|
+
description: "Click on an element in the current browser page.\n" +
|
|
305
|
+
"- Supports left, right, and middle mouse buttons\n" +
|
|
306
|
+
"- Waits for the element to be visible before clicking\n" +
|
|
307
|
+
"- Useful for interacting with buttons, links, and other clickable elements\n" +
|
|
308
|
+
"\n" +
|
|
309
|
+
"Usage notes:\n" +
|
|
310
|
+
" - Provide a CSS selector to identify the target element\n" +
|
|
311
|
+
" - The tool waits briefly after clicking for page updates\n",
|
|
312
|
+
schema: z.object({
|
|
313
|
+
selector: z.string().describe("CSS selector of the element to click"),
|
|
314
|
+
button: z
|
|
315
|
+
.enum(["left", "right", "middle"])
|
|
316
|
+
.optional()
|
|
317
|
+
.default("left")
|
|
318
|
+
.describe("Which mouse button to use"),
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
browserClick.prettyName = "Browser Click";
|
|
322
|
+
browserClick.icon = "MousePointer";
|
|
323
|
+
// Browser Type tool
|
|
324
|
+
const browserType = tool(async ({ selector, text, pressEnter = false, }) => {
|
|
325
|
+
try {
|
|
326
|
+
const { liveViewUrl } = await ensureBrowserSession();
|
|
327
|
+
const code = `
|
|
328
|
+
await page.fill('${selector.replace(/'/g, "\\'")}', '${text.replace(/'/g, "\\'")}', { timeout: 10000 });
|
|
329
|
+
${pressEnter ? `await page.press('${selector.replace(/'/g, "\\'")}', 'Enter'); await page.waitForTimeout(500);` : ""}
|
|
330
|
+
return { success: true };
|
|
331
|
+
`;
|
|
332
|
+
await executePlaywright(code);
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
message: `Typed text into element: ${selector}${pressEnter ? " (pressed Enter)" : ""}`,
|
|
336
|
+
liveViewUrl,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
error: `Type failed: ${errorMessage}`,
|
|
344
|
+
...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}, {
|
|
348
|
+
name: "BrowserType",
|
|
349
|
+
description: "Type text into an input field in the current browser page.\n" +
|
|
350
|
+
"- Fills text into input fields, textareas, and contenteditable elements\n" +
|
|
351
|
+
"- Can optionally press Enter after typing (useful for search forms)\n" +
|
|
352
|
+
"- Clears existing content before typing\n" +
|
|
353
|
+
"\n" +
|
|
354
|
+
"Usage notes:\n" +
|
|
355
|
+
" - Provide a CSS selector to identify the input element\n" +
|
|
356
|
+
" - Set pressEnter=true to submit forms after typing\n",
|
|
357
|
+
schema: z.object({
|
|
358
|
+
selector: z.string().describe("CSS selector of the input element"),
|
|
359
|
+
text: z.string().describe("The text to type into the element"),
|
|
360
|
+
pressEnter: z
|
|
361
|
+
.boolean()
|
|
362
|
+
.optional()
|
|
363
|
+
.default(false)
|
|
364
|
+
.describe("Whether to press Enter after typing"),
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
browserType.prettyName = "Browser Type";
|
|
368
|
+
browserType.icon = "Type";
|
|
369
|
+
// Browser Close tool
|
|
370
|
+
const browserClose = tool(async () => {
|
|
371
|
+
try {
|
|
372
|
+
if (_sessionId) {
|
|
373
|
+
const kernel = getKernelClient();
|
|
374
|
+
await kernel.browsers.deleteByID(_sessionId);
|
|
375
|
+
}
|
|
376
|
+
_sessionId = null;
|
|
377
|
+
_liveViewUrl = null;
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
message: "Browser session closed successfully",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
error: `Close failed: ${errorMessage}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}, {
|
|
391
|
+
name: "BrowserClose",
|
|
392
|
+
description: "Close the current browser session and release cloud resources.\n" +
|
|
393
|
+
"- Terminates the active browser connection\n" +
|
|
394
|
+
"- Frees up cloud browser resources\n" +
|
|
395
|
+
"- Call this when done with browser automation\n" +
|
|
396
|
+
"\n" +
|
|
397
|
+
"Usage notes:\n" +
|
|
398
|
+
" - A new browser session will be created on the next BrowserNavigate call\n" +
|
|
399
|
+
" - Calling this when no browser is active has no effect\n",
|
|
400
|
+
schema: z.object({}),
|
|
401
|
+
});
|
|
402
|
+
browserClose.prettyName = "Browser Close";
|
|
403
|
+
browserClose.icon = "X";
|
|
404
|
+
return [
|
|
405
|
+
browserNavigate,
|
|
406
|
+
browserScreenshot,
|
|
407
|
+
browserExtract,
|
|
408
|
+
browserClick,
|
|
409
|
+
browserType,
|
|
410
|
+
browserClose,
|
|
411
|
+
];
|
|
412
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* Registry for subagent connection info.
|
|
4
|
+
* Maps query hash to connection info so the runner can emit tool_call_update.
|
|
5
|
+
*/
|
|
6
|
+
export interface SubagentConnectionInfo {
|
|
7
|
+
port: number;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Event emitter for subagent connection events.
|
|
12
|
+
* The runner listens to these events and emits tool_call_update.
|
|
13
|
+
*/
|
|
14
|
+
export declare const subagentEvents: EventEmitter<[never]>;
|
|
15
|
+
/**
|
|
16
|
+
* Maps query hash to toolCallId.
|
|
17
|
+
* Set by the runner when it sees a subagent tool_call.
|
|
18
|
+
*/
|
|
19
|
+
export declare const queryToToolCallId: Map<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Generate a hash from the query string for correlation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function hashQuery(query: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Called by the subagent tool when connection is established.
|
|
26
|
+
* Emits an event that the runner can listen to.
|
|
27
|
+
*/
|
|
28
|
+
export declare function emitSubagentConnection(queryHash: string, connectionInfo: SubagentConnectionInfo): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { createLogger } from "@townco/core";
|
|
4
|
+
const logger = createLogger("subagent-connections");
|
|
5
|
+
/**
|
|
6
|
+
* Event emitter for subagent connection events.
|
|
7
|
+
* The runner listens to these events and emits tool_call_update.
|
|
8
|
+
*/
|
|
9
|
+
export const subagentEvents = new EventEmitter();
|
|
10
|
+
/**
|
|
11
|
+
* Maps query hash to toolCallId.
|
|
12
|
+
* Set by the runner when it sees a subagent tool_call.
|
|
13
|
+
*/
|
|
14
|
+
export const queryToToolCallId = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Generate a hash from the query string for correlation.
|
|
17
|
+
*/
|
|
18
|
+
export function hashQuery(query) {
|
|
19
|
+
const hash = createHash("sha256").update(query).digest("hex").slice(0, 16);
|
|
20
|
+
logger.debug("Generated query hash", {
|
|
21
|
+
queryPreview: query.slice(0, 50),
|
|
22
|
+
hash,
|
|
23
|
+
});
|
|
24
|
+
return hash;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Called by the subagent tool when connection is established.
|
|
28
|
+
* Emits an event that the runner can listen to.
|
|
29
|
+
*/
|
|
30
|
+
export function emitSubagentConnection(queryHash, connectionInfo) {
|
|
31
|
+
logger.info("emitSubagentConnection called", {
|
|
32
|
+
queryHash,
|
|
33
|
+
port: connectionInfo.port,
|
|
34
|
+
sessionId: connectionInfo.sessionId,
|
|
35
|
+
registeredHashes: Array.from(queryToToolCallId.keys()),
|
|
36
|
+
});
|
|
37
|
+
const toolCallId = queryToToolCallId.get(queryHash);
|
|
38
|
+
if (toolCallId) {
|
|
39
|
+
logger.info("Found toolCallId for queryHash, emitting connection event", {
|
|
40
|
+
queryHash,
|
|
41
|
+
toolCallId,
|
|
42
|
+
port: connectionInfo.port,
|
|
43
|
+
sessionId: connectionInfo.sessionId,
|
|
44
|
+
});
|
|
45
|
+
subagentEvents.emit("connection", {
|
|
46
|
+
toolCallId,
|
|
47
|
+
...connectionInfo,
|
|
48
|
+
});
|
|
49
|
+
// Clean up the mapping
|
|
50
|
+
queryToToolCallId.delete(queryHash);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
logger.warn("No toolCallId found for queryHash", {
|
|
54
|
+
queryHash,
|
|
55
|
+
registeredHashes: Array.from(queryToToolCallId.keys()),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -7,6 +7,7 @@ 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
9
|
import { findAvailablePort } from "./port-utils.js";
|
|
10
|
+
import { emitSubagentConnection, hashQuery } from "./subagent-connections.js";
|
|
10
11
|
/**
|
|
11
12
|
* Name of the Task tool created by makeSubagentsTool
|
|
12
13
|
*/
|
|
@@ -100,7 +101,7 @@ export function makeSubagentsTool(configs) {
|
|
|
100
101
|
type: "direct",
|
|
101
102
|
name: SUBAGENT_TOOL_NAME,
|
|
102
103
|
prettyName: "Subagent",
|
|
103
|
-
icon: "
|
|
104
|
+
icon: "CircleDot",
|
|
104
105
|
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
105
106
|
|
|
106
107
|
The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
|
|
@@ -310,6 +311,9 @@ async function querySubagent(agentName, agentPath, agentWorkingDirectory, query)
|
|
|
310
311
|
if (!sessionId) {
|
|
311
312
|
throw new Error("No sessionId in session/new response");
|
|
312
313
|
}
|
|
314
|
+
// Emit connection info so the GUI can connect directly to this subagent's SSE
|
|
315
|
+
const queryHash = hashQuery(query);
|
|
316
|
+
emitSubagentConnection(queryHash, { port, sessionId });
|
|
313
317
|
// Step 3: Connect to SSE for receiving streaming responses
|
|
314
318
|
sseAbortController = new AbortController();
|
|
315
319
|
let responseText = "";
|
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
|
```
|
|
@@ -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
|