@townco/agent 0.1.111 → 0.1.113

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.
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ interface GenerateImageResult {
3
+ success: boolean;
4
+ filePath?: string | undefined;
5
+ fileName?: string | undefined;
6
+ imageUrl?: string | undefined;
7
+ textResponse?: string | undefined;
8
+ mimeType?: string | undefined;
9
+ error?: string | undefined;
10
+ }
11
+ /** Create generate image tool using direct GEMINI_API_KEY/GOOGLE_API_KEY */
12
+ export declare function makeGenerateImageTool(): import("langchain").DynamicStructuredTool<z.ZodObject<{
13
+ prompt: z.ZodString;
14
+ aspectRatio: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
15
+ "16:9": "16:9";
16
+ "1:1": "1:1";
17
+ "3:4": "3:4";
18
+ "4:3": "4:3";
19
+ "5:4": "5:4";
20
+ "9:16": "9:16";
21
+ }>>>;
22
+ }, z.core.$strip>, {
23
+ prompt: string;
24
+ aspectRatio: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16";
25
+ }, {
26
+ prompt: string;
27
+ aspectRatio?: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16" | undefined;
28
+ }, GenerateImageResult>;
29
+ /** Create generate image tool using Town proxy */
30
+ export declare function makeTownGenerateImageTool(): import("langchain").DynamicStructuredTool<z.ZodObject<{
31
+ prompt: z.ZodString;
32
+ aspectRatio: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
33
+ "16:9": "16:9";
34
+ "1:1": "1:1";
35
+ "3:4": "3:4";
36
+ "4:3": "4:3";
37
+ "5:4": "5:4";
38
+ "9:16": "9:16";
39
+ }>>>;
40
+ }, z.core.$strip>, {
41
+ prompt: string;
42
+ aspectRatio: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16";
43
+ }, {
44
+ prompt: string;
45
+ aspectRatio?: "16:9" | "1:1" | "3:4" | "4:3" | "5:4" | "9:16" | undefined;
46
+ }, GenerateImageResult>;
47
+ export {};