@mazees/mark 5.0.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/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manager Engine (MARK v5.0.0)
|
|
3
|
+
* Mengelola siklus context management per-session:
|
|
4
|
+
* - Batas global konstanta 256.000 tokens (MAX_CONTEXT_TOKENS) via gpt-tokenizer BPE
|
|
5
|
+
* - Penghitungan token presisi in-memory per sesi (system prompt + multimodal + messages)
|
|
6
|
+
* - Tahap 1: Pruning output tool lama di memori (0ms delay, tanpa AI)
|
|
7
|
+
* - Tahap 2: AI Summarization inkremental (Gemini Web -> fallback active provider)
|
|
8
|
+
* - Pembersihan orphan tool pairs
|
|
9
|
+
* - Perakitan payload prompt berformat [ COMPACTED MESSAGE SUMMARY ]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { encode } from 'gpt-tokenizer'
|
|
13
|
+
import { fetchAI } from './core.js'
|
|
14
|
+
import { getSessionCompact, saveSessionCompact, saveSession } from '../db.js'
|
|
15
|
+
import { compactCodeBlocks } from './contextCompactor.js'
|
|
16
|
+
|
|
17
|
+
export const MAX_CONTEXT_TOKENS = 256000 // 256K tokens
|
|
18
|
+
export const MAX_CONTEXT_CHARS = 256000 // Legacy backward-compatibility alias disinkronkan ke 256K
|
|
19
|
+
export const GATEWAY_HYGIENE_THRESHOLD = 0.85 // Jaring pengaman pra-turn (85% kapasitas = ~217.6K tokens)
|
|
20
|
+
export const IN_LOOP_COMPACT_THRESHOLD = 0.5 // Ambang pemicu in-loop ReAct (50% kapasitas = ~128K tokens)
|
|
21
|
+
export const OLD_TOOL_PRUNE_CHAR_LIMIT = 200 // Batas karakter output tool lama untuk dipangkas
|
|
22
|
+
export const CLEARED_TOOL_PLACEHOLDER = '[Old tool output cleared to save context space]'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper menghitung token teks murni via BPE tokenizer (gpt-tokenizer)
|
|
26
|
+
*/
|
|
27
|
+
export function countTokens(text) {
|
|
28
|
+
if (!text || typeof text !== 'string') return 0
|
|
29
|
+
try {
|
|
30
|
+
return encode(text).length
|
|
31
|
+
} catch {
|
|
32
|
+
return Math.ceil(text.length / 4)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mendapatkan ID unik dari sebuah objek pesan
|
|
38
|
+
*/
|
|
39
|
+
export function getMessageId(msg, fallbackIndex = 0) {
|
|
40
|
+
if (!msg) return `msg-${fallbackIndex}`
|
|
41
|
+
return String(msg.id || msg.created_at || msg.timestamp || `msg-${fallbackIndex}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Menghitung panjang karakter representasi sebuah pesan (legacy helper)
|
|
46
|
+
*/
|
|
47
|
+
export function calculateMessageChars(msg) {
|
|
48
|
+
if (!msg) return 0
|
|
49
|
+
let total = 0
|
|
50
|
+
|
|
51
|
+
if (typeof msg.content === 'string') {
|
|
52
|
+
if (msg.content.includes('data:image/')) {
|
|
53
|
+
const normalized = msg.content.replace(
|
|
54
|
+
/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
|
|
55
|
+
''
|
|
56
|
+
)
|
|
57
|
+
total += normalized.length + 2000
|
|
58
|
+
} else {
|
|
59
|
+
total += msg.content.length
|
|
60
|
+
}
|
|
61
|
+
} else if (Array.isArray(msg.content)) {
|
|
62
|
+
for (const part of msg.content) {
|
|
63
|
+
if (!part) continue
|
|
64
|
+
if (typeof part === 'string') {
|
|
65
|
+
if (part.includes('data:image/')) {
|
|
66
|
+
const normalized = part.replace(/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g, '')
|
|
67
|
+
total += normalized.length + 2000
|
|
68
|
+
} else {
|
|
69
|
+
total += part.length
|
|
70
|
+
}
|
|
71
|
+
} else if (part.type === 'text') {
|
|
72
|
+
const textStr = part.text || ''
|
|
73
|
+
if (textStr.includes('data:image/')) {
|
|
74
|
+
const normalized = textStr.replace(
|
|
75
|
+
/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
|
|
76
|
+
''
|
|
77
|
+
)
|
|
78
|
+
total += normalized.length + 2000
|
|
79
|
+
} else {
|
|
80
|
+
total += textStr.length
|
|
81
|
+
}
|
|
82
|
+
} else if (part.type === 'image_url' || part.image_url || part.type === 'image') {
|
|
83
|
+
total += 2000
|
|
84
|
+
} else {
|
|
85
|
+
total += JSON.stringify(part).length
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (msg.content && typeof msg.content === 'object') {
|
|
89
|
+
if (msg.content.type === 'image_url' || msg.content.image_url) {
|
|
90
|
+
total += 2000
|
|
91
|
+
} else {
|
|
92
|
+
total += JSON.stringify(msg.content).length
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof msg.reasoning === 'string') total += msg.reasoning.length
|
|
97
|
+
if (typeof msg.thought === 'string') total += msg.thought.length
|
|
98
|
+
|
|
99
|
+
const steps = msg.executionSteps || msg.executedTools
|
|
100
|
+
if (Array.isArray(steps) && steps.length > 0) {
|
|
101
|
+
const sanitizedTools = steps.map((t) => {
|
|
102
|
+
if (!t) return t
|
|
103
|
+
const copy = { ...t }
|
|
104
|
+
delete copy.preview
|
|
105
|
+
return copy
|
|
106
|
+
})
|
|
107
|
+
total += JSON.stringify(sanitizedTools).length
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
110
|
+
total += JSON.stringify(msg.tool_calls).length
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return total
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Menghitung estimasi token presisi sebuah pesan.
|
|
118
|
+
* Prioritas:
|
|
119
|
+
* 1. Token yang sudah tersimpan di database/objek pesan (msg.tokens)
|
|
120
|
+
* 2. Token completion resmi dari API (msg.usage.completion_tokens)
|
|
121
|
+
* 3. Fallback hitung lokal via BPE tokenizer (gpt-tokenizer) lalu simpan ke msg.tokens
|
|
122
|
+
*/
|
|
123
|
+
export function calculateMessageTokens(msg) {
|
|
124
|
+
if (!msg) return 0
|
|
125
|
+
|
|
126
|
+
// 1. Jika token sudah tersimpan di database/objek pesan, langsung gunakan tanpa hitung ulang
|
|
127
|
+
if (typeof msg.tokens === 'number' && msg.tokens > 0) {
|
|
128
|
+
return msg.tokens
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. Jika pesan memiliki usage completion_tokens resmi dari API
|
|
132
|
+
if (
|
|
133
|
+
msg.usage &&
|
|
134
|
+
typeof msg.usage.completion_tokens === 'number' &&
|
|
135
|
+
msg.usage.completion_tokens > 0
|
|
136
|
+
) {
|
|
137
|
+
msg.tokens = msg.usage.completion_tokens
|
|
138
|
+
return msg.tokens
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let total = 0
|
|
142
|
+
|
|
143
|
+
// Konten teks & multimodal (normalisasi bobot gambar Base64)
|
|
144
|
+
if (typeof msg.content === 'string') {
|
|
145
|
+
if (msg.content.includes('data:image/')) {
|
|
146
|
+
const normalized = msg.content.replace(
|
|
147
|
+
/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
|
|
148
|
+
''
|
|
149
|
+
)
|
|
150
|
+
total += countTokens(normalized) + 1000
|
|
151
|
+
} else {
|
|
152
|
+
total += countTokens(msg.content)
|
|
153
|
+
}
|
|
154
|
+
} else if (Array.isArray(msg.content)) {
|
|
155
|
+
for (const part of msg.content) {
|
|
156
|
+
if (!part) continue
|
|
157
|
+
if (typeof part === 'string') {
|
|
158
|
+
if (part.includes('data:image/')) {
|
|
159
|
+
const normalized = part.replace(/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g, '')
|
|
160
|
+
total += countTokens(normalized) + 1000
|
|
161
|
+
} else {
|
|
162
|
+
total += countTokens(part)
|
|
163
|
+
}
|
|
164
|
+
} else if (part.type === 'text') {
|
|
165
|
+
const textStr = part.text || ''
|
|
166
|
+
if (textStr.includes('data:image/')) {
|
|
167
|
+
const normalized = textStr.replace(
|
|
168
|
+
/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
|
|
169
|
+
''
|
|
170
|
+
)
|
|
171
|
+
total += countTokens(normalized) + 1000
|
|
172
|
+
} else {
|
|
173
|
+
total += countTokens(textStr)
|
|
174
|
+
}
|
|
175
|
+
} else if (part.type === 'image_url' || part.image_url || part.type === 'image') {
|
|
176
|
+
// Satu gambar pada LLM bernilai ~1000 token
|
|
177
|
+
total += 1000
|
|
178
|
+
} else {
|
|
179
|
+
total += countTokens(JSON.stringify(part))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (msg.content && typeof msg.content === 'object') {
|
|
183
|
+
if (msg.content.type === 'image_url' || msg.content.image_url) {
|
|
184
|
+
total += 1000
|
|
185
|
+
} else {
|
|
186
|
+
total += countTokens(JSON.stringify(msg.content))
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Reasoning / Thought
|
|
191
|
+
if (typeof msg.reasoning === 'string') total += countTokens(msg.reasoning)
|
|
192
|
+
if (typeof msg.thought === 'string') total += countTokens(msg.thought)
|
|
193
|
+
|
|
194
|
+
// Tool calls & executed tools / execution steps (abaikan preview dataUrl base64 karena hanya untuk rendering UI)
|
|
195
|
+
const steps = msg.executionSteps || msg.executedTools
|
|
196
|
+
if (Array.isArray(steps) && steps.length > 0) {
|
|
197
|
+
const sanitizedTools = steps.map((t) => {
|
|
198
|
+
if (!t) return t
|
|
199
|
+
const copy = { ...t }
|
|
200
|
+
delete copy.preview
|
|
201
|
+
return copy
|
|
202
|
+
})
|
|
203
|
+
total += countTokens(JSON.stringify(sanitizedTools))
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
206
|
+
total += countTokens(JSON.stringify(msg.tool_calls))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Simpan hasil hitungan ke objek pesan agar tidak dihitung ulang lagi
|
|
210
|
+
msg.tokens = total
|
|
211
|
+
return total
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Menghitung total token pesan satu sesi + ringkasan aktif + system prompt di memori.
|
|
216
|
+
* Prioritas utama: Menggunakan usage.total_tokens dari DB/API jika ada.
|
|
217
|
+
* Fallback: BPE tokenizer (gpt-tokenizer) dengan caching per-pesan.
|
|
218
|
+
*/
|
|
219
|
+
export function calculateSessionTokens(
|
|
220
|
+
messages = [],
|
|
221
|
+
summaryBlock = '',
|
|
222
|
+
lastCompactedMessageId = null,
|
|
223
|
+
systemPrompt = '',
|
|
224
|
+
lastCompactedAt = null
|
|
225
|
+
) {
|
|
226
|
+
let startIndex = 0
|
|
227
|
+
let isBoundaryFound = false
|
|
228
|
+
if (lastCompactedMessageId) {
|
|
229
|
+
const targetId = String(lastCompactedMessageId)
|
|
230
|
+
for (let i = 0; i < messages.length; i++) {
|
|
231
|
+
const msg = messages[i]
|
|
232
|
+
if (
|
|
233
|
+
getMessageId(msg, i) === targetId ||
|
|
234
|
+
String(msg?.id) === targetId ||
|
|
235
|
+
String(msg?.timestamp) === targetId ||
|
|
236
|
+
String(msg?.created_at) === targetId
|
|
237
|
+
) {
|
|
238
|
+
startIndex = i + 1
|
|
239
|
+
isBoundaryFound = true
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 1. Cek apakah ada pesan asisten terakhir yang memiliki usage.total_tokens resmi dari DB / API
|
|
246
|
+
// Jika ada pemadatan aktif (lastCompactedAt), hanya gunakan usage dari pesan yang dibuat SETELAH pemadatan
|
|
247
|
+
let lastUsageAssistantIdx = -1
|
|
248
|
+
for (let i = messages.length - 1; i >= startIndex; i--) {
|
|
249
|
+
const msg = messages[i]
|
|
250
|
+
if (
|
|
251
|
+
msg &&
|
|
252
|
+
(msg.role === 'ai' || msg.role === 'assistant') &&
|
|
253
|
+
msg.usage &&
|
|
254
|
+
typeof msg.usage.total_tokens === 'number' &&
|
|
255
|
+
msg.usage.total_tokens > 0
|
|
256
|
+
) {
|
|
257
|
+
if (lastCompactedAt && msg.created_at && Number(msg.created_at) < Number(lastCompactedAt)) {
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
lastUsageAssistantIdx = i
|
|
261
|
+
break
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (lastUsageAssistantIdx !== -1) {
|
|
266
|
+
let total = messages[lastUsageAssistantIdx].usage.total_tokens
|
|
267
|
+
for (let i = lastUsageAssistantIdx + 1; i < messages.length; i++) {
|
|
268
|
+
const msg = messages[i]
|
|
269
|
+
if (
|
|
270
|
+
!msg ||
|
|
271
|
+
msg.isThinking ||
|
|
272
|
+
msg.isSearching ||
|
|
273
|
+
msg.isSummarizing ||
|
|
274
|
+
msg.role === 'command'
|
|
275
|
+
) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
total += calculateMessageTokens(msg)
|
|
279
|
+
}
|
|
280
|
+
return total
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 2. Fallback: Hitung token via BPE tokenizer (gpt-tokenizer) dengan caching per-pesan
|
|
284
|
+
let total = countTokens(systemPrompt)
|
|
285
|
+
|
|
286
|
+
if (isBoundaryFound && typeof summaryBlock === 'string') {
|
|
287
|
+
total += countTokens(summaryBlock)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
291
|
+
const msg = messages[i]
|
|
292
|
+
if (!msg || msg.isThinking || msg.isSearching || msg.isSummarizing || msg.role === 'command') {
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
total += calculateMessageTokens(msg)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return total
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Kompatibilitas mundur: Menghitung session tokens (dialihkan langsung ke calculateSessionTokens)
|
|
303
|
+
*/
|
|
304
|
+
export function calculateSessionChars(
|
|
305
|
+
messages = [],
|
|
306
|
+
summaryBlock = '',
|
|
307
|
+
lastCompactedMessageId = null,
|
|
308
|
+
systemPrompt = '',
|
|
309
|
+
lastCompactedAt = null
|
|
310
|
+
) {
|
|
311
|
+
return calculateSessionTokens(
|
|
312
|
+
messages,
|
|
313
|
+
summaryBlock,
|
|
314
|
+
lastCompactedMessageId,
|
|
315
|
+
systemPrompt,
|
|
316
|
+
lastCompactedAt
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Fase 1 Hermes: Prune Old Tool Results (murah, O(n), tanpa panggilan LLM)
|
|
322
|
+
* Mengganti output tool lama (> 200 karakter) di luar tail dengan placeholder:
|
|
323
|
+
* "[Old tool output cleared to save context space]"
|
|
324
|
+
*/
|
|
325
|
+
export function pruneOldToolResultsInLoop(messages = [], protectLastN = 6) {
|
|
326
|
+
if (!Array.isArray(messages) || messages.length === 0) return []
|
|
327
|
+
|
|
328
|
+
const cloned = messages.map((m) => {
|
|
329
|
+
if (!m) return m
|
|
330
|
+
return {
|
|
331
|
+
...m,
|
|
332
|
+
executionSteps: Array.isArray(m.executionSteps)
|
|
333
|
+
? m.executionSteps.map((t) => ({ ...t }))
|
|
334
|
+
: m.executionSteps,
|
|
335
|
+
executedTools: Array.isArray(m.executedTools)
|
|
336
|
+
? m.executedTools.map((t) => ({ ...t }))
|
|
337
|
+
: m.executedTools
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// Tentukan batas ekor (tail) yang dilindungi 100%
|
|
342
|
+
const tailBoundary = Math.max(0, cloned.length - protectLastN)
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < tailBoundary; i++) {
|
|
345
|
+
const msg = cloned[i]
|
|
346
|
+
if (!msg) continue
|
|
347
|
+
|
|
348
|
+
// 1. Pesan role: 'tool' (Format native OpenAI/ReAct)
|
|
349
|
+
if (msg.role === 'tool') {
|
|
350
|
+
let shouldPrune = false
|
|
351
|
+
let parsed = null
|
|
352
|
+
|
|
353
|
+
if (typeof msg.content === 'string') {
|
|
354
|
+
if (msg.content.length > OLD_TOOL_PRUNE_CHAR_LIMIT) {
|
|
355
|
+
shouldPrune = true
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(msg.content)
|
|
358
|
+
} catch {
|
|
359
|
+
parsed = null
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
363
|
+
shouldPrune = true
|
|
364
|
+
parsed = msg.content
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (shouldPrune) {
|
|
368
|
+
if (parsed && typeof parsed === 'object') {
|
|
369
|
+
if (parsed.data || parsed.output || parsed.result) {
|
|
370
|
+
parsed.data = CLEARED_TOOL_PLACEHOLDER
|
|
371
|
+
if (parsed.output) parsed.output = CLEARED_TOOL_PLACEHOLDER
|
|
372
|
+
if (parsed.result) parsed.result = CLEARED_TOOL_PLACEHOLDER
|
|
373
|
+
msg.content = JSON.stringify(parsed)
|
|
374
|
+
} else {
|
|
375
|
+
msg.content = CLEARED_TOOL_PLACEHOLDER
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
msg.content = CLEARED_TOOL_PLACEHOLDER
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 2. Pesan dengan executionSteps / executedTools (Format historis chat MARK)
|
|
384
|
+
const targetSteps = msg.executionSteps || msg.executedTools
|
|
385
|
+
if (Array.isArray(targetSteps) && targetSteps.length > 0) {
|
|
386
|
+
const pruned = targetSteps.map((t) => {
|
|
387
|
+
if (!t || t.type === 'narration') return t
|
|
388
|
+
const next = { ...t }
|
|
389
|
+
if (typeof t.fullResult === 'string' && t.fullResult.length > OLD_TOOL_PRUNE_CHAR_LIMIT) {
|
|
390
|
+
next.fullResult = t.resultSummary || CLEARED_TOOL_PLACEHOLDER
|
|
391
|
+
}
|
|
392
|
+
// Bersihkan data URL base64 preview dari pesan lama untuk menghemat RAM dan storage
|
|
393
|
+
if (typeof next.preview === 'string' && next.preview.startsWith('data:image/')) {
|
|
394
|
+
next.preview = null
|
|
395
|
+
}
|
|
396
|
+
return next
|
|
397
|
+
})
|
|
398
|
+
if (msg.executionSteps) msg.executionSteps = pruned
|
|
399
|
+
if (msg.executedTools) msg.executedTools = pruned
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 3. Kompaksi blok kode panjang di teks lama jika > 500 char
|
|
403
|
+
if (typeof msg.content === 'string' && msg.content.length > 500) {
|
|
404
|
+
msg.content = compactCodeBlocks(msg.content)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return cloned
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Kompatibilitas mundur: Pruning output tool lama di memori
|
|
413
|
+
*/
|
|
414
|
+
export function pruneOldToolResultsInMemory(messages = [], preserveRecentTurns = 4) {
|
|
415
|
+
return pruneOldToolResultsInLoop(messages, preserveRecentTurns * 2)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Fase 2 Hermes: Penyelarasan batas mundur (Boundary Backward Alignment)
|
|
420
|
+
* Menjaga agar pasangan tool_calls pada role assistant dan tool_result pada role tool
|
|
421
|
+
* tidak pernah terputus/terbelah di antara batas potongan konteks.
|
|
422
|
+
*/
|
|
423
|
+
export function alignBoundaryBackward(messages = [], splitIndex = 0) {
|
|
424
|
+
if (splitIndex <= 0 || splitIndex >= messages.length) return splitIndex
|
|
425
|
+
|
|
426
|
+
let idx = splitIndex
|
|
427
|
+
|
|
428
|
+
// Jika elemen di splitIndex adalah role: 'tool', mundur hingga ke role assistant pemanggilnya
|
|
429
|
+
while (idx > 0 && messages[idx]?.role === 'tool') {
|
|
430
|
+
idx--
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Jika elemen sebelumnya adalah assistant dengan tool_calls yang hasilnya berada di/setelah splitIndex,
|
|
434
|
+
// mundur agar assistant ini berada di blok yang sama dengan hasil tool-nya
|
|
435
|
+
if (idx > 0 && messages[idx]?.role === 'assistant' && Array.isArray(messages[idx]?.tool_calls)) {
|
|
436
|
+
return idx
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return idx
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Tahap 2: AI Summarizer inkremental
|
|
444
|
+
* Merangkum zona pesan lama + menyertakan existingSummaryBlock sebelumnya
|
|
445
|
+
*/
|
|
446
|
+
export async function summarizeMiddle(
|
|
447
|
+
messagesToSummarize = [],
|
|
448
|
+
existingSummaryBlock = '',
|
|
449
|
+
activeConfig = {}
|
|
450
|
+
) {
|
|
451
|
+
if (!Array.isArray(messagesToSummarize) || messagesToSummarize.length === 0) {
|
|
452
|
+
return existingSummaryBlock || ''
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Format percakapan baru yang meluap menjadi dialog terstruktur
|
|
456
|
+
const formattedLines = []
|
|
457
|
+
for (let i = 0; i < messagesToSummarize.length; i++) {
|
|
458
|
+
const msg = messagesToSummarize[i]
|
|
459
|
+
if (!msg || msg.isThinking || msg.isSearching || msg.isSummarizing || msg.role === 'command') {
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const sender = msg.role === 'user' ? 'User' : 'Mark'
|
|
464
|
+
let text = ''
|
|
465
|
+
if (typeof msg.content === 'string') {
|
|
466
|
+
text = msg.content
|
|
467
|
+
} else if (Array.isArray(msg.content)) {
|
|
468
|
+
const textParts = []
|
|
469
|
+
for (const part of msg.content) {
|
|
470
|
+
if (!part) continue
|
|
471
|
+
if (typeof part === 'string') textParts.push(part)
|
|
472
|
+
else if (part.type === 'text') textParts.push(part.text || '')
|
|
473
|
+
else if (part.type === 'image_url' || part.image_url || part.type === 'image') {
|
|
474
|
+
textParts.push('[Gambar terlampir oleh pengguna]')
|
|
475
|
+
} else {
|
|
476
|
+
textParts.push(JSON.stringify(part))
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
text = textParts.join('\n')
|
|
480
|
+
} else {
|
|
481
|
+
text = JSON.stringify(msg.content || '')
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Ringkas teks jika per giliran terlalu panjang agar tidak overload summarizer
|
|
485
|
+
if (text.length > 2500) {
|
|
486
|
+
text =
|
|
487
|
+
text.substring(0, 1200) + '\n... [potongan teks panjang diringkas] ...\n' + text.slice(-800)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
formattedLines.push(`[Turn ${i + 1}] ${sender}: ${text}`)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
let messagesText = formattedLines.join('\n\n')
|
|
494
|
+
|
|
495
|
+
// Safety buffer: jika pesan lama jutaan karakter (legacy backlog), ambil jendela potongan terdekat max ~90.000 karakter
|
|
496
|
+
if (messagesText.length > 90000) {
|
|
497
|
+
messagesText = messagesText.slice(-90000)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const systemPrompt = `Kamu adalah Context Compressor untuk MARK AI OS (Handover Engine).
|
|
501
|
+
Tugasmu: Hasilkan DOKUMEN SERAH-TERIMA TEKNIS yang padat, terstruktur, dan akurat dengan format markdown berikut:
|
|
502
|
+
|
|
503
|
+
## Goal
|
|
504
|
+
[Apa yang ingin dicapai pengguna dalam tugas atau percakapan ini]
|
|
505
|
+
|
|
506
|
+
## Constraints & Preferences
|
|
507
|
+
[Aturan teknis, batasan pengguna, preferensi gaya koding atau arsitektur]
|
|
508
|
+
|
|
509
|
+
## Progress
|
|
510
|
+
### Done
|
|
511
|
+
[Pekerjaan yang telah selesai dilakukan: berkas yang diedit/dibuat, perintah shell, status verifikasi]
|
|
512
|
+
### In Progress
|
|
513
|
+
[Pekerjaan atau sub-langkah yang sedang berjalan saat ini]
|
|
514
|
+
### Blocked
|
|
515
|
+
[Hambatan, bug, atau kendala jika ada]
|
|
516
|
+
|
|
517
|
+
## Key Decisions
|
|
518
|
+
[Keputusan arsitektural/teknis penting dan alasannya]
|
|
519
|
+
|
|
520
|
+
## Relevant Files
|
|
521
|
+
[Daftar berkas yang dibaca, dimodifikasi, atau dibuat beserta catatan singkat 1 baris mengenai fungsinya]
|
|
522
|
+
|
|
523
|
+
## Next Steps
|
|
524
|
+
[Langkah kerja konkret berikutnya yang harus dilanjutkan]
|
|
525
|
+
|
|
526
|
+
## Critical Context
|
|
527
|
+
[Nilai konfigurasi, port, nama variabel, ID penting, atau pesan error mentah yang esensial]
|
|
528
|
+
|
|
529
|
+
Aturan Mutlak:
|
|
530
|
+
1. Jika terdapat [RINGKASAN SERAH-TERIMA SEBELUMNYA], PERBARUI dokumen tersebut: pindahkan item dari "In Progress" ke "Done", tambahkan berkas baru ke "Relevant Files", perbarui "Next Steps", dan singkirkan detail usang.
|
|
531
|
+
2. Pertahankan akurasi path berkas, nama fungsi, dan detail teknis tanpa halusinasi.
|
|
532
|
+
3. HANYA cetak dokumen markdown serah-terima di atas secara langsung tanpa kalimat pembuka atau penutup basa-basi.`
|
|
533
|
+
|
|
534
|
+
const userPrompt = `[RINGKASAN SERAH-TERIMA SEBELUMNYA]:
|
|
535
|
+
${existingSummaryBlock ? existingSummaryBlock.trim() : '(Belum ada ringkasan sebelumnya / inisiasi pertama kali)'}
|
|
536
|
+
|
|
537
|
+
[PERCAKAPAN BARU YANG HARUS DIRANGKUM KE DOKUMEN SERAH-TERIMA]:
|
|
538
|
+
${messagesText}`
|
|
539
|
+
|
|
540
|
+
const promptPayload = [
|
|
541
|
+
{ role: 'system', content: systemPrompt },
|
|
542
|
+
{ role: 'user', content: userPrompt }
|
|
543
|
+
]
|
|
544
|
+
|
|
545
|
+
// 1. Coba Gemini Web terlebih dahulu (gratis, hemat kuota)
|
|
546
|
+
try {
|
|
547
|
+
const response = await fetchAI(promptPayload, false, {
|
|
548
|
+
isSmallTask: true,
|
|
549
|
+
configOverride: { aiProvider: 'gemini-web', geminiWebModel: 'gemini-3.5-flash-thinking' }
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
if (response && response.content && !response.error) {
|
|
553
|
+
return response.content.trim()
|
|
554
|
+
}
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.warn(
|
|
557
|
+
'[contextManager] Gemini Web summarizer gagal, beralih ke active provider:',
|
|
558
|
+
err?.message
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 2. Fallback ke active provider sesi jika offline / Gemini Web gagal
|
|
563
|
+
try {
|
|
564
|
+
const response = await fetchAI(promptPayload, false, {
|
|
565
|
+
isSmallTask: true,
|
|
566
|
+
config: activeConfig
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
if (response && response.content && !response.error) {
|
|
570
|
+
return response.content.trim()
|
|
571
|
+
}
|
|
572
|
+
} catch (err) {
|
|
573
|
+
console.error('[contextManager] Fallback active provider summarizer gagal:', err?.message)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 3. Jika AI sama sekali tidak dapat dijangkau, buat stub ringkasan darurat berbasis teks
|
|
577
|
+
return `${existingSummaryBlock ? existingSummaryBlock + '\n\n' : ''}[Arsip percakapan lampau: ${messagesToSummarize.length} pesan telah dikompaksi pada ${new Date().toLocaleString('id-ID')}]`
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Tahap 3: Pembersihan pasangan tool yang orphan
|
|
582
|
+
*/
|
|
583
|
+
export function cleanOrphanToolPairs(messages = []) {
|
|
584
|
+
if (!Array.isArray(messages)) return []
|
|
585
|
+
|
|
586
|
+
const valid = []
|
|
587
|
+
const toolCallIds = new Set()
|
|
588
|
+
|
|
589
|
+
// Kumpulkan semua tool_call IDs yang valid
|
|
590
|
+
for (const msg of messages) {
|
|
591
|
+
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
|
|
592
|
+
for (const tc of msg.tool_calls) {
|
|
593
|
+
if (tc.id) toolCallIds.add(tc.id)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (const msg of messages) {
|
|
599
|
+
// Jika ada tool response tetapi tool_call pasangannya hilang, abaikan agar tidak error 400
|
|
600
|
+
if (msg.role === 'tool' && msg.tool_call_id && !toolCallIds.has(msg.tool_call_id)) {
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
valid.push(msg)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return valid
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Orkestrator Utama: Menjalankan Hybrid Compaction untuk satu sesi
|
|
611
|
+
*/
|
|
612
|
+
export async function executeSessionCompaction({
|
|
613
|
+
sessionId = '1',
|
|
614
|
+
messages = [],
|
|
615
|
+
activeConfig = {},
|
|
616
|
+
onProgress = null,
|
|
617
|
+
force = false,
|
|
618
|
+
systemPrompt = ''
|
|
619
|
+
}) {
|
|
620
|
+
// Ambil summary & pointer sebelumnya dari tabel session_compact jika ada
|
|
621
|
+
let existingSummaryBlock = ''
|
|
622
|
+
let existingLastCompactedId = null
|
|
623
|
+
try {
|
|
624
|
+
const existingData = await getSessionCompact(sessionId)
|
|
625
|
+
if (existingData) {
|
|
626
|
+
existingSummaryBlock = existingData.summaryBlock || existingData.summary_block || ''
|
|
627
|
+
existingLastCompactedId =
|
|
628
|
+
existingData.lastCompactedMessageId || existingData.last_compacted_message_id || null
|
|
629
|
+
}
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.warn('[contextManager] Gagal mengambil session_compact lama:', err)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const currentTokens = calculateSessionTokens(
|
|
635
|
+
messages,
|
|
636
|
+
existingSummaryBlock,
|
|
637
|
+
existingLastCompactedId,
|
|
638
|
+
systemPrompt
|
|
639
|
+
)
|
|
640
|
+
if (!force && currentTokens < MAX_CONTEXT_TOKENS) {
|
|
641
|
+
return {
|
|
642
|
+
success: true,
|
|
643
|
+
isCompacted: false,
|
|
644
|
+
compactedMessages: messages,
|
|
645
|
+
currentTokens,
|
|
646
|
+
currentChars: currentTokens
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// -------------------------------------------------------------
|
|
651
|
+
// TAHAP 1: Pruning Tool Output di Memori
|
|
652
|
+
// -------------------------------------------------------------
|
|
653
|
+
if (typeof onProgress === 'function') {
|
|
654
|
+
onProgress({ stage: 'pruning', text: 'Memangkas log tool di memori...' })
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const prunedMessages = pruneOldToolResultsInMemory(messages, 4)
|
|
658
|
+
const prunedTokens = calculateSessionTokens(
|
|
659
|
+
prunedMessages,
|
|
660
|
+
existingSummaryBlock,
|
|
661
|
+
existingLastCompactedId,
|
|
662
|
+
systemPrompt
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
// Jika Tahap 1 saja sudah cukup membawa token di bawah batas (hanya saat auto-compact / !force):
|
|
666
|
+
if (!force && prunedTokens < MAX_CONTEXT_TOKENS) {
|
|
667
|
+
// Simpan hasil prune ke tabel sessions
|
|
668
|
+
try {
|
|
669
|
+
await saveSession(sessionId, prunedMessages)
|
|
670
|
+
} catch (e) {
|
|
671
|
+
console.warn('[contextManager] Gagal menyimpan pruned messages ke sessions:', e)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
success: true,
|
|
676
|
+
isCompacted: true,
|
|
677
|
+
prunedOnly: true,
|
|
678
|
+
compactedMessages: prunedMessages,
|
|
679
|
+
currentTokens: prunedTokens,
|
|
680
|
+
currentChars: prunedTokens
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// -------------------------------------------------------------
|
|
685
|
+
// TAHAP 2: AI Summarization (Sliding Window & Delta)
|
|
686
|
+
// -------------------------------------------------------------
|
|
687
|
+
if (typeof onProgress === 'function') {
|
|
688
|
+
onProgress({ stage: 'summarizing', text: 'Merangkum konteks percakapan lama...' })
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Hanya pertahankan 1 pesan terakhir (prompt user yang baru dikirim pada index N)
|
|
692
|
+
// Seluruh riwayat pesan sebelumnya (0 s/d N-1) dirangkum ke dalam summary block.
|
|
693
|
+
const tailStartIndex = Math.max(0, prunedMessages.length - 1)
|
|
694
|
+
|
|
695
|
+
// Cari posisi batas pemadatan sebelumnya jika ada
|
|
696
|
+
let startIndexToSummarize = 0
|
|
697
|
+
if (existingLastCompactedId) {
|
|
698
|
+
const targetId = String(existingLastCompactedId)
|
|
699
|
+
for (let i = 0; i < prunedMessages.length; i++) {
|
|
700
|
+
const msg = prunedMessages[i]
|
|
701
|
+
if (
|
|
702
|
+
getMessageId(msg, i) === targetId ||
|
|
703
|
+
String(msg?.id) === targetId ||
|
|
704
|
+
String(msg?.timestamp) === targetId
|
|
705
|
+
) {
|
|
706
|
+
startIndexToSummarize = i + 1
|
|
707
|
+
break
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const effectiveTailIndex = Math.max(tailStartIndex, startIndexToSummarize)
|
|
713
|
+
const messagesToSummarize = prunedMessages.slice(startIndexToSummarize, effectiveTailIndex)
|
|
714
|
+
const tailMessages = prunedMessages.slice(effectiveTailIndex)
|
|
715
|
+
|
|
716
|
+
let newSummaryBlock = existingSummaryBlock
|
|
717
|
+
let lastCompactedMessageId = existingLastCompactedId
|
|
718
|
+
|
|
719
|
+
if (messagesToSummarize.length > 0) {
|
|
720
|
+
const lastCompactedMsg = messagesToSummarize[messagesToSummarize.length - 1]
|
|
721
|
+
if (!lastCompactedMsg.id && !lastCompactedMsg.timestamp && !lastCompactedMsg.created_at) {
|
|
722
|
+
lastCompactedMsg.created_at = Date.now()
|
|
723
|
+
lastCompactedMsg.id = `msg_${lastCompactedMsg.created_at}`
|
|
724
|
+
}
|
|
725
|
+
lastCompactedMessageId = getMessageId(lastCompactedMsg, effectiveTailIndex - 1)
|
|
726
|
+
|
|
727
|
+
newSummaryBlock = await summarizeMiddle(messagesToSummarize, existingSummaryBlock, activeConfig)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Simpan metadata ke tabel session_compact
|
|
731
|
+
try {
|
|
732
|
+
await saveSessionCompact(sessionId, {
|
|
733
|
+
summaryBlock: newSummaryBlock,
|
|
734
|
+
lastCompactedMessageId,
|
|
735
|
+
lastCompactedAt: Date.now()
|
|
736
|
+
})
|
|
737
|
+
} catch (err) {
|
|
738
|
+
console.error('[contextManager] Gagal menyimpan session_compact:', err)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Simpan juga versi pesan yang sudah dipangkas ke sessions agar database ramping
|
|
742
|
+
try {
|
|
743
|
+
await saveSession(sessionId, prunedMessages)
|
|
744
|
+
} catch (e) {
|
|
745
|
+
console.warn('[contextManager] Gagal menyimpan sessions:', e)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const finalTokens = calculateSessionTokens(
|
|
749
|
+
tailMessages,
|
|
750
|
+
newSummaryBlock,
|
|
751
|
+
null, // tailMessages sudah merupakan pesan setelah cutIndex
|
|
752
|
+
systemPrompt
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
success: true,
|
|
757
|
+
isCompacted: true,
|
|
758
|
+
prunedOnly: false,
|
|
759
|
+
compactedMessages: prunedMessages,
|
|
760
|
+
tailMessages,
|
|
761
|
+
lastCompactedMessageId,
|
|
762
|
+
newSummaryBlock,
|
|
763
|
+
currentTokens: finalTokens,
|
|
764
|
+
currentChars: finalTokens
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Memformat pesan dengan riwayat executionSteps/executedTools utuh (100% fullResult tanpa batasan turn)
|
|
770
|
+
*/
|
|
771
|
+
export function formatMessageWithToolLogs(msg) {
|
|
772
|
+
if (!msg) return ''
|
|
773
|
+
const steps = msg.executionSteps || msg.executedTools
|
|
774
|
+
if (Array.isArray(msg.content)) {
|
|
775
|
+
let newContent = [...msg.content]
|
|
776
|
+
if (Array.isArray(steps) && steps.length > 0) {
|
|
777
|
+
const toolLog = steps
|
|
778
|
+
.map((t) => {
|
|
779
|
+
if (t.type === 'intervention') {
|
|
780
|
+
return ` * [Intervensi Pengguna]: "${t.text || ''}"`
|
|
781
|
+
}
|
|
782
|
+
if (t.type === 'narration' || (!t.tool && t.text)) {
|
|
783
|
+
return ` * [Catatan Narasi AI]: "${t.text || ''}"`
|
|
784
|
+
}
|
|
785
|
+
const res = t.fullResult || t.resultSummary || 'OK'
|
|
786
|
+
const reasonStr = t.reason ? ` (${t.reason})` : ''
|
|
787
|
+
return ` * [Tool: ${t.tool || t.task || 'tool'}]${reasonStr} query: "${t.query || ''}"\n Hasil:\n${res}`
|
|
788
|
+
})
|
|
789
|
+
.join('\n\n')
|
|
790
|
+
if (toolLog) {
|
|
791
|
+
newContent.push({ type: 'text', text: `\n\n[RIWAYAT LANGKAH EKSEKUSI TURN INI]:\n${toolLog}` })
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const role = (msg.role || '').toLowerCase()
|
|
795
|
+
if ((role === 'ai' || role === 'assistant') && msg.mood) {
|
|
796
|
+
const textItemIdx = newContent.findIndex((p) => p && p.type === 'text')
|
|
797
|
+
if (textItemIdx >= 0) {
|
|
798
|
+
if (!/<mark\b/i.test(newContent[textItemIdx].text || '')) {
|
|
799
|
+
newContent[textItemIdx] = {
|
|
800
|
+
...newContent[textItemIdx],
|
|
801
|
+
text: `<mark mood="${msg.mood}" done="true" /> ${newContent[textItemIdx].text || ''}`
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
newContent.unshift({ type: 'text', text: `<mark mood="${msg.mood}" done="true" />` })
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return newContent
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || '')
|
|
812
|
+
if (Array.isArray(steps) && steps.length > 0) {
|
|
813
|
+
const toolLog = steps
|
|
814
|
+
.map((t) => {
|
|
815
|
+
if (t.type === 'intervention') {
|
|
816
|
+
return ` * [Intervensi Pengguna]: "${t.text || ''}"`
|
|
817
|
+
}
|
|
818
|
+
if (t.type === 'narration' || (!t.tool && t.text)) {
|
|
819
|
+
return ` * [Catatan Narasi AI]: "${t.text || ''}"`
|
|
820
|
+
}
|
|
821
|
+
const res = t.fullResult || t.resultSummary || 'OK'
|
|
822
|
+
const reasonStr = t.reason ? ` (${t.reason})` : ''
|
|
823
|
+
return ` * [Tool: ${t.tool || t.task || 'tool'}]${reasonStr} query: "${t.query || ''}"\n Hasil:\n${res}`
|
|
824
|
+
})
|
|
825
|
+
.join('\n\n')
|
|
826
|
+
if (toolLog) {
|
|
827
|
+
content = `[RIWAYAT LANGKAH EKSEKUSI TURN INI]:\n${toolLog}\n\n[JAWABAN]:\n${content}`
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Sisipkan tag mood pada pesan asisten jika ada dan belum tersemat
|
|
832
|
+
const role = (msg.role || '').toLowerCase()
|
|
833
|
+
if ((role === 'ai' || role === 'assistant') && msg.mood && !/<mark\b/i.test(content)) {
|
|
834
|
+
content = `<mark mood="${msg.mood}" done="true" /> ${content}`
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return content
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Merakit payload prompt LLM secara non-destructive dengan summary block jika tersedia,
|
|
842
|
+
* atau seluruh pesan 100% utuh beserta log tool jika belum melewati 525K karakter.
|
|
843
|
+
*/
|
|
844
|
+
export function assembleCompactedPayload({
|
|
845
|
+
messages = [],
|
|
846
|
+
sessionCompact = null,
|
|
847
|
+
systemPrompt = ''
|
|
848
|
+
}) {
|
|
849
|
+
const payload = []
|
|
850
|
+
|
|
851
|
+
if (systemPrompt) {
|
|
852
|
+
payload.push({ role: 'system', content: systemPrompt })
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const summaryBlock = sessionCompact?.summaryBlock || sessionCompact?.summary_block
|
|
856
|
+
const lastCompactedId =
|
|
857
|
+
sessionCompact?.lastCompactedMessageId || sessionCompact?.last_compacted_message_id
|
|
858
|
+
|
|
859
|
+
if (summaryBlock && lastCompactedId) {
|
|
860
|
+
// Cari index dari pesan dengan id lastCompactedId
|
|
861
|
+
let cutIndex = -1
|
|
862
|
+
for (let i = 0; i < messages.length; i++) {
|
|
863
|
+
const msg = messages[i]
|
|
864
|
+
if (
|
|
865
|
+
getMessageId(msg, i) === String(lastCompactedId) ||
|
|
866
|
+
String(msg?.id) === String(lastCompactedId) ||
|
|
867
|
+
String(msg?.timestamp) === String(lastCompactedId) ||
|
|
868
|
+
String(msg?.created_at) === String(lastCompactedId)
|
|
869
|
+
) {
|
|
870
|
+
cutIndex = i
|
|
871
|
+
break
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (cutIndex !== -1) {
|
|
876
|
+
// Jika pesan batas ditemukan, ambil pesan setelah cutIndex
|
|
877
|
+
const activeSlice = messages.slice(cutIndex + 1)
|
|
878
|
+
|
|
879
|
+
// Sisipkan summary block sebagai pesan user pembuka konteks
|
|
880
|
+
payload.push({
|
|
881
|
+
role: 'user',
|
|
882
|
+
content: `[ COMPACTED MESSAGE SUMMARY ] ${summaryBlock}`
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
// Masukkan pesan-pesan tail terkini dengan log tool utuh
|
|
886
|
+
for (const msg of activeSlice) {
|
|
887
|
+
if (
|
|
888
|
+
!msg ||
|
|
889
|
+
msg.isThinking ||
|
|
890
|
+
msg.isSearching ||
|
|
891
|
+
msg.isSummarizing ||
|
|
892
|
+
msg.role === 'command'
|
|
893
|
+
) {
|
|
894
|
+
continue
|
|
895
|
+
}
|
|
896
|
+
payload.push({
|
|
897
|
+
role: msg.role === 'ai' || msg.role === 'planSteps' ? 'assistant' : msg.role,
|
|
898
|
+
content: formatMessageWithToolLogs(msg),
|
|
899
|
+
mood: msg.mood || undefined
|
|
900
|
+
})
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return cleanOrphanToolPairs(payload)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Jika cutIndex === -1 (ID batas tidak ditemukan di riwayat aktif),
|
|
907
|
+
// artinya pointer kompaksi sudah kadaluarsa. JANGAN sisipkan ringkasan basi
|
|
908
|
+
// dan JANGAN memenggal pesan ke slice(-1)! Alirkan seluruh riwayat pesan utuh.
|
|
909
|
+
console.warn(
|
|
910
|
+
`[contextManager] Pointer lastCompactedId "${lastCompactedId}" tidak ditemukan di array pesan aktif. Mengabaikan ringkasan basi dan mengirim seluruh riwayat pesan utuh.`
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Jika belum ada summary (konteks < 525K), susun SELURUH pesan dan log tool 100% UTUH
|
|
915
|
+
for (const msg of messages) {
|
|
916
|
+
if (!msg || msg.isThinking || msg.isSearching || msg.isSummarizing || msg.role === 'command') {
|
|
917
|
+
continue
|
|
918
|
+
}
|
|
919
|
+
payload.push({
|
|
920
|
+
role: msg.role === 'ai' || msg.role === 'planSteps' ? 'assistant' : msg.role,
|
|
921
|
+
content: formatMessageWithToolLogs(msg),
|
|
922
|
+
mood: msg.mood || undefined
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return cleanOrphanToolPairs(payload)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* In-Flight Pruning untuk loop ReAct.
|
|
931
|
+
* Berbasis kapasitas ambang batas 256K tokens (MAX_CONTEXT_TOKENS).
|
|
932
|
+
* Jika selama giliran panjang akumulasi loopMessages >= MAX_CONTEXT_TOKENS,
|
|
933
|
+
* pangkas data output tool terlama di dalam loopMessages sampai total token kembali < MAX_CONTEXT_TOKENS.
|
|
934
|
+
*/
|
|
935
|
+
export function pruneInFlightMessages(messages = [], maxTokens = MAX_CONTEXT_TOKENS) {
|
|
936
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages
|
|
937
|
+
|
|
938
|
+
let totalTokens = 0
|
|
939
|
+
for (const m of messages) {
|
|
940
|
+
totalTokens += calculateMessageTokens(m)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (totalTokens < maxTokens) return messages
|
|
944
|
+
|
|
945
|
+
// Pangkas output tool terlama satu per satu sampai di bawah maxTokens
|
|
946
|
+
for (let i = 0; i < messages.length; i++) {
|
|
947
|
+
if (totalTokens < maxTokens) break
|
|
948
|
+
const m = messages[i]
|
|
949
|
+
if (m && m.role === 'tool' && m.content) {
|
|
950
|
+
const origTokens = calculateMessageTokens(m)
|
|
951
|
+
let parsed = null
|
|
952
|
+
try {
|
|
953
|
+
parsed = typeof m.content === 'string' ? JSON.parse(m.content) : m.content
|
|
954
|
+
} catch (err) {
|
|
955
|
+
void err
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (parsed && (parsed.data || parsed.output)) {
|
|
959
|
+
const prunedText = '[Output dipangkas: kapasitas sesi mencapai 256K tokens]'
|
|
960
|
+
parsed.data = prunedText
|
|
961
|
+
if (parsed.output) parsed.output = prunedText
|
|
962
|
+
m.content = JSON.stringify(parsed)
|
|
963
|
+
m.tokens = null // invalidate cache
|
|
964
|
+
const newTokens = calculateMessageTokens(m)
|
|
965
|
+
totalTokens -= origTokens - newTokens
|
|
966
|
+
} else if (typeof m.content === 'string' && m.content.length > 200) {
|
|
967
|
+
m.content = '[Output tool lama dipangkas karena kapasitas 256K tokens]'
|
|
968
|
+
m.tokens = null
|
|
969
|
+
const newTokens = calculateMessageTokens(m)
|
|
970
|
+
totalTokens -= origTokens - newTokens
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return messages
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* In-Loop Context Guard untuk ReAct loop (Lead Agent & Sub-Agent).
|
|
980
|
+
* Dipanggil di setiap iterasi ReAct loop sebelum mengirim prompt ke LLM.
|
|
981
|
+
*
|
|
982
|
+
* Mengimplementasikan 4 Fase Hermes secara in-place:
|
|
983
|
+
* - Fase 1: Zero-cost O(n) pruning pada tool output lama di luar tail (>200 karakter)
|
|
984
|
+
* - Fase 2: Backward boundary alignment untuk menjaga pasangan tool_calls dan tool_result
|
|
985
|
+
* - Fase 3: Structured Handover Summary dengan update iteratif
|
|
986
|
+
* - Fase 4: Perakitan pesan in-place stabil pada session ID yang sama
|
|
987
|
+
*/
|
|
988
|
+
export async function checkAndCompressInLoop({
|
|
989
|
+
loopMessages = [],
|
|
990
|
+
sessionId = '1',
|
|
991
|
+
maxTokens = MAX_CONTEXT_TOKENS,
|
|
992
|
+
maxChars = null, // backward compatibility
|
|
993
|
+
thresholdRatio = IN_LOOP_COMPACT_THRESHOLD,
|
|
994
|
+
protectLastN = 6,
|
|
995
|
+
protectFirstN = 2,
|
|
996
|
+
activeConfig = {},
|
|
997
|
+
onProgress = null,
|
|
998
|
+
systemPrompt = ''
|
|
999
|
+
}) {
|
|
1000
|
+
if (!Array.isArray(loopMessages) || loopMessages.length <= protectLastN + protectFirstN) {
|
|
1001
|
+
return { compressed: false, loopMessages }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const effectiveMaxTokens = maxTokens || MAX_CONTEXT_TOKENS
|
|
1005
|
+
const triggerLimit = effectiveMaxTokens * thresholdRatio
|
|
1006
|
+
let totalTokens = countTokens(systemPrompt)
|
|
1007
|
+
let sessionTokens = 0
|
|
1008
|
+
for (const m of loopMessages) {
|
|
1009
|
+
const tokens = calculateMessageTokens(m)
|
|
1010
|
+
totalTokens += tokens
|
|
1011
|
+
if (m.role !== 'system') {
|
|
1012
|
+
sessionTokens += tokens
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Jika masih di bawah ambang batas (50% dari 256K) dan jumlah pesan belum terlalu panjang (< 24),
|
|
1017
|
+
// tidak memerlukan kompresi
|
|
1018
|
+
if (totalTokens < triggerLimit && loopMessages.length < 24) {
|
|
1019
|
+
return {
|
|
1020
|
+
compressed: false,
|
|
1021
|
+
loopMessages,
|
|
1022
|
+
totalTokens: sessionTokens,
|
|
1023
|
+
totalChars: sessionTokens
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (typeof onProgress === 'function') {
|
|
1028
|
+
onProgress({ stage: 'pruning', text: 'In-loop: Memangkas log tool lama...' })
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// FASE 1: Zero-cost O(n) Tool Pruning
|
|
1032
|
+
const prunedMessages = pruneOldToolResultsInLoop(loopMessages, protectLastN)
|
|
1033
|
+
let prunedTokens = countTokens(systemPrompt)
|
|
1034
|
+
let prunedSessionTokens = 0
|
|
1035
|
+
for (const m of prunedMessages) {
|
|
1036
|
+
m.tokens = null // invalidate cache for pruned
|
|
1037
|
+
const tokens = calculateMessageTokens(m)
|
|
1038
|
+
prunedTokens += tokens
|
|
1039
|
+
if (m.role !== 'system') {
|
|
1040
|
+
prunedSessionTokens += tokens
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Jika Fase 1 saja sudah cukup membawa konteks di bawah ambang batas 50%:
|
|
1045
|
+
if (prunedTokens < triggerLimit) {
|
|
1046
|
+
const didPrune = prunedTokens < totalTokens
|
|
1047
|
+
return {
|
|
1048
|
+
compressed: didPrune,
|
|
1049
|
+
prunedOnly: true,
|
|
1050
|
+
loopMessages: prunedMessages,
|
|
1051
|
+
totalTokens: prunedSessionTokens,
|
|
1052
|
+
totalChars: prunedSessionTokens
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// FASE 2: Tentukan Batas Head, Middle, dan Tail dengan Backward Alignment
|
|
1057
|
+
if (typeof onProgress === 'function') {
|
|
1058
|
+
onProgress({ stage: 'summarizing', text: 'In-loop: Merangkum serah-terima teknis...' })
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const head = prunedMessages.slice(0, Math.min(protectFirstN, prunedMessages.length))
|
|
1062
|
+
const rawTailStart = Math.max(head.length, prunedMessages.length - protectLastN)
|
|
1063
|
+
const alignedTailStart = alignBoundaryBackward(prunedMessages, rawTailStart)
|
|
1064
|
+
const middle = prunedMessages.slice(head.length, alignedTailStart)
|
|
1065
|
+
const tail = prunedMessages.slice(alignedTailStart)
|
|
1066
|
+
|
|
1067
|
+
if (middle.length === 0) {
|
|
1068
|
+
return {
|
|
1069
|
+
compressed: true,
|
|
1070
|
+
prunedOnly: true,
|
|
1071
|
+
loopMessages: prunedMessages,
|
|
1072
|
+
totalTokens: prunedTokens,
|
|
1073
|
+
totalChars: prunedTokens
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Ambil summary serah-terima sebelumnya jika ada
|
|
1078
|
+
let existingSummary = ''
|
|
1079
|
+
try {
|
|
1080
|
+
const compactRec = await getSessionCompact(sessionId)
|
|
1081
|
+
if (compactRec) {
|
|
1082
|
+
existingSummary = compactRec.summaryBlock || compactRec.summary_block || ''
|
|
1083
|
+
}
|
|
1084
|
+
} catch {
|
|
1085
|
+
/* ignore */
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// FASE 3: Generate Structured Handover Summary (Iteratif)
|
|
1089
|
+
const newSummary = await summarizeMiddle(middle, existingSummary, activeConfig)
|
|
1090
|
+
|
|
1091
|
+
// Simpan ringkasan baru secara in-place ke session_compact
|
|
1092
|
+
try {
|
|
1093
|
+
const lastMiddleMsg = middle[middle.length - 1]
|
|
1094
|
+
await saveSessionCompact(sessionId, {
|
|
1095
|
+
summaryBlock: newSummary,
|
|
1096
|
+
lastCompactedMessageId: getMessageId(lastMiddleMsg, alignedTailStart - 1),
|
|
1097
|
+
lastCompactedAt: Date.now()
|
|
1098
|
+
})
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
console.warn('[contextManager] In-loop saveSessionCompact gagal:', err)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// FASE 4: Perakitan Pesan In-Place [Head] + [Handover Summary] + [Tail]
|
|
1104
|
+
const handoverContent = `[ CONTEXT COMPACTION (IN-LOOP HANDOVER) ]\n${newSummary}`
|
|
1105
|
+
const assembled = [
|
|
1106
|
+
...head,
|
|
1107
|
+
{
|
|
1108
|
+
role: 'user',
|
|
1109
|
+
content: handoverContent
|
|
1110
|
+
},
|
|
1111
|
+
...tail
|
|
1112
|
+
]
|
|
1113
|
+
|
|
1114
|
+
const sanitized = cleanOrphanToolPairs(assembled)
|
|
1115
|
+
let finalSessionTokens = 0
|
|
1116
|
+
for (const m of sanitized) {
|
|
1117
|
+
if (m.role !== 'system') {
|
|
1118
|
+
finalSessionTokens += calculateMessageTokens(m)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return {
|
|
1123
|
+
compressed: true,
|
|
1124
|
+
prunedOnly: false,
|
|
1125
|
+
loopMessages: sanitized,
|
|
1126
|
+
totalTokens: finalSessionTokens,
|
|
1127
|
+
totalChars: finalSessionTokens,
|
|
1128
|
+
newSummaryBlock: newSummary
|
|
1129
|
+
}
|
|
1130
|
+
}
|