@aj-archipelago/cortex 1.4.31 → 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 +13 -6
- package/lib/pathwayTools.js +92 -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 +106 -18
- package/pathways/transcribe_gemini.js +1 -1
- package/server/clientToolCallbacks.js +241 -0
- package/server/executeWorkspace.js +7 -0
- package/server/graphql.js +3 -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 +67 -8
- package/server/plugins/grokResponsesPlugin.js +2 -0
- package/server/plugins/openAiVisionPlugin.js +4 -2
- package/server/resolver.js +37 -2
- package/test.log +42834 -0
- package/tests/integration/clientToolCallbacks.test.js +161 -0
- package/tests/integration/features/tools/fileCollection.test.js +1 -1
- package/tests/integration/rest/vendors/claude_anthropic_direct.test.js +197 -0
- package/tests/unit/plugins/claudeAnthropicPlugin.test.js +236 -0
- package/tests/unit/plugins/multimodal_conversion.test.js +16 -6
- package/tests/unit/sys_entity_agent_errors.test.js +792 -0
- package/RELEASE_NOTES_20251231_103631.md +0 -15
- package/RELEASE_NOTES_20251231_110946.md +0 -5
|
@@ -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';
|
|
@@ -79,7 +82,13 @@ export default {
|
|
|
79
82
|
entityId: ``,
|
|
80
83
|
researchMode: false,
|
|
81
84
|
userInfo: '',
|
|
82
|
-
model: 'oai-gpt41'
|
|
85
|
+
model: 'oai-gpt41',
|
|
86
|
+
contextKey: ``,
|
|
87
|
+
clientSideTools: {
|
|
88
|
+
type: 'array',
|
|
89
|
+
items: { type: 'object' },
|
|
90
|
+
default: []
|
|
91
|
+
}
|
|
83
92
|
},
|
|
84
93
|
timeout: 600,
|
|
85
94
|
|
|
@@ -132,10 +141,13 @@ export default {
|
|
|
132
141
|
// Create an isolated copy of messages for this tool
|
|
133
142
|
const toolMessages = JSON.parse(JSON.stringify(preToolCallMessages));
|
|
134
143
|
|
|
135
|
-
// Get the tool definition to check for icon
|
|
144
|
+
// Get the tool definition to check for icon and timeout
|
|
136
145
|
const toolDefinition = entityTools[toolFunction]?.definition;
|
|
137
146
|
const toolIcon = toolDefinition?.icon || '🛠️';
|
|
138
147
|
|
|
148
|
+
// Get timeout from tool definition or use default
|
|
149
|
+
const toolTimeout = toolDefinition?.timeout || TOOL_TIMEOUT_MS;
|
|
150
|
+
|
|
139
151
|
// Get the user message for the tool
|
|
140
152
|
const toolUserMessage = toolArgs.userMessage || `Executing tool: ${toolCall.function.name}`;
|
|
141
153
|
|
|
@@ -149,13 +161,19 @@ export default {
|
|
|
149
161
|
// Continue execution even if start message fails
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
);
|
|
159
177
|
|
|
160
178
|
// Tool calls and results need to be paired together in the message history
|
|
161
179
|
// Add the tool call to the isolated message history
|
|
@@ -305,7 +323,9 @@ export default {
|
|
|
305
323
|
messages: toolMessages
|
|
306
324
|
};
|
|
307
325
|
} catch (error) {
|
|
308
|
-
|
|
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}`);
|
|
309
329
|
|
|
310
330
|
// Send tool finish message (error)
|
|
311
331
|
// Get requestId and toolCallId if not already defined (in case error occurred before they were set)
|
|
@@ -406,7 +426,17 @@ export default {
|
|
|
406
426
|
);
|
|
407
427
|
}
|
|
408
428
|
|
|
409
|
-
|
|
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
|
+
});
|
|
410
440
|
|
|
411
441
|
// clear any accumulated pathwayResolver errors from the tools
|
|
412
442
|
pathwayResolver.errors = [];
|
|
@@ -423,16 +453,27 @@ export default {
|
|
|
423
453
|
|
|
424
454
|
// Check if promptAndParse returned null (model call failed)
|
|
425
455
|
if (!result) {
|
|
426
|
-
const errorMessage = pathwayResolver.errors.length > 0
|
|
456
|
+
const errorMessage = pathwayResolver.errors.length > 0
|
|
427
457
|
? pathwayResolver.errors.join(', ')
|
|
428
458
|
: 'Model request failed - no response received';
|
|
429
459
|
logger.error(`promptAndParse returned null during tool callback: ${errorMessage}`);
|
|
430
460
|
const errorResponse = await generateErrorResponse(new Error(errorMessage), args, pathwayResolver);
|
|
431
461
|
// Ensure errors are cleared before returning
|
|
432
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
|
+
});
|
|
433
474
|
return errorResponse;
|
|
434
475
|
}
|
|
435
|
-
|
|
476
|
+
|
|
436
477
|
return result;
|
|
437
478
|
} catch (parseError) {
|
|
438
479
|
// If promptAndParse fails, generate error response instead of re-throwing
|
|
@@ -440,6 +481,17 @@ export default {
|
|
|
440
481
|
const errorResponse = await generateErrorResponse(parseError, args, pathwayResolver);
|
|
441
482
|
// Ensure errors are cleared before returning
|
|
442
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
|
+
});
|
|
443
495
|
return errorResponse;
|
|
444
496
|
}
|
|
445
497
|
}
|
|
@@ -449,16 +501,48 @@ export default {
|
|
|
449
501
|
let pathwayResolver = resolver;
|
|
450
502
|
|
|
451
503
|
// Load input parameters and information into args
|
|
452
|
-
|
|
504
|
+
let { entityId, voiceResponse, aiMemorySelfModify, chatId, researchMode, clientSideTools } = { ...pathwayResolver.pathway.inputParameters, ...args };
|
|
505
|
+
|
|
506
|
+
// Parse clientSideTools if it's a string (from GraphQL)
|
|
507
|
+
if (typeof clientSideTools === 'string') {
|
|
508
|
+
try {
|
|
509
|
+
clientSideTools = JSON.parse(clientSideTools);
|
|
510
|
+
} catch (e) {
|
|
511
|
+
logger.error(`Failed to parse clientSideTools: ${e.message}`);
|
|
512
|
+
clientSideTools = [];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
453
515
|
|
|
454
516
|
const entityConfig = loadEntityConfig(entityId);
|
|
455
|
-
|
|
517
|
+
let { entityTools, entityToolsOpenAiFormat } = getToolsForEntity(entityConfig);
|
|
456
518
|
const { name: entityName, instructions: entityInstructions } = entityConfig || {};
|
|
457
519
|
|
|
458
520
|
// Determine useMemory: entityConfig.useMemory === false is a hard disable (entity can't use memory)
|
|
459
521
|
// Otherwise args.useMemory can disable it, default true
|
|
460
522
|
args.useMemory = entityConfig?.useMemory === false ? false : (args.useMemory ?? true);
|
|
461
523
|
|
|
524
|
+
// Add client-side tools from the caller
|
|
525
|
+
if (clientSideTools && Array.isArray(clientSideTools) && clientSideTools.length > 0) {
|
|
526
|
+
logger.info(`Adding ${clientSideTools.length} client-side tools from caller`);
|
|
527
|
+
clientSideTools.forEach(tool => {
|
|
528
|
+
const toolName = tool.function?.name?.toLowerCase();
|
|
529
|
+
if (toolName) {
|
|
530
|
+
// Mark as client-side tool and add to available tools
|
|
531
|
+
entityTools[toolName] = {
|
|
532
|
+
definition: {
|
|
533
|
+
...tool,
|
|
534
|
+
clientSide: true, // Mark it as client-side
|
|
535
|
+
icon: tool.icon || '📱'
|
|
536
|
+
},
|
|
537
|
+
pathwayName: 'client_side_execution', // Placeholder pathway
|
|
538
|
+
clientSide: true
|
|
539
|
+
};
|
|
540
|
+
entityToolsOpenAiFormat.push(tool);
|
|
541
|
+
logger.info(`Registered client-side tool: ${toolName}`);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
462
546
|
// Initialize chat history if needed
|
|
463
547
|
if (!args.chatHistory || args.chatHistory.length === 0) {
|
|
464
548
|
args.chatHistory = [];
|
|
@@ -562,10 +646,10 @@ export default {
|
|
|
562
646
|
|
|
563
647
|
// truncate the chat history in case there is really long content
|
|
564
648
|
const truncatedChatHistory = resolver.modelExecutor.plugin.truncateMessagesToTargetLength(args.chatHistory, null, 1000);
|
|
565
|
-
|
|
649
|
+
|
|
566
650
|
// Asynchronously manage memory for this context
|
|
567
651
|
if (args.aiMemorySelfModify && args.useMemory) {
|
|
568
|
-
callPathway('sys_memory_manager', { ...args, chatHistory: truncatedChatHistory, stream: false })
|
|
652
|
+
callPathway('sys_memory_manager', { ...args, chatHistory: truncatedChatHistory, stream: false })
|
|
569
653
|
.catch(error => logger.error(error?.message || "Error in sys_memory_manager pathway"));
|
|
570
654
|
}
|
|
571
655
|
|
|
@@ -594,6 +678,10 @@ export default {
|
|
|
594
678
|
memoryLookupRequired = false;
|
|
595
679
|
}
|
|
596
680
|
|
|
681
|
+
// Update pathwayResolver.args with stripped chatHistory
|
|
682
|
+
// This ensures toolCallback receives the processed history, not the original
|
|
683
|
+
pathwayResolver.args = {...args};
|
|
684
|
+
|
|
597
685
|
try {
|
|
598
686
|
let currentMessages = JSON.parse(JSON.stringify(args.chatHistory));
|
|
599
687
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// clientToolCallbacks.js
|
|
2
|
+
// Storage and management for pending client-side tool callbacks
|
|
3
|
+
|
|
4
|
+
import logger from '../lib/logger.js';
|
|
5
|
+
import Redis from 'ioredis';
|
|
6
|
+
import { config } from '../config.js';
|
|
7
|
+
|
|
8
|
+
// Map to store pending client tool callbacks
|
|
9
|
+
// Key: toolCallbackId, Value: { resolve, reject, timeout, requestId }
|
|
10
|
+
const pendingCallbacks = new Map();
|
|
11
|
+
|
|
12
|
+
// Default timeout for client tool responses (5 minutes)
|
|
13
|
+
// Increased from 60s to 5min to accommodate longer operations like CreateApplet
|
|
14
|
+
const DEFAULT_TIMEOUT = 300000;
|
|
15
|
+
|
|
16
|
+
// Redis setup for cross-instance communication
|
|
17
|
+
const connectionString = config.get('storageConnectionString');
|
|
18
|
+
const clientToolCallbackChannel = 'clientToolCallbacks';
|
|
19
|
+
|
|
20
|
+
let subscriptionClient;
|
|
21
|
+
let publisherClient;
|
|
22
|
+
|
|
23
|
+
if (connectionString) {
|
|
24
|
+
logger.info(`Setting up Redis pub/sub for client tool callbacks on channel: ${clientToolCallbackChannel}`);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
subscriptionClient = new Redis(connectionString);
|
|
28
|
+
subscriptionClient.on('error', (error) => {
|
|
29
|
+
logger.error(`Redis subscriptionClient error (clientToolCallbacks): ${error}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
subscriptionClient.on('connect', () => {
|
|
33
|
+
subscriptionClient.subscribe(clientToolCallbackChannel, (error) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
logger.error(`Error subscribing to Redis channel ${clientToolCallbackChannel}: ${error}`);
|
|
36
|
+
} else {
|
|
37
|
+
logger.info(`Subscribed to client tool callback channel: ${clientToolCallbackChannel}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
subscriptionClient.on('message', (channel, message) => {
|
|
43
|
+
if (channel === clientToolCallbackChannel) {
|
|
44
|
+
try {
|
|
45
|
+
const { toolCallbackId, result } = JSON.parse(message);
|
|
46
|
+
logger.debug(`Received client tool callback via Redis: ${toolCallbackId}`);
|
|
47
|
+
|
|
48
|
+
// Try to resolve it locally (will only work if this instance has the pending callback)
|
|
49
|
+
resolveClientToolCallbackLocal(toolCallbackId, result);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error(`Error processing client tool callback from Redis: ${error}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(`Redis connection error (clientToolCallbacks): ${error}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
publisherClient = new Redis(connectionString);
|
|
61
|
+
publisherClient.on('error', (error) => {
|
|
62
|
+
logger.error(`Redis publisherClient error (clientToolCallbacks): ${error}`);
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.error(`Redis connection error (clientToolCallbacks): ${error}`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
logger.info('No Redis connection configured. Client tool callbacks will only work on single instance.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Register a pending client tool callback
|
|
73
|
+
* @param {string} toolCallbackId - Unique ID for this tool call
|
|
74
|
+
* @param {string} requestId - The request ID for logging/tracking
|
|
75
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
76
|
+
* @returns {Promise} Promise that resolves when client submits the result
|
|
77
|
+
*/
|
|
78
|
+
export function waitForClientToolResult(toolCallbackId, requestId, timeoutMs = DEFAULT_TIMEOUT) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
// Set up timeout
|
|
81
|
+
const timeout = setTimeout(() => {
|
|
82
|
+
pendingCallbacks.delete(toolCallbackId);
|
|
83
|
+
logger.error(`Client tool callback timeout for ${toolCallbackId} (requestId: ${requestId})`);
|
|
84
|
+
reject(new Error(`Client tool execution timeout after ${timeoutMs}ms`));
|
|
85
|
+
}, timeoutMs);
|
|
86
|
+
|
|
87
|
+
// Store the callback
|
|
88
|
+
pendingCallbacks.set(toolCallbackId, {
|
|
89
|
+
resolve,
|
|
90
|
+
reject,
|
|
91
|
+
timeout,
|
|
92
|
+
requestId,
|
|
93
|
+
createdAt: Date.now()
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
logger.info(`Registered client tool callback: ${toolCallbackId} (requestId: ${requestId})`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a pending client tool callback locally (internal use)
|
|
102
|
+
* @param {string} toolCallbackId - The tool callback ID
|
|
103
|
+
* @param {object} result - The result from the client
|
|
104
|
+
* @returns {boolean} True if callback was found and resolved
|
|
105
|
+
*/
|
|
106
|
+
function resolveClientToolCallbackLocal(toolCallbackId, result) {
|
|
107
|
+
const callback = pendingCallbacks.get(toolCallbackId);
|
|
108
|
+
|
|
109
|
+
if (!callback) {
|
|
110
|
+
// This is normal in a multi-instance setup - the callback might be on another instance
|
|
111
|
+
logger.debug(`No pending callback found for toolCallbackId: ${toolCallbackId} (may be on another instance)`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Clear the timeout
|
|
116
|
+
clearTimeout(callback.timeout);
|
|
117
|
+
|
|
118
|
+
// Remove from pending
|
|
119
|
+
pendingCallbacks.delete(toolCallbackId);
|
|
120
|
+
|
|
121
|
+
logger.info(`Resolved client tool callback: ${toolCallbackId} (requestId: ${callback.requestId})`);
|
|
122
|
+
|
|
123
|
+
// Resolve the promise
|
|
124
|
+
callback.resolve(result);
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolve a pending client tool callback with the result
|
|
131
|
+
* This function publishes to Redis so all instances can attempt to resolve
|
|
132
|
+
* @param {string} toolCallbackId - The tool callback ID
|
|
133
|
+
* @param {object} result - The result from the client
|
|
134
|
+
* @returns {Promise<boolean>} True if callback was published/resolved
|
|
135
|
+
*/
|
|
136
|
+
export async function resolveClientToolCallback(toolCallbackId, result) {
|
|
137
|
+
if (publisherClient) {
|
|
138
|
+
// Publish to Redis so all instances can try to resolve
|
|
139
|
+
try {
|
|
140
|
+
const message = JSON.stringify({ toolCallbackId, result });
|
|
141
|
+
logger.debug(`Publishing client tool callback to Redis: ${toolCallbackId}`);
|
|
142
|
+
await publisherClient.publish(clientToolCallbackChannel, message);
|
|
143
|
+
return true;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error(`Error publishing client tool callback to Redis: ${error}`);
|
|
146
|
+
// Fall back to local resolution
|
|
147
|
+
return resolveClientToolCallbackLocal(toolCallbackId, result);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// No Redis, resolve locally
|
|
151
|
+
return resolveClientToolCallbackLocal(toolCallbackId, result);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reject a pending client tool callback locally (internal use)
|
|
157
|
+
* @param {string} toolCallbackId - The tool callback ID
|
|
158
|
+
* @param {Error} error - The error
|
|
159
|
+
* @returns {boolean} True if callback was found and rejected
|
|
160
|
+
*/
|
|
161
|
+
function rejectClientToolCallbackLocal(toolCallbackId, error) {
|
|
162
|
+
const callback = pendingCallbacks.get(toolCallbackId);
|
|
163
|
+
|
|
164
|
+
if (!callback) {
|
|
165
|
+
logger.debug(`No pending callback found for toolCallbackId: ${toolCallbackId} (may be on another instance)`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Clear the timeout
|
|
170
|
+
clearTimeout(callback.timeout);
|
|
171
|
+
|
|
172
|
+
// Remove from pending
|
|
173
|
+
pendingCallbacks.delete(toolCallbackId);
|
|
174
|
+
|
|
175
|
+
logger.info(`Rejected client tool callback: ${toolCallbackId} (requestId: ${callback.requestId})`);
|
|
176
|
+
|
|
177
|
+
// Reject the promise
|
|
178
|
+
callback.reject(error);
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reject a pending client tool callback with an error
|
|
185
|
+
* This function publishes to Redis so all instances can attempt to reject
|
|
186
|
+
* @param {string} toolCallbackId - The tool callback ID
|
|
187
|
+
* @param {Error} error - The error
|
|
188
|
+
* @returns {Promise<boolean>} True if callback was published/rejected
|
|
189
|
+
*/
|
|
190
|
+
export async function rejectClientToolCallback(toolCallbackId, error) {
|
|
191
|
+
if (publisherClient) {
|
|
192
|
+
// Publish to Redis so all instances can try to reject
|
|
193
|
+
try {
|
|
194
|
+
const message = JSON.stringify({
|
|
195
|
+
toolCallbackId,
|
|
196
|
+
result: { success: false, error: error.message || error.toString() }
|
|
197
|
+
});
|
|
198
|
+
logger.debug(`Publishing client tool callback rejection to Redis: ${toolCallbackId}`);
|
|
199
|
+
await publisherClient.publish(clientToolCallbackChannel, message);
|
|
200
|
+
return true;
|
|
201
|
+
} catch (publishError) {
|
|
202
|
+
logger.error(`Error publishing client tool callback rejection to Redis: ${publishError}`);
|
|
203
|
+
// Fall back to local rejection
|
|
204
|
+
return rejectClientToolCallbackLocal(toolCallbackId, error);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// No Redis, reject locally
|
|
208
|
+
return rejectClientToolCallbackLocal(toolCallbackId, error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get count of pending callbacks (for monitoring)
|
|
214
|
+
*/
|
|
215
|
+
export function getPendingCallbackCount() {
|
|
216
|
+
return pendingCallbacks.size;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clean up old callbacks (for maintenance)
|
|
221
|
+
*/
|
|
222
|
+
export function cleanupOldCallbacks(maxAgeMs = 120000) {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
let cleaned = 0;
|
|
225
|
+
|
|
226
|
+
for (const [id, callback] of pendingCallbacks.entries()) {
|
|
227
|
+
if (now - callback.createdAt > maxAgeMs) {
|
|
228
|
+
clearTimeout(callback.timeout);
|
|
229
|
+
pendingCallbacks.delete(id);
|
|
230
|
+
callback.reject(new Error('Callback expired during cleanup'));
|
|
231
|
+
cleaned++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (cleaned > 0) {
|
|
236
|
+
logger.info(`Cleaned up ${cleaned} old client tool callbacks`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return cleaned;
|
|
240
|
+
}
|
|
241
|
+
|
|
@@ -80,6 +80,7 @@ const resolveAndAddFileContent = async (pathways, pathwayArgs, requestId, config
|
|
|
80
80
|
|
|
81
81
|
// Helper function to execute pathway with cortex pathway name or fallback to legacy
|
|
82
82
|
const executePathwayWithFallback = async (pathway, pathwayArgs, contextValue, info, requestId, originalPrompt = null, config) => {
|
|
83
|
+
// Extract cortexPathwayName from originalPrompt (could be original object or transformed Prompt object)
|
|
83
84
|
const cortexPathwayName = (originalPrompt && typeof originalPrompt === 'object' && originalPrompt.cortexPathwayName)
|
|
84
85
|
? originalPrompt.cortexPathwayName
|
|
85
86
|
: null;
|
|
@@ -100,6 +101,12 @@ const executePathwayWithFallback = async (pathway, pathwayArgs, contextValue, in
|
|
|
100
101
|
// Remove old aiStyle parameter (no longer used)
|
|
101
102
|
delete cortexArgs.aiStyle;
|
|
102
103
|
|
|
104
|
+
// Extract researchMode from originalPrompt if not already in pathwayArgs
|
|
105
|
+
// originalPrompt could be the original object from JSON or a transformed Prompt object
|
|
106
|
+
if (originalPrompt && typeof originalPrompt === 'object' && originalPrompt.researchMode !== undefined) {
|
|
107
|
+
cortexArgs.researchMode = originalPrompt.researchMode;
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
// Transform context parameters to agentContext array format (only if agentContext not already provided)
|
|
104
111
|
if (!cortexArgs.agentContext && (cortexArgs.contextId || cortexArgs.contextKey || cortexArgs.altContextId || cortexArgs.altContextKey)) {
|
|
105
112
|
const agentContext = [];
|
package/server/graphql.js
CHANGED
|
@@ -20,7 +20,7 @@ import logger from '../lib/logger.js';
|
|
|
20
20
|
import { buildModelEndpoints } from '../lib/requestExecutor.js';
|
|
21
21
|
import { startTestServer } from '../tests/helpers/server.js';
|
|
22
22
|
import { requestState } from './requestState.js';
|
|
23
|
-
import { cancelRequestResolver } from './resolver.js';
|
|
23
|
+
import { cancelRequestResolver, submitClientToolResultResolver } from './resolver.js';
|
|
24
24
|
import subscriptions from './subscriptions.js';
|
|
25
25
|
import { getMessageTypeDefs } from './typeDef.js';
|
|
26
26
|
import { buildRestEndpoints } from './rest.js';
|
|
@@ -79,6 +79,7 @@ const getTypedefs = (pathways, pathwayManager) => {
|
|
|
79
79
|
|
|
80
80
|
type Mutation {
|
|
81
81
|
cancelRequest(requestId: String!): Boolean
|
|
82
|
+
submitClientToolResult(requestId: String!, toolCallbackId: String!, result: String!, success: Boolean!): Boolean
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
${getExecuteWorkspaceTypeDefs()}
|
|
@@ -140,6 +141,7 @@ const getResolvers = (config, pathways, pathwayManager) => {
|
|
|
140
141
|
},
|
|
141
142
|
Mutation: {
|
|
142
143
|
'cancelRequest': cancelRequestResolver,
|
|
144
|
+
'submitClientToolResult': submitClientToolResultResolver,
|
|
143
145
|
...mutationResolvers,
|
|
144
146
|
...pathwayManagerResolvers.Mutation
|
|
145
147
|
},
|
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;
|