@juspay/neurolink 9.19.1 → 9.21.0
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/CHANGELOG.md +12 -0
- package/dist/context/stages/slidingWindowTruncator.js +31 -17
- package/dist/core/redisConversationMemoryManager.d.ts +11 -1
- package/dist/core/redisConversationMemoryManager.js +87 -0
- package/dist/lib/context/stages/slidingWindowTruncator.js +31 -17
- package/dist/lib/core/redisConversationMemoryManager.d.ts +11 -1
- package/dist/lib/core/redisConversationMemoryManager.js +87 -0
- package/dist/lib/neurolink.d.ts +20 -0
- package/dist/lib/neurolink.js +33 -0
- package/dist/lib/providers/googleAiStudio.d.ts +1 -1
- package/dist/lib/providers/googleAiStudio.js +12 -3
- package/dist/lib/providers/googleNativeGemini3.js +33 -2
- package/dist/lib/providers/googleVertex.js +11 -1
- package/dist/lib/types/conversation.d.ts +37 -0
- package/dist/neurolink.d.ts +20 -0
- package/dist/neurolink.js +33 -0
- package/dist/providers/googleAiStudio.d.ts +1 -1
- package/dist/providers/googleAiStudio.js +12 -3
- package/dist/providers/googleNativeGemini3.js +33 -2
- package/dist/providers/googleVertex.js +11 -1
- package/dist/types/conversation.d.ts +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [9.21.0](https://github.com/juspay/neurolink/compare/v9.20.0...v9.21.0) (2026-03-09)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **(reports-conversation):** Add support for report metaData in getUserAllSessionsHistory ([2273af0](https://github.com/juspay/neurolink/commit/2273af00f2089dba4f691f771b8e16a6a71274b5))
|
|
6
|
+
|
|
7
|
+
## [9.20.0](https://github.com/juspay/neurolink/compare/v9.19.1...v9.20.0) (2026-03-09)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **(landing):** redesign nervous system visualization with performance and mobile fixes ([a4c7e91](https://github.com/juspay/neurolink/commit/a4c7e91a22622b41821db09e17885a54b26c66aa))
|
|
12
|
+
|
|
1
13
|
## [9.19.1](https://github.com/juspay/neurolink/compare/v9.19.0...v9.19.1) (2026-03-07)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -11,10 +11,19 @@
|
|
|
11
11
|
* - Small conversation handling (BUG-005): for <= 4 messages, truncates
|
|
12
12
|
* message content proportionally instead of returning no-op.
|
|
13
13
|
*/
|
|
14
|
-
import { randomUUID } from "crypto";
|
|
15
14
|
import { estimateTokens, estimateMessagesTokens, truncateToTokenBudget, } from "../../utils/tokenEstimation.js";
|
|
16
15
|
import { logger } from "../../utils/logger.js";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
17
|
const TRUNCATION_MARKER_CONTENT = "[Earlier conversation history was truncated to fit within context limits]";
|
|
18
|
+
function validateRoleAlternation(messages) {
|
|
19
|
+
for (let i = 1; i < messages.length; i++) {
|
|
20
|
+
if (messages[i].role === messages[i - 1].role &&
|
|
21
|
+
messages[i].role !== "system") {
|
|
22
|
+
logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* For conversations with <= 4 messages that exceed token budget,
|
|
20
29
|
* truncate the CONTENT of the longest messages rather than removing messages.
|
|
@@ -122,18 +131,19 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
122
131
|
break;
|
|
123
132
|
}
|
|
124
133
|
const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
// Insert a dedicated system-role truncation marker with machine-readable
|
|
135
|
+
// metadata so effectiveHistory.ts can detect it via isTruncationMarker /
|
|
136
|
+
// truncationId and removeTruncationTags can rewind it.
|
|
137
|
+
const truncId = randomUUID();
|
|
138
|
+
const marker = {
|
|
139
|
+
id: `truncation-marker-${truncId}`,
|
|
140
|
+
role: "system",
|
|
128
141
|
content: TRUNCATION_MARKER_CONTENT,
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
isTruncationMarker: true,
|
|
143
|
+
truncationId: truncId,
|
|
131
144
|
};
|
|
132
|
-
const candidateMessages = [
|
|
133
|
-
|
|
134
|
-
truncationMarker,
|
|
135
|
-
...keptAfterTruncation,
|
|
136
|
-
];
|
|
145
|
+
const candidateMessages = [...firstPair, marker, ...keptAfterTruncation];
|
|
146
|
+
validateRoleAlternation(candidateMessages);
|
|
137
147
|
// If we have token targets, verify the result fits
|
|
138
148
|
if (config?.targetTokens) {
|
|
139
149
|
const candidateTokens = estimateMessagesTokens(candidateMessages, config.provider);
|
|
@@ -160,16 +170,20 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
160
170
|
const evenMaxRemove = maxRemove - (maxRemove % 2);
|
|
161
171
|
if (evenMaxRemove > 0) {
|
|
162
172
|
const keptMessages = remainingMessages.slice(evenMaxRemove);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
// Insert a dedicated system-role truncation marker (see iterative block above)
|
|
174
|
+
const fallbackTruncId = randomUUID();
|
|
175
|
+
const fallbackMarker = {
|
|
176
|
+
id: `truncation-marker-${fallbackTruncId}`,
|
|
177
|
+
role: "system",
|
|
166
178
|
content: TRUNCATION_MARKER_CONTENT,
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
isTruncationMarker: true,
|
|
180
|
+
truncationId: fallbackTruncId,
|
|
169
181
|
};
|
|
182
|
+
const fallbackMessages = [...firstPair, fallbackMarker, ...keptMessages];
|
|
183
|
+
validateRoleAlternation(fallbackMessages);
|
|
170
184
|
return {
|
|
171
185
|
truncated: true,
|
|
172
|
-
messages:
|
|
186
|
+
messages: fallbackMessages,
|
|
173
187
|
messagesRemoved: evenMaxRemove,
|
|
174
188
|
};
|
|
175
189
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Redis Conversation Memory Manager for NeuroLink
|
|
3
3
|
* Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
|
|
4
4
|
*/
|
|
5
|
-
import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions } from "../types/conversation.js";
|
|
5
|
+
import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata } from "../types/conversation.js";
|
|
6
6
|
import type { IConversationMemoryManager } from "../types/conversationMemoryInterface.js";
|
|
7
7
|
/**
|
|
8
8
|
* Redis-based implementation of the ConversationMemoryManager
|
|
@@ -184,4 +184,14 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
|
|
|
184
184
|
* Flush pending tool execution data for a session and merge into conversation
|
|
185
185
|
*/
|
|
186
186
|
private flushPendingToolData;
|
|
187
|
+
/**
|
|
188
|
+
* Update agentic loop report metadata for a conversation session.
|
|
189
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
190
|
+
* Follows the read → patch → write pattern (same as title generation).
|
|
191
|
+
*
|
|
192
|
+
* @param sessionId The session identifier
|
|
193
|
+
* @param userId The user identifier (optional)
|
|
194
|
+
* @param report The report metadata to upsert
|
|
195
|
+
*/
|
|
196
|
+
updateAgenticLoopReport(sessionId: string, userId: string | undefined, report: AgenticLoopReportMetadata): Promise<void>;
|
|
187
197
|
}
|
|
@@ -10,6 +10,7 @@ import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
|
|
|
10
10
|
import { SummarizationEngine } from "../context/summarizationEngine.js";
|
|
11
11
|
import { NeuroLink } from "../neurolink.js";
|
|
12
12
|
import { ConversationMemoryError } from "../types/conversation.js";
|
|
13
|
+
import { withTimeout } from "../utils/errorHandling.js";
|
|
13
14
|
import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
|
|
14
15
|
import { runWithCurrentLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
|
|
15
16
|
import { logger } from "../utils/logger.js";
|
|
@@ -749,6 +750,11 @@ export class RedisConversationMemoryManager {
|
|
|
749
750
|
title: conversation.title,
|
|
750
751
|
createdAt: conversation.createdAt,
|
|
751
752
|
updatedAt: conversation.updatedAt,
|
|
753
|
+
metadata: conversation.additionalMetadata?.agenticLoopReports
|
|
754
|
+
? {
|
|
755
|
+
agenticLoopReports: conversation.additionalMetadata.agenticLoopReports,
|
|
756
|
+
}
|
|
757
|
+
: undefined,
|
|
752
758
|
};
|
|
753
759
|
}
|
|
754
760
|
logger.debug("[RedisConversationMemoryManager] No valid conversation data found", {
|
|
@@ -1345,4 +1351,85 @@ User message: "${userMessage}"`;
|
|
|
1345
1351
|
this.pendingToolExecutions.delete(pendingKey);
|
|
1346
1352
|
}
|
|
1347
1353
|
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Update agentic loop report metadata for a conversation session.
|
|
1356
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
1357
|
+
* Follows the read → patch → write pattern (same as title generation).
|
|
1358
|
+
*
|
|
1359
|
+
* @param sessionId The session identifier
|
|
1360
|
+
* @param userId The user identifier (optional)
|
|
1361
|
+
* @param report The report metadata to upsert
|
|
1362
|
+
*/
|
|
1363
|
+
async updateAgenticLoopReport(sessionId, userId, report) {
|
|
1364
|
+
logger.debug("[RedisConversationMemoryManager] Updating agentic loop report", {
|
|
1365
|
+
sessionId,
|
|
1366
|
+
userId,
|
|
1367
|
+
reportId: report.reportId,
|
|
1368
|
+
reportType: report.reportType,
|
|
1369
|
+
reportStatus: report.reportStatus,
|
|
1370
|
+
});
|
|
1371
|
+
await this.ensureInitialized();
|
|
1372
|
+
if (!this.redisClient) {
|
|
1373
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available for report update", { sessionId, userId });
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
|
|
1378
|
+
const conversationData = await withTimeout(this.redisClient.get(redisKey), 5000);
|
|
1379
|
+
if (!conversationData) {
|
|
1380
|
+
logger.warn("[RedisConversationMemoryManager] No conversation found for report update", { sessionId, userId });
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const conversation = deserializeConversation(conversationData);
|
|
1384
|
+
if (!conversation) {
|
|
1385
|
+
logger.warn("[RedisConversationMemoryManager] Failed to deserialize conversation for report update", { sessionId, userId });
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
// Initialize additionalMetadata and agenticLoopReports if needed
|
|
1389
|
+
if (!conversation.additionalMetadata) {
|
|
1390
|
+
conversation.additionalMetadata = {};
|
|
1391
|
+
}
|
|
1392
|
+
if (!conversation.additionalMetadata.agenticLoopReports) {
|
|
1393
|
+
conversation.additionalMetadata.agenticLoopReports = [];
|
|
1394
|
+
}
|
|
1395
|
+
// Upsert: find existing report by reportId and update, or push new entry
|
|
1396
|
+
const existingIndex = conversation.additionalMetadata.agenticLoopReports.findIndex((r) => r.reportId === report.reportId);
|
|
1397
|
+
if (existingIndex >= 0) {
|
|
1398
|
+
conversation.additionalMetadata.agenticLoopReports[existingIndex] =
|
|
1399
|
+
report;
|
|
1400
|
+
logger.debug("[RedisConversationMemoryManager] Updated existing agentic loop report", { sessionId, reportId: report.reportId });
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
conversation.additionalMetadata.agenticLoopReports.push(report);
|
|
1404
|
+
logger.debug("[RedisConversationMemoryManager] Added new agentic loop report", { sessionId, reportId: report.reportId });
|
|
1405
|
+
}
|
|
1406
|
+
conversation.updatedAt = new Date().toISOString();
|
|
1407
|
+
// Write back to Redis
|
|
1408
|
+
const serializedData = serializeConversation(conversation);
|
|
1409
|
+
await withTimeout(this.redisClient.set(redisKey, serializedData), 5000);
|
|
1410
|
+
if (this.redisConfig.ttl > 0) {
|
|
1411
|
+
await withTimeout(this.redisClient.expire(redisKey, this.redisConfig.ttl), 5000);
|
|
1412
|
+
}
|
|
1413
|
+
logger.info("[RedisConversationMemoryManager] Successfully updated agentic loop report", {
|
|
1414
|
+
sessionId,
|
|
1415
|
+
userId,
|
|
1416
|
+
reportId: report.reportId,
|
|
1417
|
+
reportStatus: report.reportStatus,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
logger.error("[RedisConversationMemoryManager] Failed to update agentic loop report", {
|
|
1422
|
+
sessionId,
|
|
1423
|
+
userId,
|
|
1424
|
+
reportId: report.reportId,
|
|
1425
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1426
|
+
});
|
|
1427
|
+
throw new ConversationMemoryError("Failed to update agentic loop report", "STORAGE_ERROR", {
|
|
1428
|
+
sessionId,
|
|
1429
|
+
userId,
|
|
1430
|
+
reportId: report.reportId,
|
|
1431
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1348
1435
|
}
|
|
@@ -11,10 +11,19 @@
|
|
|
11
11
|
* - Small conversation handling (BUG-005): for <= 4 messages, truncates
|
|
12
12
|
* message content proportionally instead of returning no-op.
|
|
13
13
|
*/
|
|
14
|
-
import { randomUUID } from "crypto";
|
|
15
14
|
import { estimateTokens, estimateMessagesTokens, truncateToTokenBudget, } from "../../utils/tokenEstimation.js";
|
|
16
15
|
import { logger } from "../../utils/logger.js";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
17
|
const TRUNCATION_MARKER_CONTENT = "[Earlier conversation history was truncated to fit within context limits]";
|
|
18
|
+
function validateRoleAlternation(messages) {
|
|
19
|
+
for (let i = 1; i < messages.length; i++) {
|
|
20
|
+
if (messages[i].role === messages[i - 1].role &&
|
|
21
|
+
messages[i].role !== "system") {
|
|
22
|
+
logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* For conversations with <= 4 messages that exceed token budget,
|
|
20
29
|
* truncate the CONTENT of the longest messages rather than removing messages.
|
|
@@ -122,18 +131,19 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
122
131
|
break;
|
|
123
132
|
}
|
|
124
133
|
const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
// Insert a dedicated system-role truncation marker with machine-readable
|
|
135
|
+
// metadata so effectiveHistory.ts can detect it via isTruncationMarker /
|
|
136
|
+
// truncationId and removeTruncationTags can rewind it.
|
|
137
|
+
const truncId = randomUUID();
|
|
138
|
+
const marker = {
|
|
139
|
+
id: `truncation-marker-${truncId}`,
|
|
140
|
+
role: "system",
|
|
128
141
|
content: TRUNCATION_MARKER_CONTENT,
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
isTruncationMarker: true,
|
|
143
|
+
truncationId: truncId,
|
|
131
144
|
};
|
|
132
|
-
const candidateMessages = [
|
|
133
|
-
|
|
134
|
-
truncationMarker,
|
|
135
|
-
...keptAfterTruncation,
|
|
136
|
-
];
|
|
145
|
+
const candidateMessages = [...firstPair, marker, ...keptAfterTruncation];
|
|
146
|
+
validateRoleAlternation(candidateMessages);
|
|
137
147
|
// If we have token targets, verify the result fits
|
|
138
148
|
if (config?.targetTokens) {
|
|
139
149
|
const candidateTokens = estimateMessagesTokens(candidateMessages, config.provider);
|
|
@@ -160,16 +170,20 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
160
170
|
const evenMaxRemove = maxRemove - (maxRemove % 2);
|
|
161
171
|
if (evenMaxRemove > 0) {
|
|
162
172
|
const keptMessages = remainingMessages.slice(evenMaxRemove);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
// Insert a dedicated system-role truncation marker (see iterative block above)
|
|
174
|
+
const fallbackTruncId = randomUUID();
|
|
175
|
+
const fallbackMarker = {
|
|
176
|
+
id: `truncation-marker-${fallbackTruncId}`,
|
|
177
|
+
role: "system",
|
|
166
178
|
content: TRUNCATION_MARKER_CONTENT,
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
isTruncationMarker: true,
|
|
180
|
+
truncationId: fallbackTruncId,
|
|
169
181
|
};
|
|
182
|
+
const fallbackMessages = [...firstPair, fallbackMarker, ...keptMessages];
|
|
183
|
+
validateRoleAlternation(fallbackMessages);
|
|
170
184
|
return {
|
|
171
185
|
truncated: true,
|
|
172
|
-
messages:
|
|
186
|
+
messages: fallbackMessages,
|
|
173
187
|
messagesRemoved: evenMaxRemove,
|
|
174
188
|
};
|
|
175
189
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Redis Conversation Memory Manager for NeuroLink
|
|
3
3
|
* Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
|
|
4
4
|
*/
|
|
5
|
-
import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions } from "../types/conversation.js";
|
|
5
|
+
import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata } from "../types/conversation.js";
|
|
6
6
|
import type { IConversationMemoryManager } from "../types/conversationMemoryInterface.js";
|
|
7
7
|
/**
|
|
8
8
|
* Redis-based implementation of the ConversationMemoryManager
|
|
@@ -184,4 +184,14 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
|
|
|
184
184
|
* Flush pending tool execution data for a session and merge into conversation
|
|
185
185
|
*/
|
|
186
186
|
private flushPendingToolData;
|
|
187
|
+
/**
|
|
188
|
+
* Update agentic loop report metadata for a conversation session.
|
|
189
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
190
|
+
* Follows the read → patch → write pattern (same as title generation).
|
|
191
|
+
*
|
|
192
|
+
* @param sessionId The session identifier
|
|
193
|
+
* @param userId The user identifier (optional)
|
|
194
|
+
* @param report The report metadata to upsert
|
|
195
|
+
*/
|
|
196
|
+
updateAgenticLoopReport(sessionId: string, userId: string | undefined, report: AgenticLoopReportMetadata): Promise<void>;
|
|
187
197
|
}
|
|
@@ -10,6 +10,7 @@ import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
|
|
|
10
10
|
import { SummarizationEngine } from "../context/summarizationEngine.js";
|
|
11
11
|
import { NeuroLink } from "../neurolink.js";
|
|
12
12
|
import { ConversationMemoryError } from "../types/conversation.js";
|
|
13
|
+
import { withTimeout } from "../utils/errorHandling.js";
|
|
13
14
|
import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
|
|
14
15
|
import { runWithCurrentLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
|
|
15
16
|
import { logger } from "../utils/logger.js";
|
|
@@ -749,6 +750,11 @@ export class RedisConversationMemoryManager {
|
|
|
749
750
|
title: conversation.title,
|
|
750
751
|
createdAt: conversation.createdAt,
|
|
751
752
|
updatedAt: conversation.updatedAt,
|
|
753
|
+
metadata: conversation.additionalMetadata?.agenticLoopReports
|
|
754
|
+
? {
|
|
755
|
+
agenticLoopReports: conversation.additionalMetadata.agenticLoopReports,
|
|
756
|
+
}
|
|
757
|
+
: undefined,
|
|
752
758
|
};
|
|
753
759
|
}
|
|
754
760
|
logger.debug("[RedisConversationMemoryManager] No valid conversation data found", {
|
|
@@ -1345,5 +1351,86 @@ User message: "${userMessage}"`;
|
|
|
1345
1351
|
this.pendingToolExecutions.delete(pendingKey);
|
|
1346
1352
|
}
|
|
1347
1353
|
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Update agentic loop report metadata for a conversation session.
|
|
1356
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
1357
|
+
* Follows the read → patch → write pattern (same as title generation).
|
|
1358
|
+
*
|
|
1359
|
+
* @param sessionId The session identifier
|
|
1360
|
+
* @param userId The user identifier (optional)
|
|
1361
|
+
* @param report The report metadata to upsert
|
|
1362
|
+
*/
|
|
1363
|
+
async updateAgenticLoopReport(sessionId, userId, report) {
|
|
1364
|
+
logger.debug("[RedisConversationMemoryManager] Updating agentic loop report", {
|
|
1365
|
+
sessionId,
|
|
1366
|
+
userId,
|
|
1367
|
+
reportId: report.reportId,
|
|
1368
|
+
reportType: report.reportType,
|
|
1369
|
+
reportStatus: report.reportStatus,
|
|
1370
|
+
});
|
|
1371
|
+
await this.ensureInitialized();
|
|
1372
|
+
if (!this.redisClient) {
|
|
1373
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available for report update", { sessionId, userId });
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
|
|
1378
|
+
const conversationData = await withTimeout(this.redisClient.get(redisKey), 5000);
|
|
1379
|
+
if (!conversationData) {
|
|
1380
|
+
logger.warn("[RedisConversationMemoryManager] No conversation found for report update", { sessionId, userId });
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const conversation = deserializeConversation(conversationData);
|
|
1384
|
+
if (!conversation) {
|
|
1385
|
+
logger.warn("[RedisConversationMemoryManager] Failed to deserialize conversation for report update", { sessionId, userId });
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
// Initialize additionalMetadata and agenticLoopReports if needed
|
|
1389
|
+
if (!conversation.additionalMetadata) {
|
|
1390
|
+
conversation.additionalMetadata = {};
|
|
1391
|
+
}
|
|
1392
|
+
if (!conversation.additionalMetadata.agenticLoopReports) {
|
|
1393
|
+
conversation.additionalMetadata.agenticLoopReports = [];
|
|
1394
|
+
}
|
|
1395
|
+
// Upsert: find existing report by reportId and update, or push new entry
|
|
1396
|
+
const existingIndex = conversation.additionalMetadata.agenticLoopReports.findIndex((r) => r.reportId === report.reportId);
|
|
1397
|
+
if (existingIndex >= 0) {
|
|
1398
|
+
conversation.additionalMetadata.agenticLoopReports[existingIndex] =
|
|
1399
|
+
report;
|
|
1400
|
+
logger.debug("[RedisConversationMemoryManager] Updated existing agentic loop report", { sessionId, reportId: report.reportId });
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
conversation.additionalMetadata.agenticLoopReports.push(report);
|
|
1404
|
+
logger.debug("[RedisConversationMemoryManager] Added new agentic loop report", { sessionId, reportId: report.reportId });
|
|
1405
|
+
}
|
|
1406
|
+
conversation.updatedAt = new Date().toISOString();
|
|
1407
|
+
// Write back to Redis
|
|
1408
|
+
const serializedData = serializeConversation(conversation);
|
|
1409
|
+
await withTimeout(this.redisClient.set(redisKey, serializedData), 5000);
|
|
1410
|
+
if (this.redisConfig.ttl > 0) {
|
|
1411
|
+
await withTimeout(this.redisClient.expire(redisKey, this.redisConfig.ttl), 5000);
|
|
1412
|
+
}
|
|
1413
|
+
logger.info("[RedisConversationMemoryManager] Successfully updated agentic loop report", {
|
|
1414
|
+
sessionId,
|
|
1415
|
+
userId,
|
|
1416
|
+
reportId: report.reportId,
|
|
1417
|
+
reportStatus: report.reportStatus,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
logger.error("[RedisConversationMemoryManager] Failed to update agentic loop report", {
|
|
1422
|
+
sessionId,
|
|
1423
|
+
userId,
|
|
1424
|
+
reportId: report.reportId,
|
|
1425
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1426
|
+
});
|
|
1427
|
+
throw new ConversationMemoryError("Failed to update agentic loop report", "STORAGE_ERROR", {
|
|
1428
|
+
sessionId,
|
|
1429
|
+
userId,
|
|
1430
|
+
reportId: report.reportId,
|
|
1431
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1348
1435
|
}
|
|
1349
1436
|
//# sourceMappingURL=redisConversationMemoryManager.js.map
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -925,6 +925,26 @@ export declare class NeuroLink {
|
|
|
925
925
|
* @returns true if the tool was removed, false if it didn't exist
|
|
926
926
|
*/
|
|
927
927
|
unregisterTool(name: string): boolean;
|
|
928
|
+
/**
|
|
929
|
+
* Update agentic loop report metadata for a conversation session.
|
|
930
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
931
|
+
* Only supported when using Redis conversation memory.
|
|
932
|
+
*
|
|
933
|
+
* @param sessionId The session identifier
|
|
934
|
+
* @param report The agentic loop report metadata to upsert
|
|
935
|
+
* @param userId Optional user identifier
|
|
936
|
+
* @throws Error if conversation memory is not initialized or is not Redis-backed
|
|
937
|
+
*
|
|
938
|
+
* @example
|
|
939
|
+
* ```typescript
|
|
940
|
+
* await neurolink.updateAgenticLoopReport("session-123", {
|
|
941
|
+
* reportId: "report-abc",
|
|
942
|
+
* reportType: "META",
|
|
943
|
+
* reportStatus: "INPROGRESS",
|
|
944
|
+
* });
|
|
945
|
+
* ```
|
|
946
|
+
*/
|
|
947
|
+
updateAgenticLoopReport(sessionId: string, report: import("./types/conversation.js").AgenticLoopReportMetadata, userId?: string): Promise<void>;
|
|
928
948
|
/**
|
|
929
949
|
* Get all registered custom tools
|
|
930
950
|
* @returns Map of tool names to MCPExecutableTool format
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -38,6 +38,7 @@ import { initializeMem0 } from "./memory/mem0Initializer.js";
|
|
|
38
38
|
import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
|
|
39
39
|
import { initializeHippocampus, } from "./memory/hippocampusInitializer.js";
|
|
40
40
|
import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
41
|
+
import { ConversationMemoryError } from "./types/conversation.js";
|
|
41
42
|
import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
|
|
42
43
|
// Enhanced error handling imports
|
|
43
44
|
import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
|
|
@@ -4641,6 +4642,38 @@ Current user's request: ${currentInput}`;
|
|
|
4641
4642
|
}
|
|
4642
4643
|
return removed;
|
|
4643
4644
|
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Update agentic loop report metadata for a conversation session.
|
|
4647
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
4648
|
+
* Only supported when using Redis conversation memory.
|
|
4649
|
+
*
|
|
4650
|
+
* @param sessionId The session identifier
|
|
4651
|
+
* @param report The agentic loop report metadata to upsert
|
|
4652
|
+
* @param userId Optional user identifier
|
|
4653
|
+
* @throws Error if conversation memory is not initialized or is not Redis-backed
|
|
4654
|
+
*
|
|
4655
|
+
* @example
|
|
4656
|
+
* ```typescript
|
|
4657
|
+
* await neurolink.updateAgenticLoopReport("session-123", {
|
|
4658
|
+
* reportId: "report-abc",
|
|
4659
|
+
* reportType: "META",
|
|
4660
|
+
* reportStatus: "INPROGRESS",
|
|
4661
|
+
* });
|
|
4662
|
+
* ```
|
|
4663
|
+
*/
|
|
4664
|
+
async updateAgenticLoopReport(sessionId, report, userId) {
|
|
4665
|
+
if (!this.conversationMemory) {
|
|
4666
|
+
throw new ConversationMemoryError("Conversation memory is not initialized. Enable conversationMemory in NeuroLink options.", "CONFIG_ERROR");
|
|
4667
|
+
}
|
|
4668
|
+
// Check if the memory manager is Redis-backed (has updateAgenticLoopReport method)
|
|
4669
|
+
if (!("updateAgenticLoopReport" in this.conversationMemory) ||
|
|
4670
|
+
typeof this.conversationMemory
|
|
4671
|
+
.updateAgenticLoopReport !== "function") {
|
|
4672
|
+
throw new ConversationMemoryError("updateAgenticLoopReport is only supported with Redis conversation memory.", "CONFIG_ERROR");
|
|
4673
|
+
}
|
|
4674
|
+
await withTimeout(this
|
|
4675
|
+
.conversationMemory.updateAgenticLoopReport(sessionId, userId, report), 5000);
|
|
4676
|
+
}
|
|
4644
4677
|
/**
|
|
4645
4678
|
* Get all registered custom tools
|
|
4646
4679
|
* @returns Map of tool names to MCPExecutableTool format
|
|
@@ -59,7 +59,7 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
|
|
|
59
59
|
* Estimate token count from text using centralized estimation with provider multipliers
|
|
60
60
|
*/
|
|
61
61
|
private estimateTokenCount;
|
|
62
|
-
protected executeStream(options: StreamOptions,
|
|
62
|
+
protected executeStream(options: StreamOptions, analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
|
|
63
63
|
/**
|
|
64
64
|
* Execute stream using native @google/genai SDK for Gemini 3 models
|
|
65
65
|
* This bypasses @ai-sdk/google to properly handle thought_signature
|
|
@@ -385,12 +385,15 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
385
385
|
return estimateTokens(text, "google-ai");
|
|
386
386
|
}
|
|
387
387
|
// executeGenerate removed - BaseProvider handles all generation with tools
|
|
388
|
-
async executeStream(options,
|
|
388
|
+
async executeStream(options, analysisSchema) {
|
|
389
389
|
// Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
|
|
390
390
|
const gemini3CheckModelName = options.model || this.modelName;
|
|
391
|
+
// Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
|
|
392
|
+
// Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
|
|
393
|
+
const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
|
|
391
394
|
// Check for tools from options AND from SDK (MCP tools)
|
|
392
395
|
// Need to check early if we should route to native SDK
|
|
393
|
-
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
|
|
396
|
+
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
|
|
394
397
|
const optionTools = options.tools || {};
|
|
395
398
|
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
396
399
|
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
@@ -442,7 +445,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
442
445
|
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
443
446
|
try {
|
|
444
447
|
// Get tools consistently with generate method (include user-provided RAG tools)
|
|
445
|
-
|
|
448
|
+
// wantsStructuredOutput already computed before the Gemini 3 native-routing gate
|
|
449
|
+
if (wantsStructuredOutput &&
|
|
450
|
+
!options.disableTools &&
|
|
451
|
+
this.supportsTools()) {
|
|
452
|
+
logger.warn("[GoogleAIStudio] Structured output active — disabling tools (Gemini limitation).");
|
|
453
|
+
}
|
|
454
|
+
const shouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
|
|
446
455
|
const baseTools = shouldUseTools ? await this.getAllTools() : {};
|
|
447
456
|
const rawTools = shouldUseTools
|
|
448
457
|
? { ...baseTools, ...(options.tools || {}) }
|
|
@@ -88,6 +88,18 @@ export function sanitizeSchemaForGemini(schema) {
|
|
|
88
88
|
result[key] = value;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
// Recurse through composed schema branches
|
|
92
|
+
if (Array.isArray(result.allOf)) {
|
|
93
|
+
result.allOf = result.allOf.map((s) => sanitizeSchemaForGemini(s));
|
|
94
|
+
}
|
|
95
|
+
if (result.not && typeof result.not === "object") {
|
|
96
|
+
result.not = sanitizeSchemaForGemini(result.not);
|
|
97
|
+
}
|
|
98
|
+
for (const branch of ["if", "then", "else"]) {
|
|
99
|
+
if (result[branch] && typeof result[branch] === "object") {
|
|
100
|
+
result[branch] = sanitizeSchemaForGemini(result[branch]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
91
103
|
return result;
|
|
92
104
|
}
|
|
93
105
|
/**
|
|
@@ -111,6 +123,10 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
111
123
|
typeof params.parse === "function") {
|
|
112
124
|
const rawJsonSchema = convertZodToJsonSchema(params);
|
|
113
125
|
const inlined = inlineJsonSchema(rawJsonSchema);
|
|
126
|
+
// Gemini sanitization strips Zod-only features not supported by the Gemini API:
|
|
127
|
+
// union types (anyOf/oneOf) are collapsed to string, default values and
|
|
128
|
+
// additionalProperties are removed. The resulting schema is Gemini-compatible
|
|
129
|
+
// but loses some type constraints from the original Zod schema.
|
|
114
130
|
const sanitizedSchema = sanitizeSchemaForGemini(inlined);
|
|
115
131
|
sanitized[name] = createAISDKTool({
|
|
116
132
|
description: tool.description || `Tool: ${name}`,
|
|
@@ -119,13 +135,28 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
119
135
|
execute: tool.execute,
|
|
120
136
|
});
|
|
121
137
|
}
|
|
138
|
+
else if (params &&
|
|
139
|
+
typeof params === "object" &&
|
|
140
|
+
"jsonSchema" in params) {
|
|
141
|
+
// Non-Zod JSON schema (e.g., from ai SDK jsonSchema() helper) — still needs sanitization
|
|
142
|
+
const rawSchema = params
|
|
143
|
+
.jsonSchema;
|
|
144
|
+
const sanitizedSchema = sanitizeSchemaForGemini(inlineJsonSchema(rawSchema));
|
|
145
|
+
sanitized[name] = createAISDKTool({
|
|
146
|
+
description: tool.description || `Tool: ${name}`,
|
|
147
|
+
parameters: aiJsonSchema(sanitizedSchema),
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
execute: tool.execute,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
122
152
|
else {
|
|
123
153
|
sanitized[name] = tool;
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
catch (error) {
|
|
127
|
-
logger.warn(`[Gemini] Failed to sanitize tool "${name}",
|
|
128
|
-
|
|
157
|
+
logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
// Don't fall back to the original tool — an incompatible schema would fail the Gemini request
|
|
159
|
+
continue;
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
return sanitized;
|
|
@@ -784,7 +784,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
784
784
|
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
785
785
|
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
786
786
|
const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
|
|
787
|
-
if (isGemini3Model(gemini3CheckModelName) && hasTools) {
|
|
787
|
+
if (isGemini3Model(gemini3CheckModelName) && hasTools && !analysisSchema) {
|
|
788
788
|
// Process CSV files before routing to native SDK (bypasses normal message builder)
|
|
789
789
|
const processedOptions = await this.processCSVFilesForNativeSDK(options);
|
|
790
790
|
// Merge SDK tools into options for native SDK path
|
|
@@ -792,6 +792,16 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
792
792
|
...processedOptions,
|
|
793
793
|
tools: { ...sdkTools, ...optionTools },
|
|
794
794
|
};
|
|
795
|
+
// Gemini cannot use tools and JSON schema simultaneously
|
|
796
|
+
const wantsStructuredOutput = analysisSchema ||
|
|
797
|
+
processedOptions.output?.format === "json" ||
|
|
798
|
+
processedOptions.schema;
|
|
799
|
+
if (wantsStructuredOutput) {
|
|
800
|
+
mergedOptions.tools = {};
|
|
801
|
+
mergedOptions.toolChoice = undefined;
|
|
802
|
+
mergedOptions.maxSteps = undefined;
|
|
803
|
+
logger.warn("[GoogleVertex] Structured output active — disabling tools for Gemini 3 (Gemini limitation).");
|
|
804
|
+
}
|
|
795
805
|
logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
|
|
796
806
|
model: gemini3CheckModelName,
|
|
797
807
|
optionToolCount: Object.keys(optionTools).length,
|
|
@@ -377,6 +377,36 @@ export type SessionMetadata = {
|
|
|
377
377
|
title: string;
|
|
378
378
|
createdAt: string;
|
|
379
379
|
updatedAt: string;
|
|
380
|
+
/** Additional metadata including agentic loop reports */
|
|
381
|
+
metadata?: {
|
|
382
|
+
agenticLoopReports?: AgenticLoopReportMetadata[];
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Report type for agentic loop reports
|
|
387
|
+
* Identifies the platform or category of the report
|
|
388
|
+
*/
|
|
389
|
+
export type AgenticLoopReportType = "META" | "GOOGLEADS" | "GOOGLEGA4" | "OTHER";
|
|
390
|
+
/**
|
|
391
|
+
* Status of an agentic loop report
|
|
392
|
+
*/
|
|
393
|
+
export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED";
|
|
394
|
+
/**
|
|
395
|
+
* Metadata for an individual agentic loop report
|
|
396
|
+
* A conversation session can have multiple reports tracked via this type
|
|
397
|
+
*/
|
|
398
|
+
export type AgenticLoopReportMetadata = {
|
|
399
|
+
/** Unique identifier for this report */
|
|
400
|
+
reportId: string;
|
|
401
|
+
/** Platform/category of the report */
|
|
402
|
+
reportType: AgenticLoopReportType;
|
|
403
|
+
/** Current status of the report */
|
|
404
|
+
reportStatus: AgenticLoopReportStatus;
|
|
405
|
+
/** Optional audit period date range for the report */
|
|
406
|
+
auditPeriod?: {
|
|
407
|
+
startDate: string;
|
|
408
|
+
endDate: string;
|
|
409
|
+
};
|
|
380
410
|
};
|
|
381
411
|
/**
|
|
382
412
|
* Base conversation metadata (shared fields across all conversation types)
|
|
@@ -413,6 +443,13 @@ export type ConversationBase = {
|
|
|
413
443
|
cacheReadTokens?: number;
|
|
414
444
|
cacheWriteTokens?: number;
|
|
415
445
|
};
|
|
446
|
+
/** Additional metadata for extensible conversation-level data */
|
|
447
|
+
additionalMetadata?: {
|
|
448
|
+
/** Agentic loop reports associated with this conversation */
|
|
449
|
+
agenticLoopReports?: AgenticLoopReportMetadata[];
|
|
450
|
+
/** Allow future extensibility */
|
|
451
|
+
[key: string]: unknown;
|
|
452
|
+
};
|
|
416
453
|
};
|
|
417
454
|
/**
|
|
418
455
|
* Redis conversation storage object format
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -925,6 +925,26 @@ export declare class NeuroLink {
|
|
|
925
925
|
* @returns true if the tool was removed, false if it didn't exist
|
|
926
926
|
*/
|
|
927
927
|
unregisterTool(name: string): boolean;
|
|
928
|
+
/**
|
|
929
|
+
* Update agentic loop report metadata for a conversation session.
|
|
930
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
931
|
+
* Only supported when using Redis conversation memory.
|
|
932
|
+
*
|
|
933
|
+
* @param sessionId The session identifier
|
|
934
|
+
* @param report The agentic loop report metadata to upsert
|
|
935
|
+
* @param userId Optional user identifier
|
|
936
|
+
* @throws Error if conversation memory is not initialized or is not Redis-backed
|
|
937
|
+
*
|
|
938
|
+
* @example
|
|
939
|
+
* ```typescript
|
|
940
|
+
* await neurolink.updateAgenticLoopReport("session-123", {
|
|
941
|
+
* reportId: "report-abc",
|
|
942
|
+
* reportType: "META",
|
|
943
|
+
* reportStatus: "INPROGRESS",
|
|
944
|
+
* });
|
|
945
|
+
* ```
|
|
946
|
+
*/
|
|
947
|
+
updateAgenticLoopReport(sessionId: string, report: import("./types/conversation.js").AgenticLoopReportMetadata, userId?: string): Promise<void>;
|
|
928
948
|
/**
|
|
929
949
|
* Get all registered custom tools
|
|
930
950
|
* @returns Map of tool names to MCPExecutableTool format
|
package/dist/neurolink.js
CHANGED
|
@@ -38,6 +38,7 @@ import { initializeMem0 } from "./memory/mem0Initializer.js";
|
|
|
38
38
|
import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
|
|
39
39
|
import { initializeHippocampus, } from "./memory/hippocampusInitializer.js";
|
|
40
40
|
import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
41
|
+
import { ConversationMemoryError } from "./types/conversation.js";
|
|
41
42
|
import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
|
|
42
43
|
// Enhanced error handling imports
|
|
43
44
|
import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
|
|
@@ -4641,6 +4642,38 @@ Current user's request: ${currentInput}`;
|
|
|
4641
4642
|
}
|
|
4642
4643
|
return removed;
|
|
4643
4644
|
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Update agentic loop report metadata for a conversation session.
|
|
4647
|
+
* Upserts a report entry by reportId — updates existing or adds new.
|
|
4648
|
+
* Only supported when using Redis conversation memory.
|
|
4649
|
+
*
|
|
4650
|
+
* @param sessionId The session identifier
|
|
4651
|
+
* @param report The agentic loop report metadata to upsert
|
|
4652
|
+
* @param userId Optional user identifier
|
|
4653
|
+
* @throws Error if conversation memory is not initialized or is not Redis-backed
|
|
4654
|
+
*
|
|
4655
|
+
* @example
|
|
4656
|
+
* ```typescript
|
|
4657
|
+
* await neurolink.updateAgenticLoopReport("session-123", {
|
|
4658
|
+
* reportId: "report-abc",
|
|
4659
|
+
* reportType: "META",
|
|
4660
|
+
* reportStatus: "INPROGRESS",
|
|
4661
|
+
* });
|
|
4662
|
+
* ```
|
|
4663
|
+
*/
|
|
4664
|
+
async updateAgenticLoopReport(sessionId, report, userId) {
|
|
4665
|
+
if (!this.conversationMemory) {
|
|
4666
|
+
throw new ConversationMemoryError("Conversation memory is not initialized. Enable conversationMemory in NeuroLink options.", "CONFIG_ERROR");
|
|
4667
|
+
}
|
|
4668
|
+
// Check if the memory manager is Redis-backed (has updateAgenticLoopReport method)
|
|
4669
|
+
if (!("updateAgenticLoopReport" in this.conversationMemory) ||
|
|
4670
|
+
typeof this.conversationMemory
|
|
4671
|
+
.updateAgenticLoopReport !== "function") {
|
|
4672
|
+
throw new ConversationMemoryError("updateAgenticLoopReport is only supported with Redis conversation memory.", "CONFIG_ERROR");
|
|
4673
|
+
}
|
|
4674
|
+
await withTimeout(this
|
|
4675
|
+
.conversationMemory.updateAgenticLoopReport(sessionId, userId, report), 5000);
|
|
4676
|
+
}
|
|
4644
4677
|
/**
|
|
4645
4678
|
* Get all registered custom tools
|
|
4646
4679
|
* @returns Map of tool names to MCPExecutableTool format
|
|
@@ -59,7 +59,7 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
|
|
|
59
59
|
* Estimate token count from text using centralized estimation with provider multipliers
|
|
60
60
|
*/
|
|
61
61
|
private estimateTokenCount;
|
|
62
|
-
protected executeStream(options: StreamOptions,
|
|
62
|
+
protected executeStream(options: StreamOptions, analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
|
|
63
63
|
/**
|
|
64
64
|
* Execute stream using native @google/genai SDK for Gemini 3 models
|
|
65
65
|
* This bypasses @ai-sdk/google to properly handle thought_signature
|
|
@@ -385,12 +385,15 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
385
385
|
return estimateTokens(text, "google-ai");
|
|
386
386
|
}
|
|
387
387
|
// executeGenerate removed - BaseProvider handles all generation with tools
|
|
388
|
-
async executeStream(options,
|
|
388
|
+
async executeStream(options, analysisSchema) {
|
|
389
389
|
// Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
|
|
390
390
|
const gemini3CheckModelName = options.model || this.modelName;
|
|
391
|
+
// Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
|
|
392
|
+
// Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
|
|
393
|
+
const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
|
|
391
394
|
// Check for tools from options AND from SDK (MCP tools)
|
|
392
395
|
// Need to check early if we should route to native SDK
|
|
393
|
-
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
|
|
396
|
+
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
|
|
394
397
|
const optionTools = options.tools || {};
|
|
395
398
|
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
396
399
|
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
@@ -442,7 +445,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
442
445
|
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
443
446
|
try {
|
|
444
447
|
// Get tools consistently with generate method (include user-provided RAG tools)
|
|
445
|
-
|
|
448
|
+
// wantsStructuredOutput already computed before the Gemini 3 native-routing gate
|
|
449
|
+
if (wantsStructuredOutput &&
|
|
450
|
+
!options.disableTools &&
|
|
451
|
+
this.supportsTools()) {
|
|
452
|
+
logger.warn("[GoogleAIStudio] Structured output active — disabling tools (Gemini limitation).");
|
|
453
|
+
}
|
|
454
|
+
const shouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
|
|
446
455
|
const baseTools = shouldUseTools ? await this.getAllTools() : {};
|
|
447
456
|
const rawTools = shouldUseTools
|
|
448
457
|
? { ...baseTools, ...(options.tools || {}) }
|
|
@@ -88,6 +88,18 @@ export function sanitizeSchemaForGemini(schema) {
|
|
|
88
88
|
result[key] = value;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
// Recurse through composed schema branches
|
|
92
|
+
if (Array.isArray(result.allOf)) {
|
|
93
|
+
result.allOf = result.allOf.map((s) => sanitizeSchemaForGemini(s));
|
|
94
|
+
}
|
|
95
|
+
if (result.not && typeof result.not === "object") {
|
|
96
|
+
result.not = sanitizeSchemaForGemini(result.not);
|
|
97
|
+
}
|
|
98
|
+
for (const branch of ["if", "then", "else"]) {
|
|
99
|
+
if (result[branch] && typeof result[branch] === "object") {
|
|
100
|
+
result[branch] = sanitizeSchemaForGemini(result[branch]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
91
103
|
return result;
|
|
92
104
|
}
|
|
93
105
|
/**
|
|
@@ -111,6 +123,10 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
111
123
|
typeof params.parse === "function") {
|
|
112
124
|
const rawJsonSchema = convertZodToJsonSchema(params);
|
|
113
125
|
const inlined = inlineJsonSchema(rawJsonSchema);
|
|
126
|
+
// Gemini sanitization strips Zod-only features not supported by the Gemini API:
|
|
127
|
+
// union types (anyOf/oneOf) are collapsed to string, default values and
|
|
128
|
+
// additionalProperties are removed. The resulting schema is Gemini-compatible
|
|
129
|
+
// but loses some type constraints from the original Zod schema.
|
|
114
130
|
const sanitizedSchema = sanitizeSchemaForGemini(inlined);
|
|
115
131
|
sanitized[name] = createAISDKTool({
|
|
116
132
|
description: tool.description || `Tool: ${name}`,
|
|
@@ -119,13 +135,28 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
119
135
|
execute: tool.execute,
|
|
120
136
|
});
|
|
121
137
|
}
|
|
138
|
+
else if (params &&
|
|
139
|
+
typeof params === "object" &&
|
|
140
|
+
"jsonSchema" in params) {
|
|
141
|
+
// Non-Zod JSON schema (e.g., from ai SDK jsonSchema() helper) — still needs sanitization
|
|
142
|
+
const rawSchema = params
|
|
143
|
+
.jsonSchema;
|
|
144
|
+
const sanitizedSchema = sanitizeSchemaForGemini(inlineJsonSchema(rawSchema));
|
|
145
|
+
sanitized[name] = createAISDKTool({
|
|
146
|
+
description: tool.description || `Tool: ${name}`,
|
|
147
|
+
parameters: aiJsonSchema(sanitizedSchema),
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
execute: tool.execute,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
122
152
|
else {
|
|
123
153
|
sanitized[name] = tool;
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
catch (error) {
|
|
127
|
-
logger.warn(`[Gemini] Failed to sanitize tool "${name}",
|
|
128
|
-
|
|
157
|
+
logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
// Don't fall back to the original tool — an incompatible schema would fail the Gemini request
|
|
159
|
+
continue;
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
return sanitized;
|
|
@@ -784,7 +784,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
784
784
|
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
785
785
|
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
786
786
|
const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
|
|
787
|
-
if (isGemini3Model(gemini3CheckModelName) && hasTools) {
|
|
787
|
+
if (isGemini3Model(gemini3CheckModelName) && hasTools && !analysisSchema) {
|
|
788
788
|
// Process CSV files before routing to native SDK (bypasses normal message builder)
|
|
789
789
|
const processedOptions = await this.processCSVFilesForNativeSDK(options);
|
|
790
790
|
// Merge SDK tools into options for native SDK path
|
|
@@ -792,6 +792,16 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
792
792
|
...processedOptions,
|
|
793
793
|
tools: { ...sdkTools, ...optionTools },
|
|
794
794
|
};
|
|
795
|
+
// Gemini cannot use tools and JSON schema simultaneously
|
|
796
|
+
const wantsStructuredOutput = analysisSchema ||
|
|
797
|
+
processedOptions.output?.format === "json" ||
|
|
798
|
+
processedOptions.schema;
|
|
799
|
+
if (wantsStructuredOutput) {
|
|
800
|
+
mergedOptions.tools = {};
|
|
801
|
+
mergedOptions.toolChoice = undefined;
|
|
802
|
+
mergedOptions.maxSteps = undefined;
|
|
803
|
+
logger.warn("[GoogleVertex] Structured output active — disabling tools for Gemini 3 (Gemini limitation).");
|
|
804
|
+
}
|
|
795
805
|
logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
|
|
796
806
|
model: gemini3CheckModelName,
|
|
797
807
|
optionToolCount: Object.keys(optionTools).length,
|
|
@@ -377,6 +377,36 @@ export type SessionMetadata = {
|
|
|
377
377
|
title: string;
|
|
378
378
|
createdAt: string;
|
|
379
379
|
updatedAt: string;
|
|
380
|
+
/** Additional metadata including agentic loop reports */
|
|
381
|
+
metadata?: {
|
|
382
|
+
agenticLoopReports?: AgenticLoopReportMetadata[];
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Report type for agentic loop reports
|
|
387
|
+
* Identifies the platform or category of the report
|
|
388
|
+
*/
|
|
389
|
+
export type AgenticLoopReportType = "META" | "GOOGLEADS" | "GOOGLEGA4" | "OTHER";
|
|
390
|
+
/**
|
|
391
|
+
* Status of an agentic loop report
|
|
392
|
+
*/
|
|
393
|
+
export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED";
|
|
394
|
+
/**
|
|
395
|
+
* Metadata for an individual agentic loop report
|
|
396
|
+
* A conversation session can have multiple reports tracked via this type
|
|
397
|
+
*/
|
|
398
|
+
export type AgenticLoopReportMetadata = {
|
|
399
|
+
/** Unique identifier for this report */
|
|
400
|
+
reportId: string;
|
|
401
|
+
/** Platform/category of the report */
|
|
402
|
+
reportType: AgenticLoopReportType;
|
|
403
|
+
/** Current status of the report */
|
|
404
|
+
reportStatus: AgenticLoopReportStatus;
|
|
405
|
+
/** Optional audit period date range for the report */
|
|
406
|
+
auditPeriod?: {
|
|
407
|
+
startDate: string;
|
|
408
|
+
endDate: string;
|
|
409
|
+
};
|
|
380
410
|
};
|
|
381
411
|
/**
|
|
382
412
|
* Base conversation metadata (shared fields across all conversation types)
|
|
@@ -413,6 +443,13 @@ export type ConversationBase = {
|
|
|
413
443
|
cacheReadTokens?: number;
|
|
414
444
|
cacheWriteTokens?: number;
|
|
415
445
|
};
|
|
446
|
+
/** Additional metadata for extensible conversation-level data */
|
|
447
|
+
additionalMetadata?: {
|
|
448
|
+
/** Agentic loop reports associated with this conversation */
|
|
449
|
+
agenticLoopReports?: AgenticLoopReportMetadata[];
|
|
450
|
+
/** Allow future extensibility */
|
|
451
|
+
[key: string]: unknown;
|
|
452
|
+
};
|
|
416
453
|
};
|
|
417
454
|
/**
|
|
418
455
|
* Redis conversation storage object format
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.21.0",
|
|
4
4
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Juspay Technologies",
|