@townco/agent 0.1.122 → 0.1.123
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 +133 -11
- package/dist/runner/agent-runner.d.ts +7 -0
- package/dist/runner/hooks/executor.js +1 -1
- package/dist/runner/hooks/predefined/context-validator.d.ts +1 -1
- package/dist/runner/hooks/predefined/context-validator.js +2 -2
- package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.d.ts +1 -1
- package/dist/runner/hooks/predefined/document-context-extractor/chunk-manager.js +3 -3
- package/dist/runner/hooks/predefined/document-context-extractor/content-extractor.js +2 -2
- package/dist/runner/hooks/predefined/document-context-extractor/index.js +5 -5
- package/dist/runner/hooks/predefined/document-context-extractor/relevance-scorer.js +2 -2
- package/dist/runner/hooks/predefined/tool-response-compactor.js +9 -9
- package/dist/runner/langchain/index.js +301 -9
- package/dist/runner/langchain/otel-callbacks.d.ts +5 -0
- package/dist/runner/langchain/otel-callbacks.js +8 -0
- package/dist/runner/langchain/tools/artifacts.d.ts +68 -0
- package/dist/runner/langchain/tools/artifacts.js +474 -0
- package/dist/runner/langchain/tools/conversation_search.d.ts +22 -0
- package/dist/runner/langchain/tools/conversation_search.js +137 -0
- package/dist/runner/langchain/tools/document_extract.js +1 -1
- package/dist/runner/langchain/tools/generate_image.d.ts +47 -0
- package/dist/runner/langchain/tools/generate_image.js +175 -0
- package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
- package/dist/runner/langchain/tools/port-utils.js +35 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/context-size-calculator.d.ts +1 -1
- package/dist/utils/context-size-calculator.js +9 -14
- package/dist/utils/token-counter.d.ts +9 -7
- package/dist/utils/token-counter.js +30 -11
- package/dist/utils/tool-overhead-calculator.d.ts +2 -2
- package/dist/utils/tool-overhead-calculator.js +5 -4
- package/package.json +8 -7
|
@@ -0,0 +1,474 @@
|
|
|
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 { createClient } from "@townco/apiclient";
|
|
15
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
16
|
+
import { tool } from "langchain";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Storage Key Prefix Logic
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Generates the storage key prefix based on environment and configuration.
|
|
23
|
+
*
|
|
24
|
+
* Priority order:
|
|
25
|
+
* 1. Explicit ARTIFACTS_KEY_PREFIX env var (highest priority)
|
|
26
|
+
* 2. Parse username from AGENT_NAME (deployment mode: "username-agentname")
|
|
27
|
+
* 3. Local mode fallback: "local/agentname" or "local/unknown"
|
|
28
|
+
*
|
|
29
|
+
* @returns Prefix string without trailing slash (e.g., "alice/my-agent" or "local/my-agent")
|
|
30
|
+
*/
|
|
31
|
+
function getArtifactsKeyPrefix() {
|
|
32
|
+
// 1. Explicit override (highest priority)
|
|
33
|
+
const explicitPrefix = process.env.ARTIFACTS_KEY_PREFIX;
|
|
34
|
+
// Check for valid prefix (not empty, not just quotes from env parsing)
|
|
35
|
+
if (explicitPrefix &&
|
|
36
|
+
explicitPrefix !== '""' &&
|
|
37
|
+
explicitPrefix.trim() !== "") {
|
|
38
|
+
return explicitPrefix.replace(/\/+$/, ""); // Remove trailing slashes
|
|
39
|
+
}
|
|
40
|
+
// 2. Check deployment vs local mode
|
|
41
|
+
const agentName = process.env.AGENT_NAME;
|
|
42
|
+
if (!agentName) {
|
|
43
|
+
return "local/unknown";
|
|
44
|
+
}
|
|
45
|
+
// 3. Parse username from AGENT_NAME (format: "username-agentname")
|
|
46
|
+
// In deployment, AGENT_NAME follows pattern "username-agentname"
|
|
47
|
+
const parts = agentName.split("-");
|
|
48
|
+
if (parts.length >= 2) {
|
|
49
|
+
const username = parts[0];
|
|
50
|
+
const agentNamePart = parts.slice(1).join("-");
|
|
51
|
+
return `${username}/${agentNamePart}`;
|
|
52
|
+
}
|
|
53
|
+
// Local mode fallback
|
|
54
|
+
return `local/${agentName}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Builds the full storage key for a file.
|
|
58
|
+
*
|
|
59
|
+
* @param sessionId Current session ID
|
|
60
|
+
* @param filePath Relative file path within the session
|
|
61
|
+
* @returns Full storage key (e.g., "alice/my-agent/sess-123/data/output.json")
|
|
62
|
+
*/
|
|
63
|
+
function buildStorageKey(sessionId, filePath) {
|
|
64
|
+
// Validate session ID is provided
|
|
65
|
+
if (!sessionId || sessionId.trim() === "") {
|
|
66
|
+
throw new Error("FATAL: session_id is required but was not provided. " +
|
|
67
|
+
"This is a system error - session_id should be automatically injected. " +
|
|
68
|
+
"Please report this bug.");
|
|
69
|
+
}
|
|
70
|
+
const prefix = getArtifactsKeyPrefix();
|
|
71
|
+
// Normalize file path (remove leading slashes)
|
|
72
|
+
const normalizedPath = filePath.replace(/^\/+/, "");
|
|
73
|
+
return `${prefix}/${sessionId}/${normalizedPath}`;
|
|
74
|
+
}
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Path Validation
|
|
77
|
+
// ============================================================================
|
|
78
|
+
function validateLocalPath(path) {
|
|
79
|
+
if (!path.startsWith("/")) {
|
|
80
|
+
throw new Error(`Local paths must be absolute. Got: ${path}\n` +
|
|
81
|
+
"Hint: Use an absolute path starting with '/'");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validateStoragePath(path) {
|
|
85
|
+
if (path.startsWith("/")) {
|
|
86
|
+
throw new Error(`Storage paths must be relative (no leading slash). Got: ${path}\n` +
|
|
87
|
+
"Hint: Use a relative path like 'data/file.json'");
|
|
88
|
+
}
|
|
89
|
+
if (path.includes("..")) {
|
|
90
|
+
throw new Error(`Storage paths cannot contain '..' for security reasons. Got: ${path}`);
|
|
91
|
+
}
|
|
92
|
+
// Check for empty path
|
|
93
|
+
if (!path || path.trim() === "") {
|
|
94
|
+
throw new Error("Storage path cannot be empty");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Supabase Storage Operation Helpers
|
|
99
|
+
// ============================================================================
|
|
100
|
+
/**
|
|
101
|
+
* Upload a file to Supabase Storage using signed URL
|
|
102
|
+
*/
|
|
103
|
+
async function uploadToSupabase(storageKey, localPath) {
|
|
104
|
+
const auth = getShedAuth();
|
|
105
|
+
if (auth === null)
|
|
106
|
+
throw new Error(`agent/artifacts: could not fetch shed auth`);
|
|
107
|
+
const { shedUrl, accessToken } = auth;
|
|
108
|
+
// Get signed upload URL from server
|
|
109
|
+
const signedUrl = await createClient({
|
|
110
|
+
shedUrl,
|
|
111
|
+
accessToken,
|
|
112
|
+
}).getArtifactUrl.query({
|
|
113
|
+
kind: "user",
|
|
114
|
+
key: storageKey,
|
|
115
|
+
upload: true,
|
|
116
|
+
});
|
|
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
|
+
// Upload using signed URL
|
|
137
|
+
const response = await fetch(signedUrl, {
|
|
138
|
+
method: "PUT",
|
|
139
|
+
body: fileContent,
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": contentType,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`Failed to upload to Supabase Storage: ${response.status} ${response.statusText}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Download a file from Supabase Storage using signed URL
|
|
150
|
+
*/
|
|
151
|
+
async function downloadFromSupabase(storageKey, localPath) {
|
|
152
|
+
const auth = getShedAuth();
|
|
153
|
+
if (auth === null)
|
|
154
|
+
throw new Error(`agent/artifacts: could not fetch shed auth`);
|
|
155
|
+
const { shedUrl, accessToken } = auth;
|
|
156
|
+
// Get signed download URL from server
|
|
157
|
+
const signedUrl = await createClient({
|
|
158
|
+
shedUrl,
|
|
159
|
+
accessToken,
|
|
160
|
+
}).getArtifactUrl.query({
|
|
161
|
+
kind: "user",
|
|
162
|
+
key: storageKey,
|
|
163
|
+
upload: false,
|
|
164
|
+
});
|
|
165
|
+
// Download file using signed URL
|
|
166
|
+
const response = await fetch(signedUrl);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`Failed to download from Supabase Storage: ${response.status} ${response.statusText}`);
|
|
169
|
+
}
|
|
170
|
+
const data = await response.arrayBuffer();
|
|
171
|
+
if (!data) {
|
|
172
|
+
throw new Error("Supabase Storage response body is empty");
|
|
173
|
+
}
|
|
174
|
+
// Ensure parent directory exists
|
|
175
|
+
const dir = path.dirname(localPath);
|
|
176
|
+
await fs.mkdir(dir, { recursive: true });
|
|
177
|
+
// Convert ArrayBuffer to Buffer and write
|
|
178
|
+
const buffer = Buffer.from(data);
|
|
179
|
+
await fs.writeFile(localPath, buffer);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Delete a file from Supabase Storage
|
|
183
|
+
*/
|
|
184
|
+
async function deleteFromSupabase(storageKey) {
|
|
185
|
+
const auth = getShedAuth();
|
|
186
|
+
if (auth === null)
|
|
187
|
+
throw new Error(`agent/artifacts: could not fetch shed auth`);
|
|
188
|
+
const { shedUrl, accessToken } = auth;
|
|
189
|
+
// Call the delete API endpoint
|
|
190
|
+
await createClient({ shedUrl, accessToken }).deleteArtifact.mutate({
|
|
191
|
+
kind: "user",
|
|
192
|
+
key: storageKey,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* List files in Supabase Storage with optional prefix and recursion
|
|
197
|
+
*/
|
|
198
|
+
async function listFilesInSupabase(sessionId, relativePath, recursive = false) {
|
|
199
|
+
const auth = getShedAuth();
|
|
200
|
+
if (auth === null)
|
|
201
|
+
throw new Error(`agent/artifacts: could not fetch shed auth`);
|
|
202
|
+
const { shedUrl, accessToken } = auth;
|
|
203
|
+
return await createClient({ shedUrl, accessToken }).listArtifacts.query({
|
|
204
|
+
kind: "user",
|
|
205
|
+
key: buildStorageKey(sessionId, relativePath ?? ""),
|
|
206
|
+
recursive,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Generate a signed URL for a Supabase Storage object
|
|
211
|
+
*/
|
|
212
|
+
const generateSignedUrl = async (storageKey) => {
|
|
213
|
+
const shedAuth = getShedAuth();
|
|
214
|
+
if (shedAuth === null)
|
|
215
|
+
throw new Error("agent/artifacts: could not fetch shed auth");
|
|
216
|
+
const { accessToken, shedUrl } = shedAuth;
|
|
217
|
+
return await createClient({ shedUrl, accessToken }).getArtifactUrl.query({
|
|
218
|
+
kind: "user",
|
|
219
|
+
key: storageKey,
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Error Handling Helpers
|
|
224
|
+
// ============================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Maps Supabase Storage errors to user-friendly error messages
|
|
227
|
+
*/
|
|
228
|
+
function handleStorageError(error, context) {
|
|
229
|
+
if (error instanceof Error) {
|
|
230
|
+
const message = error.message.toLowerCase();
|
|
231
|
+
if (message.includes("object not found") || message.includes("not found")) {
|
|
232
|
+
return `Error: File not found in storage. ${context}`;
|
|
233
|
+
}
|
|
234
|
+
if (message.includes("bucket not found")) {
|
|
235
|
+
return `Error: Artifacts bucket not configured. Please create the 'artifacts' bucket in Supabase. ${context}`;
|
|
236
|
+
}
|
|
237
|
+
if (message.includes("invalid jwt") || message.includes("jwt")) {
|
|
238
|
+
return `Error: Authentication error. Please check Supabase credentials. ${context}`;
|
|
239
|
+
}
|
|
240
|
+
if (message.includes("enoent")) {
|
|
241
|
+
return `Error: Local file not found. ${context}\n${error.message}`;
|
|
242
|
+
}
|
|
243
|
+
if (message.includes("eacces")) {
|
|
244
|
+
return `Error: Permission denied accessing local file. ${context}\n${error.message}`;
|
|
245
|
+
}
|
|
246
|
+
return `Error: ${error.message}. ${context}`;
|
|
247
|
+
}
|
|
248
|
+
return `Error: Unknown error occurred. ${context}`;
|
|
249
|
+
}
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Tool Implementations
|
|
252
|
+
// ============================================================================
|
|
253
|
+
/**
|
|
254
|
+
* Tool 1: artifacts_cp - Copy files to/from Supabase Storage
|
|
255
|
+
*/
|
|
256
|
+
const artifactsCp = tool(async ({ session_id, source, destination, direction, }) => {
|
|
257
|
+
try {
|
|
258
|
+
if (direction === "upload") {
|
|
259
|
+
// Upload: local file -> Storage
|
|
260
|
+
validateLocalPath(source);
|
|
261
|
+
validateStoragePath(destination);
|
|
262
|
+
const storageKey = buildStorageKey(session_id, destination);
|
|
263
|
+
await uploadToSupabase(storageKey, source);
|
|
264
|
+
return `Successfully uploaded ${source} to storage at ${destination}`;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Download: Storage -> local file
|
|
268
|
+
validateStoragePath(source);
|
|
269
|
+
validateLocalPath(destination);
|
|
270
|
+
const storageKey = buildStorageKey(session_id, source);
|
|
271
|
+
await downloadFromSupabase(storageKey, destination);
|
|
272
|
+
return `Successfully downloaded ${source} from storage to ${destination}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
return handleStorageError(error, direction === "upload"
|
|
277
|
+
? `Failed to upload ${source} to ${destination}`
|
|
278
|
+
: `Failed to download ${source} to ${destination}`);
|
|
279
|
+
}
|
|
280
|
+
}, {
|
|
281
|
+
name: "artifacts_cp",
|
|
282
|
+
description: "Copy files between local filesystem and Supabase Storage-backed artifact storage. " +
|
|
283
|
+
"Use direction='upload' to copy from local to storage, or direction='download' to copy from storage to local. " +
|
|
284
|
+
"Local paths must be absolute (e.g., '/tmp/file.txt'), storage paths must be relative (e.g., 'data/file.txt').",
|
|
285
|
+
schema: z.object({
|
|
286
|
+
session_id: z
|
|
287
|
+
.string()
|
|
288
|
+
.optional()
|
|
289
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
290
|
+
source: z
|
|
291
|
+
.string()
|
|
292
|
+
.describe("Source path: absolute path if local, relative path if storage"),
|
|
293
|
+
destination: z
|
|
294
|
+
.string()
|
|
295
|
+
.describe("Destination path: absolute path if local, relative path if storage"),
|
|
296
|
+
direction: z
|
|
297
|
+
.enum(["upload", "download"])
|
|
298
|
+
.describe("Direction: 'upload' (local to storage) or 'download' (storage to local)"),
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
/**
|
|
302
|
+
* Tool 2: artifacts_del - Delete files from Supabase Storage
|
|
303
|
+
*/
|
|
304
|
+
const artifactsDel = tool(async ({ session_id, path, }) => {
|
|
305
|
+
try {
|
|
306
|
+
validateStoragePath(path);
|
|
307
|
+
const storageKey = buildStorageKey(session_id, path);
|
|
308
|
+
await deleteFromSupabase(storageKey);
|
|
309
|
+
return `Successfully deleted ${path} from storage`;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
return handleStorageError(error, `Failed to delete ${path}`);
|
|
313
|
+
}
|
|
314
|
+
}, {
|
|
315
|
+
name: "artifacts_del",
|
|
316
|
+
description: "Delete a file from Supabase Storage-backed artifact storage. " +
|
|
317
|
+
"The path must be relative (e.g., 'data/file.txt').",
|
|
318
|
+
schema: z.object({
|
|
319
|
+
session_id: z
|
|
320
|
+
.string()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
323
|
+
path: z
|
|
324
|
+
.string()
|
|
325
|
+
.describe("Relative path to the file to delete in storage"),
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
/**
|
|
329
|
+
* Tool 3: artifacts_ls - List files in Supabase Storage
|
|
330
|
+
*/
|
|
331
|
+
const artifactsLs = tool(async ({ session_id, path, recursive = false, }) => {
|
|
332
|
+
try {
|
|
333
|
+
if (path) {
|
|
334
|
+
validateStoragePath(path);
|
|
335
|
+
}
|
|
336
|
+
const files = await listFilesInSupabase(session_id, path, recursive);
|
|
337
|
+
if (files.length === 0) {
|
|
338
|
+
return path
|
|
339
|
+
? `No files found in ${path}`
|
|
340
|
+
: "No files found in this session";
|
|
341
|
+
}
|
|
342
|
+
// Format the file list
|
|
343
|
+
const fileList = files
|
|
344
|
+
.map((file) => {
|
|
345
|
+
const size = file.metadata?.size || 0;
|
|
346
|
+
const lastModified = file.updated_at || file.created_at || "unknown";
|
|
347
|
+
// Format size in human-readable format
|
|
348
|
+
let sizeStr;
|
|
349
|
+
if (size < 1024) {
|
|
350
|
+
sizeStr = `${size} B`;
|
|
351
|
+
}
|
|
352
|
+
else if (size < 1024 * 1024) {
|
|
353
|
+
sizeStr = `${(size / 1024).toFixed(2)} KB`;
|
|
354
|
+
}
|
|
355
|
+
else if (size < 1024 * 1024 * 1024) {
|
|
356
|
+
sizeStr = `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
sizeStr = `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
360
|
+
}
|
|
361
|
+
return `${file.name}\t${sizeStr}\t${lastModified}`;
|
|
362
|
+
})
|
|
363
|
+
.join("\n");
|
|
364
|
+
const header = path
|
|
365
|
+
? `Files in ${path} (${files.length} total):\n`
|
|
366
|
+
: `Files in session (${files.length} total):\n`;
|
|
367
|
+
return `${header}Name\tSize\tLast Modified\n${fileList}`;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
return handleStorageError(error, `Failed to list files${path ? ` in ${path}` : ""}`);
|
|
371
|
+
}
|
|
372
|
+
}, {
|
|
373
|
+
name: "artifacts_ls",
|
|
374
|
+
description: "List files in Supabase Storage-backed artifact storage for the current session. " +
|
|
375
|
+
"Optionally specify a relative directory path to list. " +
|
|
376
|
+
"Use recursive=true to list files recursively in subdirectories.",
|
|
377
|
+
schema: z.object({
|
|
378
|
+
session_id: z
|
|
379
|
+
.string()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
382
|
+
path: z
|
|
383
|
+
.string()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("Optional relative directory path to list (e.g., 'data/'). If omitted, lists session root."),
|
|
386
|
+
recursive: z
|
|
387
|
+
.boolean()
|
|
388
|
+
.optional()
|
|
389
|
+
.describe("Whether to list files recursively in subdirectories (default: false)"),
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
/**
|
|
393
|
+
* Tool 4: artifacts_url - Generate signed URL
|
|
394
|
+
*/
|
|
395
|
+
const artifactsUrl = tool(async ({ session_id, path, }) => {
|
|
396
|
+
try {
|
|
397
|
+
validateStoragePath(path);
|
|
398
|
+
const storageKey = buildStorageKey(session_id, path);
|
|
399
|
+
const url = await generateSignedUrl(storageKey);
|
|
400
|
+
return `Signed URL for ${path}:\n${url}`;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return handleStorageError(error, `Failed to generate URL for ${path}`);
|
|
404
|
+
}
|
|
405
|
+
}, {
|
|
406
|
+
name: "artifacts_url",
|
|
407
|
+
description: "Generate a signed URL for accessing an artifact in Supabase Storage. " +
|
|
408
|
+
"The URL will be valid for the specified duration (default: 1 hour, max: 365 days). " +
|
|
409
|
+
"Anyone with this URL can access the file until it expires.",
|
|
410
|
+
schema: z.object({
|
|
411
|
+
session_id: z
|
|
412
|
+
.string()
|
|
413
|
+
.optional()
|
|
414
|
+
.describe("INTERNAL USE ONLY - Auto-injected by system"),
|
|
415
|
+
path: z.string().describe("Relative path to the artifact in storage"),
|
|
416
|
+
expires_in: z
|
|
417
|
+
.number()
|
|
418
|
+
.optional()
|
|
419
|
+
.describe("Expiration time in seconds (1-31536000). Default: 3600 (1 hour)"),
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Tool Metadata
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// Add metadata for UI display
|
|
426
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
427
|
+
artifactsCp.prettyName = "Copy Artifact";
|
|
428
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
429
|
+
artifactsCp.icon = "Upload";
|
|
430
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
431
|
+
artifactsCp.verbiage = {
|
|
432
|
+
active: "Copying artifact to {destination}",
|
|
433
|
+
past: "Copied artifact to {destination}",
|
|
434
|
+
paramKey: "destination",
|
|
435
|
+
};
|
|
436
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
437
|
+
artifactsDel.prettyName = "Delete Artifact";
|
|
438
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
439
|
+
artifactsDel.icon = "Trash";
|
|
440
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
441
|
+
artifactsDel.verbiage = {
|
|
442
|
+
active: "Deleting artifact {path}",
|
|
443
|
+
past: "Deleted artifact {path}",
|
|
444
|
+
paramKey: "path",
|
|
445
|
+
};
|
|
446
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
447
|
+
artifactsLs.prettyName = "List Artifacts";
|
|
448
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
449
|
+
artifactsLs.icon = "List";
|
|
450
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
451
|
+
artifactsLs.verbiage = {
|
|
452
|
+
active: "Listing artifacts",
|
|
453
|
+
past: "Listed artifacts",
|
|
454
|
+
};
|
|
455
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
456
|
+
artifactsUrl.prettyName = "Generate Artifact URL";
|
|
457
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
458
|
+
artifactsUrl.icon = "Link";
|
|
459
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic metadata assignment to tool objects
|
|
460
|
+
artifactsUrl.verbiage = {
|
|
461
|
+
active: "Generating URL for {path}",
|
|
462
|
+
past: "Generated URL for {path}",
|
|
463
|
+
paramKey: "path",
|
|
464
|
+
};
|
|
465
|
+
// ============================================================================
|
|
466
|
+
// Factory Function
|
|
467
|
+
// ============================================================================
|
|
468
|
+
/**
|
|
469
|
+
* Factory function to create the artifacts tools
|
|
470
|
+
* Returns an array of all four artifact management tools
|
|
471
|
+
*/
|
|
472
|
+
export function makeArtifactsTools() {
|
|
473
|
+
return [artifactsCp, artifactsDel, artifactsLs, artifactsUrl];
|
|
474
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CONVERSATION_SEARCH_TOOL_NAME = "conversation_search";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a conversation search tool that searches past agent chat sessions.
|
|
5
|
+
* @param agentDir - The directory of the current agent (e.g., agents/researcher)
|
|
6
|
+
*/
|
|
7
|
+
export declare function makeConversationSearchTool(agentDir: string): import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
8
|
+
query: z.ZodString;
|
|
9
|
+
date_from: z.ZodOptional<z.ZodString>;
|
|
10
|
+
date_to: z.ZodOptional<z.ZodString>;
|
|
11
|
+
max_results: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
}, z.core.$strip>, {
|
|
13
|
+
query: string;
|
|
14
|
+
date_from?: string | undefined;
|
|
15
|
+
date_to?: string | undefined;
|
|
16
|
+
max_results?: number | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
query: string;
|
|
19
|
+
date_from?: string | undefined;
|
|
20
|
+
date_to?: string | undefined;
|
|
21
|
+
max_results?: number | undefined;
|
|
22
|
+
}, string>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tool } from "langchain";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export const CONVERSATION_SEARCH_TOOL_NAME = "conversation_search";
|
|
6
|
+
const conversationSearchSchema = z.object({
|
|
7
|
+
query: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.describe("The text to search for in conversation messages (case-insensitive)"),
|
|
11
|
+
date_from: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("ISO date string to filter conversations from this date onwards"),
|
|
15
|
+
date_to: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("ISO date string to filter conversations up to this date"),
|
|
19
|
+
max_results: z
|
|
20
|
+
.number()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Maximum number of matching conversations to return (default: 10)"),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Creates a conversation search tool that searches past agent chat sessions.
|
|
26
|
+
* @param agentDir - The directory of the current agent (e.g., agents/researcher)
|
|
27
|
+
*/
|
|
28
|
+
export function makeConversationSearchTool(agentDir) {
|
|
29
|
+
const conversationSearch = tool(async ({ query, date_from, date_to, max_results = 10 }) => {
|
|
30
|
+
const results = [];
|
|
31
|
+
const queryLower = query.toLowerCase();
|
|
32
|
+
const dateFromParsed = date_from ? new Date(date_from) : null;
|
|
33
|
+
const dateToParsed = date_to ? new Date(date_to) : null;
|
|
34
|
+
const sessionsDir = join(agentDir, ".sessions");
|
|
35
|
+
if (!existsSync(sessionsDir)) {
|
|
36
|
+
return `No conversations found matching "${query}".`;
|
|
37
|
+
}
|
|
38
|
+
let sessionFiles;
|
|
39
|
+
try {
|
|
40
|
+
sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return `No conversations found matching "${query}".`;
|
|
44
|
+
}
|
|
45
|
+
for (const file of sessionFiles) {
|
|
46
|
+
if (results.length >= max_results)
|
|
47
|
+
break;
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(join(sessionsDir, file), "utf-8");
|
|
50
|
+
const session = JSON.parse(content);
|
|
51
|
+
// Date filtering
|
|
52
|
+
const sessionDate = new Date(session.metadata.updatedAt);
|
|
53
|
+
if (dateFromParsed && sessionDate < dateFromParsed)
|
|
54
|
+
continue;
|
|
55
|
+
if (dateToParsed && sessionDate > dateToParsed)
|
|
56
|
+
continue;
|
|
57
|
+
// Search messages
|
|
58
|
+
const matches = [];
|
|
59
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
60
|
+
const msg = session.messages[i];
|
|
61
|
+
if (!msg)
|
|
62
|
+
continue;
|
|
63
|
+
const textBlocks = msg.content.filter((c) => c.type === "text");
|
|
64
|
+
for (const block of textBlocks) {
|
|
65
|
+
if (block.text.toLowerCase().includes(queryLower)) {
|
|
66
|
+
// Extract matched snippet with surrounding context
|
|
67
|
+
const matchIndex = block.text.toLowerCase().indexOf(queryLower);
|
|
68
|
+
const snippetStart = Math.max(0, matchIndex - 50);
|
|
69
|
+
const snippetEnd = Math.min(block.text.length, matchIndex + query.length + 50);
|
|
70
|
+
const matchedText = block.text.slice(snippetStart, snippetEnd);
|
|
71
|
+
matches.push({
|
|
72
|
+
messageIndex: i,
|
|
73
|
+
role: msg.role,
|
|
74
|
+
timestamp: msg.timestamp,
|
|
75
|
+
matchedText: (snippetStart > 0 ? "..." : "") +
|
|
76
|
+
matchedText +
|
|
77
|
+
(snippetEnd < block.text.length ? "..." : ""),
|
|
78
|
+
});
|
|
79
|
+
break; // One match per message is enough
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (matches.length > 0) {
|
|
84
|
+
results.push({
|
|
85
|
+
sessionId: session.sessionId,
|
|
86
|
+
agentName: session.metadata.agentName,
|
|
87
|
+
createdAt: session.metadata.createdAt,
|
|
88
|
+
updatedAt: session.metadata.updatedAt,
|
|
89
|
+
matches,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Skip invalid session files
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (results.length === 0) {
|
|
99
|
+
return `No conversations found matching "${query}".`;
|
|
100
|
+
}
|
|
101
|
+
// Format results as readable string
|
|
102
|
+
let output = `Found ${results.length} conversation(s) matching "${query}":\n\n`;
|
|
103
|
+
for (const result of results) {
|
|
104
|
+
output += `=== Session: ${result.sessionId} (${result.agentName}) ===\n`;
|
|
105
|
+
output += `Created: ${result.createdAt} | Updated: ${result.updatedAt}\n\n`;
|
|
106
|
+
for (const match of result.matches) {
|
|
107
|
+
output += `[Message ${match.messageIndex + 1} - ${match.role} @ ${match.timestamp}]\n`;
|
|
108
|
+
output += `"${match.matchedText}"\n\n`;
|
|
109
|
+
}
|
|
110
|
+
output += "---\n\n";
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}, {
|
|
114
|
+
name: CONVERSATION_SEARCH_TOOL_NAME,
|
|
115
|
+
description: `Search across past chat conversations to find previous discussions on specific topics.
|
|
116
|
+
|
|
117
|
+
Use this tool to:
|
|
118
|
+
- Find previous conversations that discussed specific topics
|
|
119
|
+
- Recall information from past sessions
|
|
120
|
+
- Search for patterns or recurring themes across conversations
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
- query: The text to search for (case-insensitive)
|
|
124
|
+
- date_from: (optional) Only search conversations from this date onwards (ISO format, e.g., "2025-01-01")
|
|
125
|
+
- date_to: (optional) Only search conversations up to this date (ISO format)
|
|
126
|
+
- max_results: (optional) Maximum results to return (default: 10)`,
|
|
127
|
+
schema: conversationSearchSchema,
|
|
128
|
+
});
|
|
129
|
+
conversationSearch.prettyName = "Conversation Search";
|
|
130
|
+
conversationSearch.icon = "MessageSquare";
|
|
131
|
+
conversationSearch.verbiage = {
|
|
132
|
+
active: "Searching conversations for {query}",
|
|
133
|
+
past: "Searched conversations for {query}",
|
|
134
|
+
paramKey: "query",
|
|
135
|
+
};
|
|
136
|
+
return conversationSearch;
|
|
137
|
+
}
|
|
@@ -37,7 +37,7 @@ const documentExtract = tool(async ({ session_id, file_path, query, target_token
|
|
|
37
37
|
parsedContent = { content };
|
|
38
38
|
}
|
|
39
39
|
// Count tokens in the document
|
|
40
|
-
const documentTokens = countTokens(content);
|
|
40
|
+
const documentTokens = await countTokens(content);
|
|
41
41
|
logger.info("Document extraction requested", {
|
|
42
42
|
filePath: file_path,
|
|
43
43
|
documentTokens,
|