@juspay/neurolink 9.85.1 → 9.86.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 +6 -0
- package/dist/browser/neurolink.min.js +370 -362
- package/dist/context/contextCompactor.js +16 -2
- package/dist/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/core/conversationMemoryManager.js +13 -2
- package/dist/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/context/contextCompactor.js +16 -2
- package/dist/lib/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/lib/core/conversationMemoryManager.js +13 -2
- package/dist/lib/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/neurolink.d.ts +31 -6
- package/dist/lib/neurolink.js +163 -33
- package/dist/lib/skills/skillMatcher.d.ts +33 -4
- package/dist/lib/skills/skillMatcher.js +81 -6
- package/dist/lib/skills/skillSessionTracker.d.ts +52 -0
- package/dist/lib/skills/skillSessionTracker.js +150 -0
- package/dist/lib/skills/skillStoreRedis.d.ts +8 -0
- package/dist/lib/skills/skillStoreRedis.js +18 -2
- package/dist/lib/skills/skillStoreS3.d.ts +17 -2
- package/dist/lib/skills/skillStoreS3.js +78 -6
- package/dist/lib/skills/skillStores.d.ts +16 -2
- package/dist/lib/skills/skillStores.js +94 -5
- package/dist/lib/skills/skillTools.d.ts +26 -10
- package/dist/lib/skills/skillTools.js +190 -79
- package/dist/lib/skills/skillsManager.d.ts +25 -1
- package/dist/lib/skills/skillsManager.js +46 -4
- package/dist/lib/types/config.d.ts +5 -5
- package/dist/lib/types/conversation.d.ts +27 -0
- package/dist/lib/types/skills.d.ts +145 -14
- package/dist/lib/types/skills.js +3 -3
- package/dist/lib/utils/conversationMemory.d.ts +1 -1
- package/dist/lib/utils/conversationMemory.js +15 -2
- package/dist/neurolink.d.ts +31 -6
- package/dist/neurolink.js +163 -33
- package/dist/skills/skillMatcher.d.ts +33 -4
- package/dist/skills/skillMatcher.js +81 -6
- package/dist/skills/skillSessionTracker.d.ts +52 -0
- package/dist/skills/skillSessionTracker.js +149 -0
- package/dist/skills/skillStoreRedis.d.ts +8 -0
- package/dist/skills/skillStoreRedis.js +18 -2
- package/dist/skills/skillStoreS3.d.ts +17 -2
- package/dist/skills/skillStoreS3.js +78 -6
- package/dist/skills/skillStores.d.ts +16 -2
- package/dist/skills/skillStores.js +94 -5
- package/dist/skills/skillTools.d.ts +26 -10
- package/dist/skills/skillTools.js +190 -79
- package/dist/skills/skillsManager.d.ts +25 -1
- package/dist/skills/skillsManager.js +46 -4
- package/dist/types/config.d.ts +5 -5
- package/dist/types/conversation.d.ts +27 -0
- package/dist/types/skills.d.ts +145 -14
- package/dist/types/skills.js +3 -3
- package/dist/utils/conversationMemory.d.ts +1 -1
- package/dist/utils/conversationMemory.js +15 -2
- package/package.json +1 -1
|
@@ -26,7 +26,7 @@ const DEFAULT_CONFIG = {
|
|
|
26
26
|
enableTruncate: true,
|
|
27
27
|
pruneProtectTokens: 40_000,
|
|
28
28
|
pruneMinimumSavings: 500,
|
|
29
|
-
pruneProtectedTools: ["skill"],
|
|
29
|
+
pruneProtectedTools: ["skill", "use_skill", "read_skill_resource"],
|
|
30
30
|
summarizationProvider: "vertex",
|
|
31
31
|
summarizationModel: "gemini-2.5-flash",
|
|
32
32
|
keepRecentRatio: 0.3,
|
|
@@ -120,7 +120,21 @@ export class ContextCompactor {
|
|
|
120
120
|
targetTokens,
|
|
121
121
|
}), 120_000, "LLM summarization timed out after 120s");
|
|
122
122
|
if (summarizeResult.summarized) {
|
|
123
|
-
|
|
123
|
+
// Pinned skill instructions must survive summarization:
|
|
124
|
+
// re-seat any that the summarized region swallowed right
|
|
125
|
+
// after the summary message (index 0 of the result).
|
|
126
|
+
const survivorIds = new Set(summarizeResult.messages.map((m) => m.id));
|
|
127
|
+
const swallowedSkills = currentMessages.filter((m) => m.metadata?.isSkill && !survivorIds.has(m.id));
|
|
128
|
+
currentMessages =
|
|
129
|
+
swallowedSkills.length > 0
|
|
130
|
+
? summarizeResult.messages.length > 0
|
|
131
|
+
? [
|
|
132
|
+
summarizeResult.messages[0],
|
|
133
|
+
...swallowedSkills,
|
|
134
|
+
...summarizeResult.messages.slice(1),
|
|
135
|
+
]
|
|
136
|
+
: swallowedSkills
|
|
137
|
+
: summarizeResult.messages;
|
|
124
138
|
stagesUsed.push("summarize");
|
|
125
139
|
}
|
|
126
140
|
const stageTokensAfter = estimateMessagesTokens(currentMessages, provider);
|
|
@@ -58,8 +58,10 @@ function truncateSmallConversation(messages, config) {
|
|
|
58
58
|
let totalSaved = 0;
|
|
59
59
|
for (let i = 0; i < result.length; i++) {
|
|
60
60
|
const msg = result[i];
|
|
61
|
-
// Don't truncate system/summary messages
|
|
62
|
-
if (msg.role === "system" ||
|
|
61
|
+
// Don't truncate system/summary messages or pinned skill instructions
|
|
62
|
+
if (msg.role === "system" ||
|
|
63
|
+
msg.metadata?.isSummary ||
|
|
64
|
+
msg.metadata?.isSkill) {
|
|
63
65
|
continue;
|
|
64
66
|
}
|
|
65
67
|
const proportionalBudget = Math.floor((msgTokens[i] / totalContentTokens) * contentBudget);
|
|
@@ -93,8 +95,40 @@ function truncateSmallConversation(messages, config) {
|
|
|
93
95
|
return { truncated: false, messages, messagesRemoved: 0 };
|
|
94
96
|
}
|
|
95
97
|
export function truncateWithSlidingWindow(messages, config) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
// Partition pinned skill messages out so the pair-based arithmetic below
|
|
99
|
+
// runs on the alternating user/assistant stream (skill pins are extra
|
|
100
|
+
// user-role messages inside a turn and would misalign every even-count
|
|
101
|
+
// cut). Each skill anchors to the next conversational message; kept
|
|
102
|
+
// anchors put their skills back in place, dropped anchors re-seat them
|
|
103
|
+
// after the truncation marker.
|
|
104
|
+
const skillsByAnchor = new Map();
|
|
105
|
+
const trailingSkills = [];
|
|
106
|
+
const conversational = [];
|
|
107
|
+
for (let i = 0; i < messages.length; i++) {
|
|
108
|
+
const msg = messages[i];
|
|
109
|
+
if (!msg.metadata?.isSkill) {
|
|
110
|
+
conversational.push(msg);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const anchor = messages
|
|
114
|
+
.slice(i + 1)
|
|
115
|
+
.find((next) => !next.metadata?.isSkill);
|
|
116
|
+
if (anchor) {
|
|
117
|
+
const anchored = skillsByAnchor.get(anchor.id);
|
|
118
|
+
if (anchored) {
|
|
119
|
+
anchored.push(msg);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
skillsByAnchor.set(anchor.id, [msg]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
trailingSkills.push(msg);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (conversational.length <= 4) {
|
|
130
|
+
// Delegate to content truncation for small conversations (BUG-005);
|
|
131
|
+
// it never removes messages, so skills keep their positions.
|
|
98
132
|
return truncateSmallConversation(messages, config);
|
|
99
133
|
}
|
|
100
134
|
// ADAPTIVE MODE: calculate fraction from actual overage (PERF-001)
|
|
@@ -118,8 +152,42 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
118
152
|
fraction = config?.fraction ?? 0.5;
|
|
119
153
|
}
|
|
120
154
|
// Always preserve first user-assistant pair
|
|
121
|
-
const firstPair =
|
|
122
|
-
const remainingMessages =
|
|
155
|
+
const firstPair = conversational.slice(0, 2);
|
|
156
|
+
const remainingMessages = conversational.slice(2);
|
|
157
|
+
/** Reassemble a candidate: skills inline before kept anchors, orphans after the marker. */
|
|
158
|
+
const buildCandidate = (keptAfterTruncation, marker) => {
|
|
159
|
+
const keptIds = new Set([...firstPair, ...keptAfterTruncation].map((m) => m.id));
|
|
160
|
+
const orphanedSkills = [];
|
|
161
|
+
for (const [anchorId, anchored] of skillsByAnchor) {
|
|
162
|
+
if (!keptIds.has(anchorId)) {
|
|
163
|
+
orphanedSkills.push(...anchored);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const withInlineSkills = (msg) => {
|
|
167
|
+
const anchored = skillsByAnchor.get(msg.id);
|
|
168
|
+
return anchored ? [...anchored, msg] : [msg];
|
|
169
|
+
};
|
|
170
|
+
return [
|
|
171
|
+
...firstPair.flatMap(withInlineSkills),
|
|
172
|
+
marker,
|
|
173
|
+
...orphanedSkills,
|
|
174
|
+
...keptAfterTruncation.flatMap(withInlineSkills),
|
|
175
|
+
...trailingSkills,
|
|
176
|
+
];
|
|
177
|
+
};
|
|
178
|
+
// Insert a truncation marker with machine-readable metadata so
|
|
179
|
+
// effectiveHistory.ts can detect it via isTruncationMarker /
|
|
180
|
+
// truncationId and removeTruncationTags can rewind it.
|
|
181
|
+
const makeMarker = () => {
|
|
182
|
+
const truncId = randomUUID();
|
|
183
|
+
return {
|
|
184
|
+
id: `truncation-marker-${truncId}`,
|
|
185
|
+
role: "user",
|
|
186
|
+
content: TRUNCATION_MARKER_CONTENT,
|
|
187
|
+
isTruncationMarker: true,
|
|
188
|
+
truncationId: truncId,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
123
191
|
// ITERATIVE: if first pass isn't enough, increase fraction
|
|
124
192
|
const maxIterations = config?.maxIterations ?? 3;
|
|
125
193
|
let currentFraction = fraction;
|
|
@@ -129,19 +197,7 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
129
197
|
if (evenRemoveCount <= 0) {
|
|
130
198
|
break;
|
|
131
199
|
}
|
|
132
|
-
const
|
|
133
|
-
// Insert a truncation marker with machine-readable metadata so
|
|
134
|
-
// effectiveHistory.ts can detect it via isTruncationMarker /
|
|
135
|
-
// truncationId and removeTruncationTags can rewind it.
|
|
136
|
-
const truncId = randomUUID();
|
|
137
|
-
const marker = {
|
|
138
|
-
id: `truncation-marker-${truncId}`,
|
|
139
|
-
role: "user",
|
|
140
|
-
content: TRUNCATION_MARKER_CONTENT,
|
|
141
|
-
isTruncationMarker: true,
|
|
142
|
-
truncationId: truncId,
|
|
143
|
-
};
|
|
144
|
-
const candidateMessages = [...firstPair, marker, ...keptAfterTruncation];
|
|
200
|
+
const candidateMessages = buildCandidate(remainingMessages.slice(evenRemoveCount), makeMarker());
|
|
145
201
|
validateRoleAlternation(candidateMessages);
|
|
146
202
|
// If we have token targets, verify the result fits
|
|
147
203
|
if (config?.targetTokens) {
|
|
@@ -168,17 +224,7 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
168
224
|
const maxRemove = Math.floor(remainingMessages.length * 0.95);
|
|
169
225
|
const evenMaxRemove = maxRemove - (maxRemove % 2);
|
|
170
226
|
if (evenMaxRemove > 0) {
|
|
171
|
-
const
|
|
172
|
-
// Insert a truncation marker (see iterative block above)
|
|
173
|
-
const fallbackTruncId = randomUUID();
|
|
174
|
-
const fallbackMarker = {
|
|
175
|
-
id: `truncation-marker-${fallbackTruncId}`,
|
|
176
|
-
role: "user",
|
|
177
|
-
content: TRUNCATION_MARKER_CONTENT,
|
|
178
|
-
isTruncationMarker: true,
|
|
179
|
-
truncationId: fallbackTruncId,
|
|
180
|
-
};
|
|
181
|
-
const fallbackMessages = [...firstPair, fallbackMarker, ...keptMessages];
|
|
227
|
+
const fallbackMessages = buildCandidate(remainingMessages.slice(evenMaxRemove), makeMarker());
|
|
182
228
|
validateRoleAlternation(fallbackMessages);
|
|
183
229
|
return {
|
|
184
230
|
truncated: true,
|
|
@@ -89,7 +89,14 @@ export class ConversationMemoryManager {
|
|
|
89
89
|
if (options.events && options.events.length > 0) {
|
|
90
90
|
assistantMsg.events = options.events;
|
|
91
91
|
}
|
|
92
|
-
session.messages.push(userMsg
|
|
92
|
+
session.messages.push(userMsg);
|
|
93
|
+
// Pinned skill activations ride between ask and answer, mirroring
|
|
94
|
+
// the actual order (ask → skill loaded → answer). Stored verbatim —
|
|
95
|
+
// skill instructions are never truncated.
|
|
96
|
+
if (options.skillMessages && options.skillMessages.length > 0) {
|
|
97
|
+
session.messages.push(...options.skillMessages);
|
|
98
|
+
}
|
|
99
|
+
session.messages.push(assistantMsg);
|
|
93
100
|
session.lastActivity = Date.now();
|
|
94
101
|
// Store API-reported token counts if available
|
|
95
102
|
if (options.tokenUsage) {
|
|
@@ -275,7 +282,11 @@ export class ConversationMemoryManager {
|
|
|
275
282
|
async getStats() {
|
|
276
283
|
await this.ensureInitialized();
|
|
277
284
|
const sessions = Array.from(this.sessions.values());
|
|
278
|
-
|
|
285
|
+
// Pinned skill messages are extra rows inside a turn — exclude them so
|
|
286
|
+
// a skill-activating turn still counts as one turn.
|
|
287
|
+
const totalTurns = sessions.reduce((sum, session) => sum +
|
|
288
|
+
session.messages.filter((msg) => !msg.metadata?.isSkill).length /
|
|
289
|
+
MESSAGES_PER_TURN, 0);
|
|
279
290
|
return {
|
|
280
291
|
totalSessions: sessions.length,
|
|
281
292
|
totalTurns,
|
|
@@ -457,6 +457,12 @@ export class RedisConversationMemoryManager {
|
|
|
457
457
|
};
|
|
458
458
|
conversation.messages.push(userMsg);
|
|
459
459
|
await this.flushPendingToolData(conversation, options.sessionId, normalizedUserId);
|
|
460
|
+
// Pinned skill activations ride between ask and answer, mirroring
|
|
461
|
+
// the actual order (ask → skill loaded → answer). Stored verbatim —
|
|
462
|
+
// skill instructions are never truncated.
|
|
463
|
+
if (options.skillMessages && options.skillMessages.length > 0) {
|
|
464
|
+
conversation.messages.push(...options.skillMessages);
|
|
465
|
+
}
|
|
460
466
|
const assistantMsg = {
|
|
461
467
|
id: randomUUID(),
|
|
462
468
|
timestamp: this.generateTimestamp(),
|
|
@@ -1097,7 +1103,10 @@ User message: "${userMessage}"`;
|
|
|
1097
1103
|
const conversationData = await this.redisClient.get(key);
|
|
1098
1104
|
const conversation = deserializeConversation(conversationData);
|
|
1099
1105
|
if (conversation?.messages) {
|
|
1100
|
-
|
|
1106
|
+
// Pinned skill messages are extra rows inside a turn — exclude them
|
|
1107
|
+
// so a skill-activating turn still counts as one turn.
|
|
1108
|
+
totalTurns +=
|
|
1109
|
+
conversation.messages.filter((msg) => !msg.metadata?.isSkill).length / MESSAGES_PER_TURN;
|
|
1101
1110
|
}
|
|
1102
1111
|
}
|
|
1103
1112
|
return {
|
|
@@ -26,7 +26,7 @@ const DEFAULT_CONFIG = {
|
|
|
26
26
|
enableTruncate: true,
|
|
27
27
|
pruneProtectTokens: 40_000,
|
|
28
28
|
pruneMinimumSavings: 500,
|
|
29
|
-
pruneProtectedTools: ["skill"],
|
|
29
|
+
pruneProtectedTools: ["skill", "use_skill", "read_skill_resource"],
|
|
30
30
|
summarizationProvider: "vertex",
|
|
31
31
|
summarizationModel: "gemini-2.5-flash",
|
|
32
32
|
keepRecentRatio: 0.3,
|
|
@@ -120,7 +120,21 @@ export class ContextCompactor {
|
|
|
120
120
|
targetTokens,
|
|
121
121
|
}), 120_000, "LLM summarization timed out after 120s");
|
|
122
122
|
if (summarizeResult.summarized) {
|
|
123
|
-
|
|
123
|
+
// Pinned skill instructions must survive summarization:
|
|
124
|
+
// re-seat any that the summarized region swallowed right
|
|
125
|
+
// after the summary message (index 0 of the result).
|
|
126
|
+
const survivorIds = new Set(summarizeResult.messages.map((m) => m.id));
|
|
127
|
+
const swallowedSkills = currentMessages.filter((m) => m.metadata?.isSkill && !survivorIds.has(m.id));
|
|
128
|
+
currentMessages =
|
|
129
|
+
swallowedSkills.length > 0
|
|
130
|
+
? summarizeResult.messages.length > 0
|
|
131
|
+
? [
|
|
132
|
+
summarizeResult.messages[0],
|
|
133
|
+
...swallowedSkills,
|
|
134
|
+
...summarizeResult.messages.slice(1),
|
|
135
|
+
]
|
|
136
|
+
: swallowedSkills
|
|
137
|
+
: summarizeResult.messages;
|
|
124
138
|
stagesUsed.push("summarize");
|
|
125
139
|
}
|
|
126
140
|
const stageTokensAfter = estimateMessagesTokens(currentMessages, provider);
|
|
@@ -58,8 +58,10 @@ function truncateSmallConversation(messages, config) {
|
|
|
58
58
|
let totalSaved = 0;
|
|
59
59
|
for (let i = 0; i < result.length; i++) {
|
|
60
60
|
const msg = result[i];
|
|
61
|
-
// Don't truncate system/summary messages
|
|
62
|
-
if (msg.role === "system" ||
|
|
61
|
+
// Don't truncate system/summary messages or pinned skill instructions
|
|
62
|
+
if (msg.role === "system" ||
|
|
63
|
+
msg.metadata?.isSummary ||
|
|
64
|
+
msg.metadata?.isSkill) {
|
|
63
65
|
continue;
|
|
64
66
|
}
|
|
65
67
|
const proportionalBudget = Math.floor((msgTokens[i] / totalContentTokens) * contentBudget);
|
|
@@ -93,8 +95,40 @@ function truncateSmallConversation(messages, config) {
|
|
|
93
95
|
return { truncated: false, messages, messagesRemoved: 0 };
|
|
94
96
|
}
|
|
95
97
|
export function truncateWithSlidingWindow(messages, config) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
// Partition pinned skill messages out so the pair-based arithmetic below
|
|
99
|
+
// runs on the alternating user/assistant stream (skill pins are extra
|
|
100
|
+
// user-role messages inside a turn and would misalign every even-count
|
|
101
|
+
// cut). Each skill anchors to the next conversational message; kept
|
|
102
|
+
// anchors put their skills back in place, dropped anchors re-seat them
|
|
103
|
+
// after the truncation marker.
|
|
104
|
+
const skillsByAnchor = new Map();
|
|
105
|
+
const trailingSkills = [];
|
|
106
|
+
const conversational = [];
|
|
107
|
+
for (let i = 0; i < messages.length; i++) {
|
|
108
|
+
const msg = messages[i];
|
|
109
|
+
if (!msg.metadata?.isSkill) {
|
|
110
|
+
conversational.push(msg);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const anchor = messages
|
|
114
|
+
.slice(i + 1)
|
|
115
|
+
.find((next) => !next.metadata?.isSkill);
|
|
116
|
+
if (anchor) {
|
|
117
|
+
const anchored = skillsByAnchor.get(anchor.id);
|
|
118
|
+
if (anchored) {
|
|
119
|
+
anchored.push(msg);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
skillsByAnchor.set(anchor.id, [msg]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
trailingSkills.push(msg);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (conversational.length <= 4) {
|
|
130
|
+
// Delegate to content truncation for small conversations (BUG-005);
|
|
131
|
+
// it never removes messages, so skills keep their positions.
|
|
98
132
|
return truncateSmallConversation(messages, config);
|
|
99
133
|
}
|
|
100
134
|
// ADAPTIVE MODE: calculate fraction from actual overage (PERF-001)
|
|
@@ -118,8 +152,42 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
118
152
|
fraction = config?.fraction ?? 0.5;
|
|
119
153
|
}
|
|
120
154
|
// Always preserve first user-assistant pair
|
|
121
|
-
const firstPair =
|
|
122
|
-
const remainingMessages =
|
|
155
|
+
const firstPair = conversational.slice(0, 2);
|
|
156
|
+
const remainingMessages = conversational.slice(2);
|
|
157
|
+
/** Reassemble a candidate: skills inline before kept anchors, orphans after the marker. */
|
|
158
|
+
const buildCandidate = (keptAfterTruncation, marker) => {
|
|
159
|
+
const keptIds = new Set([...firstPair, ...keptAfterTruncation].map((m) => m.id));
|
|
160
|
+
const orphanedSkills = [];
|
|
161
|
+
for (const [anchorId, anchored] of skillsByAnchor) {
|
|
162
|
+
if (!keptIds.has(anchorId)) {
|
|
163
|
+
orphanedSkills.push(...anchored);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const withInlineSkills = (msg) => {
|
|
167
|
+
const anchored = skillsByAnchor.get(msg.id);
|
|
168
|
+
return anchored ? [...anchored, msg] : [msg];
|
|
169
|
+
};
|
|
170
|
+
return [
|
|
171
|
+
...firstPair.flatMap(withInlineSkills),
|
|
172
|
+
marker,
|
|
173
|
+
...orphanedSkills,
|
|
174
|
+
...keptAfterTruncation.flatMap(withInlineSkills),
|
|
175
|
+
...trailingSkills,
|
|
176
|
+
];
|
|
177
|
+
};
|
|
178
|
+
// Insert a truncation marker with machine-readable metadata so
|
|
179
|
+
// effectiveHistory.ts can detect it via isTruncationMarker /
|
|
180
|
+
// truncationId and removeTruncationTags can rewind it.
|
|
181
|
+
const makeMarker = () => {
|
|
182
|
+
const truncId = randomUUID();
|
|
183
|
+
return {
|
|
184
|
+
id: `truncation-marker-${truncId}`,
|
|
185
|
+
role: "user",
|
|
186
|
+
content: TRUNCATION_MARKER_CONTENT,
|
|
187
|
+
isTruncationMarker: true,
|
|
188
|
+
truncationId: truncId,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
123
191
|
// ITERATIVE: if first pass isn't enough, increase fraction
|
|
124
192
|
const maxIterations = config?.maxIterations ?? 3;
|
|
125
193
|
let currentFraction = fraction;
|
|
@@ -129,19 +197,7 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
129
197
|
if (evenRemoveCount <= 0) {
|
|
130
198
|
break;
|
|
131
199
|
}
|
|
132
|
-
const
|
|
133
|
-
// Insert a truncation marker with machine-readable metadata so
|
|
134
|
-
// effectiveHistory.ts can detect it via isTruncationMarker /
|
|
135
|
-
// truncationId and removeTruncationTags can rewind it.
|
|
136
|
-
const truncId = randomUUID();
|
|
137
|
-
const marker = {
|
|
138
|
-
id: `truncation-marker-${truncId}`,
|
|
139
|
-
role: "user",
|
|
140
|
-
content: TRUNCATION_MARKER_CONTENT,
|
|
141
|
-
isTruncationMarker: true,
|
|
142
|
-
truncationId: truncId,
|
|
143
|
-
};
|
|
144
|
-
const candidateMessages = [...firstPair, marker, ...keptAfterTruncation];
|
|
200
|
+
const candidateMessages = buildCandidate(remainingMessages.slice(evenRemoveCount), makeMarker());
|
|
145
201
|
validateRoleAlternation(candidateMessages);
|
|
146
202
|
// If we have token targets, verify the result fits
|
|
147
203
|
if (config?.targetTokens) {
|
|
@@ -168,17 +224,7 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
168
224
|
const maxRemove = Math.floor(remainingMessages.length * 0.95);
|
|
169
225
|
const evenMaxRemove = maxRemove - (maxRemove % 2);
|
|
170
226
|
if (evenMaxRemove > 0) {
|
|
171
|
-
const
|
|
172
|
-
// Insert a truncation marker (see iterative block above)
|
|
173
|
-
const fallbackTruncId = randomUUID();
|
|
174
|
-
const fallbackMarker = {
|
|
175
|
-
id: `truncation-marker-${fallbackTruncId}`,
|
|
176
|
-
role: "user",
|
|
177
|
-
content: TRUNCATION_MARKER_CONTENT,
|
|
178
|
-
isTruncationMarker: true,
|
|
179
|
-
truncationId: fallbackTruncId,
|
|
180
|
-
};
|
|
181
|
-
const fallbackMessages = [...firstPair, fallbackMarker, ...keptMessages];
|
|
227
|
+
const fallbackMessages = buildCandidate(remainingMessages.slice(evenMaxRemove), makeMarker());
|
|
182
228
|
validateRoleAlternation(fallbackMessages);
|
|
183
229
|
return {
|
|
184
230
|
truncated: true,
|
|
@@ -89,7 +89,14 @@ export class ConversationMemoryManager {
|
|
|
89
89
|
if (options.events && options.events.length > 0) {
|
|
90
90
|
assistantMsg.events = options.events;
|
|
91
91
|
}
|
|
92
|
-
session.messages.push(userMsg
|
|
92
|
+
session.messages.push(userMsg);
|
|
93
|
+
// Pinned skill activations ride between ask and answer, mirroring
|
|
94
|
+
// the actual order (ask → skill loaded → answer). Stored verbatim —
|
|
95
|
+
// skill instructions are never truncated.
|
|
96
|
+
if (options.skillMessages && options.skillMessages.length > 0) {
|
|
97
|
+
session.messages.push(...options.skillMessages);
|
|
98
|
+
}
|
|
99
|
+
session.messages.push(assistantMsg);
|
|
93
100
|
session.lastActivity = Date.now();
|
|
94
101
|
// Store API-reported token counts if available
|
|
95
102
|
if (options.tokenUsage) {
|
|
@@ -275,7 +282,11 @@ export class ConversationMemoryManager {
|
|
|
275
282
|
async getStats() {
|
|
276
283
|
await this.ensureInitialized();
|
|
277
284
|
const sessions = Array.from(this.sessions.values());
|
|
278
|
-
|
|
285
|
+
// Pinned skill messages are extra rows inside a turn — exclude them so
|
|
286
|
+
// a skill-activating turn still counts as one turn.
|
|
287
|
+
const totalTurns = sessions.reduce((sum, session) => sum +
|
|
288
|
+
session.messages.filter((msg) => !msg.metadata?.isSkill).length /
|
|
289
|
+
MESSAGES_PER_TURN, 0);
|
|
279
290
|
return {
|
|
280
291
|
totalSessions: sessions.length,
|
|
281
292
|
totalTurns,
|
|
@@ -457,6 +457,12 @@ export class RedisConversationMemoryManager {
|
|
|
457
457
|
};
|
|
458
458
|
conversation.messages.push(userMsg);
|
|
459
459
|
await this.flushPendingToolData(conversation, options.sessionId, normalizedUserId);
|
|
460
|
+
// Pinned skill activations ride between ask and answer, mirroring
|
|
461
|
+
// the actual order (ask → skill loaded → answer). Stored verbatim —
|
|
462
|
+
// skill instructions are never truncated.
|
|
463
|
+
if (options.skillMessages && options.skillMessages.length > 0) {
|
|
464
|
+
conversation.messages.push(...options.skillMessages);
|
|
465
|
+
}
|
|
460
466
|
const assistantMsg = {
|
|
461
467
|
id: randomUUID(),
|
|
462
468
|
timestamp: this.generateTimestamp(),
|
|
@@ -1097,7 +1103,10 @@ User message: "${userMessage}"`;
|
|
|
1097
1103
|
const conversationData = await this.redisClient.get(key);
|
|
1098
1104
|
const conversation = deserializeConversation(conversationData);
|
|
1099
1105
|
if (conversation?.messages) {
|
|
1100
|
-
|
|
1106
|
+
// Pinned skill messages are extra rows inside a turn — exclude them
|
|
1107
|
+
// so a skill-activating turn still counts as one turn.
|
|
1108
|
+
totalTurns +=
|
|
1109
|
+
conversation.messages.filter((msg) => !msg.metadata?.isSkill).length / MESSAGES_PER_TURN;
|
|
1101
1110
|
}
|
|
1102
1111
|
}
|
|
1103
1112
|
return {
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -274,7 +274,7 @@ export declare class NeuroLink {
|
|
|
274
274
|
*/
|
|
275
275
|
private ensureSkillsReady;
|
|
276
276
|
/**
|
|
277
|
-
* Register the built-in skill tools (
|
|
277
|
+
* Register the built-in skill tools (list_skills, plus
|
|
278
278
|
* mutation tools when allowMutations is set). Follows the
|
|
279
279
|
* registerMemoryRetrievalTools() pattern: registered via registerTool()
|
|
280
280
|
* so they land in the "user-defined" category that reaches the LLM tool
|
|
@@ -282,11 +282,36 @@ export declare class NeuroLink {
|
|
|
282
282
|
*/
|
|
283
283
|
private registerSkillTools;
|
|
284
284
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
* Skills augmentation for one generate()/stream() call:
|
|
286
|
+
* 1. Discovery — surface the skills listing per the resolved mode:
|
|
287
|
+
* embedded in the use_skill tool description ("tool", default) or
|
|
288
|
+
* appended to the system prompt ("system-prompt").
|
|
289
|
+
* 2. Per-call tools — inject use_skill + read_skill_resource into
|
|
290
|
+
* options.tools (the RAG-tool pattern; same-name entries shadow
|
|
291
|
+
* registered tools) with the sessionId closure-bound so activations
|
|
292
|
+
* pin to the session.
|
|
293
|
+
* 3. Preload — activate host-requested skills up front, injecting their
|
|
294
|
+
* instructions into this call's system prompt and pinning them.
|
|
295
|
+
* Fails open: any error leaves the call untouched.
|
|
296
|
+
*/
|
|
297
|
+
private applySkillsAugmentation;
|
|
298
|
+
/** Session id for skill activation tracking, from the call context. */
|
|
299
|
+
private resolveSkillSessionId;
|
|
300
|
+
/** User id for skill-session hydration (Redis keys sessions by userId). */
|
|
301
|
+
private resolveSkillUserId;
|
|
302
|
+
/**
|
|
303
|
+
* Activate host-requested skills before the model runs: instructions go
|
|
304
|
+
* into this call's system prompt; when persisting, the activation is
|
|
305
|
+
* pinned so later turns replay it from history instead. Unknown or
|
|
306
|
+
* already-active names are skipped with a warn/debug log (fail-open).
|
|
307
|
+
*/
|
|
308
|
+
private preloadSkills;
|
|
309
|
+
/**
|
|
310
|
+
* Pinned skill messages recorded during this turn, ready for
|
|
311
|
+
* StoreConversationTurnOptions.skillMessages. Empty when skills are off
|
|
312
|
+
* or nothing was activated.
|
|
313
|
+
*/
|
|
314
|
+
private drainPendingSkillMessages;
|
|
290
315
|
/**
|
|
291
316
|
* Programmatic access to the skills subsystem (search/list/get/mutations).
|
|
292
317
|
* Returns null when skills are not configured or failed to initialize.
|