@juspay/neurolink 10.10.6 → 10.10.8
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 +15 -0
- package/README.md +37 -8
- package/dist/browser/neurolink.min.js +399 -399
- package/dist/cli/factories/commandFactory.js +8 -4
- package/dist/constants/contextWindows.js +10 -1
- package/dist/context/anthropicLoopGuard.d.ts +1 -0
- package/dist/context/anthropicLoopGuard.js +30 -12
- package/dist/context/contextCompactor.js +19 -0
- package/dist/context/geminiLoopGuard.d.ts +54 -0
- package/dist/context/geminiLoopGuard.js +140 -0
- package/dist/core/redisConversationMemoryManager.d.ts +27 -0
- package/dist/core/redisConversationMemoryManager.js +146 -25
- package/dist/lib/constants/contextWindows.js +10 -1
- package/dist/lib/context/anthropicLoopGuard.d.ts +1 -0
- package/dist/lib/context/anthropicLoopGuard.js +30 -12
- package/dist/lib/context/contextCompactor.js +19 -0
- package/dist/lib/context/geminiLoopGuard.d.ts +54 -0
- package/dist/lib/context/geminiLoopGuard.js +141 -0
- package/dist/lib/core/redisConversationMemoryManager.d.ts +27 -0
- package/dist/lib/core/redisConversationMemoryManager.js +146 -25
- package/dist/lib/processors/media/VideoProcessor.d.ts +13 -3
- package/dist/lib/processors/media/VideoProcessor.js +53 -12
- package/dist/lib/providers/googleAiStudio/client.d.ts +0 -31
- package/dist/lib/providers/googleAiStudio/client.js +118 -1
- package/dist/lib/providers/googleNativeGemini3/utils.d.ts +9 -0
- package/dist/lib/providers/googleNativeGemini3/utils.js +12 -0
- package/dist/lib/providers/googleVertex/client.d.ts +0 -45
- package/dist/lib/providers/googleVertex/client.js +201 -21
- package/dist/lib/types/context.d.ts +9 -0
- package/dist/lib/types/file.d.ts +37 -0
- package/dist/lib/types/generate.d.ts +4 -0
- package/dist/lib/types/stream.d.ts +4 -0
- package/dist/lib/utils/errorHandling.d.ts +21 -0
- package/dist/lib/utils/errorHandling.js +53 -0
- package/dist/lib/utils/fileDetector.js +9 -6
- package/dist/lib/utils/messageBuilder.js +111 -36
- package/dist/lib/utils/pdfProcessor.d.ts +11 -0
- package/dist/lib/utils/pdfProcessor.js +17 -0
- package/dist/lib/utils/redis.d.ts +60 -1
- package/dist/lib/utils/redis.js +143 -12
- package/dist/processors/media/VideoProcessor.d.ts +13 -3
- package/dist/processors/media/VideoProcessor.js +53 -12
- package/dist/providers/googleAiStudio/client.d.ts +0 -31
- package/dist/providers/googleAiStudio/client.js +118 -1
- package/dist/providers/googleNativeGemini3/utils.d.ts +9 -0
- package/dist/providers/googleNativeGemini3/utils.js +12 -0
- package/dist/providers/googleVertex/client.d.ts +0 -45
- package/dist/providers/googleVertex/client.js +201 -21
- package/dist/types/context.d.ts +9 -0
- package/dist/types/file.d.ts +37 -0
- package/dist/types/generate.d.ts +4 -0
- package/dist/types/stream.d.ts +4 -0
- package/dist/utils/errorHandling.d.ts +21 -0
- package/dist/utils/errorHandling.js +53 -0
- package/dist/utils/fileDetector.js +9 -6
- package/dist/utils/messageBuilder.js +111 -36
- package/dist/utils/pdfProcessor.d.ts +11 -0
- package/dist/utils/pdfProcessor.js +17 -0
- package/dist/utils/redis.d.ts +60 -1
- package/dist/utils/redis.js +143 -12
- package/package.json +3 -1
|
@@ -166,15 +166,19 @@ export class CLICommandFactory {
|
|
|
166
166
|
type: "string",
|
|
167
167
|
description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)",
|
|
168
168
|
},
|
|
169
|
+
// No yargs `default:` on these two. They used to declare 8 / 85 while the
|
|
170
|
+
// processor actually picks a duration-based frame count (up to 100) and
|
|
171
|
+
// encodes at quality 80 — harmless only because the values were never read
|
|
172
|
+
// (#478). Now that they reach the encoder, a default here would silently
|
|
173
|
+
// re-cap every existing CLI video at 8 frames. Unset means "let the
|
|
174
|
+
// processor choose", which is what callers have always effectively had.
|
|
169
175
|
"video-frames": {
|
|
170
176
|
type: "number",
|
|
171
|
-
default:
|
|
172
|
-
description: "Number of frames to extract (default: 8)",
|
|
177
|
+
description: "Number of frames to extract (default: chosen from video duration, max 100)",
|
|
173
178
|
},
|
|
174
179
|
"video-quality": {
|
|
175
180
|
type: "number",
|
|
176
|
-
default:
|
|
177
|
-
description: "Frame quality 0-100 (default: 85)",
|
|
181
|
+
description: "Frame quality 1-100 (default: 80)",
|
|
178
182
|
},
|
|
179
183
|
"video-format": {
|
|
180
184
|
type: "string",
|
|
@@ -376,7 +376,16 @@ export const MODEL_CONTEXT_WINDOWS = {
|
|
|
376
376
|
* `lm-studio` -> `lmstudio`, `nvidia-nim` -> `nvidianim`, `llama.cpp` -> `llamacpp`.
|
|
377
377
|
*/
|
|
378
378
|
const PROVIDER_ALIAS_MAP = {
|
|
379
|
-
|
|
379
|
+
// Both spellings resolve to `google-ai`, the ONLY key this table holds for
|
|
380
|
+
// AI Studio. `google-ai-studio` was never a key, so every realistic spelling
|
|
381
|
+
// ("googleAiStudio" from the native client, "google-ai-studio", "googleai")
|
|
382
|
+
// missed the table and fell back to DEFAULT_CONTEXT_WINDOW — reporting
|
|
383
|
+
// 128K for a 1,048,576-token Gemini. That understated the loop-guard budget
|
|
384
|
+
// by ~8x, so AI Studio agent loops reclaimed tool history that still fitted
|
|
385
|
+
// comfortably. `google-ai` itself only resolved via the raw-provider
|
|
386
|
+
// fallback below, which the normalized lookup now covers directly.
|
|
387
|
+
googleaistudio: "google-ai",
|
|
388
|
+
googleai: "google-ai",
|
|
380
389
|
lmstudio: "lm-studio",
|
|
381
390
|
llamacpp: "llamacpp",
|
|
382
391
|
nvidianim: "nvidia-nim",
|
|
@@ -125,25 +125,43 @@ export function isAnthropicToolResultMessage(message) {
|
|
|
125
125
|
* `cache_control` prefix this path depends on.
|
|
126
126
|
*/
|
|
127
127
|
export function planAnthropicLoopReclaim(args) {
|
|
128
|
-
const { conversation, availableInputTokens, fixedOverheadTokens, provider, observedPromptTokens, previousSentEstimate, onSentEstimate, } = args;
|
|
128
|
+
const { conversation, availableInputTokens, fixedOverheadTokens, provider, observedPromptTokens, previousSentEstimate, onSentEstimate, observedDescribesCurrentPayload = false, } = args;
|
|
129
129
|
const entries = toEntries(conversation, provider);
|
|
130
130
|
const rawEstimate = fixedOverheadTokens + entries.reduce((sum, e) => sum + e.tokens, 0);
|
|
131
|
-
// Calibration
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
131
|
+
// Calibration divides a real prompt-token count by THIS guard's estimate for
|
|
132
|
+
// the payload that count describes. Both halves must describe the SAME
|
|
133
|
+
// payload, and there are two legitimate pairings:
|
|
134
|
+
//
|
|
135
|
+
// - A caller that plans on EVERY step passes the provider's count for the
|
|
136
|
+
// previous request together with `previousSentEstimate`, the estimate
|
|
137
|
+
// recorded for that same request. Dividing that count by the estimate for
|
|
138
|
+
// the CURRENT conversation would be a category error — the loop has since
|
|
139
|
+
// appended an assistant tool_use message plus its tool_result, so the
|
|
140
|
+
// denominator is always the larger of the two, the ratio reads below 1 and
|
|
141
|
+
// the `Math.max(1, …)` floor pins calibration at 1. The correction then
|
|
142
|
+
// silently never applies, which is exactly when a dense-code run overflows.
|
|
143
|
+
//
|
|
144
|
+
// - A caller that plans only when a real-token guard trips has no estimate
|
|
145
|
+
// for an earlier request, but its trigger (`projectedNextPromptTokens`) is
|
|
146
|
+
// already a projection of the payload ABOUT TO BE SENT. It sets
|
|
147
|
+
// `observedDescribesCurrentPayload`, and the denominator is this module's
|
|
148
|
+
// estimate of the current history — the same payload again.
|
|
149
|
+
//
|
|
150
|
+
// Without the flag such a caller gets calibration 1, which makes its reclaim
|
|
151
|
+
// inert: the guard trips on real tokens at the same ratio this planner tests
|
|
152
|
+
// its (smaller) char estimate against, so the plan never fires and the loop
|
|
153
|
+
// falls back to stopping the turn.
|
|
154
|
+
const sentEstimate = observedDescribesCurrentPayload
|
|
155
|
+
? rawEstimate
|
|
156
|
+
: previousSentEstimate;
|
|
139
157
|
let calibration = 1;
|
|
140
158
|
if (observedPromptTokens &&
|
|
141
159
|
observedPromptTokens > 0 &&
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
sentEstimate &&
|
|
161
|
+
sentEstimate > 0) {
|
|
144
162
|
// Clamped: real tokenizers run up to ~1.3x the char estimate on dense
|
|
145
163
|
// code, and an unbounded ratio would compact the loop into uselessness.
|
|
146
|
-
calibration = Math.min(3, Math.max(1, observedPromptTokens /
|
|
164
|
+
calibration = Math.min(3, Math.max(1, observedPromptTokens / sentEstimate));
|
|
147
165
|
}
|
|
148
166
|
const plan = planLoopGuardReclaim(entries, {
|
|
149
167
|
availableInputTokens,
|
|
@@ -200,6 +200,25 @@ export class ContextCompactor {
|
|
|
200
200
|
stagesUsed,
|
|
201
201
|
durationMs: Date.now() - spanStartTime,
|
|
202
202
|
});
|
|
203
|
+
// A compaction that was ASKED to reclaim and reclaimed nothing is the
|
|
204
|
+
// signature of a mis-aimed target: the caller decided the request was
|
|
205
|
+
// over budget, but every stage gate compared against a number that
|
|
206
|
+
// said otherwise, so the pipeline no-opped and the request went out
|
|
207
|
+
// oversized anyway. That is exactly how the history-budget defect hid
|
|
208
|
+
// in production — silently, because "Complete" looked healthy. Warn
|
|
209
|
+
// loudly and stamp the span so it is greppable and alertable.
|
|
210
|
+
const reclaimedNothing = stagesUsed.length === 0;
|
|
211
|
+
if (reclaimedNothing) {
|
|
212
|
+
logger.warn("[Compaction] No-op — invoked but reclaimed nothing", {
|
|
213
|
+
requestId,
|
|
214
|
+
tokensBefore,
|
|
215
|
+
targetTokens,
|
|
216
|
+
messageCount: currentMessages.length,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
span = SpanSerializer.updateAttributes(span, {
|
|
220
|
+
"context.noop": reclaimedNothing,
|
|
221
|
+
});
|
|
203
222
|
const result = {
|
|
204
223
|
compacted: stagesUsed.length > 0,
|
|
205
224
|
stagesUsed,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-turn context guard for Gemini-shaped agent loops (native Vertex + AI
|
|
3
|
+
* Studio + Gemini 3).
|
|
4
|
+
*
|
|
5
|
+
* These loops already had `createContextGuard`, but it is **stop-only**: once
|
|
6
|
+
* the projected prompt crosses the threshold it breaks the loop and synthesizes
|
|
7
|
+
* an answer from whatever it has. That avoids a provider rejection, but it also
|
|
8
|
+
* ends the turn early — the model stops doing work it was mid-way through.
|
|
9
|
+
*
|
|
10
|
+
* This module brings them to parity with the other loops: reclaim budget and
|
|
11
|
+
* CONTINUE, falling back to the existing stop only when reclaiming cannot get
|
|
12
|
+
* back under the line. The reclaim policy is shared with every other provider
|
|
13
|
+
* via `loopGuardCore`; this module owns only the Gemini shape mapping.
|
|
14
|
+
*
|
|
15
|
+
* Gemini history is `{ role, parts[] }`, where a part is a `functionCall`
|
|
16
|
+
* (tool invocation) or `functionResponse` (its result). One model turn can
|
|
17
|
+
* carry several `functionCall` parts and the following user turn carries the
|
|
18
|
+
* matching `functionResponse` parts, so the CONTENT is the batch unit —
|
|
19
|
+
* dropping a call turn together with its response turn can never orphan a part,
|
|
20
|
+
* which Gemini rejects.
|
|
21
|
+
*/
|
|
22
|
+
import type { GeminiGuardContent, LoopGuardPlan } from "../types/index.js";
|
|
23
|
+
/** Marker left where dropped history used to be. */
|
|
24
|
+
export declare const GEMINI_ELISION_NOTE = "[Earlier tool exchanges were removed to fit the context window.]";
|
|
25
|
+
/** True when this content carries tool results worth previewing. */
|
|
26
|
+
export declare function isGeminiToolResponseContent(content: GeminiGuardContent): boolean;
|
|
27
|
+
/** Head/tail preview for an oversized tool response payload. */
|
|
28
|
+
export declare function previewGeminiToolResponseText(text: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Decide what to reclaim from a Gemini agent loop.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` when the history still fits, in which case the caller
|
|
33
|
+
* must leave it byte-identical — any rewrite invalidates the provider's cached
|
|
34
|
+
* prefix, so "no change" has to mean no change.
|
|
35
|
+
*
|
|
36
|
+
* `observedPromptTokens` must be a count for the payload ABOUT TO BE SENT —
|
|
37
|
+
* every Gemini-shaped loop plans only when its `createContextGuard` trips, and
|
|
38
|
+
* that guard's `projectedNextPromptTokens` is exactly that: the provider's real
|
|
39
|
+
* count for the last request plus the growth measured since. Dividing it by
|
|
40
|
+
* this module's estimate of the current history therefore compares two views of
|
|
41
|
+
* one payload, which is what makes the correction meaningful. A count for an
|
|
42
|
+
* EARLIER request must not be passed here: the loop has appended a model turn
|
|
43
|
+
* and its tool turn since, so the denominator would always be the larger of the
|
|
44
|
+
* two, the ratio would read below 1 and the `Math.max(1, …)` floor would pin
|
|
45
|
+
* calibration at 1 — silently disabling the correction. `planAnthropicLoopReclaim`
|
|
46
|
+
* carries `previousSentEstimate` for callers in that other position.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planGeminiLoopReclaim(args: {
|
|
49
|
+
contents: readonly GeminiGuardContent[];
|
|
50
|
+
availableInputTokens: number;
|
|
51
|
+
fixedOverheadTokens?: number;
|
|
52
|
+
provider?: string;
|
|
53
|
+
observedPromptTokens?: number;
|
|
54
|
+
}): LoopGuardPlan | undefined;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-turn context guard for Gemini-shaped agent loops (native Vertex + AI
|
|
3
|
+
* Studio + Gemini 3).
|
|
4
|
+
*
|
|
5
|
+
* These loops already had `createContextGuard`, but it is **stop-only**: once
|
|
6
|
+
* the projected prompt crosses the threshold it breaks the loop and synthesizes
|
|
7
|
+
* an answer from whatever it has. That avoids a provider rejection, but it also
|
|
8
|
+
* ends the turn early — the model stops doing work it was mid-way through.
|
|
9
|
+
*
|
|
10
|
+
* This module brings them to parity with the other loops: reclaim budget and
|
|
11
|
+
* CONTINUE, falling back to the existing stop only when reclaiming cannot get
|
|
12
|
+
* back under the line. The reclaim policy is shared with every other provider
|
|
13
|
+
* via `loopGuardCore`; this module owns only the Gemini shape mapping.
|
|
14
|
+
*
|
|
15
|
+
* Gemini history is `{ role, parts[] }`, where a part is a `functionCall`
|
|
16
|
+
* (tool invocation) or `functionResponse` (its result). One model turn can
|
|
17
|
+
* carry several `functionCall` parts and the following user turn carries the
|
|
18
|
+
* matching `functionResponse` parts, so the CONTENT is the batch unit —
|
|
19
|
+
* dropping a call turn together with its response turn can never orphan a part,
|
|
20
|
+
* which Gemini rejects.
|
|
21
|
+
*/
|
|
22
|
+
import { estimateTokens, TOKENS_PER_MESSAGE, } from "../utils/tokenEstimation.js";
|
|
23
|
+
import { generateToolOutputPreview } from "./toolOutputLimits.js";
|
|
24
|
+
import { planLoopGuardReclaim } from "./loopGuardCore.js";
|
|
25
|
+
import { logger } from "../utils/logger.js";
|
|
26
|
+
/** Preview budget for an old tool output. Matches the other loop guards. */
|
|
27
|
+
const OLD_TOOL_OUTPUT_PREVIEW_BYTES = 2_048;
|
|
28
|
+
const OLD_TOOL_OUTPUT_PREVIEW_LINES = 60;
|
|
29
|
+
/** Marker left where dropped history used to be. */
|
|
30
|
+
export const GEMINI_ELISION_NOTE = "[Earlier tool exchanges were removed to fit the context window.]";
|
|
31
|
+
/** Serialize any value for estimation. Never throws. */
|
|
32
|
+
function toText(value) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
if (value === null || value === undefined) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.stringify(value) ?? "";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Past V8's string cap: enormous by definition, so charge a large fixed
|
|
44
|
+
// size rather than aborting the estimate and with it the turn.
|
|
45
|
+
return "x".repeat(200_000);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function hasPart(content, key) {
|
|
49
|
+
return (Array.isArray(content.parts) &&
|
|
50
|
+
content.parts.some((part) => part && typeof part === "object" && key in part));
|
|
51
|
+
}
|
|
52
|
+
/** True when this content carries tool results worth previewing. */
|
|
53
|
+
export function isGeminiToolResponseContent(content) {
|
|
54
|
+
return hasPart(content, "functionResponse");
|
|
55
|
+
}
|
|
56
|
+
/** Head/tail preview for an oversized tool response payload. */
|
|
57
|
+
export function previewGeminiToolResponseText(text) {
|
|
58
|
+
const { preview } = generateToolOutputPreview(text, {
|
|
59
|
+
maxBytes: OLD_TOOL_OUTPUT_PREVIEW_BYTES,
|
|
60
|
+
maxLines: OLD_TOOL_OUTPUT_PREVIEW_LINES,
|
|
61
|
+
});
|
|
62
|
+
return preview;
|
|
63
|
+
}
|
|
64
|
+
function contentTokens(content, provider) {
|
|
65
|
+
return estimateTokens(toText(content.parts), provider) + TOKENS_PER_MESSAGE;
|
|
66
|
+
}
|
|
67
|
+
/** Map Gemini history onto the neutral view the shared policy operates on. */
|
|
68
|
+
function toEntries(contents, provider) {
|
|
69
|
+
return contents.map((content) => {
|
|
70
|
+
const tokens = contentTokens(content, provider);
|
|
71
|
+
if (isGeminiToolResponseContent(content)) {
|
|
72
|
+
// Only advertise a preview when it actually saves something: an
|
|
73
|
+
// already-small response must fall through to stage 2 rather than look
|
|
74
|
+
// shrinkable and stall the reclaim.
|
|
75
|
+
const previewed = toText(content.parts);
|
|
76
|
+
const previewTokens = previewed.length > OLD_TOOL_OUTPUT_PREVIEW_BYTES
|
|
77
|
+
? estimateTokens(previewGeminiToolResponseText(previewed), provider) +
|
|
78
|
+
TOKENS_PER_MESSAGE
|
|
79
|
+
: tokens;
|
|
80
|
+
return {
|
|
81
|
+
kind: "toolResult",
|
|
82
|
+
tokens,
|
|
83
|
+
...(previewTokens < tokens ? { previewTokens } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (hasPart(content, "functionCall")) {
|
|
87
|
+
return { kind: "toolCall", tokens };
|
|
88
|
+
}
|
|
89
|
+
return { kind: "other", tokens };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Decide what to reclaim from a Gemini agent loop.
|
|
94
|
+
*
|
|
95
|
+
* Returns `undefined` when the history still fits, in which case the caller
|
|
96
|
+
* must leave it byte-identical — any rewrite invalidates the provider's cached
|
|
97
|
+
* prefix, so "no change" has to mean no change.
|
|
98
|
+
*
|
|
99
|
+
* `observedPromptTokens` must be a count for the payload ABOUT TO BE SENT —
|
|
100
|
+
* every Gemini-shaped loop plans only when its `createContextGuard` trips, and
|
|
101
|
+
* that guard's `projectedNextPromptTokens` is exactly that: the provider's real
|
|
102
|
+
* count for the last request plus the growth measured since. Dividing it by
|
|
103
|
+
* this module's estimate of the current history therefore compares two views of
|
|
104
|
+
* one payload, which is what makes the correction meaningful. A count for an
|
|
105
|
+
* EARLIER request must not be passed here: the loop has appended a model turn
|
|
106
|
+
* and its tool turn since, so the denominator would always be the larger of the
|
|
107
|
+
* two, the ratio would read below 1 and the `Math.max(1, …)` floor would pin
|
|
108
|
+
* calibration at 1 — silently disabling the correction. `planAnthropicLoopReclaim`
|
|
109
|
+
* carries `previousSentEstimate` for callers in that other position.
|
|
110
|
+
*/
|
|
111
|
+
export function planGeminiLoopReclaim(args) {
|
|
112
|
+
const { contents, availableInputTokens, fixedOverheadTokens = 0, provider, observedPromptTokens, } = args;
|
|
113
|
+
const entries = toEntries(contents, provider);
|
|
114
|
+
let calibration = 1;
|
|
115
|
+
if (observedPromptTokens && observedPromptTokens > 0) {
|
|
116
|
+
const rawEstimate = fixedOverheadTokens + entries.reduce((sum, e) => sum + e.tokens, 0);
|
|
117
|
+
if (rawEstimate > 0) {
|
|
118
|
+
// Clamped: real tokenizers run up to ~1.3x the char estimate on dense
|
|
119
|
+
// code, and an unbounded ratio would compact the loop into uselessness.
|
|
120
|
+
calibration = Math.min(3, Math.max(1, observedPromptTokens / rawEstimate));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const plan = planLoopGuardReclaim(entries, {
|
|
124
|
+
availableInputTokens,
|
|
125
|
+
fixedOverheadTokens,
|
|
126
|
+
calibration,
|
|
127
|
+
});
|
|
128
|
+
if (!plan.fire) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
logger.info("[GeminiLoopGuard] Reclaiming agent-loop context", {
|
|
132
|
+
provider,
|
|
133
|
+
contents: contents.length,
|
|
134
|
+
toolResponsesTruncated: plan.truncate.length,
|
|
135
|
+
contentsDropped: plan.drop.length,
|
|
136
|
+
projectedTokens: plan.projectedTokens,
|
|
137
|
+
calibration,
|
|
138
|
+
});
|
|
139
|
+
return plan;
|
|
140
|
+
}
|
|
@@ -93,6 +93,33 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
|
|
|
93
93
|
* Check if summarization is needed based on token count
|
|
94
94
|
*/
|
|
95
95
|
private checkAndSummarize;
|
|
96
|
+
/**
|
|
97
|
+
* True only for keys holding a conversation BLOB — the sole key type these
|
|
98
|
+
* scan-then-GET paths may read.
|
|
99
|
+
*
|
|
100
|
+
* `${keyPrefix}*` also matches the companion message LISTs and, when a
|
|
101
|
+
* custom key prefix does not end in `conversation:`, the user-index SETs
|
|
102
|
+
* (whose derived prefix then collapses onto `keyPrefix`). `GET` against
|
|
103
|
+
* either raises WRONGTYPE, and counting them would inflate session totals.
|
|
104
|
+
*/
|
|
105
|
+
private isConversationBlobKey;
|
|
106
|
+
/**
|
|
107
|
+
* Hydrate a deserialized blob's messages from the companion LIST when the
|
|
108
|
+
* session uses split storage. Legacy blobs (messages inline) pass through
|
|
109
|
+
* untouched — that is what makes the migration backward compatible.
|
|
110
|
+
*/
|
|
111
|
+
private hydrateMessages;
|
|
112
|
+
/** Message count without materializing them — LLEN for split sessions. */
|
|
113
|
+
private countMessages;
|
|
114
|
+
/** Load a session, messages included, regardless of storage format. */
|
|
115
|
+
private loadConversation;
|
|
116
|
+
/**
|
|
117
|
+
* Persist a conversation, splitting messages into the companion LIST.
|
|
118
|
+
* `appendFrom` appends only messages from that index onward (the per-turn
|
|
119
|
+
* fast path); omit it to rewrite the LIST wholesale, which is also how a
|
|
120
|
+
* legacy blob gets converted.
|
|
121
|
+
*/
|
|
122
|
+
private persistConversation;
|
|
96
123
|
/**
|
|
97
124
|
* Build context messages for AI prompt injection (TOKEN-BASED)
|
|
98
125
|
* Returns messages from pointer onwards (or all if no pointer)
|