nyxora 26.7.9 → 26.7.10
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 +17 -0
- package/README.md +9 -8
- package/SUPPORT.md +18 -0
- package/dist/packages/core/src/agent/osAgent.js +64 -8
- package/dist/packages/core/src/agent/promptBuilder.js +6 -10
- package/dist/packages/core/src/agent/reasoning.js +63 -11
- package/dist/packages/core/src/agent/web3Agent.js +64 -8
- package/dist/packages/core/src/channels/telegram.js +12 -32
- package/dist/packages/core/src/gateway/setup.js +2 -0
- package/dist/packages/core/src/utils/contextSummarizer.js +21 -44
- package/dist/packages/core/src/utils/historySanitizer.js +9 -2
- package/dist/packages/core/src/web3/aggregator/providers/ArbitrumBridgeProvider.js +58 -12
- package/dist/packages/core/src/web3/aggregator/providers/KyberSwapProvider.js +1 -1
- package/dist/packages/core/src/web3/aggregator/providers/LifiProvider.js +1 -1
- package/dist/packages/core/src/web3/aggregator/providers/OneInchProvider.js +1 -1
- package/dist/packages/core/src/web3/aggregator/providers/OpenOceanProvider.js +1 -1
- package/dist/packages/core/src/web3/aggregator/providers/RelayProvider.js +1 -1
- package/dist/packages/core/src/web3/aggregator/providers/ZeroXProvider.js +1 -1
- package/dist/packages/core/src/web3/skills/bridgeToken.js +1 -1
- package/dist/packages/core/src/web3/skills/checkSecurity.js +2 -0
- package/dist/packages/core/src/web3/utils/chains.js +4 -0
- package/dist/packages/core/src/web3/utils/rpcEngine.js +10 -0
- package/dist/packages/core/src/web3/utils/tokens.js +6 -0
- package/dist/packages/policy/src/engine.js +260 -0
- package/dist/packages/policy/src/index.js +17 -0
- package/dist/packages/policy/src/server.js +9 -248
- package/dist/packages/signer/src/NyxoraSigner.js +22 -1
- package/package.json +7 -1
- package/packages/core/package.json +2 -2
- package/packages/core/src/agent/osAgent.ts +67 -7
- package/packages/core/src/agent/promptBuilder.ts +6 -10
- package/packages/core/src/agent/reasoning.ts +62 -11
- package/packages/core/src/agent/web3Agent.ts +66 -7
- package/packages/core/src/channels/telegram.ts +10 -36
- package/packages/core/src/config/parser.d.ts +83 -0
- package/packages/core/src/config/parser.js +290 -0
- package/packages/core/src/config/paths.d.ts +2 -0
- package/packages/core/src/config/paths.js +68 -0
- package/packages/core/src/gateway/setup.ts +2 -0
- package/packages/core/src/utils/contextSummarizer.ts +19 -52
- package/packages/core/src/utils/historySanitizer.ts +9 -2
- package/packages/core/src/utils/safeLogger.d.ts +4 -0
- package/packages/core/src/utils/safeLogger.js +59 -0
- package/packages/core/src/web3/aggregator/providers/ArbitrumBridgeProvider.ts +60 -13
- package/packages/core/src/web3/aggregator/providers/KyberSwapProvider.ts +1 -1
- package/packages/core/src/web3/aggregator/providers/LifiProvider.ts +1 -1
- package/packages/core/src/web3/aggregator/providers/OneInchProvider.ts +1 -1
- package/packages/core/src/web3/aggregator/providers/OpenOceanProvider.ts +1 -1
- package/packages/core/src/web3/aggregator/providers/RelayProvider.ts +1 -1
- package/packages/core/src/web3/aggregator/providers/ZeroXProvider.ts +1 -1
- package/packages/core/src/web3/config.d.ts +3 -0
- package/packages/core/src/web3/config.js +20 -0
- package/packages/core/src/web3/skills/bridgeToken.ts +1 -1
- package/packages/core/src/web3/skills/checkRegistryStatus.d.ts +21 -0
- package/packages/core/src/web3/skills/checkRegistryStatus.js +92 -0
- package/packages/core/src/web3/skills/checkSecurity.ts +2 -0
- package/packages/core/src/web3/utils/chains.d.ts +1596 -0
- package/packages/core/src/web3/utils/chains.js +36 -0
- package/packages/core/src/web3/utils/chains.ts +4 -1
- package/packages/core/src/web3/utils/rpcEngine.d.ts +4 -0
- package/packages/core/src/web3/utils/rpcEngine.js +138 -0
- package/packages/core/src/web3/utils/rpcEngine.ts +6 -0
- package/packages/core/src/web3/utils/tokens.ts +6 -0
- package/packages/core/src/web3/utils/vaultClient.d.ts +2 -0
- package/packages/core/src/web3/utils/vaultClient.js +122 -0
- package/packages/dashboard/dist/assets/{index-LAS2_trL.js → index-p3yL9asH.js} +3 -3
- package/packages/dashboard/dist/index.html +1 -1
- package/packages/dashboard/dist/robinhood.png +0 -0
- package/packages/dashboard/dist/robinhood.svg +0 -0
- package/packages/dashboard/package.json +2 -2
- package/packages/mcp-server/package.json +2 -1
- package/packages/policy/package.json +19 -4
- package/packages/policy/src/engine.ts +288 -0
- package/packages/policy/src/index.ts +1 -0
- package/packages/policy/src/server.ts +9 -278
- package/packages/signer/package.json +3 -2
- package/packages/signer/src/NyxoraSigner.ts +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepashangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [26.7.10]
|
|
8
|
+
### Web3 Integrations
|
|
9
|
+
- **Robinhood Chain Support**: Officially integrated `Robinhood Chain` (Mainnet) and `Robinhood Testnet` support natively via `viem` `v2.55.0` upgrade. Added seamless RPC mappings, updated the dashboard network selectors with official SVG branding, and ensured high-frequency trading capabilities in the Signer remain stable.
|
|
10
|
+
|
|
11
|
+
### Core Architecture & Routing
|
|
12
|
+
- **Deterministic Intent Fast-Path**: Bypassed the LLM semantic router for short confirmation phrases. Messages containing global confirmation keywords (e.g., "yes", "sure", "proceed") now deterministically inherit the strict contextual boundary (`os` or `web3`) of the Assistant's previous permission request, completely eradicating routing hallucinations.
|
|
13
|
+
- **Global Codebase Standardization**: Refactored hardcoded regional slang from the routing logic into a comprehensive, professional English-first `CONFIRM_WORDS` array, natively supporting global users while retaining common localized confirmations as secondary fallbacks.
|
|
14
|
+
|
|
15
|
+
### Fallback Execution Engine
|
|
16
|
+
- **Native `<execute_bash>` Support**: Enhanced the `osAgent` and `web3Agent` Fallback Parsers to autonomously intercept and parse `<execute_bash>` and `<execute>` XML tags. This restores full execution capability for open-weight models that instinctively utilize these tags instead of strict JSON tool calls.
|
|
17
|
+
- **Display Sanitization Hardening**: Appended `execute_bash` and `execute` to the automated UI Sanitizer `tagsToRemove` registry. Raw bash execution blocks hallucinated by the LLM are now stripped at the edge layer before streaming to Telegram or the Dashboard.
|
|
18
|
+
|
|
19
|
+
### Web3 Signer SDK & Execution Guardrails
|
|
20
|
+
- **Receipt Waiter Integration (Anti-False-Positive)**: Remediated a severe "Fire-and-Forget" architectural bug in `NyxoraSigner.ts` where broadcasted transactions were immediately reported as successful to the LLM regardless of actual on-chain finality. The Signer now strictly executes a `waitForTransactionReceipt` hook (capped at a 20-second timeout).
|
|
21
|
+
- **Revert Detection**: If a transaction reverts on-chain (e.g., due to strict MEV slippage or gas exhaustion), the Signer violently rejects the promise with `reverted`, preventing the AI from falsely declaring success.
|
|
22
|
+
- **Pending Timeout Grace**: If the blockchain experiences congestion and fails to confirm within the 20-second window, the system falls back gracefully by returning `"Transaction broadcasted (Pending receipt)"` to ensure the 30-second Policy Engine HTTP timeout is never triggered, allowing the AI to report accurate pending status.
|
|
23
|
+
|
|
7
24
|
## [26.7.9]
|
|
8
25
|
### Features & Architecture
|
|
9
26
|
|
package/README.md
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
**Your Personal Web3 Assistant.**
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
[](#)
|
|
6
|
+
[](https://debank.com/profile/0xe5c21f46993c67cfe04fcf1579486d390be7b535)
|
|
7
|
+
|
|
8
|
+
[](#)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](#️-advanced-security-threat-model)
|
|
11
|
+
[](#️-advanced-security-threat-model)
|
|
12
|
+
[](#️-advanced-security-threat-model)
|
|
13
|
+
[](https://makeapullrequest.com)
|
|
13
14
|
|
|
14
15
|
Nyxora is a **secure, non-custodial runtime infrastructure for autonomous onchain agents** built with a robust Monorepo architecture (Node.js & React). Designed for autonomous workflows with a premium Utility-Centric dark-themed UI and strict client-side key isolation.
|
|
15
16
|
|
package/SUPPORT.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Support Nyxora Protocol
|
|
2
|
+
|
|
3
|
+
Nyxora is an open-source initiative built on the belief that powerful, zero-trust Web3 AI infrastructure should be accessible to everyone. We dedicate countless hours to research, continuous development, and maintaining this ecosystem to ensure it remains secure and cutting-edge.
|
|
4
|
+
|
|
5
|
+
If Nyxora has brought value to you, your projects, or your organization, consider supporting our ongoing development. While completely optional, your contribution plays a vital role in helping us cover infrastructure costs, fund security audits, and dedicate more time to shipping new features.
|
|
6
|
+
|
|
7
|
+
### How to Contribute
|
|
8
|
+
|
|
9
|
+
We gratefully accept community support via any EVM-compatible network (Ethereum, Arbitrum, Optimism, Polygon, Binance Smart Chain, etc.).
|
|
10
|
+
|
|
11
|
+
**EVM Address:**
|
|
12
|
+
`0xE5c21F46993C67CFe04FCF1579486D390Be7B535`
|
|
13
|
+
|
|
14
|
+
> 🔗 **[View & Donate via DeBank](https://debank.com/profile/0xe5c21f46993c67cfe04fcf1579486d390be7b535)**
|
|
15
|
+
|
|
16
|
+
*Every contribution, no matter the size, is deeply appreciated and directly fuels the future of autonomous Web3 AI.*
|
|
17
|
+
|
|
18
|
+
Thank you for being an amazing part of the Nyxora journey! 🌌
|
|
@@ -171,7 +171,7 @@ CRITICAL INSTRUCTIONS:
|
|
|
171
171
|
}
|
|
172
172
|
tracker_1.Tracker.addEvent('llm.response', { provider: config.llm.provider, tool_calls: responseMessage.tool_calls?.length || 0 });
|
|
173
173
|
// P1: Capture <think> blocks for scratchpad, get clean content
|
|
174
|
-
|
|
174
|
+
let cleanedContent = scratchpad.capture(responseMessage.content || '', turnCount);
|
|
175
175
|
exports.logger.addEntry({
|
|
176
176
|
role: 'assistant',
|
|
177
177
|
content: cleanedContent || '',
|
|
@@ -225,9 +225,51 @@ CRITICAL INSTRUCTIONS:
|
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
// 3. <tool_code> JSON arrays
|
|
229
|
+
const toolCodeRegex = /<tool_code>([\s\S]*?)<\/tool_code>/g;
|
|
230
|
+
let toolCodeMatch;
|
|
231
|
+
while ((toolCodeMatch = toolCodeRegex.exec(responseMessage.content)) !== null) {
|
|
232
|
+
try {
|
|
233
|
+
const jsonStr = toolCodeMatch[1].trim();
|
|
234
|
+
const parsedArray = JSON.parse(jsonStr);
|
|
235
|
+
if (Array.isArray(parsedArray)) {
|
|
236
|
+
for (const item of parsedArray) {
|
|
237
|
+
if (item.function_name && item.tool_args) {
|
|
238
|
+
fallbacks.push({
|
|
239
|
+
id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
|
|
240
|
+
type: 'function',
|
|
241
|
+
function: {
|
|
242
|
+
name: item.function_name,
|
|
243
|
+
arguments: typeof item.tool_args === 'string' ? item.tool_args : JSON.stringify(item.tool_args)
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
// Ignore parse errors, maybe it's not JSON
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// 4. <execute_bash> or <execute> blocks
|
|
255
|
+
const executeRegex = /<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g;
|
|
256
|
+
let executeMatch;
|
|
257
|
+
while ((executeMatch = executeRegex.exec(responseMessage.content)) !== null) {
|
|
258
|
+
const bashCmd = executeMatch[1].trim();
|
|
259
|
+
if (bashCmd) {
|
|
260
|
+
fallbacks.push({
|
|
261
|
+
id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
|
|
262
|
+
type: 'function',
|
|
263
|
+
function: {
|
|
264
|
+
name: 'run_terminal_command',
|
|
265
|
+
arguments: JSON.stringify({ command: bashCmd })
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
228
270
|
if (fallbacks.length > 0) {
|
|
229
271
|
responseMessage.tool_calls = fallbacks;
|
|
230
|
-
responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').trim();
|
|
272
|
+
responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').replace(/<tool_code>([\s\S]*?)<\/tool_code>/g, '').replace(/<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g, '').trim();
|
|
231
273
|
console.log(picocolors_1.default.cyan(`[Fallback Parser] Intercepted ${fallbacks.length} raw text commands and converted to tool_calls.`));
|
|
232
274
|
// Update logger entry with the intercepted tool calls
|
|
233
275
|
exports.logger.addEntry({
|
|
@@ -238,6 +280,22 @@ CRITICAL INSTRUCTIONS:
|
|
|
238
280
|
}
|
|
239
281
|
}
|
|
240
282
|
// -----------------------------------------------------
|
|
283
|
+
// --- DISPLAY SANITIZATION ---
|
|
284
|
+
// Remove commonly leaked XML tags (reasoning & tool calls) to prevent UI clutter
|
|
285
|
+
if (responseMessage.content) {
|
|
286
|
+
const tagsToRemove = ['tool_code', 'tool_call', 'tool_calls', 'function_call', 'function_calls', 'execute', 'execute_bash', 'think', 'thought', 'reasoning'];
|
|
287
|
+
tagsToRemove.forEach(tag => {
|
|
288
|
+
// Remove complete tags and their contents
|
|
289
|
+
const regex = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>\\s*`, 'gi');
|
|
290
|
+
responseMessage.content = responseMessage.content.replace(regex, '');
|
|
291
|
+
// Remove any orphaned closing tags
|
|
292
|
+
const orphanRegex = new RegExp(`<\\/${tag}>\\s*`, 'gi');
|
|
293
|
+
responseMessage.content = responseMessage.content.replace(orphanRegex, '');
|
|
294
|
+
});
|
|
295
|
+
responseMessage.content = responseMessage.content.trim();
|
|
296
|
+
cleanedContent = responseMessage.content;
|
|
297
|
+
}
|
|
298
|
+
// -----------------------------------------------------
|
|
241
299
|
if (!responseMessage.tool_calls || responseMessage.tool_calls.length === 0) {
|
|
242
300
|
// --- CRITIC PASS REMOVED ---
|
|
243
301
|
// The new PromptBuilder handles robust reasoning. Post-generation critic
|
|
@@ -368,7 +426,7 @@ CRITICAL INSTRUCTIONS:
|
|
|
368
426
|
// V2 Optimization: Zero-LLM Fast Return for transaction tools
|
|
369
427
|
if (canFastReturnAll && accumulatedResults.length > 0) {
|
|
370
428
|
const finalContent = accumulatedResults.join('\n\n---\n\n');
|
|
371
|
-
exports.logger.addEntry({ role: '
|
|
429
|
+
exports.logger.addEntry({ role: 'user', content: `[System Notification: The tool executed successfully and returned the following result directly to the user]\n${finalContent}` }, sessionId);
|
|
372
430
|
triggerBackgroundReview(sessionId);
|
|
373
431
|
return finalContent;
|
|
374
432
|
}
|
|
@@ -454,11 +512,9 @@ CRITICAL INSTRUCTIONS:
|
|
|
454
512
|
let streamedContent = '';
|
|
455
513
|
const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
456
514
|
streamedContent = '';
|
|
457
|
-
// RC#1 FIX:
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
if (turnCount === 1)
|
|
461
|
-
onChunk('[CLEAR_STREAM]');
|
|
515
|
+
// RC#1 FIX: Always clear the buffer at the start of the stream turn.
|
|
516
|
+
// This ensures the client UI doesn't append duplicate preambles across multi-turn executions.
|
|
517
|
+
onChunk('[CLEAR_STREAM]');
|
|
462
518
|
return await client.stream({ model: config.llm.model, temperature: config.llm.temperature, messages, tools: activeTools, reasoning_effort: (!config.llm.reasoning_effort || config.llm.reasoning_effort === 'none') ? undefined : config.llm.reasoning_effort }, (chunk) => {
|
|
463
519
|
streamedContent += chunk;
|
|
464
520
|
onChunk(chunk);
|
|
@@ -231,19 +231,15 @@ When using the search_web tool to look up news, current events, or factual data:
|
|
|
231
231
|
</web_search_accuracy>
|
|
232
232
|
|
|
233
233
|
<act_dont_ask_os>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
Only report failure AFTER actually attempting the tool call and receiving an error.
|
|
234
|
+
For harmless commands (e.g. ls, cat, checking system info), CALL the tool directly without asking for confirmation.
|
|
235
|
+
CRITICAL: For ANY command that modifies the system (e.g., sudo, apt-get, install, rm), you MUST ask the user for explicit permission FIRST.
|
|
236
|
+
When asking for permission, simply ask: "Do you want me to run [command]?" and STOP.
|
|
237
|
+
Once the user replies "yes", you MUST immediately emit the tool call to execute the command. Do NOT just reply with text confirming it.
|
|
239
238
|
</act_dont_ask_os>
|
|
240
239
|
|
|
241
240
|
<anti_hallucination_execution>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
If you write \`\`\`bash\nsudo apt install steam\n\`\`\` instead of calling run_terminal_command, you are hallucinating execution.
|
|
245
|
-
The ONLY way to run a command is to emit a proper tool_call for run_terminal_command.
|
|
246
|
-
If the tool is available, USE IT. Do not simulate or describe running it.
|
|
241
|
+
Use the native JSON tool call format provided by the API. If your model architecture requires it, you may use <tool_code> JSON arrays.
|
|
242
|
+
NEVER claim you have executed a tool or are running it in the background if you haven't actually emitted the tool call payload. Writing text describing execution is a lie.
|
|
247
243
|
</anti_hallucination_execution>
|
|
248
244
|
|
|
249
245
|
<search_hallucination_prevention>
|
|
@@ -47,10 +47,10 @@ const OS_KEYWORDS = [
|
|
|
47
47
|
'excel', 'xlsx', 'spreadsheet', 'generate excel',
|
|
48
48
|
'file', 'folder', 'directory', 'read file', 'write file', 'pdf', 'word', 'docx', 'document',
|
|
49
49
|
'note', 'catat', 'catatan', 'keep', 'download', 'unduh', 'link',
|
|
50
|
-
// Terminal & Git
|
|
50
|
+
// Terminal & Git & System
|
|
51
51
|
'terminal', 'command', 'shell', 'bash', 'script', 'run command',
|
|
52
52
|
'git', 'commit', 'push', 'pull', 'clone', 'branch', 'merge',
|
|
53
|
-
|
|
53
|
+
'install', 'installin', 'uninstall', 'apt', 'npm', 'pip', 'yarn', 'docker', 'system',
|
|
54
54
|
'search web', 'google', 'browse', 'scrape', 'weather', 'news', 'search', 'find', 'look up',
|
|
55
55
|
// Email & Workspace
|
|
56
56
|
'email', 'gmail', 'google docs', 'google sheets', 'notion', 'calendar',
|
|
@@ -78,7 +78,7 @@ const WEB3_KEYWORDS = [
|
|
|
78
78
|
'token', 'crypto', 'coin', 'nft', 'wallet', 'address', 'contract',
|
|
79
79
|
// Major Chains & L2s
|
|
80
80
|
'ethereum', 'polygon', 'arbitrum', 'optimism', 'bsc', 'mainnet', 'testnet',
|
|
81
|
-
'on-chain', 'blockchain', 'solana', 'avalanche', 'base',
|
|
81
|
+
'on-chain', 'blockchain', 'solana', 'avalanche', 'base', 'robinhood',
|
|
82
82
|
// Top 50+ Global Assets / Tickers
|
|
83
83
|
'eth', 'bnb', 'usdt', 'usdc', 'sol', 'matic', 'arb', 'op', 'btc', 'bitcoin',
|
|
84
84
|
'xrp', 'doge', 'dogecoin', 'avax', 'ada', 'cardano', 'pepe', 'shib', 'shiba',
|
|
@@ -221,18 +221,44 @@ async function processUserInput(input, role = 'user', onProgress, sessionId) {
|
|
|
221
221
|
context = 'web3';
|
|
222
222
|
preCheckMatched = true;
|
|
223
223
|
}
|
|
224
|
+
if (!preCheckMatched) {
|
|
225
|
+
// Support global confirmation keywords for short fast-path routing
|
|
226
|
+
const CONFIRM_WORDS = [
|
|
227
|
+
// English (Global Defaults)
|
|
228
|
+
'yes', 'yeah', 'yep', 'yup', 'sure', 'ok', 'okay', 'alright', 'confirm', 'proceed', 'continue', 'do it', 'go ahead', 'exactly', 'indeed',
|
|
229
|
+
// Common Localizations (e.g., ID, ES)
|
|
230
|
+
'ya', 'yakin', 'boleh', 'gas', 'lanjut', 'setuju', 'si', 'sí', 'vale', 'claro'
|
|
231
|
+
];
|
|
232
|
+
const isConfirm = lowerInput.length < 25 && CONFIRM_WORDS.some(kw => lowerInput.includes(kw));
|
|
233
|
+
if (isConfirm && textOnlyHistory.length > 0) {
|
|
234
|
+
const lastMsg = textOnlyHistory[textOnlyHistory.length - 1];
|
|
235
|
+
if (lastMsg.role === 'assistant') {
|
|
236
|
+
const lastContent = lastMsg.content.toLowerCase();
|
|
237
|
+
// Check if the assistant was asking for permission to run an OS command
|
|
238
|
+
if (lastContent.includes('sudo ') || lastContent.includes('terminal') || lastContent.includes('apt') || lastContent.includes('perintah') || lastContent.includes('install') || lastContent.includes('command')) {
|
|
239
|
+
context = 'os';
|
|
240
|
+
preCheckMatched = true;
|
|
241
|
+
// Check if the assistant was asking for permission to run a Web3 transaction
|
|
242
|
+
}
|
|
243
|
+
else if (lastContent.includes('swap') || lastContent.includes('transfer') || lastContent.includes('token') || lastContent.includes('wallet') || lastContent.includes('transaction')) {
|
|
244
|
+
context = 'web3';
|
|
245
|
+
preCheckMatched = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
224
250
|
if (preCheckMatched) {
|
|
225
|
-
console.log(picocolors_1.default.cyan(`[Orchestrator] Intent pre-classified
|
|
251
|
+
console.log(picocolors_1.default.cyan(`[Orchestrator] Intent pre-classified as: ${context.toUpperCase()}`));
|
|
226
252
|
}
|
|
227
253
|
else {
|
|
228
|
-
// ── Fallback: LLM Router (
|
|
254
|
+
// ── Fallback: LLM Router (for ambiguous intents / general conversation) ─────────
|
|
229
255
|
const routerPrompt = `You are Nyxora's Semantic Intent Router. Classify the user's FINAL message into one of four categories: 'web3', 'os', 'compound', or 'general'.
|
|
230
256
|
Rules:
|
|
231
|
-
1. FOCUS
|
|
257
|
+
1. FOCUS ON THE FINAL MESSAGE, but use history to understand short answers. If the final message is a short confirmation (e.g. "ya", "yes", "do it") or answer to a pending permission request for a tool, CLASSIFY IT BASED ON THE CONTEXT. If the previous message was about an OS command, reply 'os'. If it was about crypto/web3, reply 'web3'.
|
|
232
258
|
2. The user may speak in ANY language, including casual slang, idioms, or abbreviations.
|
|
233
|
-
3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message.
|
|
259
|
+
3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message.
|
|
234
260
|
4. If the core intent involves ONLY blockchain, crypto, bridging, swapping, trading, sending/receiving, tokens, wallets, transactions, OR asking for the price/conversion of ANY asset to fiat, reply 'web3'.
|
|
235
|
-
5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup
|
|
261
|
+
5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup, reply 'os'.
|
|
236
262
|
6. If the message is casual conversation, chit-chat, greetings, capability questions (e.g., 'what can you do?', 'bisa ngapain?', 'help', 'menu'), or any open-ended/vague question, reply 'general'.
|
|
237
263
|
Reply with EXACTLY ONE WORD: compound, web3, os, or general.`;
|
|
238
264
|
const routerMessages = [
|
|
@@ -401,14 +427,40 @@ async function processUserInputStream(input, originalOnChunk, onProgress, sessio
|
|
|
401
427
|
context = 'web3';
|
|
402
428
|
preCheckMatched = true;
|
|
403
429
|
}
|
|
430
|
+
if (!preCheckMatched) {
|
|
431
|
+
// Support global confirmation keywords for short fast-path routing
|
|
432
|
+
const CONFIRM_WORDS = [
|
|
433
|
+
// English (Global Defaults)
|
|
434
|
+
'yes', 'yeah', 'yep', 'yup', 'sure', 'ok', 'okay', 'alright', 'confirm', 'proceed', 'continue', 'do it', 'go ahead', 'exactly', 'indeed',
|
|
435
|
+
// Common Localizations (e.g., ID, ES)
|
|
436
|
+
'ya', 'yakin', 'boleh', 'gas', 'lanjut', 'setuju', 'si', 'sí', 'vale', 'claro'
|
|
437
|
+
];
|
|
438
|
+
const isConfirm = lowerInput.length < 25 && CONFIRM_WORDS.some(kw => lowerInput.includes(kw));
|
|
439
|
+
if (isConfirm && textOnlyHistory.length > 0) {
|
|
440
|
+
const lastMsg = textOnlyHistory[textOnlyHistory.length - 1];
|
|
441
|
+
if (lastMsg.role === 'assistant') {
|
|
442
|
+
const lastContent = lastMsg.content.toLowerCase();
|
|
443
|
+
// Check if the assistant was asking for permission to run an OS command
|
|
444
|
+
if (lastContent.includes('sudo ') || lastContent.includes('terminal') || lastContent.includes('apt') || lastContent.includes('perintah') || lastContent.includes('install') || lastContent.includes('command')) {
|
|
445
|
+
context = 'os';
|
|
446
|
+
preCheckMatched = true;
|
|
447
|
+
// Check if the assistant was asking for permission to run a Web3 transaction
|
|
448
|
+
}
|
|
449
|
+
else if (lastContent.includes('swap') || lastContent.includes('transfer') || lastContent.includes('token') || lastContent.includes('wallet') || lastContent.includes('transaction')) {
|
|
450
|
+
context = 'web3';
|
|
451
|
+
preCheckMatched = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
404
456
|
if (!preCheckMatched) {
|
|
405
457
|
const routerPrompt = `You are Nyxora's Semantic Intent Router. Classify the user's FINAL message into one of four categories: 'web3', 'os', 'compound', or 'general'.
|
|
406
458
|
Rules:
|
|
407
|
-
1. FOCUS
|
|
459
|
+
1. FOCUS ON THE FINAL MESSAGE, but use history to understand short answers. If the final message is a short confirmation (e.g. "ya", "yes", "do it") or answer to a pending permission request for a tool, CLASSIFY IT BASED ON THE CONTEXT. If the previous message was about an OS command, reply 'os'. If it was about crypto/web3, reply 'web3'.
|
|
408
460
|
2. The user may speak in ANY language, including casual slang, idioms, or abbreviations.
|
|
409
|
-
3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message.
|
|
461
|
+
3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message.
|
|
410
462
|
4. If the core intent involves ONLY blockchain, crypto, bridging, swapping, trading, sending/receiving, tokens, wallets, transactions, OR asking for the price/conversion of ANY asset to fiat, reply 'web3'.
|
|
411
|
-
5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup
|
|
463
|
+
5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup, reply 'os'.
|
|
412
464
|
6. If the message is casual conversation, chit-chat, greetings, capability questions (e.g., 'what can you do?', 'bisa ngapain?', 'help', 'menu'), or any open-ended/vague question, reply 'general'.
|
|
413
465
|
Reply with EXACTLY ONE WORD: compound, web3, os, or general.`;
|
|
414
466
|
const routerMessages = [
|
|
@@ -96,7 +96,7 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
96
96
|
}
|
|
97
97
|
tracker_1.Tracker.addEvent('llm.response', { provider: config.llm.provider, tool_calls: responseMessage.tool_calls?.length || 0 });
|
|
98
98
|
// P1: Capture <think> blocks for scratchpad, get clean content
|
|
99
|
-
|
|
99
|
+
let cleanedContent = scratchpad.capture(responseMessage.content || '', turnCount);
|
|
100
100
|
exports.logger.addEntry({
|
|
101
101
|
role: 'assistant',
|
|
102
102
|
content: cleanedContent || '',
|
|
@@ -150,9 +150,51 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
// 3. <tool_code> JSON arrays
|
|
154
|
+
const toolCodeRegex = /<tool_code>([\s\S]*?)<\/tool_code>/g;
|
|
155
|
+
let toolCodeMatch;
|
|
156
|
+
while ((toolCodeMatch = toolCodeRegex.exec(responseMessage.content)) !== null) {
|
|
157
|
+
try {
|
|
158
|
+
const jsonStr = toolCodeMatch[1].trim();
|
|
159
|
+
const parsedArray = JSON.parse(jsonStr);
|
|
160
|
+
if (Array.isArray(parsedArray)) {
|
|
161
|
+
for (const item of parsedArray) {
|
|
162
|
+
if (item.function_name && item.tool_args) {
|
|
163
|
+
fallbacks.push({
|
|
164
|
+
id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
|
|
165
|
+
type: 'function',
|
|
166
|
+
function: {
|
|
167
|
+
name: item.function_name,
|
|
168
|
+
arguments: typeof item.tool_args === 'string' ? item.tool_args : JSON.stringify(item.tool_args)
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
// Ignore parse errors, maybe it's not JSON
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// 4. <execute_bash> or <execute> blocks
|
|
180
|
+
const executeRegex = /<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g;
|
|
181
|
+
let executeMatch;
|
|
182
|
+
while ((executeMatch = executeRegex.exec(responseMessage.content)) !== null) {
|
|
183
|
+
const bashCmd = executeMatch[1].trim();
|
|
184
|
+
if (bashCmd) {
|
|
185
|
+
fallbacks.push({
|
|
186
|
+
id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
|
|
187
|
+
type: 'function',
|
|
188
|
+
function: {
|
|
189
|
+
name: 'run_terminal_command',
|
|
190
|
+
arguments: JSON.stringify({ command: bashCmd })
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
153
195
|
if (fallbacks.length > 0) {
|
|
154
196
|
responseMessage.tool_calls = fallbacks;
|
|
155
|
-
responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').trim();
|
|
197
|
+
responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').replace(/<tool_code>([\s\S]*?)<\/tool_code>/g, '').replace(/<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g, '').trim();
|
|
156
198
|
console.log(picocolors_1.default.cyan(`[Fallback Parser] Intercepted ${fallbacks.length} raw text commands and converted to tool_calls.`));
|
|
157
199
|
// Update logger entry with the intercepted tool calls
|
|
158
200
|
exports.logger.addEntry({
|
|
@@ -163,6 +205,22 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
163
205
|
}
|
|
164
206
|
}
|
|
165
207
|
// ---------------------------------------------------------------
|
|
208
|
+
// --- DISPLAY SANITIZATION ---
|
|
209
|
+
// Remove commonly leaked XML tags (reasoning & tool calls) to prevent UI clutter
|
|
210
|
+
if (responseMessage.content) {
|
|
211
|
+
const tagsToRemove = ['tool_code', 'tool_call', 'tool_calls', 'function_call', 'function_calls', 'execute', 'think', 'thought', 'reasoning'];
|
|
212
|
+
tagsToRemove.forEach(tag => {
|
|
213
|
+
// Remove complete tags and their contents
|
|
214
|
+
const regex = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>\\s*`, 'gi');
|
|
215
|
+
responseMessage.content = responseMessage.content.replace(regex, '');
|
|
216
|
+
// Remove any orphaned closing tags
|
|
217
|
+
const orphanRegex = new RegExp(`<\\/${tag}>\\s*`, 'gi');
|
|
218
|
+
responseMessage.content = responseMessage.content.replace(orphanRegex, '');
|
|
219
|
+
});
|
|
220
|
+
responseMessage.content = responseMessage.content.trim();
|
|
221
|
+
cleanedContent = responseMessage.content;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------
|
|
166
224
|
if (!responseMessage.tool_calls || responseMessage.tool_calls.length === 0) {
|
|
167
225
|
return cleanedContent || '⚠️ I encountered an issue processing your request. This can happen with very complex multi-step tasks. Please try rephrasing or breaking the request into smaller steps.';
|
|
168
226
|
}
|
|
@@ -284,10 +342,9 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
284
342
|
else {
|
|
285
343
|
consecutiveToolErrors = 0;
|
|
286
344
|
}
|
|
287
|
-
// V2 Optimization: Zero-LLM Fast Return for transaction tools
|
|
288
345
|
if (canFastReturnAll && accumulatedResults.length > 0) {
|
|
289
346
|
const finalContent = accumulatedResults.join('\n\n---\n\n');
|
|
290
|
-
exports.logger.addEntry({ role: '
|
|
347
|
+
exports.logger.addEntry({ role: 'user', content: `[System Notification: The tool executed successfully and returned the following result directly to the user]\n${finalContent}` }, sessionId);
|
|
291
348
|
return finalContent;
|
|
292
349
|
}
|
|
293
350
|
// Loop continues, sending tool results in the next turn
|
|
@@ -346,10 +403,9 @@ async function processWeb3IntentStream(input, onChunk, onProgress, sessionId) {
|
|
|
346
403
|
let streamedContent = '';
|
|
347
404
|
const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
348
405
|
streamedContent = '';
|
|
349
|
-
// RC#1 FIX:
|
|
350
|
-
// Subsequent turns must
|
|
351
|
-
|
|
352
|
-
onChunk('[CLEAR_STREAM]');
|
|
406
|
+
// RC#1 FIX: Always clear the buffer at the start of the stream turn.
|
|
407
|
+
// Subsequent turns must wipe the buffer to prevent UI duplication of the planning text.
|
|
408
|
+
onChunk('[CLEAR_STREAM]');
|
|
353
409
|
return await client.stream({ model: config.llm.model, temperature: config.llm.temperature, messages, tools: activeTools, reasoning_effort: config.llm.reasoning_effort }, (chunk) => {
|
|
354
410
|
streamedContent += chunk;
|
|
355
411
|
onChunk(chunk);
|
|
@@ -204,23 +204,12 @@ function startTelegramBot() {
|
|
|
204
204
|
buffer = '⏳ Processing...';
|
|
205
205
|
finalContentAlreadyStreamed = false;
|
|
206
206
|
}
|
|
207
|
-
else if (chunk === '[TOOL_CALL_DETECTED]'
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
finalContentAlreadyStreamed = false;
|
|
214
|
-
if (pendingEdit) {
|
|
215
|
-
clearTimeout(pendingEdit);
|
|
216
|
-
pendingEdit = null;
|
|
217
|
-
}
|
|
218
|
-
if (msgIdToSave && htmlToSave) {
|
|
219
|
-
await ctx.api.editMessageText(ctx.chat.id, msgIdToSave, htmlToSave, { parse_mode: 'HTML' }).catch(() => { });
|
|
220
|
-
}
|
|
221
|
-
else if (!msgIdToSave && htmlToSave) {
|
|
222
|
-
await ctx.reply(htmlToSave, { parse_mode: 'HTML', reply_parameters: { message_id: ctx.message.message_id } }).catch(() => { });
|
|
223
|
-
}
|
|
207
|
+
else if (chunk === '[TOOL_CALL_DETECTED]') {
|
|
208
|
+
buffer = '⏳ Processing...';
|
|
209
|
+
scheduleEdit();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
else if (chunk === '[TOOL_CALL_FINISHED]') {
|
|
224
213
|
return;
|
|
225
214
|
}
|
|
226
215
|
else {
|
|
@@ -412,21 +401,12 @@ function startTelegramBot() {
|
|
|
412
401
|
if (chunk === '[CLEAR_STREAM]') {
|
|
413
402
|
buffer = '⏳ Processing...';
|
|
414
403
|
}
|
|
415
|
-
else if (chunk === '[TOOL_CALL_DETECTED]'
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
clearTimeout(pendingEdit);
|
|
422
|
-
pendingEdit = null;
|
|
423
|
-
}
|
|
424
|
-
if (msgIdToSave && htmlToSave) {
|
|
425
|
-
await ctx.api.editMessageText(ctx.chat.id, msgIdToSave, htmlToSave, { parse_mode: 'HTML' }).catch(() => { });
|
|
426
|
-
}
|
|
427
|
-
else if (!msgIdToSave && htmlToSave) {
|
|
428
|
-
await ctx.reply(htmlToSave, { parse_mode: 'HTML', reply_parameters: { message_id: ctx.message.message_id } }).catch(() => { });
|
|
429
|
-
}
|
|
404
|
+
else if (chunk === '[TOOL_CALL_DETECTED]') {
|
|
405
|
+
buffer = '⏳ Processing...';
|
|
406
|
+
scheduleEdit();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
else if (chunk === '[TOOL_CALL_FINISHED]') {
|
|
430
410
|
return;
|
|
431
411
|
}
|
|
432
412
|
else {
|
|
@@ -277,6 +277,8 @@ Provider: ${config.llm.provider}`;
|
|
|
277
277
|
{ value: 'base_sepolia', label: 'Base Sepolia (Testnet)' },
|
|
278
278
|
{ value: 'arbitrum_sepolia', label: 'Arbitrum Sepolia (Testnet)' },
|
|
279
279
|
{ value: 'optimism_sepolia', label: 'OP Sepolia (Testnet)' },
|
|
280
|
+
{ value: 'robinhood', label: 'Robinhood Chain' },
|
|
281
|
+
{ value: 'robinhood_testnet', label: 'Robinhood Testnet' },
|
|
280
282
|
],
|
|
281
283
|
});
|
|
282
284
|
if ((0, prompts_1.isCancel)(defaultChain))
|
|
@@ -16,51 +16,37 @@ exports.needsCompression = needsCompression;
|
|
|
16
16
|
const llmUtils_1 = require("./llmUtils");
|
|
17
17
|
const parser_1 = require("../config/parser");
|
|
18
18
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
/** Minimum history length (user+assistant pairs) before we bother summarising. */
|
|
22
|
-
const SUMMARISE_THRESHOLD = 20;
|
|
23
|
-
/**
|
|
24
|
-
* Cache: prevents re-compressing the same history on every nudge iteration.
|
|
25
|
-
* Key: sessionId — value contains the textCount at time of compression and
|
|
26
|
-
* the resulting compressed history.
|
|
27
|
-
*
|
|
28
|
-
* During nudge loops, agent adds only system-role nudge messages. Those do NOT
|
|
29
|
-
* increment the textCount (user+assistant only). So the cache key stays the same
|
|
30
|
-
* and we return the cached result instead of making another LLM call.
|
|
31
|
-
* Cache is invalidated when the textCount grows (i.e. real LLM response landed).
|
|
32
|
-
*/
|
|
19
|
+
const VERBATIM_TAIL = 20;
|
|
20
|
+
const SUMMARISE_THRESHOLD = 40;
|
|
33
21
|
const compressionCache = new Map();
|
|
34
|
-
/**
|
|
35
|
-
* Returns a history array that fits in context.
|
|
36
|
-
* If history is short, returns as-is.
|
|
37
|
-
* If long, prepends a "Conversation Summary" system message and keeps the tail verbatim.
|
|
38
|
-
*
|
|
39
|
-
* @param history Full raw history from logger.
|
|
40
|
-
* @param sessionId Optional session identifier used to cache results across nudge turns.
|
|
41
|
-
*/
|
|
42
22
|
async function compressHistory(history, sessionId) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return history; // nothing to compress yet
|
|
23
|
+
if (history.length < SUMMARISE_THRESHOLD) {
|
|
24
|
+
return history;
|
|
46
25
|
}
|
|
47
|
-
// Cache hit: textCount hasn't grown since last compression for this session.
|
|
48
|
-
// This prevents 4x redundant LLM summarization calls during nudge iterations.
|
|
49
26
|
if (sessionId) {
|
|
50
27
|
const cached = compressionCache.get(sessionId);
|
|
51
|
-
if
|
|
28
|
+
// FIX: Invalidate cache if ANY message is added to history, including tool calls
|
|
29
|
+
if (cached && cached.textCount === history.length) {
|
|
52
30
|
return cached.compressed;
|
|
53
31
|
}
|
|
54
32
|
}
|
|
55
|
-
//
|
|
56
|
-
const tailMessages =
|
|
57
|
-
const oldMessages =
|
|
33
|
+
// FIX: Keep the tail strictly chronological so LLM doesn't get confused
|
|
34
|
+
const tailMessages = history.slice(-VERBATIM_TAIL);
|
|
35
|
+
const oldMessages = history.slice(0, -VERBATIM_TAIL);
|
|
58
36
|
if (oldMessages.length === 0)
|
|
59
37
|
return history;
|
|
60
38
|
try {
|
|
61
39
|
const config = (0, parser_1.loadConfig)();
|
|
62
40
|
const historyText = oldMessages
|
|
63
|
-
.map(m =>
|
|
41
|
+
.map(m => {
|
|
42
|
+
let name = m.role.toUpperCase();
|
|
43
|
+
let content = m.content || '';
|
|
44
|
+
if (m.role === 'assistant' && m.tool_calls)
|
|
45
|
+
content = `[Called tools: ${m.tool_calls.map(tc => tc.function.name).join(', ')}]`;
|
|
46
|
+
if (m.role === 'tool')
|
|
47
|
+
content = `[Tool Result: ${m.name}]`;
|
|
48
|
+
return `${name}: ${content}`;
|
|
49
|
+
})
|
|
64
50
|
.join('\n');
|
|
65
51
|
const summaryRes = await (0, llmUtils_1.executeWithRetry)(async (client) => client.chat({
|
|
66
52
|
model: config.llm.model,
|
|
@@ -79,31 +65,22 @@ Write in third person. Be factual, not narrative.`
|
|
|
79
65
|
if (!summaryText)
|
|
80
66
|
return history;
|
|
81
67
|
console.log(picocolors_1.default.magenta(`[ContextSummarizer] Compressed ${oldMessages.length} old messages into summary.`));
|
|
82
|
-
// Return: summary as system note + verbatim tail + all tool messages (never drop tool messages)
|
|
83
|
-
const toolMessages = history.filter(m => m.role === 'tool' || (m.role === 'assistant' && m.tool_calls));
|
|
84
68
|
const compressed = [
|
|
85
69
|
{
|
|
86
70
|
role: 'system',
|
|
87
71
|
content: `--- CONVERSATION SUMMARY (earlier context) ---\n${summaryText}\n--- END SUMMARY ---`
|
|
88
72
|
},
|
|
89
|
-
...tailMessages
|
|
90
|
-
...toolMessages.slice(-6) // keep last 6 tool interactions
|
|
73
|
+
...tailMessages
|
|
91
74
|
];
|
|
92
|
-
// Persist in cache for this session
|
|
93
75
|
if (sessionId) {
|
|
94
|
-
compressionCache.set(sessionId, { textCount:
|
|
76
|
+
compressionCache.set(sessionId, { textCount: history.length, compressed });
|
|
95
77
|
}
|
|
96
78
|
return compressed;
|
|
97
79
|
}
|
|
98
80
|
catch (e) {
|
|
99
|
-
// On failure, just return the tail — never crash the agent
|
|
100
81
|
return history.slice(-VERBATIM_TAIL * 2);
|
|
101
82
|
}
|
|
102
83
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Fast check — should we attempt compression?
|
|
105
|
-
*/
|
|
106
84
|
function needsCompression(history) {
|
|
107
|
-
|
|
108
|
-
return textCount >= SUMMARISE_THRESHOLD;
|
|
85
|
+
return history.length >= SUMMARISE_THRESHOLD;
|
|
109
86
|
}
|