@townco/agent 0.1.82 → 0.1.84
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.js +150 -49
- package/dist/acp-server/http.js +56 -1
- package/dist/acp-server/session-storage.d.ts +44 -12
- package/dist/acp-server/session-storage.js +153 -59
- package/dist/definition/index.d.ts +2 -2
- package/dist/definition/index.js +1 -1
- package/dist/runner/agent-runner.d.ts +4 -2
- package/dist/runner/hooks/executor.d.ts +1 -0
- package/dist/runner/hooks/executor.js +18 -2
- package/dist/runner/hooks/predefined/compaction-tool.js +3 -2
- package/dist/runner/hooks/predefined/tool-response-compactor.d.ts +0 -4
- package/dist/runner/hooks/predefined/tool-response-compactor.js +30 -16
- package/dist/runner/hooks/types.d.ts +4 -5
- package/dist/runner/langchain/index.d.ts +1 -0
- package/dist/runner/langchain/index.js +156 -33
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +466 -0
- package/dist/runner/langchain/tools/browser.js +15 -3
- package/dist/runner/langchain/tools/filesystem.d.ts +8 -4
- package/dist/runner/langchain/tools/filesystem.js +118 -82
- package/dist/runner/langchain/tools/generate_image.d.ts +19 -0
- package/dist/runner/langchain/tools/generate_image.js +54 -14
- package/dist/runner/langchain/tools/subagent.js +2 -2
- package/dist/runner/langchain/tools/todo.js +3 -0
- package/dist/runner/langchain/tools/web_search.js +6 -0
- package/dist/runner/session-context.d.ts +40 -0
- package/dist/runner/session-context.js +69 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +2 -0
- package/dist/scaffold/project-scaffold.js +7 -3
- package/dist/telemetry/setup.js +1 -1
- package/dist/templates/index.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +1 -10
- package/dist/utils/context-size-calculator.js +1 -12
- package/dist/utils/token-counter.js +2 -2
- package/package.json +10 -10
- package/templates/index.ts +1 -1
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Storage-backed artifacts tool for agent backend
|
|
3
|
+
*
|
|
4
|
+
* Provides file storage capabilities using Supabase Storage with the following operations:
|
|
5
|
+
* - artifacts_cp: Copy files to/from Supabase Storage
|
|
6
|
+
* - artifacts_del: Delete files from Supabase Storage
|
|
7
|
+
* - artifacts_ls: List files in Supabase Storage
|
|
8
|
+
* - artifacts_url: Generate signed URLs
|
|
9
|
+
*
|
|
10
|
+
* Storage keys are scoped by: <deploying_user>/<agent_name>/<session_id>/<file_path>
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { createSecretKeyClient } from "@townco/supabase/server";
|
|
15
|
+
import { tool } from "langchain";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Supabase Client and Bucket Configuration
|
|
19
|
+
// ============================================================================
|
|
20
|
+
function getSupabaseClient() {
|
|
21
|
+
return createSecretKeyClient();
|
|
22
|
+
}
|
|
23
|
+
function getBucketName() {
|
|
24
|
+
return "town-builder-artifacts";
|
|
25
|
+
}
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Storage Key Prefix Logic
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Generates the storage key prefix based on environment and configuration.
|
|
31
|
+
*
|
|
32
|
+
* Priority order:
|
|
33
|
+
* 1. Explicit ARTIFACTS_KEY_PREFIX env var (highest priority)
|
|
34
|
+
* 2. Parse username from AGENT_NAME (deployment mode: "username-agentname")
|
|
35
|
+
* 3. Local mode fallback: "local/agentname" or "local/unknown"
|
|
36
|
+
*
|
|
37
|
+
* @returns Prefix string without trailing slash (e.g., "alice/my-agent" or "local/my-agent")
|
|
38
|
+
*/
|
|
39
|
+
function getArtifactsKeyPrefix() {
|
|
40
|
+
// 1. Explicit override (highest priority)
|
|
41
|
+
const explicitPrefix = process.env.ARTIFACTS_KEY_PREFIX;
|
|
42
|
+
if (explicitPrefix) {
|
|
43
|
+
return explicitPrefix.replace(/\/+$/, ""); // Remove trailing slashes
|
|
44
|
+
}
|
|
45
|
+
// 2. Check deployment vs local mode
|
|
46
|
+
const agentName = process.env.AGENT_NAME;
|
|
47
|
+
if (!agentName) {
|
|
48
|
+
return "local/unknown";
|
|
49
|
+
}
|
|
50
|
+
// 3. Parse username from AGENT_NAME (format: "username-agentname")
|
|
51
|
+
// In deployment, AGENT_NAME follows pattern "username-agentname"
|
|
52
|
+
const parts = agentName.split("-");
|
|
53
|
+
if (parts.length >= 2) {
|
|
54
|
+
const username = parts[0];
|
|
55
|
+
const agentNamePart = parts.slice(1).join("-");
|
|
56
|
+
return `${username}/${agentNamePart}`;
|
|
57
|
+
}
|
|
58
|
+
// Local mode fallback
|
|
59
|
+
return `local/${agentName}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds the full storage key for a file.
|
|
63
|
+
*
|
|
64
|
+
* @param sessionId Current session ID
|
|
65
|
+
* @param filePath Relative file path within the session
|
|
66
|
+
* @returns Full storage key (e.g., "alice/my-agent/sess-123/data/output.json")
|
|
67
|
+
*/
|
|
68
|
+
function buildStorageKey(sessionId, filePath) {
|
|
69
|
+
// Validate session ID is provided
|
|
70
|
+
if (!sessionId || sessionId.trim() === "") {
|
|
71
|
+
throw new Error("FATAL: session_id is required but was not provided. " +
|
|
72
|
+
"This is a system error - session_id should be automatically injected. " +
|
|
73
|
+
"Please report this bug.");
|
|
74
|
+
}
|
|
75
|
+
const prefix = getArtifactsKeyPrefix();
|
|
76
|
+
// Normalize file path (remove leading slashes)
|
|
77
|
+
const normalizedPath = filePath.replace(/^\/+/, "");
|
|
78
|
+
return `${prefix}/${sessionId}/${normalizedPath}`;
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Path Validation
|
|
82
|
+
// ============================================================================
|
|
83
|
+
function validateLocalPath(path) {
|
|
84
|
+
if (!path.startsWith("/")) {
|
|
85
|
+
throw new Error(`Local paths must be absolute. Got: ${path}\n` +
|
|
86
|
+
"Hint: Use an absolute path starting with '/'");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function validateStoragePath(path) {
|
|
90
|
+
if (path.startsWith("/")) {
|
|
91
|
+
throw new Error(`Storage paths must be relative (no leading slash). Got: ${path}\n` +
|
|
92
|
+
"Hint: Use a relative path like 'data/file.json'");
|
|
93
|
+
}
|
|
94
|
+
if (path.includes("..")) {
|
|
95
|
+
throw new Error(`Storage paths cannot contain '..' for security reasons. Got: ${path}`);
|
|
96
|
+
}
|
|
97
|
+
// Check for empty path
|
|
98
|
+
if (!path || path.trim() === "") {
|
|
99
|
+
throw new Error("Storage path cannot be empty");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function validateExpiration(expiresIn) {
|
|
103
|
+
// Supabase supports up to 365 days (31536000 seconds)
|
|
104
|
+
if (expiresIn < 1 || expiresIn > 31536000) {
|
|
105
|
+
throw new Error(`URL expiration must be between 1 and 31536000 seconds (365 days). Got: ${expiresIn}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Supabase Storage Operation Helpers
|
|
110
|
+
// ============================================================================
|
|
111
|
+
/**
|
|
112
|
+
* Upload a file to Supabase Storage
|
|
113
|
+
*/
|
|
114
|
+
async function uploadToSupabase(storageKey, localPath) {
|
|
115
|
+
const supabase = getSupabaseClient();
|
|
116
|
+
const bucket = getBucketName();
|
|
117
|
+
// Read file content
|
|
118
|
+
const fileContent = await fs.readFile(localPath);
|
|
119
|
+
// Determine content type based on file extension
|
|
120
|
+
const ext = path.extname(localPath).toLowerCase();
|
|
121
|
+
const contentTypeMap = {
|
|
122
|
+
".json": "application/json",
|
|
123
|
+
".txt": "text/plain",
|
|
124
|
+
".html": "text/html",
|
|
125
|
+
".css": "text/css",
|
|
126
|
+
".js": "application/javascript",
|
|
127
|
+
".png": "image/png",
|
|
128
|
+
".jpg": "image/jpeg",
|
|
129
|
+
".jpeg": "image/jpeg",
|
|
130
|
+
".gif": "image/gif",
|
|
131
|
+
".svg": "image/svg+xml",
|
|
132
|
+
".pdf": "application/pdf",
|
|
133
|
+
".zip": "application/zip",
|
|
134
|
+
};
|
|
135
|
+
const contentType = contentTypeMap[ext] || "application/octet-stream";
|
|
136
|
+
const { error } = await supabase.storage
|
|
137
|
+
.from(bucket)
|
|
138
|
+
.upload(storageKey, fileContent, {
|
|
139
|
+
contentType,
|
|
140
|
+
upsert: true, // Allow overwriting
|
|
141
|
+
});
|
|
142
|
+
if (error) {
|
|
143
|
+
throw new Error(`Failed to upload to Supabase Storage: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Download a file from Supabase Storage
|
|
148
|
+
*/
|
|
149
|
+
async function downloadFromSupabase(storageKey, localPath) {
|
|
150
|
+
const supabase = getSupabaseClient();
|
|
151
|
+
const bucket = getBucketName();
|
|
152
|
+
const { data, error } = await supabase.storage
|
|
153
|
+
.from(bucket)
|
|
154
|
+
.download(storageKey);
|
|
155
|
+
if (error) {
|
|
156
|
+
throw new Error(`Failed to download from Supabase Storage: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
if (!data) {
|
|
159
|
+
throw new Error("Supabase Storage response body is empty");
|
|
160
|
+
}
|
|
161
|
+
// Ensure parent directory exists
|
|
162
|
+
const dir = path.dirname(localPath);
|
|
163
|
+
await fs.mkdir(dir, { recursive: true });
|
|
164
|
+
// Convert Blob to Buffer and write
|
|
165
|
+
const arrayBuffer = await data.arrayBuffer();
|
|
166
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
167
|
+
await fs.writeFile(localPath, buffer);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Delete a file from Supabase Storage
|
|
171
|
+
*/
|
|
172
|
+
async function deleteFromSupabase(storageKey) {
|
|
173
|
+
const supabase = getSupabaseClient();
|
|
174
|
+
const bucket = getBucketName();
|
|
175
|
+
const { error } = await supabase.storage.from(bucket).remove([storageKey]);
|
|
176
|
+
if (error) {
|
|
177
|
+
throw new Error(`Failed to delete from Supabase Storage: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function listFilesInSupabase(sessionId, relativePath, recursive = false) {
|
|
181
|
+
const supabase = getSupabaseClient();
|
|
182
|
+
const bucket = getBucketName();
|
|
183
|
+
// Build the prefix for listing
|
|
184
|
+
const basePath = relativePath
|
|
185
|
+
? buildStorageKey(sessionId, relativePath)
|
|
186
|
+
: buildStorageKey(sessionId, "");
|
|
187
|
+
// For Supabase, we list at the base path
|
|
188
|
+
// If recursive, we'll need to handle it differently
|
|
189
|
+
const options = recursive
|
|
190
|
+
? undefined
|
|
191
|
+
: {
|
|
192
|
+
limit: 100,
|
|
193
|
+
offset: 0,
|
|
194
|
+
sortBy: { column: "name", order: "asc" },
|
|
195
|
+
};
|
|
196
|
+
const { data, error } = await supabase.storage
|
|
197
|
+
.from(bucket)
|
|
198
|
+
.list(basePath, options);
|
|
199
|
+
if (error) {
|
|
200
|
+
throw new Error(`Failed to list files in Supabase Storage: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
return data || [];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Generate a signed URL for a Supabase Storage object
|
|
206
|
+
*/
|
|
207
|
+
async function generateSignedUrl(storageKey, expiresIn) {
|
|
208
|
+
const supabase = getSupabaseClient();
|
|
209
|
+
const bucket = getBucketName();
|
|
210
|
+
const { data, error } = await supabase.storage
|
|
211
|
+
.from(bucket)
|
|
212
|
+
.createSignedUrl(storageKey, expiresIn);
|
|
213
|
+
if (error) {
|
|
214
|
+
throw new Error(`Failed to generate signed URL: ${error.message}`);
|
|
215
|
+
}
|
|
216
|
+
if (!data?.signedUrl) {
|
|
217
|
+
throw new Error("Signed URL not returned from Supabase Storage");
|
|
218
|
+
}
|
|
219
|
+
return data.signedUrl;
|
|
220
|
+
}
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Error Handling Helpers
|
|
223
|
+
// ============================================================================
|
|
224
|
+
/**
|
|
225
|
+
* Maps Supabase Storage errors to user-friendly error messages
|
|
226
|
+
*/
|
|
227
|
+
function handleStorageError(error, context) {
|
|
228
|
+
if (error instanceof Error) {
|
|
229
|
+
const message = error.message.toLowerCase();
|
|
230
|
+
if (message.includes("object not found") || message.includes("not found")) {
|
|
231
|
+
return `Error: File not found in storage. ${context}`;
|
|
232
|
+
}
|
|
233
|
+
if (message.includes("bucket not found")) {
|
|
234
|
+
return `Error: Artifacts bucket not configured. Please create the 'artifacts' bucket in Supabase. ${context}`;
|
|
235
|
+
}
|
|
236
|
+
if (message.includes("invalid jwt") || message.includes("jwt")) {
|
|
237
|
+
return `Error: Authentication error. Please check Supabase credentials. ${context}`;
|
|
238
|
+
}
|
|
239
|
+
if (message.includes("enoent")) {
|
|
240
|
+
return `Error: Local file not found. ${context}\n${error.message}`;
|
|
241
|
+
}
|
|
242
|
+
if (message.includes("eacces")) {
|
|
243
|
+
return `Error: Permission denied accessing local file. ${context}\n${error.message}`;
|
|
244
|
+
}
|
|
245
|
+
return `Error: ${error.message}. ${context}`;
|
|
246
|
+
}
|
|
247
|
+
return `Error: Unknown error occurred. ${context}`;
|
|
248
|
+
}
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Tool Implementations
|
|
251
|
+
// ============================================================================
|
|
252
|
+
/**
|
|
253
|
+
* Tool 1: artifacts_cp - Copy files to/from Supabase Storage
|
|
254
|
+
*/
|
|
255
|
+
const artifactsCp = tool(async ({ session_id, source, destination, direction, }) => {
|
|
256
|
+
try {
|
|
257
|
+
if (direction === "upload") {
|
|
258
|
+
// Upload: local file -> Storage
|
|
259
|
+
validateLocalPath(source);
|
|
260
|
+
validateStoragePath(destination);
|
|
261
|
+
const storageKey = buildStorageKey(session_id, destination);
|
|
262
|
+
await uploadToSupabase(storageKey, source);
|
|
263
|
+
return `Successfully uploaded ${source} to storage at ${destination}`;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Download: Storage -> local file
|
|
267
|
+
validateStoragePath(source);
|
|
268
|
+
validateLocalPath(destination);
|
|
269
|
+
const storageKey = buildStorageKey(session_id, source);
|
|
270
|
+
await downloadFromSupabase(storageKey, destination);
|
|
271
|
+
return `Successfully downloaded ${source} from storage to ${destination}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return handleStorageError(error, direction === "upload"
|
|
276
|
+
? `Failed to upload ${source} to ${destination}`
|
|
277
|
+
: `Failed to download ${source} to ${destination}`);
|
|
278
|
+
}
|
|
279
|
+
}, {
|
|
280
|
+
name: "artifacts_cp",
|
|
281
|
+
description: "Copy files between local filesystem and Supabase Storage-backed artifact storage. " +
|
|
282
|
+
"Use direction='upload' to copy from local to storage, or direction='download' to copy from storage to local. " +
|
|
283
|
+
"Local paths must be absolute (e.g., '/tmp/file.txt'), storage paths must be relative (e.g., 'data/file.txt').",
|
|
284
|
+
schema: z.object({
|
|
285
|
+
session_id: z
|
|
286
|
+
.string()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
289
|
+
source: z
|
|
290
|
+
.string()
|
|
291
|
+
.describe("Source path: absolute path if local, relative path if storage"),
|
|
292
|
+
destination: z
|
|
293
|
+
.string()
|
|
294
|
+
.describe("Destination path: absolute path if local, relative path if storage"),
|
|
295
|
+
direction: z
|
|
296
|
+
.enum(["upload", "download"])
|
|
297
|
+
.describe("Direction: 'upload' (local to storage) or 'download' (storage to local)"),
|
|
298
|
+
}),
|
|
299
|
+
});
|
|
300
|
+
/**
|
|
301
|
+
* Tool 2: artifacts_del - Delete files from Supabase Storage
|
|
302
|
+
*/
|
|
303
|
+
const artifactsDel = tool(async ({ session_id, path, }) => {
|
|
304
|
+
try {
|
|
305
|
+
validateStoragePath(path);
|
|
306
|
+
const storageKey = buildStorageKey(session_id, path);
|
|
307
|
+
await deleteFromSupabase(storageKey);
|
|
308
|
+
return `Successfully deleted ${path} from storage`;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return handleStorageError(error, `Failed to delete ${path}`);
|
|
312
|
+
}
|
|
313
|
+
}, {
|
|
314
|
+
name: "artifacts_del",
|
|
315
|
+
description: "Delete a file from Supabase Storage-backed artifact storage. " +
|
|
316
|
+
"The path must be relative (e.g., 'data/file.txt').",
|
|
317
|
+
schema: z.object({
|
|
318
|
+
session_id: z
|
|
319
|
+
.string()
|
|
320
|
+
.optional()
|
|
321
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
322
|
+
path: z
|
|
323
|
+
.string()
|
|
324
|
+
.describe("Relative path to the file to delete in storage"),
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
/**
|
|
328
|
+
* Tool 3: artifacts_ls - List files in Supabase Storage
|
|
329
|
+
*/
|
|
330
|
+
const artifactsLs = tool(async ({ session_id, path, recursive = false, }) => {
|
|
331
|
+
try {
|
|
332
|
+
if (path) {
|
|
333
|
+
validateStoragePath(path);
|
|
334
|
+
}
|
|
335
|
+
const files = await listFilesInSupabase(session_id, path, recursive);
|
|
336
|
+
if (files.length === 0) {
|
|
337
|
+
return path
|
|
338
|
+
? `No files found in ${path}`
|
|
339
|
+
: "No files found in this session";
|
|
340
|
+
}
|
|
341
|
+
// Format the file list
|
|
342
|
+
const fileList = files
|
|
343
|
+
.map((file) => {
|
|
344
|
+
const size = file.metadata?.size || 0;
|
|
345
|
+
const lastModified = file.updated_at || file.created_at || "unknown";
|
|
346
|
+
// Format size in human-readable format
|
|
347
|
+
let sizeStr;
|
|
348
|
+
if (size < 1024) {
|
|
349
|
+
sizeStr = `${size} B`;
|
|
350
|
+
}
|
|
351
|
+
else if (size < 1024 * 1024) {
|
|
352
|
+
sizeStr = `${(size / 1024).toFixed(2)} KB`;
|
|
353
|
+
}
|
|
354
|
+
else if (size < 1024 * 1024 * 1024) {
|
|
355
|
+
sizeStr = `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
sizeStr = `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
359
|
+
}
|
|
360
|
+
return `${file.name}\t${sizeStr}\t${lastModified}`;
|
|
361
|
+
})
|
|
362
|
+
.join("\n");
|
|
363
|
+
const header = path
|
|
364
|
+
? `Files in ${path} (${files.length} total):\n`
|
|
365
|
+
: `Files in session (${files.length} total):\n`;
|
|
366
|
+
return `${header}Name\tSize\tLast Modified\n${fileList}`;
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return handleStorageError(error, `Failed to list files${path ? ` in ${path}` : ""}`);
|
|
370
|
+
}
|
|
371
|
+
}, {
|
|
372
|
+
name: "artifacts_ls",
|
|
373
|
+
description: "List files in Supabase Storage-backed artifact storage for the current session. " +
|
|
374
|
+
"Optionally specify a relative directory path to list. " +
|
|
375
|
+
"Use recursive=true to list files recursively in subdirectories.",
|
|
376
|
+
schema: z.object({
|
|
377
|
+
session_id: z
|
|
378
|
+
.string()
|
|
379
|
+
.optional()
|
|
380
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
381
|
+
path: z
|
|
382
|
+
.string()
|
|
383
|
+
.optional()
|
|
384
|
+
.describe("Optional relative directory path to list (e.g., 'data/'). If omitted, lists session root."),
|
|
385
|
+
recursive: z
|
|
386
|
+
.boolean()
|
|
387
|
+
.optional()
|
|
388
|
+
.describe("Whether to list files recursively in subdirectories (default: false)"),
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
/**
|
|
392
|
+
* Tool 4: artifacts_url - Generate signed URL
|
|
393
|
+
*/
|
|
394
|
+
const artifactsUrl = tool(async ({ session_id, path, expires_in = 3600, }) => {
|
|
395
|
+
try {
|
|
396
|
+
validateStoragePath(path);
|
|
397
|
+
validateExpiration(expires_in);
|
|
398
|
+
const storageKey = buildStorageKey(session_id, path);
|
|
399
|
+
const url = await generateSignedUrl(storageKey, expires_in);
|
|
400
|
+
const expiryHours = expires_in / 3600;
|
|
401
|
+
const expiryStr = expiryHours >= 1
|
|
402
|
+
? `${expiryHours.toFixed(1)} hour(s)`
|
|
403
|
+
: `${expires_in} second(s)`;
|
|
404
|
+
return `Signed URL for ${path} (expires in ${expiryStr}):\n${url}`;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
return handleStorageError(error, `Failed to generate URL for ${path}`);
|
|
408
|
+
}
|
|
409
|
+
}, {
|
|
410
|
+
name: "artifacts_url",
|
|
411
|
+
description: "Generate a signed URL for accessing an artifact in Supabase Storage. " +
|
|
412
|
+
"The URL will be valid for the specified duration (default: 1 hour, max: 365 days). " +
|
|
413
|
+
"Anyone with this URL can access the file until it expires.",
|
|
414
|
+
schema: z.object({
|
|
415
|
+
session_id: z
|
|
416
|
+
.string()
|
|
417
|
+
.optional()
|
|
418
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
419
|
+
path: z.string().describe("Relative path to the artifact in storage"),
|
|
420
|
+
expires_in: z
|
|
421
|
+
.number()
|
|
422
|
+
.optional()
|
|
423
|
+
.describe("Expiration time in seconds (1-31536000). Default: 3600 (1 hour)"),
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
// Add metadata for UI display
|
|
427
|
+
artifactsCp.prettyName = "Copy Artifact";
|
|
428
|
+
artifactsCp.icon = "Upload";
|
|
429
|
+
artifactsCp.verbiage = {
|
|
430
|
+
active: "Copying artifact to {destination}",
|
|
431
|
+
past: "Copied artifact to {destination}",
|
|
432
|
+
paramKey: "destination",
|
|
433
|
+
};
|
|
434
|
+
artifactsDel.prettyName =
|
|
435
|
+
"Delete Artifact";
|
|
436
|
+
artifactsDel.icon = "Trash";
|
|
437
|
+
artifactsDel.verbiage = {
|
|
438
|
+
active: "Deleting artifact {path}",
|
|
439
|
+
past: "Deleted artifact {path}",
|
|
440
|
+
paramKey: "path",
|
|
441
|
+
};
|
|
442
|
+
artifactsLs.prettyName =
|
|
443
|
+
"List Artifacts";
|
|
444
|
+
artifactsLs.icon = "List";
|
|
445
|
+
artifactsLs.verbiage = {
|
|
446
|
+
active: "Listing artifacts",
|
|
447
|
+
past: "Listed artifacts",
|
|
448
|
+
};
|
|
449
|
+
artifactsUrl.prettyName =
|
|
450
|
+
"Generate Artifact URL";
|
|
451
|
+
artifactsUrl.icon = "Link";
|
|
452
|
+
artifactsUrl.verbiage = {
|
|
453
|
+
active: "Generating URL for {path}",
|
|
454
|
+
past: "Generated URL for {path}",
|
|
455
|
+
paramKey: "path",
|
|
456
|
+
};
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// Factory Function
|
|
459
|
+
// ============================================================================
|
|
460
|
+
/**
|
|
461
|
+
* Factory function to create the artifacts tools
|
|
462
|
+
* Returns an array of all four artifact management tools
|
|
463
|
+
*/
|
|
464
|
+
export function makeArtifactsTools() {
|
|
465
|
+
return [artifactsCp, artifactsDel, artifactsLs, artifactsUrl];
|
|
466
|
+
}
|
|
@@ -59,9 +59,9 @@ export function makeBrowserTools() {
|
|
|
59
59
|
try {
|
|
60
60
|
const { liveViewUrl } = await ensureBrowserSession();
|
|
61
61
|
const code = `
|
|
62
|
-
await page.goto('${url.replace(/'/g, "\\'")}', {
|
|
62
|
+
await page.goto('${url.replace(/'/g, "\\'")}', {
|
|
63
63
|
waitUntil: '${waitUntil}',
|
|
64
|
-
timeout: 30000
|
|
64
|
+
timeout: 30000
|
|
65
65
|
});
|
|
66
66
|
return { url: page.url(), title: await page.title() };
|
|
67
67
|
`;
|
|
@@ -103,7 +103,9 @@ export function makeBrowserTools() {
|
|
|
103
103
|
.describe("When to consider navigation complete"),
|
|
104
104
|
}),
|
|
105
105
|
});
|
|
106
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
106
107
|
browserNavigate.prettyName = "Browser Navigate";
|
|
108
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
107
109
|
browserNavigate.icon = "Globe";
|
|
108
110
|
// Browser Screenshot tool
|
|
109
111
|
const browserScreenshot = tool(async ({ fullPage = false, selector, }) => {
|
|
@@ -180,7 +182,9 @@ export function makeBrowserTools() {
|
|
|
180
182
|
.describe("CSS selector of element to screenshot (captures whole page if not provided)"),
|
|
181
183
|
}),
|
|
182
184
|
});
|
|
185
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
183
186
|
browserScreenshot.prettyName = "Browser Screenshot";
|
|
187
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
184
188
|
browserScreenshot.icon = "Camera";
|
|
185
189
|
// Browser Extract Content tool
|
|
186
190
|
const browserExtract = tool(async ({ selector, extractType = "text", }) => {
|
|
@@ -270,14 +274,16 @@ export function makeBrowserTools() {
|
|
|
270
274
|
.describe("Type of content to extract"),
|
|
271
275
|
}),
|
|
272
276
|
});
|
|
277
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
273
278
|
browserExtract.prettyName = "Browser Extract";
|
|
279
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
274
280
|
browserExtract.icon = "FileText";
|
|
275
281
|
// Browser Click tool
|
|
276
282
|
const browserClick = tool(async ({ selector, button = "left" }) => {
|
|
277
283
|
try {
|
|
278
284
|
const { liveViewUrl } = await ensureBrowserSession();
|
|
279
285
|
const code = `
|
|
280
|
-
await page.click('${selector.replace(/'/g, "\\'")}', {
|
|
286
|
+
await page.click('${selector.replace(/'/g, "\\'")}', {
|
|
281
287
|
button: '${button}',
|
|
282
288
|
timeout: 10000,
|
|
283
289
|
});
|
|
@@ -318,7 +324,9 @@ export function makeBrowserTools() {
|
|
|
318
324
|
.describe("Which mouse button to use"),
|
|
319
325
|
}),
|
|
320
326
|
});
|
|
327
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
321
328
|
browserClick.prettyName = "Browser Click";
|
|
329
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
322
330
|
browserClick.icon = "MousePointer";
|
|
323
331
|
// Browser Type tool
|
|
324
332
|
const browserType = tool(async ({ selector, text, pressEnter = false, }) => {
|
|
@@ -364,7 +372,9 @@ export function makeBrowserTools() {
|
|
|
364
372
|
.describe("Whether to press Enter after typing"),
|
|
365
373
|
}),
|
|
366
374
|
});
|
|
375
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
367
376
|
browserType.prettyName = "Browser Type";
|
|
377
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
368
378
|
browserType.icon = "Type";
|
|
369
379
|
// Browser Close tool
|
|
370
380
|
const browserClose = tool(async () => {
|
|
@@ -399,7 +409,9 @@ export function makeBrowserTools() {
|
|
|
399
409
|
" - Calling this when no browser is active has no effect\n",
|
|
400
410
|
schema: z.object({}),
|
|
401
411
|
});
|
|
412
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
402
413
|
browserClose.prettyName = "Browser Close";
|
|
414
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
403
415
|
browserClose.icon = "X";
|
|
404
416
|
return [
|
|
405
417
|
browserNavigate,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Create filesystem tools that are scoped to the current session.
|
|
4
|
+
* Tools use session context (via AsyncLocalStorage) to determine the session directory.
|
|
5
|
+
*/
|
|
6
|
+
export declare function makeFilesystemTools(): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
3
7
|
pattern: z.ZodString;
|
|
4
8
|
path: z.ZodOptional<z.ZodString>;
|
|
5
9
|
glob: z.ZodOptional<z.ZodString>;
|
|
6
10
|
output_mode: z.ZodOptional<z.ZodEnum<{
|
|
7
11
|
content: "content";
|
|
8
|
-
count: "count";
|
|
9
12
|
files_with_matches: "files_with_matches";
|
|
13
|
+
count: "count";
|
|
10
14
|
}>>;
|
|
11
15
|
"-B": z.ZodOptional<z.ZodNumber>;
|
|
12
16
|
"-A": z.ZodOptional<z.ZodNumber>;
|
|
@@ -20,7 +24,7 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
|
|
|
20
24
|
pattern: string;
|
|
21
25
|
path?: string | undefined;
|
|
22
26
|
glob?: string | undefined;
|
|
23
|
-
output_mode?: "content" | "
|
|
27
|
+
output_mode?: "content" | "files_with_matches" | "count" | undefined;
|
|
24
28
|
"-B"?: number | undefined;
|
|
25
29
|
"-A"?: number | undefined;
|
|
26
30
|
"-C"?: number | undefined;
|
|
@@ -33,7 +37,7 @@ export declare function makeFilesystemTools(workingDirectory: string): readonly
|
|
|
33
37
|
pattern: string;
|
|
34
38
|
path?: string | undefined;
|
|
35
39
|
glob?: string | undefined;
|
|
36
|
-
output_mode?: "content" | "
|
|
40
|
+
output_mode?: "content" | "files_with_matches" | "count" | undefined;
|
|
37
41
|
"-B"?: number | undefined;
|
|
38
42
|
"-A"?: number | undefined;
|
|
39
43
|
"-C"?: number | undefined;
|