@aj-archipelago/cortex 1.4.25 → 1.4.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.4.25",
3
+ "version": "1.4.27",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -60,6 +60,7 @@ export default {
60
60
  useInputChunking: false,
61
61
  enableDuplicateRequests: false,
62
62
  useSingleTokenStream: false,
63
+ manageTokenLength: false, // Agentic models handle context management themselves
63
64
  inputParameters: {
64
65
  privateData: false,
65
66
  chatHistory: [{role: '', content: []}],
@@ -163,7 +163,23 @@ export default {
163
163
 
164
164
  // Generate file message content if provided
165
165
  if (args.file) {
166
- const fileContent = await generateFileMessageContent(args.file, args.contextId, args.contextKey);
166
+ // Use agentContext if available, otherwise fall back to creating it from contextId/contextKey
167
+ const agentContext = args.agentContext || (args.contextId ? [{
168
+ contextId: args.contextId,
169
+ contextKey: args.contextKey || null,
170
+ default: true
171
+ }] : null);
172
+
173
+ if (!agentContext || !Array.isArray(agentContext) || agentContext.length === 0) {
174
+ const errorMessage = `File not found: "${args.file}". agentContext is required to look up files in the collection.`;
175
+ resolver.tool = JSON.stringify({ toolUsed: "vision" });
176
+ return JSON.stringify({
177
+ error: errorMessage,
178
+ recoveryMessage: "The file was not found. Please verify the file exists in the collection or provide a valid file reference."
179
+ });
180
+ }
181
+
182
+ const fileContent = await generateFileMessageContent(args.file, agentContext);
167
183
  if (!fileContent) {
168
184
  const errorMessage = `File not found: "${args.file}". Use ListFileCollection or SearchFileCollection to find available files.`;
169
185
  resolver.tool = JSON.stringify({ toolUsed: "vision" });
@@ -88,6 +88,7 @@ export default {
88
88
 
89
89
  executePathway: async ({args, runAllPrompts, resolver}) => {
90
90
  const pathwayResolver = resolver;
91
+ const chatId = args.chatId || null;
91
92
 
92
93
  try {
93
94
  let model = "replicate-seedream-4";
@@ -104,8 +105,8 @@ export default {
104
105
  // Fail early if any provided image cannot be resolved
105
106
  const resolvedInputImages = [];
106
107
  if (args.inputImages && Array.isArray(args.inputImages)) {
107
- if (!args.contextId) {
108
- throw new Error("contextId is required when using the 'inputImages' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
108
+ if (!args.agentContext || !Array.isArray(args.agentContext) || args.agentContext.length === 0) {
109
+ throw new Error("agentContext is required when using the 'inputImages' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
109
110
  }
110
111
 
111
112
  // Limit to 3 images maximum
@@ -175,13 +176,13 @@ export default {
175
176
  const uploadedGcs = uploadResult.gcs || null;
176
177
  const uploadedHash = uploadResult.hash || null;
177
178
 
178
- uploadedImages.push({
179
+ const imageData = {
179
180
  type: 'image',
180
181
  url: uploadedUrl,
181
182
  gcs: uploadedGcs,
182
183
  hash: uploadedHash,
183
184
  mimeType: mimeType
184
- });
185
+ };
185
186
 
186
187
  // Add uploaded image to file collection if contextId is available
187
188
  if (args.contextId && uploadedUrl) {
@@ -205,8 +206,8 @@ export default {
205
206
  const providedTags = Array.isArray(args.tags) ? args.tags : [];
206
207
  const allTags = [...defaultTags, ...providedTags.filter(tag => !defaultTags.includes(tag))];
207
208
 
208
- // Use the centralized utility function to add to collection
209
- await addFileToCollection(
209
+ // Use the centralized utility function to add to collection - capture returned entry
210
+ const fileEntry = await addFileToCollection(
210
211
  args.contextId,
211
212
  args.contextKey || '',
212
213
  uploadedUrl,
@@ -219,13 +220,19 @@ export default {
219
220
  uploadedHash,
220
221
  null, // fileUrl - not needed since we already uploaded
221
222
  pathwayResolver,
222
- true // permanent => retention=permanent
223
+ true, // permanent => retention=permanent
224
+ chatId
223
225
  );
226
+
227
+ // Use the file entry data for the return message
228
+ imageData.fileEntry = fileEntry;
224
229
  } catch (collectionError) {
225
230
  // Log but don't fail - file collection is optional
226
231
  pathwayResolver.logWarning(`Failed to add image to file collection: ${collectionError.message}`);
227
232
  }
228
233
  }
234
+
235
+ uploadedImages.push(imageData);
229
236
  } catch (uploadError) {
230
237
  pathwayResolver.logError(`Failed to upload image from Replicate: ${uploadError.message}`);
231
238
  // Keep original URL as fallback
@@ -241,11 +248,59 @@ export default {
241
248
  }
242
249
  }
243
250
 
244
- // Return the URLs of the uploaded images as text in the result
251
+ // Return the URLs of the uploaded images in structured format
245
252
  // Replace the result with uploaded cloud URLs (not the original Replicate URLs)
246
253
  if (uploadedImages.length > 0) {
247
- const imageUrls = uploadedImages.map(image => image.url || image).filter(Boolean);
248
- result = imageUrls.join('\n');
254
+ const successfulImages = uploadedImages.filter(img => img.url);
255
+ if (successfulImages.length > 0) {
256
+ // Build imageUrls array in the format expected by pathwayTools.js for toolImages injection
257
+ // This format matches ViewImages tool so images get properly injected into chat history
258
+ const imageUrls = successfulImages.map((img) => {
259
+ const url = img.fileEntry?.url || img.url;
260
+ const gcs = img.fileEntry?.gcs || img.gcs;
261
+ const hash = img.fileEntry?.hash || img.hash;
262
+
263
+ return {
264
+ type: "image_url",
265
+ url: url,
266
+ gcs: gcs || null,
267
+ image_url: { url: url },
268
+ hash: hash || null
269
+ };
270
+ });
271
+
272
+ // Return image info in the same format as availableFiles for the text message
273
+ // Format: hash | filename | url | date | tags
274
+ const imageList = successfulImages.map((img) => {
275
+ if (img.fileEntry) {
276
+ // Use the file entry data from addFileToCollection
277
+ const fe = img.fileEntry;
278
+ const dateStr = fe.addedDate
279
+ ? new Date(fe.addedDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
280
+ : '';
281
+ const tagsStr = Array.isArray(fe.tags) ? fe.tags.join(',') : '';
282
+ return `${fe.hash || ''} | ${fe.displayFilename || ''} | ${fe.url || img.url} | ${dateStr} | ${tagsStr}`;
283
+ } else {
284
+ // Fallback if file collection wasn't available
285
+ return `${img.hash || 'unknown'} | | ${img.url} | |`;
286
+ }
287
+ }).join('\n');
288
+
289
+ const count = successfulImages.length;
290
+ const isModification = args.inputImages && Array.isArray(args.inputImages) && args.inputImages.length > 0;
291
+
292
+ // Make the success message very explicit so the agent knows files were created and added to collection
293
+ // This format matches availableFiles so the agent can reference them by hash/filename
294
+ const action = isModification ? 'Image modification' : 'Image generation';
295
+ const message = `${action} completed successfully. ${count} image${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The image${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${imageList}\n\nYou can reference these images by their hash, filename, or URL in future tool calls.`;
296
+
297
+ // Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
298
+ result = JSON.stringify({
299
+ success: true,
300
+ message: message,
301
+ imageUrls: imageUrls
302
+ });
303
+ }
249
304
  }
250
305
  }
251
306
  }
@@ -90,6 +90,7 @@ export default {
90
90
  }],
91
91
  executePathway: async ({args, runAllPrompts, resolver}) => {
92
92
  const pathwayResolver = resolver;
93
+ const chatId = args.chatId || null;
93
94
 
94
95
  try {
95
96
  let model = "gemini-flash-25-image";
@@ -100,8 +101,8 @@ export default {
100
101
  // Fail early if any provided image cannot be resolved
101
102
  const resolvedInputImages = [];
102
103
  if (args.inputImages && Array.isArray(args.inputImages)) {
103
- if (!args.contextId) {
104
- throw new Error("contextId is required when using the 'inputImages' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
104
+ if (!args.agentContext || !Array.isArray(args.agentContext) || args.agentContext.length === 0) {
105
+ throw new Error("agentContext is required when using the 'inputImages' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
105
106
  }
106
107
 
107
108
  // Limit to 3 images maximum
@@ -203,7 +204,8 @@ export default {
203
204
  imageHash,
204
205
  null,
205
206
  pathwayResolver,
206
- true // permanent => retention=permanent
207
+ true, // permanent => retention=permanent
208
+ chatId
207
209
  );
208
210
 
209
211
  // Use the file entry data for the return message
@@ -229,7 +231,23 @@ export default {
229
231
  // Check if we successfully uploaded any images
230
232
  const successfulImages = uploadedImages.filter(img => img.url);
231
233
  if (successfulImages.length > 0) {
232
- // Return image info in the same format as availableFiles
234
+ // Build imageUrls array in the format expected by pathwayTools.js for toolImages injection
235
+ // This format matches ViewImages tool so images get properly injected into chat history
236
+ const imageUrls = successfulImages.map((img) => {
237
+ const url = img.fileEntry?.url || img.url;
238
+ const gcs = img.fileEntry?.gcs || img.gcs;
239
+ const hash = img.fileEntry?.hash || img.hash;
240
+
241
+ return {
242
+ type: "image_url",
243
+ url: url,
244
+ gcs: gcs || null,
245
+ image_url: { url: url },
246
+ hash: hash || null
247
+ };
248
+ });
249
+
250
+ // Return image info in the same format as availableFiles for the text message
233
251
  // Format: hash | filename | url | date | tags
234
252
  const imageList = successfulImages.map((img) => {
235
253
  if (img.fileEntry) {
@@ -247,7 +265,17 @@ export default {
247
265
  }).join('\n');
248
266
 
249
267
  const count = successfulImages.length;
250
- return `Image generation successful. Generated ${count} image${count > 1 ? 's' : ''}:\n${imageList}`;
268
+
269
+ // Make the success message very explicit so the agent knows files were created and added to collection
270
+ // This format matches availableFiles so the agent can reference them by hash/filename
271
+ const message = `Image generation completed successfully. ${count} image${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The image${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${imageList}\n\nYou can reference these images by their hash, filename, or URL in future tool calls.`;
272
+
273
+ // Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
274
+ return JSON.stringify({
275
+ success: true,
276
+ message: message,
277
+ imageUrls: imageUrls
278
+ });
251
279
  } else {
252
280
  throw new Error('Image generation failed: Images were generated but could not be uploaded to storage');
253
281
  }
@@ -71,8 +71,8 @@ export default {
71
71
  // Fail early if any provided image cannot be resolved
72
72
  const resolvedInputImages = [];
73
73
  if (args.inputImages && Array.isArray(args.inputImages)) {
74
- if (!args.contextId) {
75
- throw new Error("contextId is required when using the 'inputImages' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
74
+ if (!args.agentContext || !Array.isArray(args.agentContext) || args.agentContext.length === 0) {
75
+ throw new Error("agentContext is required when using the 'inputImages' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
76
76
  }
77
77
 
78
78
  // Limit to 3 images maximum
@@ -210,7 +210,23 @@ export default {
210
210
  // Check if we successfully uploaded any images
211
211
  const successfulImages = uploadedImages.filter(img => img.url);
212
212
  if (successfulImages.length > 0) {
213
- // Return image info in the same format as availableFiles
213
+ // Build imageUrls array in the format expected by pathwayTools.js for toolImages injection
214
+ // This format matches ViewImages tool so images get properly injected into chat history
215
+ const imageUrls = successfulImages.map((img) => {
216
+ const url = img.fileEntry?.url || img.url;
217
+ const gcs = img.fileEntry?.gcs || img.gcs;
218
+ const hash = img.fileEntry?.hash || img.hash;
219
+
220
+ return {
221
+ type: "image_url",
222
+ url: url,
223
+ gcs: gcs || null,
224
+ image_url: { url: url },
225
+ hash: hash || null
226
+ };
227
+ });
228
+
229
+ // Return image info in the same format as availableFiles for the text message
214
230
  // Format: hash | filename | url | date | tags
215
231
  const imageList = successfulImages.map((img) => {
216
232
  if (img.fileEntry) {
@@ -228,7 +244,18 @@ export default {
228
244
  }).join('\n');
229
245
 
230
246
  const count = successfulImages.length;
231
- return `Slide/infographic generation successful. Generated ${count} image${count > 1 ? 's' : ''}:\n${imageList}`;
247
+
248
+ // Make the success message very explicit so the agent knows files were created and added to collection
249
+ // This format matches availableFiles so the agent can reference them by hash/filename
250
+ const message = `Slide/infographic generation completed successfully. ${count} image${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The image${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${imageList}\n\nYou can reference these images by their hash, filename, or URL in future tool calls.`;
251
+
252
+ // Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
253
+ // This prevents the agent from looping because it can't see the generated images
254
+ return JSON.stringify({
255
+ success: true,
256
+ message: message,
257
+ imageUrls: imageUrls
258
+ });
232
259
  } else {
233
260
  throw new Error('Slide generation failed: Content was generated but could not be uploaded to storage');
234
261
  }
@@ -115,6 +115,7 @@ export default {
115
115
  }],
116
116
  executePathway: async ({args, runAllPrompts, resolver}) => {
117
117
  const pathwayResolver = resolver;
118
+ const chatId = args.chatId || null;
118
119
 
119
120
  try {
120
121
  const model = "veo-3.1-fast-generate";
@@ -124,8 +125,8 @@ export default {
124
125
  // Veo requires GCS URLs for input images
125
126
  let imageParam = undefined;
126
127
  if (args.inputImage) {
127
- if (!args.contextId) {
128
- throw new Error("contextId is required when using the 'inputImage' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
128
+ if (!args.agentContext || !Array.isArray(args.agentContext) || args.agentContext.length === 0) {
129
+ throw new Error("agentContext is required when using the 'inputImage' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
129
130
  }
130
131
 
131
132
  const resolved = await resolveFileParameter(args.inputImage, args.agentContext, { preferGcs: true });
@@ -297,7 +298,8 @@ export default {
297
298
  uploadedHash,
298
299
  null,
299
300
  pathwayResolver,
300
- true // permanent => retention=permanent
301
+ true, // permanent => retention=permanent
302
+ chatId
301
303
  );
302
304
 
303
305
  // Use the file entry data for the return message
@@ -323,11 +325,27 @@ export default {
323
325
  }
324
326
  }
325
327
 
326
- // Return the URLs of the uploaded videos as text in the result
328
+ // Return the URLs of the uploaded videos in structured format
327
329
  if (uploadedVideos.length > 0) {
328
330
  const successfulVideos = uploadedVideos.filter(v => v.url);
329
331
  if (successfulVideos.length > 0) {
330
- // Return video info in the same format as availableFiles
332
+ // Build imageUrls array in the format expected by pathwayTools.js for toolImages injection
333
+ // Videos can use image_url type since they can be displayed with markdown image syntax
334
+ const imageUrls = successfulVideos.map((vid) => {
335
+ const url = vid.fileEntry?.url || vid.url;
336
+ const gcs = vid.fileEntry?.gcs || vid.gcs;
337
+ const hash = vid.fileEntry?.hash || vid.hash;
338
+
339
+ return {
340
+ type: "image_url",
341
+ url: url,
342
+ gcs: gcs || null,
343
+ image_url: { url: url },
344
+ hash: hash || null
345
+ };
346
+ });
347
+
348
+ // Return video info in the same format as availableFiles for the text message
331
349
  // Format: hash | filename | url | date | tags
332
350
  const videoList = successfulVideos.map((vid) => {
333
351
  if (vid.fileEntry) {
@@ -345,8 +363,17 @@ export default {
345
363
  }).join('\n');
346
364
 
347
365
  const count = successfulVideos.length;
348
- // Note: The UI supports displaying videos using markdown image syntax
349
- return `Video generation successful. Generated ${count} video${count > 1 ? 's' : ''}. Videos can be displayed using markdown image syntax, e.g. ![video](url)\n${videoList}`;
366
+
367
+ // Make the success message very explicit so the agent knows files were created and added to collection
368
+ // This format matches availableFiles so the agent can reference them by hash/filename
369
+ const message = `Video generation completed successfully. ${count} video${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The video${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${videoList}\n\nYou can reference these videos by their hash, filename, or URL in future tool calls. Videos can be displayed using markdown image syntax, e.g. ![video](url)`;
370
+
371
+ // Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
372
+ return JSON.stringify({
373
+ success: true,
374
+ message: message,
375
+ imageUrls: imageUrls
376
+ });
350
377
  } else {
351
378
  // All videos failed to upload
352
379
  const errors = uploadedVideos.map(v => v.error).filter(Boolean);
@@ -1996,6 +1996,94 @@ test('Analyzer tool: Returns error JSON format when file not found', async t =>
1996
1996
  }
1997
1997
  });
1998
1998
 
1999
+ test('Analyzer tool: Works with legacy contextId/contextKey parameters (backward compatibility)', async t => {
2000
+ const contextId = createTestContext();
2001
+
2002
+ try {
2003
+ // First add a file to the collection
2004
+ await callPathway('sys_tool_file_collection', {
2005
+ agentContext: [{ contextId, contextKey: null, default: true }],
2006
+ url: 'https://example.com/test-document.pdf',
2007
+ filename: 'test-document.pdf',
2008
+ userMessage: 'Add test file for analyzer'
2009
+ });
2010
+
2011
+ // Get the file ID from the collection
2012
+ const collection = await loadFileCollection(contextId, null, false);
2013
+ const fileId = collection[0].id;
2014
+
2015
+ // Test analyzer tool with legacy contextId/contextKey (without agentContext)
2016
+ // This tests that the tool correctly handles backward compatibility
2017
+ const result = await callPathway('sys_tool_analyzefile', {
2018
+ contextId, // Legacy format - no agentContext
2019
+ contextKey: null,
2020
+ file: fileId,
2021
+ detailedInstructions: 'What is this file?',
2022
+ userMessage: 'Testing backward compatibility'
2023
+ });
2024
+
2025
+ t.truthy(result, 'Should have a result');
2026
+
2027
+ // The result should be a string (not an error JSON)
2028
+ // If it's an error, it should be properly formatted
2029
+ let parsedResult;
2030
+ try {
2031
+ parsedResult = JSON.parse(result);
2032
+ // If it parsed as JSON, check if it's an error
2033
+ if (parsedResult.error) {
2034
+ t.fail(`Tool returned error when it should have worked: ${parsedResult.error}`);
2035
+ }
2036
+ } catch (error) {
2037
+ // If it doesn't parse as JSON, that's fine - it's likely the model response
2038
+ t.truthy(typeof result === 'string', 'Result should be a string');
2039
+ }
2040
+ } finally {
2041
+ await cleanup(contextId);
2042
+ }
2043
+ });
2044
+
2045
+ test('Analyzer tool: File resolution works with agentContext', async t => {
2046
+ const contextId = createTestContext();
2047
+
2048
+ try {
2049
+ // Add a file to the collection
2050
+ await callPathway('sys_tool_file_collection', {
2051
+ agentContext: [{ contextId, contextKey: null, default: true }],
2052
+ url: 'https://example.com/test-file.pdf',
2053
+ filename: 'test-file.pdf',
2054
+ userMessage: 'Add test file'
2055
+ });
2056
+
2057
+ // Get the file ID from the collection
2058
+ const collection = await loadFileCollection(contextId, null, false);
2059
+ const fileId = collection[0].id;
2060
+
2061
+ // Test analyzer tool with agentContext (modern format)
2062
+ const result = await callPathway('sys_tool_analyzefile', {
2063
+ agentContext: [{ contextId, contextKey: null, default: true }],
2064
+ file: fileId,
2065
+ detailedInstructions: 'What is this file?',
2066
+ userMessage: 'Testing with agentContext'
2067
+ });
2068
+
2069
+ t.truthy(result, 'Should have a result');
2070
+
2071
+ // The result should not be an error
2072
+ let parsedResult;
2073
+ try {
2074
+ parsedResult = JSON.parse(result);
2075
+ if (parsedResult.error) {
2076
+ t.fail(`Tool returned error: ${parsedResult.error}`);
2077
+ }
2078
+ } catch (error) {
2079
+ // If it doesn't parse as JSON, that's fine - it's likely the model response
2080
+ t.truthy(typeof result === 'string', 'Result should be a string');
2081
+ }
2082
+ } finally {
2083
+ await cleanup(contextId);
2084
+ }
2085
+ });
2086
+
1999
2087
  // ============================================
2000
2088
  // Converted Files Tests (displayFilename != URL extension)
2001
2089
  // ============================================