@aj-archipelago/cortex 1.4.32 → 1.4.33
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/config.js +72 -0
- package/helper-apps/cortex-file-handler/Dockerfile +1 -1
- package/lib/fileUtils.js +24 -5
- package/lib/pathwayManager.js +6 -6
- package/lib/pathwayTools.js +21 -1
- package/lib/requestExecutor.js +49 -5
- package/package.json +1 -1
- package/pathways/system/entity/sys_compress_context.js +82 -0
- package/pathways/system/entity/sys_entity_agent.js +65 -15
- package/pathways/transcribe_gemini.js +1 -1
- package/server/modelExecutor.js +4 -0
- package/server/pathwayResolver.js +102 -12
- package/server/plugins/claudeAnthropicPlugin.js +84 -0
- package/server/plugins/gemini15ChatPlugin.js +17 -0
- package/server/plugins/gemini15VisionPlugin.js +51 -5
- package/server/plugins/grokResponsesPlugin.js +2 -0
- package/server/plugins/openAiVisionPlugin.js +4 -2
- package/test.log +42834 -0
- package/tests/integration/rest/vendors/claude_anthropic_direct.test.js +197 -0
- package/tests/unit/plugins/claudeAnthropicPlugin.test.js +236 -0
- package/tests/unit/sys_entity_agent_errors.test.js +792 -0
package/config.js
CHANGED
|
@@ -575,6 +575,72 @@ var config = convict({
|
|
|
575
575
|
"maxImageSize": 5242880,
|
|
576
576
|
"supportsStreaming": true
|
|
577
577
|
},
|
|
578
|
+
"claude-sonnet-4": {
|
|
579
|
+
"type": "CLAUDE-ANTHROPIC",
|
|
580
|
+
"emulateOpenAIChatModel": "claude-sonnet-4",
|
|
581
|
+
"endpoints": [
|
|
582
|
+
{
|
|
583
|
+
"name": "Anthropic Claude Sonnet 4",
|
|
584
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
585
|
+
"headers": {
|
|
586
|
+
"x-api-key": "{{CLAUDE_API_KEY}}",
|
|
587
|
+
"Content-Type": "application/json"
|
|
588
|
+
},
|
|
589
|
+
"params": {
|
|
590
|
+
"model": "claude-sonnet-4-20250514"
|
|
591
|
+
},
|
|
592
|
+
"requestsPerSecond": 10
|
|
593
|
+
}
|
|
594
|
+
],
|
|
595
|
+
"maxTokenLength": 200000,
|
|
596
|
+
"maxReturnTokens": 64000,
|
|
597
|
+
"maxImageSize": 31457280,
|
|
598
|
+
"supportsStreaming": true
|
|
599
|
+
},
|
|
600
|
+
"claude-45-sonnet": {
|
|
601
|
+
"type": "CLAUDE-ANTHROPIC",
|
|
602
|
+
"emulateOpenAIChatModel": "claude-4.5-sonnet",
|
|
603
|
+
"endpoints": [
|
|
604
|
+
{
|
|
605
|
+
"name": "Anthropic Claude 4.5 Sonnet",
|
|
606
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
607
|
+
"headers": {
|
|
608
|
+
"x-api-key": "{{CLAUDE_API_KEY}}",
|
|
609
|
+
"Content-Type": "application/json"
|
|
610
|
+
},
|
|
611
|
+
"params": {
|
|
612
|
+
"model": "claude-sonnet-4-5-20250514"
|
|
613
|
+
},
|
|
614
|
+
"requestsPerSecond": 10
|
|
615
|
+
}
|
|
616
|
+
],
|
|
617
|
+
"maxTokenLength": 200000,
|
|
618
|
+
"maxReturnTokens": 64000,
|
|
619
|
+
"maxImageSize": 31457280,
|
|
620
|
+
"supportsStreaming": true
|
|
621
|
+
},
|
|
622
|
+
"claude-45-opus": {
|
|
623
|
+
"type": "CLAUDE-ANTHROPIC",
|
|
624
|
+
"emulateOpenAIChatModel": "claude-4.5-opus",
|
|
625
|
+
"endpoints": [
|
|
626
|
+
{
|
|
627
|
+
"name": "Anthropic Claude 4.5 Opus",
|
|
628
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
629
|
+
"headers": {
|
|
630
|
+
"x-api-key": "{{CLAUDE_API_KEY}}",
|
|
631
|
+
"Content-Type": "application/json"
|
|
632
|
+
},
|
|
633
|
+
"params": {
|
|
634
|
+
"model": "claude-opus-4-5-20250514"
|
|
635
|
+
},
|
|
636
|
+
"requestsPerSecond": 10
|
|
637
|
+
}
|
|
638
|
+
],
|
|
639
|
+
"maxTokenLength": 200000,
|
|
640
|
+
"maxReturnTokens": 32000,
|
|
641
|
+
"maxImageSize": 31457280,
|
|
642
|
+
"supportsStreaming": true
|
|
643
|
+
},
|
|
578
644
|
"gemini-flash-25-vision": {
|
|
579
645
|
"type": "GEMINI-1.5-VISION",
|
|
580
646
|
"emulateOpenAIChatModel": "gemini-flash-25",
|
|
@@ -778,6 +844,12 @@ var config = convict({
|
|
|
778
844
|
env: 'OPENAI_API_KEY',
|
|
779
845
|
sensitive: true
|
|
780
846
|
},
|
|
847
|
+
claudeApiKey: {
|
|
848
|
+
format: String,
|
|
849
|
+
default: null,
|
|
850
|
+
env: 'CLAUDE_API_KEY',
|
|
851
|
+
sensitive: true
|
|
852
|
+
},
|
|
781
853
|
openaiApiUrl: {
|
|
782
854
|
format: String,
|
|
783
855
|
default: 'https://api.openai.com/v1/completions',
|
package/lib/fileUtils.js
CHANGED
|
@@ -1561,11 +1561,27 @@ async function syncAndStripFilesFromChatHistory(chatHistory, agentContext, chatI
|
|
|
1561
1561
|
|
|
1562
1562
|
// Build lookup map from contextId to contextKey for updates
|
|
1563
1563
|
const contextKeyMap = new Map(agentContext.map(ctx => [ctx.contextId, ctx.contextKey || null]));
|
|
1564
|
-
|
|
1564
|
+
|
|
1565
|
+
// Helper to normalize URLs by stripping query parameters (SAS tokens, etc.)
|
|
1566
|
+
// This allows matching URLs that have different query params but same base path
|
|
1567
|
+
const normalizeUrl = (url) => {
|
|
1568
|
+
if (!url) return null;
|
|
1569
|
+
try {
|
|
1570
|
+
// Handle GCS URLs (gs://) by keeping them as-is (no query params)
|
|
1571
|
+
if (url.startsWith('gs://')) return url;
|
|
1572
|
+
const parsed = new URL(url);
|
|
1573
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
1574
|
+
} catch {
|
|
1575
|
+
// If URL parsing fails, return as-is
|
|
1576
|
+
return url;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1565
1580
|
// Build lookup maps for fast matching and context lookup (use ALL files, not just filtered)
|
|
1566
1581
|
// This allows us to find files that exist in Redis but don't have inCollection set yet
|
|
1582
|
+
// URLs are normalized (query params stripped) to handle SAS tokens and other transient params
|
|
1567
1583
|
const collectionByHash = new Map(allFiles.filter(f => f.hash).map(f => [f.hash, f]));
|
|
1568
|
-
const collectionByUrl = new Map(allFiles.filter(f => f.url).map(f => [f.url, f]));
|
|
1584
|
+
const collectionByUrl = new Map(allFiles.filter(f => f.url).map(f => [normalizeUrl(f.url), f]));
|
|
1569
1585
|
const collectionByGcs = new Map(allFiles.filter(f => f.gcs).map(f => [f.gcs, f]));
|
|
1570
1586
|
|
|
1571
1587
|
// Helper to get file from collection (by hash, URL, or GCS) to find _contextId
|
|
@@ -1573,12 +1589,15 @@ async function syncAndStripFilesFromChatHistory(chatHistory, agentContext, chatI
|
|
|
1573
1589
|
const fileHash = contentObj.hash;
|
|
1574
1590
|
const fileUrl = contentObj.url || contentObj.image_url?.url;
|
|
1575
1591
|
const fileGcs = contentObj.gcs;
|
|
1576
|
-
|
|
1592
|
+
|
|
1577
1593
|
if (fileHash && collectionByHash.has(fileHash)) {
|
|
1578
1594
|
return collectionByHash.get(fileHash);
|
|
1579
1595
|
}
|
|
1580
|
-
if (fileUrl
|
|
1581
|
-
|
|
1596
|
+
if (fileUrl) {
|
|
1597
|
+
const normalizedUrl = normalizeUrl(fileUrl);
|
|
1598
|
+
if (normalizedUrl && collectionByUrl.has(normalizedUrl)) {
|
|
1599
|
+
return collectionByUrl.get(normalizedUrl);
|
|
1600
|
+
}
|
|
1582
1601
|
}
|
|
1583
1602
|
if (fileGcs && collectionByGcs.has(fileGcs)) {
|
|
1584
1603
|
return collectionByGcs.get(fileGcs);
|
package/lib/pathwayManager.js
CHANGED
|
@@ -31,7 +31,7 @@ class LocalStorage extends StorageStrategy {
|
|
|
31
31
|
const data = await fs.promises.readFile(this.filePath, 'utf8');
|
|
32
32
|
return JSON.parse(data);
|
|
33
33
|
} catch (error) {
|
|
34
|
-
logger.error(`Error loading pathways from ${this.filePath}
|
|
34
|
+
logger.error(`Error loading pathways from ${this.filePath}: ${error.message}`);
|
|
35
35
|
throw error;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -88,7 +88,7 @@ class AzureBlobStorage extends StorageStrategy {
|
|
|
88
88
|
logger.info(`Loaded pathways from Azure Blob Storage. ${Object.keys(parsedData).map(user => `${user}(${Object.keys(parsedData[user])})`).join(', ')}`);
|
|
89
89
|
return parsedData;
|
|
90
90
|
} catch (error) {
|
|
91
|
-
logger.error(
|
|
91
|
+
logger.error(`Error loading pathways from Azure Blob Storage: ${error.message}`);
|
|
92
92
|
throw error;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -99,7 +99,7 @@ class AzureBlobStorage extends StorageStrategy {
|
|
|
99
99
|
const content = JSON.stringify(data, null, 2);
|
|
100
100
|
await blockBlobClient.upload(content, content.length);
|
|
101
101
|
} catch (error) {
|
|
102
|
-
logger.error(
|
|
102
|
+
logger.error(`Error saving pathways to Azure Blob Storage: ${error.message}`);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -155,7 +155,7 @@ class S3Storage extends StorageStrategy {
|
|
|
155
155
|
const dataString = await streamToString(readableStream);
|
|
156
156
|
return JSON.parse(dataString);
|
|
157
157
|
} catch (error) {
|
|
158
|
-
logger.error(
|
|
158
|
+
logger.error(`Error loading pathways from S3: ${error.message}`);
|
|
159
159
|
throw error;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -170,7 +170,7 @@ class S3Storage extends StorageStrategy {
|
|
|
170
170
|
};
|
|
171
171
|
await this.s3.putObject(params);
|
|
172
172
|
} catch (error) {
|
|
173
|
-
logger.error(
|
|
173
|
+
logger.error(`Error saving pathways to S3: ${error.message}`);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -544,7 +544,7 @@ class PathwayManager {
|
|
|
544
544
|
|
|
545
545
|
return this.pathways;
|
|
546
546
|
} catch (error) {
|
|
547
|
-
logger.error(
|
|
547
|
+
logger.error(`Error in getLatestPathways: ${error.message}`);
|
|
548
548
|
throw error;
|
|
549
549
|
}
|
|
550
550
|
}
|
package/lib/pathwayTools.js
CHANGED
|
@@ -427,4 +427,24 @@ const sendToolFinish = async (requestId, toolCallId, success, error = null) => {
|
|
|
427
427
|
}
|
|
428
428
|
};
|
|
429
429
|
|
|
430
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Wrap a promise with a timeout
|
|
432
|
+
* @param {Promise} promise - The promise to wrap
|
|
433
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
434
|
+
* @param {string} errorMessage - Error message if timeout occurs
|
|
435
|
+
* @returns {Promise} - The original promise or rejection on timeout
|
|
436
|
+
*/
|
|
437
|
+
const withTimeout = (promise, timeoutMs, errorMessage = 'Operation timed out') => {
|
|
438
|
+
let timeoutId;
|
|
439
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
440
|
+
timeoutId = setTimeout(() => {
|
|
441
|
+
reject(new Error(errorMessage));
|
|
442
|
+
}, timeoutMs);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
446
|
+
clearTimeout(timeoutId);
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export { callPathway, gpt3Encode, gpt3Decode, say, callTool, addCitationsToResolver, sendToolStart, sendToolFinish, withTimeout };
|
package/lib/requestExecutor.js
CHANGED
|
@@ -101,7 +101,16 @@ const createLimiter = (endpoint, name, index) => {
|
|
|
101
101
|
logger.debug(`Limiter request cancelled for ${cortexId}-${name}-${index}: Id: ${info.options.id || 'none'}`);
|
|
102
102
|
endpoint.monitor.incrementErrorCount();
|
|
103
103
|
} else {
|
|
104
|
-
|
|
104
|
+
const errorMsg = error?.message || error;
|
|
105
|
+
const status = error?.status || 'unknown';
|
|
106
|
+
logger.error(`Limiter request failed for ${cortexId}-${name}-${index}: Id: ${info.options.id || 'none'}: [${status}] ${errorMsg}`);
|
|
107
|
+
// Log response data if available (helpful for debugging 400 errors)
|
|
108
|
+
if (error?.responseData) {
|
|
109
|
+
const responseDataStr = typeof error.responseData === 'string'
|
|
110
|
+
? error.responseData.substring(0, 1000)
|
|
111
|
+
: JSON.stringify(error.responseData, null, 2).substring(0, 1000);
|
|
112
|
+
logger.error(`Response data: ${responseDataStr}`);
|
|
113
|
+
}
|
|
105
114
|
}
|
|
106
115
|
});
|
|
107
116
|
|
|
@@ -239,11 +248,46 @@ const requestWithMonitor = async (endpoint, url, data, axiosConfigObj) => {
|
|
|
239
248
|
} catch (error) {
|
|
240
249
|
// throw new error with duration as part of the error data
|
|
241
250
|
const { code, name } = error;
|
|
242
|
-
const finalStatus = error?.response?.status ?? error?.status
|
|
243
|
-
const statusText = error?.response?.statusText ?? error?.statusText
|
|
244
|
-
|
|
251
|
+
const finalStatus = error?.response?.status ?? error?.status;
|
|
252
|
+
const statusText = error?.response?.statusText ?? error?.statusText;
|
|
253
|
+
let responseData = error?.response?.data;
|
|
254
|
+
|
|
255
|
+
// For streaming requests with errors, the response data is a stream that needs to be consumed
|
|
256
|
+
if (responseData && typeof responseData.on === 'function') {
|
|
257
|
+
try {
|
|
258
|
+
const chunks = [];
|
|
259
|
+
for await (const chunk of responseData) {
|
|
260
|
+
chunks.push(chunk);
|
|
261
|
+
}
|
|
262
|
+
const streamContent = Buffer.concat(chunks).toString('utf-8');
|
|
263
|
+
try {
|
|
264
|
+
responseData = JSON.parse(streamContent);
|
|
265
|
+
} catch {
|
|
266
|
+
responseData = { rawContent: streamContent.substring(0, 2000) };
|
|
267
|
+
}
|
|
268
|
+
} catch (streamError) {
|
|
269
|
+
logger.debug(`Could not read error stream: ${streamError.message}`);
|
|
270
|
+
responseData = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Extract error message from various possible locations in the response
|
|
275
|
+
const errorMessage = responseData?.message
|
|
276
|
+
?? responseData?.error?.message
|
|
277
|
+
?? responseData?.error?.status
|
|
278
|
+
?? responseData?.rawContent?.substring(0, 500)
|
|
279
|
+
?? error?.message
|
|
280
|
+
?? String(error);
|
|
281
|
+
|
|
282
|
+
// Log full response data for debugging 4xx errors (especially 400)
|
|
283
|
+
if (finalStatus >= 400 && finalStatus < 500 && responseData) {
|
|
284
|
+
const responseDataStr = typeof responseData === 'string'
|
|
285
|
+
? responseData.substring(0, 2000)
|
|
286
|
+
: JSON.stringify(responseData, null, 2).substring(0, 2000);
|
|
287
|
+
logger.error(`HTTP ${finalStatus} error response data: ${responseDataStr}`);
|
|
288
|
+
}
|
|
245
289
|
|
|
246
|
-
throw { code, message: errorMessage, status: finalStatus, statusText, name, duration: endpoint?.monitor?.incrementErrorCount(callId, finalStatus) };
|
|
290
|
+
throw { code, message: errorMessage, status: finalStatus, statusText, name, responseData, duration: endpoint?.monitor?.incrementErrorCount(callId, finalStatus) };
|
|
247
291
|
}
|
|
248
292
|
let duration;
|
|
249
293
|
if (response.status >= 200 && response.status < 300) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.33",
|
|
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": {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// sys_compress_context.js
|
|
2
|
+
// Compresses chat history containing tool calls and results while preserving critical information
|
|
3
|
+
// Used when context window is approaching limits to prevent 400 errors
|
|
4
|
+
|
|
5
|
+
import { Prompt } from '../../../server/prompt.js';
|
|
6
|
+
import logger from '../../../lib/logger.js';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
prompt: [
|
|
10
|
+
new Prompt({ messages: [
|
|
11
|
+
{
|
|
12
|
+
"role": "system",
|
|
13
|
+
"content": `You are an AI assistant that compresses conversation history while preserving critical information needed to continue the task.
|
|
14
|
+
|
|
15
|
+
Your job is to create a concise summary from tool calls and their results that:
|
|
16
|
+
|
|
17
|
+
1. **Preserves the research intent** - What was the user trying to find or accomplish?
|
|
18
|
+
|
|
19
|
+
2. **Summarizes tool calls and results** - For each tool call, include:
|
|
20
|
+
- The tool name and purpose
|
|
21
|
+
- Key results and findings (especially data, facts, file names, URLs)
|
|
22
|
+
- Important decisions made based on results
|
|
23
|
+
|
|
24
|
+
3. **CRITICAL: Preserve exact data** - You MUST preserve:
|
|
25
|
+
- Exact numbers, percentages, dollar amounts, dates, statistics
|
|
26
|
+
- ALL URLs exactly as written (never truncate URLs)
|
|
27
|
+
- Source citations (publication names, report numbers, author names)
|
|
28
|
+
- File names and paths exactly as they appear
|
|
29
|
+
|
|
30
|
+
4. **Maintain citation integrity** - Preserve file names, URLs, source references
|
|
31
|
+
|
|
32
|
+
5. **Keep it actionable** - What has been accomplished? What still needs to be done?
|
|
33
|
+
|
|
34
|
+
Format as a clear narrative that another AI agent could read to understand the research progress.
|
|
35
|
+
|
|
36
|
+
Be concise but comprehensive. When in doubt, preserve more detail for numbers, URLs, and citations.`
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"role": "user",
|
|
40
|
+
"content": `Please compress the following tool calls and results into a concise research summary:
|
|
41
|
+
|
|
42
|
+
{{{researchContent}}}
|
|
43
|
+
|
|
44
|
+
Provide a clear summary preserving all URLs, citations, and numerical data.`
|
|
45
|
+
}
|
|
46
|
+
]})
|
|
47
|
+
],
|
|
48
|
+
inputParameters: {
|
|
49
|
+
researchContent: '',
|
|
50
|
+
language: "English",
|
|
51
|
+
},
|
|
52
|
+
model: 'gemini-flash-3-vision',
|
|
53
|
+
useInputChunking: false,
|
|
54
|
+
enableDuplicateRequests: false,
|
|
55
|
+
timeout: 120,
|
|
56
|
+
|
|
57
|
+
executePathway: async ({args, runAllPrompts}) => {
|
|
58
|
+
try {
|
|
59
|
+
// Extract URLs for validation
|
|
60
|
+
const urls = new Set();
|
|
61
|
+
const content = args.researchContent || '';
|
|
62
|
+
const urlMatches = content.match(/https?:\/\/[^\s\)\]"']+/g);
|
|
63
|
+
if (urlMatches) urlMatches.forEach(url => urls.add(url));
|
|
64
|
+
|
|
65
|
+
const result = await runAllPrompts(args);
|
|
66
|
+
|
|
67
|
+
// Validate URL preservation
|
|
68
|
+
if (urls.size > 0 && typeof result === 'string') {
|
|
69
|
+
const preserved = Array.from(urls).filter(url => result.includes(url));
|
|
70
|
+
const rate = preserved.length / urls.size;
|
|
71
|
+
if (rate < 0.7) {
|
|
72
|
+
logger.warn(`Context compression preserved only ${(rate * 100).toFixed(0)}% of URLs (${preserved.length}/${urls.size})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error(`Error in sys_compress_context: ${error.message}`);
|
|
79
|
+
return `[Compression failed] Previous tool calls have been summarized to save context space.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// sys_entity_agent.js
|
|
2
2
|
// Agentic extension of the entity system that uses OpenAI's tool calling API
|
|
3
3
|
const MAX_TOOL_CALLS = 50;
|
|
4
|
+
const TOOL_TIMEOUT_MS = 120000; // 2 minute timeout per tool call
|
|
5
|
+
const MAX_TOOL_RESULT_LENGTH = 50000; // Truncate oversized tool results to prevent context overflow
|
|
4
6
|
|
|
5
|
-
import { callPathway, callTool, say, sendToolStart, sendToolFinish } from '../../../lib/pathwayTools.js';
|
|
7
|
+
import { callPathway, callTool, say, sendToolStart, sendToolFinish, withTimeout } from '../../../lib/pathwayTools.js';
|
|
8
|
+
import { publishRequestProgress } from '../../../lib/redisSubscription.js';
|
|
6
9
|
import logger from '../../../lib/logger.js';
|
|
7
10
|
import { config } from '../../../config.js';
|
|
8
11
|
import { syncAndStripFilesFromChatHistory } from '../../../lib/fileUtils.js';
|
|
@@ -138,10 +141,13 @@ export default {
|
|
|
138
141
|
// Create an isolated copy of messages for this tool
|
|
139
142
|
const toolMessages = JSON.parse(JSON.stringify(preToolCallMessages));
|
|
140
143
|
|
|
141
|
-
// Get the tool definition to check for icon
|
|
144
|
+
// Get the tool definition to check for icon and timeout
|
|
142
145
|
const toolDefinition = entityTools[toolFunction]?.definition;
|
|
143
146
|
const toolIcon = toolDefinition?.icon || '🛠️';
|
|
144
147
|
|
|
148
|
+
// Get timeout from tool definition or use default
|
|
149
|
+
const toolTimeout = toolDefinition?.timeout || TOOL_TIMEOUT_MS;
|
|
150
|
+
|
|
145
151
|
// Get the user message for the tool
|
|
146
152
|
const toolUserMessage = toolArgs.userMessage || `Executing tool: ${toolCall.function.name}`;
|
|
147
153
|
|
|
@@ -155,13 +161,19 @@ export default {
|
|
|
155
161
|
// Continue execution even if start message fails
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
// Wrap tool call with timeout to prevent hanging
|
|
165
|
+
const toolResult = await withTimeout(
|
|
166
|
+
callTool(toolFunction, {
|
|
167
|
+
...args,
|
|
168
|
+
...toolArgs,
|
|
169
|
+
toolFunction,
|
|
170
|
+
chatHistory: toolMessages,
|
|
171
|
+
stream: false,
|
|
172
|
+
useMemory: false // Disable memory synthesis for tool calls
|
|
173
|
+
}, entityTools, pathwayResolver),
|
|
174
|
+
toolTimeout,
|
|
175
|
+
`Tool ${toolCall.function.name} timed out after ${toolTimeout / 1000}s`
|
|
176
|
+
);
|
|
165
177
|
|
|
166
178
|
// Tool calls and results need to be paired together in the message history
|
|
167
179
|
// Add the tool call to the isolated message history
|
|
@@ -311,7 +323,9 @@ export default {
|
|
|
311
323
|
messages: toolMessages
|
|
312
324
|
};
|
|
313
325
|
} catch (error) {
|
|
314
|
-
|
|
326
|
+
// Detect if this is a timeout error for clearer logging
|
|
327
|
+
const isTimeout = error.message?.includes('timed out');
|
|
328
|
+
logger.error(`${isTimeout ? 'Timeout' : 'Error'} executing tool ${toolCall?.function?.name || 'unknown'}: ${error.message}`);
|
|
315
329
|
|
|
316
330
|
// Send tool finish message (error)
|
|
317
331
|
// Get requestId and toolCallId if not already defined (in case error occurred before they were set)
|
|
@@ -412,7 +426,17 @@ export default {
|
|
|
412
426
|
);
|
|
413
427
|
}
|
|
414
428
|
|
|
415
|
-
|
|
429
|
+
// Truncate oversized tool results to prevent context overflow
|
|
430
|
+
args.chatHistory = finalMessages.map(msg => {
|
|
431
|
+
if (msg.role === 'tool' && msg.content && msg.content.length > MAX_TOOL_RESULT_LENGTH) {
|
|
432
|
+
logger.warn(`Truncating oversized tool result (${msg.content.length} chars) for ${msg.name || 'unknown tool'}`);
|
|
433
|
+
return {
|
|
434
|
+
...msg,
|
|
435
|
+
content: msg.content.substring(0, MAX_TOOL_RESULT_LENGTH) + '\n\n[Content truncated due to length]'
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return msg;
|
|
439
|
+
});
|
|
416
440
|
|
|
417
441
|
// clear any accumulated pathwayResolver errors from the tools
|
|
418
442
|
pathwayResolver.errors = [];
|
|
@@ -429,16 +453,27 @@ export default {
|
|
|
429
453
|
|
|
430
454
|
// Check if promptAndParse returned null (model call failed)
|
|
431
455
|
if (!result) {
|
|
432
|
-
const errorMessage = pathwayResolver.errors.length > 0
|
|
456
|
+
const errorMessage = pathwayResolver.errors.length > 0
|
|
433
457
|
? pathwayResolver.errors.join(', ')
|
|
434
458
|
: 'Model request failed - no response received';
|
|
435
459
|
logger.error(`promptAndParse returned null during tool callback: ${errorMessage}`);
|
|
436
460
|
const errorResponse = await generateErrorResponse(new Error(errorMessage), args, pathwayResolver);
|
|
437
461
|
// Ensure errors are cleared before returning
|
|
438
462
|
pathwayResolver.errors = [];
|
|
463
|
+
|
|
464
|
+
// In streaming mode, the toolCallback is invoked fire-and-forget by the plugin,
|
|
465
|
+
// so we must stream the error response directly to the client and close the stream
|
|
466
|
+
const requestId = pathwayResolver.rootRequestId || pathwayResolver.requestId;
|
|
467
|
+
publishRequestProgress({
|
|
468
|
+
requestId,
|
|
469
|
+
progress: 1,
|
|
470
|
+
data: JSON.stringify(errorResponse),
|
|
471
|
+
info: JSON.stringify(pathwayResolver.pathwayResultData || {}),
|
|
472
|
+
error: ''
|
|
473
|
+
});
|
|
439
474
|
return errorResponse;
|
|
440
475
|
}
|
|
441
|
-
|
|
476
|
+
|
|
442
477
|
return result;
|
|
443
478
|
} catch (parseError) {
|
|
444
479
|
// If promptAndParse fails, generate error response instead of re-throwing
|
|
@@ -446,6 +481,17 @@ export default {
|
|
|
446
481
|
const errorResponse = await generateErrorResponse(parseError, args, pathwayResolver);
|
|
447
482
|
// Ensure errors are cleared before returning
|
|
448
483
|
pathwayResolver.errors = [];
|
|
484
|
+
|
|
485
|
+
// In streaming mode, the toolCallback is invoked fire-and-forget by the plugin,
|
|
486
|
+
// so we must stream the error response directly to the client and close the stream
|
|
487
|
+
const requestId = pathwayResolver.rootRequestId || pathwayResolver.requestId;
|
|
488
|
+
publishRequestProgress({
|
|
489
|
+
requestId,
|
|
490
|
+
progress: 1,
|
|
491
|
+
data: JSON.stringify(errorResponse),
|
|
492
|
+
info: JSON.stringify(pathwayResolver.pathwayResultData || {}),
|
|
493
|
+
error: ''
|
|
494
|
+
});
|
|
449
495
|
return errorResponse;
|
|
450
496
|
}
|
|
451
497
|
}
|
|
@@ -600,10 +646,10 @@ export default {
|
|
|
600
646
|
|
|
601
647
|
// truncate the chat history in case there is really long content
|
|
602
648
|
const truncatedChatHistory = resolver.modelExecutor.plugin.truncateMessagesToTargetLength(args.chatHistory, null, 1000);
|
|
603
|
-
|
|
649
|
+
|
|
604
650
|
// Asynchronously manage memory for this context
|
|
605
651
|
if (args.aiMemorySelfModify && args.useMemory) {
|
|
606
|
-
callPathway('sys_memory_manager', { ...args, chatHistory: truncatedChatHistory, stream: false })
|
|
652
|
+
callPathway('sys_memory_manager', { ...args, chatHistory: truncatedChatHistory, stream: false })
|
|
607
653
|
.catch(error => logger.error(error?.message || "Error in sys_memory_manager pathway"));
|
|
608
654
|
}
|
|
609
655
|
|
|
@@ -632,6 +678,10 @@ export default {
|
|
|
632
678
|
memoryLookupRequired = false;
|
|
633
679
|
}
|
|
634
680
|
|
|
681
|
+
// Update pathwayResolver.args with stripped chatHistory
|
|
682
|
+
// This ensures toolCallback receives the processed history, not the original
|
|
683
|
+
pathwayResolver.args = {...args};
|
|
684
|
+
|
|
635
685
|
try {
|
|
636
686
|
let currentMessages = JSON.parse(JSON.stringify(args.chatHistory));
|
|
637
687
|
|
package/server/modelExecutor.js
CHANGED
|
@@ -26,6 +26,7 @@ import Gemini3ImagePlugin from './plugins/gemini3ImagePlugin.js';
|
|
|
26
26
|
import Gemini3ReasoningVisionPlugin from './plugins/gemini3ReasoningVisionPlugin.js';
|
|
27
27
|
import Claude3VertexPlugin from './plugins/claude3VertexPlugin.js';
|
|
28
28
|
import Claude4VertexPlugin from './plugins/claude4VertexPlugin.js';
|
|
29
|
+
import ClaudeAnthropicPlugin from './plugins/claudeAnthropicPlugin.js';
|
|
29
30
|
import NeuralSpacePlugin from './plugins/neuralSpacePlugin.js';
|
|
30
31
|
import RunwareAiPlugin from './plugins/runwareAiPlugin.js';
|
|
31
32
|
import ReplicateApiPlugin from './plugins/replicateApiPlugin.js';
|
|
@@ -122,6 +123,9 @@ class ModelExecutor {
|
|
|
122
123
|
case 'CLAUDE-4-VERTEX':
|
|
123
124
|
plugin = new Claude4VertexPlugin(pathway, model);
|
|
124
125
|
break;
|
|
126
|
+
case 'CLAUDE-ANTHROPIC':
|
|
127
|
+
plugin = new ClaudeAnthropicPlugin(pathway, model);
|
|
128
|
+
break;
|
|
125
129
|
case 'RUNWARE-AI':
|
|
126
130
|
plugin = new RunwareAiPlugin(pathway, model);
|
|
127
131
|
break;
|