@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,2739 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { buildPlanningSystemPrompt } from '../../api/ai/planning'
|
|
3
|
+
import { getYoutubeSummary } from '../../api/ai/tools'
|
|
4
|
+
import { fetchAI } from '../../api/ai/core'
|
|
5
|
+
import { playVoice, speechQueue, getCurrentTimeInfo } from '../../api/ai/utils'
|
|
6
|
+
import { parseMarkTag, stripMarkTags } from '@shared/parsers/mark-tag-parser.js'
|
|
7
|
+
import { executeAgentTool } from './executeAgentTool.js'
|
|
8
|
+
import { webApi } from '../../api/web-bridge.js'
|
|
9
|
+
import {
|
|
10
|
+
deleteMemory,
|
|
11
|
+
getAllMemory,
|
|
12
|
+
insertMemory,
|
|
13
|
+
updateMemory,
|
|
14
|
+
saveSession,
|
|
15
|
+
getChatData,
|
|
16
|
+
getSessionCompact,
|
|
17
|
+
db
|
|
18
|
+
} from '../../api/db'
|
|
19
|
+
import { checkTools, getActiveToolsSchema } from '../../api/tools/index'
|
|
20
|
+
import { startAgentTaskStep, transitionAgentTask } from '../../api/taskStore'
|
|
21
|
+
import { getUnifiedContext, generateVector, executeMemorySearch } from '../../api/vectorMemory'
|
|
22
|
+
import { searchMemoriesInOrama } from '../../api/oramaStore'
|
|
23
|
+
import {
|
|
24
|
+
MAX_CONTEXT_TOKENS,
|
|
25
|
+
MAX_CONTEXT_CHARS,
|
|
26
|
+
GATEWAY_HYGIENE_THRESHOLD,
|
|
27
|
+
IN_LOOP_COMPACT_THRESHOLD,
|
|
28
|
+
calculateSessionTokens,
|
|
29
|
+
calculateMessageTokens,
|
|
30
|
+
calculateSessionChars,
|
|
31
|
+
executeSessionCompaction,
|
|
32
|
+
assembleCompactedPayload,
|
|
33
|
+
pruneInFlightMessages,
|
|
34
|
+
checkAndCompressInLoop
|
|
35
|
+
} from '../../api/ai/contextManager'
|
|
36
|
+
import { saveWorkspaceWorkingMemory } from '../../api/workspaceRag'
|
|
37
|
+
import { synthesizeSkillAndSave } from '../../api/ai/skillSynthesizer'
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// HELPER UTILITIES
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']
|
|
44
|
+
|
|
45
|
+
const isImagePath = (filePath = '') => {
|
|
46
|
+
if (typeof filePath === 'string' && filePath.startsWith('data:image/')) return true
|
|
47
|
+
const ext = String(filePath || '')
|
|
48
|
+
.split('.')
|
|
49
|
+
.pop()
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
return IMAGE_EXTS.includes(`.${ext}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const convertFilePathToBase64 = async (filePath) => {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof filePath === 'string' && filePath.startsWith('data:image/')) {
|
|
57
|
+
return filePath
|
|
58
|
+
}
|
|
59
|
+
if (window.api?.executeNativeTool) {
|
|
60
|
+
try {
|
|
61
|
+
const toolRes = await window.api.executeNativeTool('read-file', { path: filePath })
|
|
62
|
+
if (toolRes && toolRes.success && toolRes.dataUrl) {
|
|
63
|
+
return toolRes.dataUrl
|
|
64
|
+
}
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
}
|
|
67
|
+
const formattedUrl = filePath.startsWith('file://')
|
|
68
|
+
? filePath
|
|
69
|
+
: `file:///${filePath.replace(/\\/g, '/')}`
|
|
70
|
+
const res = await fetch(formattedUrl)
|
|
71
|
+
const blob = await res.blob()
|
|
72
|
+
return await new Promise((resolve, reject) => {
|
|
73
|
+
const reader = new FileReader()
|
|
74
|
+
reader.onloadend = () => resolve(reader.result)
|
|
75
|
+
reader.onerror = reject
|
|
76
|
+
reader.readAsDataURL(blob)
|
|
77
|
+
})
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('[useMarkPlan] Failed to convert image file to Base64:', filePath, err)
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// MAIN HOOK: useMarkPlan
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export const useMarkPlan = ({
|
|
89
|
+
chatData,
|
|
90
|
+
setChatData,
|
|
91
|
+
config,
|
|
92
|
+
isSpeak,
|
|
93
|
+
abortControllerRef,
|
|
94
|
+
setIsLoading,
|
|
95
|
+
setIsAgentBusy,
|
|
96
|
+
runningSessionId,
|
|
97
|
+
setRunningSessionId,
|
|
98
|
+
runningSessionIds,
|
|
99
|
+
setRunningSessionIds,
|
|
100
|
+
addRunningSessionId,
|
|
101
|
+
removeRunningSessionId,
|
|
102
|
+
setMessage,
|
|
103
|
+
handleYoutubeSearch,
|
|
104
|
+
handleSearchCommand,
|
|
105
|
+
handleYoutubeSummary,
|
|
106
|
+
handleMusic,
|
|
107
|
+
getYoutubeData,
|
|
108
|
+
pushProcess,
|
|
109
|
+
dismissProcess,
|
|
110
|
+
activeTopic,
|
|
111
|
+
setActiveTopic,
|
|
112
|
+
currentMusicTrack,
|
|
113
|
+
requestApproval,
|
|
114
|
+
requestCameraCapture
|
|
115
|
+
}) => {
|
|
116
|
+
const chatDataRef = useRef(chatData)
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
chatDataRef.current = chatData
|
|
119
|
+
}, [chatData])
|
|
120
|
+
|
|
121
|
+
// Map menyimpan sesi yang sedang berjalan: key = sessionId, value = { abortController, startTime, prompt }
|
|
122
|
+
const activeSessionsRef = useRef(new Map())
|
|
123
|
+
// Map menyimpan updater fungsi setChatData per sesi untuk IPC status AI
|
|
124
|
+
const activeSessionUpdatersRef = useRef(new Map())
|
|
125
|
+
|
|
126
|
+
// Listener event status AI dari Main Process (IPC)
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (window.api && window.api.onAiStatus) {
|
|
129
|
+
window.api.onAiStatus((msg) => {
|
|
130
|
+
if (activeSessionUpdatersRef.current.size > 0) {
|
|
131
|
+
for (const updater of activeSessionUpdatersRef.current.values()) {
|
|
132
|
+
try {
|
|
133
|
+
updater((prev) => {
|
|
134
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
135
|
+
return [...filtered, { role: 'ai', content: msg, isThinking: true }]
|
|
136
|
+
})
|
|
137
|
+
} catch (e) {}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
setChatData((prev) => {
|
|
141
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
142
|
+
return [...filtered, { role: 'ai', content: msg, isThinking: true }]
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (window.api && window.api.onAiAbort) {
|
|
149
|
+
const unsub = window.api.onAiAbort((payload) => {
|
|
150
|
+
console.warn('[useMarkPlan] Sinyal ai:abort diterima:', payload)
|
|
151
|
+
handleStop()
|
|
152
|
+
})
|
|
153
|
+
return () => {
|
|
154
|
+
if (typeof unsub === 'function') unsub()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}, [setChatData])
|
|
158
|
+
|
|
159
|
+
const activeTaskObjectiveRef = useRef(null)
|
|
160
|
+
const lastUserPromptRef = useRef('')
|
|
161
|
+
const activeRunningSessionIdRef = useRef(1)
|
|
162
|
+
|
|
163
|
+
const targetPushProcess = (proc) => {
|
|
164
|
+
if (
|
|
165
|
+
(activeRunningSessionIdRef.current === 1 || !activeRunningSessionIdRef.current) &&
|
|
166
|
+
pushProcess
|
|
167
|
+
) {
|
|
168
|
+
pushProcess(proc)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Menampung arahan/intervensi user saat ReAct loop sedang berjalan
|
|
173
|
+
const handleIntervention = (msg, targetSessionId = null, opts = {}) => {
|
|
174
|
+
if (!msg || (typeof msg === 'string' && !msg.trim())) return
|
|
175
|
+
const textMsg = typeof msg === 'string' ? msg.trim() : String(msg)
|
|
176
|
+
const displayMsg = opts.displayPrompt || textMsg
|
|
177
|
+
const sId =
|
|
178
|
+
targetSessionId !== null && targetSessionId !== undefined
|
|
179
|
+
? Number(targetSessionId)
|
|
180
|
+
: activeRunningSessionIdRef.current || 1
|
|
181
|
+
|
|
182
|
+
const interventionItem = {
|
|
183
|
+
type: 'intervention',
|
|
184
|
+
text: displayMsg,
|
|
185
|
+
timestamp: Date.now()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const session = activeSessionsRef.current.get(sId)
|
|
189
|
+
if (session) {
|
|
190
|
+
if (!session.interventions) session.interventions = []
|
|
191
|
+
session.interventions.push(textMsg)
|
|
192
|
+
if (session.executedToolsList) {
|
|
193
|
+
session.executedToolsList.push(interventionItem)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const updater = activeSessionUpdatersRef.current.get(sId) || (sId === 1 ? setChatData : null)
|
|
198
|
+
if (updater) {
|
|
199
|
+
updater((prev) => {
|
|
200
|
+
const thinkingIndex = prev.findIndex((item) => item.isThinking)
|
|
201
|
+
if (thinkingIndex === -1) return prev
|
|
202
|
+
const thinkingItem = prev[thinkingIndex]
|
|
203
|
+
const currentTools = Array.isArray(thinkingItem.executedTools)
|
|
204
|
+
? thinkingItem.executedTools
|
|
205
|
+
: []
|
|
206
|
+
const updatedThinking = {
|
|
207
|
+
...thinkingItem,
|
|
208
|
+
executedTools: [...currentTools, interventionItem]
|
|
209
|
+
}
|
|
210
|
+
const newArr = [...prev]
|
|
211
|
+
newArr[thinkingIndex] = updatedThinking
|
|
212
|
+
return newArr
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Penghentian tugas per-sesi secara independen
|
|
218
|
+
const handleStop = async (targetSessionId = null) => {
|
|
219
|
+
try {
|
|
220
|
+
speechQueue.reset()
|
|
221
|
+
} catch (_) {}
|
|
222
|
+
|
|
223
|
+
const numId =
|
|
224
|
+
targetSessionId !== null && targetSessionId !== undefined ? Number(targetSessionId) : null
|
|
225
|
+
const strId =
|
|
226
|
+
targetSessionId !== null && targetSessionId !== undefined ? String(targetSessionId) : null
|
|
227
|
+
|
|
228
|
+
const sessionsToAbort = []
|
|
229
|
+
if (numId !== null) {
|
|
230
|
+
const s = activeSessionsRef.current.get(numId) || activeSessionsRef.current.get(strId)
|
|
231
|
+
if (s) sessionsToAbort.push(s)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (sessionsToAbort.length === 0) {
|
|
235
|
+
for (const s of activeSessionsRef.current.values()) {
|
|
236
|
+
sessionsToAbort.push(s)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const session of sessionsToAbort) {
|
|
241
|
+
if (session.abortController) {
|
|
242
|
+
try {
|
|
243
|
+
session.abortController.abort()
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (abortControllerRef?.current) {
|
|
249
|
+
try {
|
|
250
|
+
abortControllerRef.current.abort()
|
|
251
|
+
} catch (_) {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (window.api && window.api.abortFetchAI) {
|
|
255
|
+
try {
|
|
256
|
+
window.api.abortFetchAI()
|
|
257
|
+
} catch (_) {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (window.api && window.api.browserClose) {
|
|
261
|
+
const closeId = numId === 1 || !numId ? 'main' : `workspace-${numId}`
|
|
262
|
+
window.api.browserClose({ sessionId: closeId }).catch(() => {})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fail-safe: Langsung batalkan semua agent tasks yang berstatus running di SQLite
|
|
266
|
+
try {
|
|
267
|
+
const runningTasks = await db.agentTasks.where('status').equals('running').toArray()
|
|
268
|
+
for (const t of runningTasks) {
|
|
269
|
+
await transitionAgentTask(t.id, 'cancelled', 'user_abort').catch(() => {})
|
|
270
|
+
}
|
|
271
|
+
} catch (_) {}
|
|
272
|
+
|
|
273
|
+
// Fail-safe: Langsung update chatData UI agar alur kerja dan spinner berhenti seketika
|
|
274
|
+
const stopUpdater = (prev) => {
|
|
275
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
276
|
+
return filtered.map((msg) => {
|
|
277
|
+
if (!msg.isPlanSteps || msg.taskStatus !== 'running') return msg
|
|
278
|
+
return {
|
|
279
|
+
...msg,
|
|
280
|
+
taskStatus: 'stopped',
|
|
281
|
+
plan: (msg.plan || []).map((s) => ({
|
|
282
|
+
...s,
|
|
283
|
+
status: s.status === 'running' ? 'stopped' : s.status
|
|
284
|
+
}))
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (numId !== null && activeSessionUpdatersRef.current.has(numId)) {
|
|
290
|
+
activeSessionUpdatersRef.current.get(numId)(stopUpdater)
|
|
291
|
+
} else {
|
|
292
|
+
setChatData(stopUpdater)
|
|
293
|
+
for (const updater of activeSessionUpdatersRef.current.values()) {
|
|
294
|
+
updater(stopUpdater)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (numId === 1 || numId === null) {
|
|
299
|
+
setIsLoading(false)
|
|
300
|
+
setIsAgentBusy(false)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ==========================================================================
|
|
305
|
+
// DISPATCHER EKSEKUSI INDIVIDUAL TOOL (Native & Functional)
|
|
306
|
+
// ==========================================================================
|
|
307
|
+
const executeSingleTool = async (tool, rawArgs, context) => {
|
|
308
|
+
const {
|
|
309
|
+
tgContext,
|
|
310
|
+
isAutonomous,
|
|
311
|
+
pluginProcessId,
|
|
312
|
+
activeSessionNum = 1,
|
|
313
|
+
activeTopic = null,
|
|
314
|
+
userInput = '',
|
|
315
|
+
durableTask = null,
|
|
316
|
+
durableActiveStep = null,
|
|
317
|
+
agenticProcessId = null,
|
|
318
|
+
targetSetChatData = setChatData,
|
|
319
|
+
signal
|
|
320
|
+
} = context
|
|
321
|
+
const currentSignal = signal || abortControllerRef?.current?.signal
|
|
322
|
+
let resultString = 'Tidak ada hasil.'
|
|
323
|
+
|
|
324
|
+
const stringQuery =
|
|
325
|
+
typeof rawArgs === 'string'
|
|
326
|
+
? rawArgs
|
|
327
|
+
: typeof rawArgs === 'object' && rawArgs !== null
|
|
328
|
+
? rawArgs.query ||
|
|
329
|
+
rawArgs.prompt ||
|
|
330
|
+
rawArgs.text ||
|
|
331
|
+
rawArgs.path ||
|
|
332
|
+
JSON.stringify(rawArgs)
|
|
333
|
+
: ''
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// 1. YouTube Search
|
|
337
|
+
if (tool === 'yt-search') {
|
|
338
|
+
const q = typeof rawArgs === 'object' && rawArgs?.query ? rawArgs.query : stringQuery
|
|
339
|
+
const ytResults = await window.api.searchYoutube(q)
|
|
340
|
+
resultString = JSON.stringify(ytResults)
|
|
341
|
+
}
|
|
342
|
+
// 2. YouTube Summary
|
|
343
|
+
else if (tool === 'yt-summary') {
|
|
344
|
+
const url = typeof rawArgs === 'object' && rawArgs?.url ? rawArgs.url : stringQuery
|
|
345
|
+
targetSetChatData((prev) => [
|
|
346
|
+
...prev,
|
|
347
|
+
{
|
|
348
|
+
role: 'ai',
|
|
349
|
+
content: 'Menonton video youtube...',
|
|
350
|
+
isSummarizing: true,
|
|
351
|
+
youtubeLink: url
|
|
352
|
+
}
|
|
353
|
+
])
|
|
354
|
+
const yData = await getYoutubeData(url)
|
|
355
|
+
resultString = await getYoutubeSummary(url, yData, currentSignal)
|
|
356
|
+
targetSetChatData((prev) => prev.filter((item) => !item.isSummarizing))
|
|
357
|
+
}
|
|
358
|
+
// 3. Music Control
|
|
359
|
+
else if (tool.startsWith('music')) {
|
|
360
|
+
const musicQuery =
|
|
361
|
+
typeof rawArgs === 'object' && rawArgs?.query ? rawArgs.query : stringQuery
|
|
362
|
+
resultString = await handleMusic(tool, musicQuery, targetSetChatData)
|
|
363
|
+
}
|
|
364
|
+
// 4. Memory Vector Search
|
|
365
|
+
else if (tool === 'memory-search') {
|
|
366
|
+
const q = typeof rawArgs === 'object' && rawArgs?.query ? rawArgs.query : stringQuery
|
|
367
|
+
resultString = await executeMemorySearch(q)
|
|
368
|
+
}
|
|
369
|
+
// 5. Memory Management Tool
|
|
370
|
+
else if (tool === 'manage-memory') {
|
|
371
|
+
const memArgs = typeof rawArgs === 'object' && rawArgs !== null ? rawArgs : {}
|
|
372
|
+
const action = memArgs.action || 'insert'
|
|
373
|
+
const type = memArgs.type || 'profile'
|
|
374
|
+
const summary = memArgs.summary || ''
|
|
375
|
+
const detail = memArgs.detail || summary
|
|
376
|
+
|
|
377
|
+
let memContent = `[${getCurrentTimeInfo()}] ${detail || summary}`
|
|
378
|
+
const memoryData = {
|
|
379
|
+
type,
|
|
380
|
+
summary,
|
|
381
|
+
memory: memContent
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Orama Auto-Dedup check untuk profile / preference
|
|
385
|
+
if (action === 'insert' && (type === 'profile' || type === 'preference')) {
|
|
386
|
+
try {
|
|
387
|
+
const newVec = await generateVector(memContent)
|
|
388
|
+
if (newVec) {
|
|
389
|
+
const similarMemories = await searchMemoriesInOrama(memContent, newVec, 1, type)
|
|
390
|
+
if (similarMemories.length > 0 && similarMemories[0].score > 0.82) {
|
|
391
|
+
memoryData.id = similarMemories[0].id
|
|
392
|
+
await updateMemory(memoryData)
|
|
393
|
+
resultString = `Memori yang mirip ditemukan (ID: ${memoryData.id}). Berhasil diperbarui.`
|
|
394
|
+
return {
|
|
395
|
+
resultString,
|
|
396
|
+
rejected: false,
|
|
397
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error('Error in Orama auto-dedup check:', err)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (action === 'insert') {
|
|
407
|
+
await insertMemory(memoryData)
|
|
408
|
+
resultString = `Fakta baru berhasil disimpan ke memori jangka panjang: "${summary}"`
|
|
409
|
+
} else if (action === 'update') {
|
|
410
|
+
await updateMemory(memoryData)
|
|
411
|
+
resultString = `Memori berhasil diperbarui: "${summary}"`
|
|
412
|
+
} else if (action === 'delete') {
|
|
413
|
+
if (memArgs.id) {
|
|
414
|
+
await deleteMemory(memArgs.id)
|
|
415
|
+
resultString = `Memori ID ${memArgs.id} berhasil dihapus.`
|
|
416
|
+
} else {
|
|
417
|
+
resultString = `Gagal menghapus memori: ID memori tidak disertakan.`
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// 6. Working Memory Update Tool
|
|
422
|
+
else if (tool === 'update-working-memory') {
|
|
423
|
+
const notes = typeof rawArgs === 'object' && rawArgs?.notes ? rawArgs.notes : stringQuery
|
|
424
|
+
if (context?.workspaceRoot && notes) {
|
|
425
|
+
await saveWorkspaceWorkingMemory(context.workspaceRoot, { notes })
|
|
426
|
+
resultString = `Catatan progres koding berhasil disimpan ke .mark/working-memory.json.`
|
|
427
|
+
} else {
|
|
428
|
+
resultString = `Working memory dicatat untuk sesi ini: ${notes}`
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// 7. Speak (TTS)
|
|
432
|
+
else if (tool === 'speak') {
|
|
433
|
+
const textToSpeak =
|
|
434
|
+
typeof rawArgs === 'object' && rawArgs?.text ? rawArgs.text : stringQuery
|
|
435
|
+
if (textToSpeak && textToSpeak.trim() !== '') {
|
|
436
|
+
targetSetChatData((prev) => {
|
|
437
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
438
|
+
return [
|
|
439
|
+
...filtered,
|
|
440
|
+
{ role: 'ai', content: `(Sedang berbicara) ${textToSpeak}`, isThinking: true }
|
|
441
|
+
]
|
|
442
|
+
})
|
|
443
|
+
await playVoice(textToSpeak)
|
|
444
|
+
resultString = `Berhasil berbicara secara lisan: "${textToSpeak}"`
|
|
445
|
+
} else {
|
|
446
|
+
resultString = 'Gagal: teks yang mau diucapkan kosong.'
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// 8. Screenshot ke Telegram
|
|
450
|
+
else if (tool === 'screenshot-to-tg') {
|
|
451
|
+
if (window.api && window.api.tgTakeScreenshot) {
|
|
452
|
+
const targetChatId = tgContext?.chatId || null
|
|
453
|
+
try {
|
|
454
|
+
const ssRes = await window.api.tgTakeScreenshot(targetChatId)
|
|
455
|
+
if (ssRes && ssRes.success === false) {
|
|
456
|
+
resultString = `Gagal mengirim screenshot ke Telegram: ${ssRes.error || 'Terjadi kesalahan'}`
|
|
457
|
+
} else {
|
|
458
|
+
resultString =
|
|
459
|
+
'Screenshot layar PC berhasil diambil dan dikirimkan ke Telegram Admin.'
|
|
460
|
+
}
|
|
461
|
+
} catch (e) {
|
|
462
|
+
resultString = `Gagal mengirim screenshot ke Telegram: ${e.message}`
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
resultString = 'Gagal: Fitur Telegram Bot belum tersedia.'
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// 9. Vision: Analyze Screen
|
|
469
|
+
else if (tool === 'analyze-screen') {
|
|
470
|
+
try {
|
|
471
|
+
const screens = await window.api.takeScreenshot()
|
|
472
|
+
const screenArray = Array.isArray(screens) ? screens : screens ? [screens] : []
|
|
473
|
+
if (screenArray.length > 0) {
|
|
474
|
+
targetSetChatData((prev) => [
|
|
475
|
+
...prev.filter((item) => !item.isThinking),
|
|
476
|
+
{ role: 'ai', content: 'Memproses Vision AI...', isThinking: true }
|
|
477
|
+
])
|
|
478
|
+
|
|
479
|
+
const promptText =
|
|
480
|
+
(typeof rawArgs === 'object' && rawArgs?.prompt) ||
|
|
481
|
+
(typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
|
|
482
|
+
(typeof rawArgs === 'string' ? rawArgs : '') ||
|
|
483
|
+
'Jelaskan apa yang kamu lihat di layar ini secara ringkas.'
|
|
484
|
+
;('Jelaskan apa yang kamu lihat di layar ini secara ringkas, fokus pada jendela aplikasi, teks, dan status UI.')
|
|
485
|
+
|
|
486
|
+
const contentArray = [
|
|
487
|
+
{
|
|
488
|
+
type: 'text',
|
|
489
|
+
text: promptText
|
|
490
|
+
},
|
|
491
|
+
...screenArray.map((scr) => ({
|
|
492
|
+
type: 'image_url',
|
|
493
|
+
image_url: { url: scr }
|
|
494
|
+
}))
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
let textContent = ''
|
|
498
|
+
try {
|
|
499
|
+
const visionResponse = await fetchAI(
|
|
500
|
+
[{ role: 'user', content: contentArray }],
|
|
501
|
+
false,
|
|
502
|
+
{
|
|
503
|
+
signal: currentSignal,
|
|
504
|
+
isSmallTask: true
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
textContent =
|
|
508
|
+
typeof visionResponse === 'object' && visionResponse.content
|
|
509
|
+
? visionResponse.content
|
|
510
|
+
: String(visionResponse)
|
|
511
|
+
} catch (vErr) {
|
|
512
|
+
textContent = `(Analisis teks awal dilewati: ${vErr.message})`
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(
|
|
516
|
+
`[Vision AI - analyze-screen] Hasil analisis (${screenArray.length} monitor):`,
|
|
517
|
+
textContent
|
|
518
|
+
)
|
|
519
|
+
resultString = `Hasil Analisis Layar (${screenArray.length} monitor):\n${textContent}`
|
|
520
|
+
return {
|
|
521
|
+
success: true,
|
|
522
|
+
resultString,
|
|
523
|
+
rejected: false,
|
|
524
|
+
imageUrls: screenArray,
|
|
525
|
+
previewUrl: screenArray[0],
|
|
526
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString }
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
resultString = 'Gagal mengambil screenshot layar untuk analisis.'
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
resultString = `Gagal memproses analisis layar: ${e.message}`
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// 10. Vision: Camera Look
|
|
536
|
+
else if (tool === 'camera-look') {
|
|
537
|
+
try {
|
|
538
|
+
if (config[0]?.cameraEnabled === false) {
|
|
539
|
+
resultString =
|
|
540
|
+
'Fitur kamera dimatikan di pengaturan. Beri tahu user untuk mengaktifkannya.'
|
|
541
|
+
} else if (!requestCameraCapture) {
|
|
542
|
+
resultString = 'Internal Error: Callback requestCameraCapture tidak tersedia.'
|
|
543
|
+
} else {
|
|
544
|
+
targetSetChatData((prev) => [
|
|
545
|
+
...prev.filter((item) => !item.isThinking),
|
|
546
|
+
{ role: 'ai', content: 'Mengakses kamera...', isThinking: true }
|
|
547
|
+
])
|
|
548
|
+
|
|
549
|
+
const cameraFrame = await requestCameraCapture({
|
|
550
|
+
isAutonomous: isAutonomous,
|
|
551
|
+
deviceId: config[0]?.cameraDeviceId !== 'default' ? config[0]?.cameraDeviceId : null
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
if (cameraFrame) {
|
|
555
|
+
targetSetChatData((prev) => [
|
|
556
|
+
...prev.filter((item) => !item.isThinking),
|
|
557
|
+
{ role: 'ai', content: 'Menganalisis hasil kamera...', isThinking: true }
|
|
558
|
+
])
|
|
559
|
+
|
|
560
|
+
const promptText =
|
|
561
|
+
(typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
|
|
562
|
+
(typeof rawArgs === 'string' ? rawArgs : '') ||
|
|
563
|
+
'Jelaskan dengan detail apa yang terlihat dari kamera ini.'
|
|
564
|
+
|
|
565
|
+
const contentArray = [
|
|
566
|
+
{
|
|
567
|
+
type: 'text',
|
|
568
|
+
text: promptText
|
|
569
|
+
},
|
|
570
|
+
{ type: 'image_url', image_url: { url: cameraFrame } }
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
let textContent = ''
|
|
574
|
+
try {
|
|
575
|
+
const visionResponse = await fetchAI(
|
|
576
|
+
[{ role: 'user', content: contentArray }],
|
|
577
|
+
false,
|
|
578
|
+
{ signal: currentSignal, isSmallTask: true }
|
|
579
|
+
)
|
|
580
|
+
textContent =
|
|
581
|
+
typeof visionResponse === 'object' && visionResponse.content
|
|
582
|
+
? visionResponse.content
|
|
583
|
+
: String(visionResponse)
|
|
584
|
+
} catch (vErr) {
|
|
585
|
+
textContent = `(Analisis teks awal dilewati: ${vErr.message})`
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
console.log(`[Vision AI - camera-look] Hasil analisis:`, textContent)
|
|
589
|
+
resultString = `Hasil Analisis Kamera:\n${textContent}`
|
|
590
|
+
return {
|
|
591
|
+
success: true,
|
|
592
|
+
resultString,
|
|
593
|
+
rejected: false,
|
|
594
|
+
imageUrls: [cameraFrame],
|
|
595
|
+
previewUrl: cameraFrame,
|
|
596
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString }
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
resultString = 'Gagal mengambil gambar dari kamera.'
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {
|
|
603
|
+
resultString = `Gagal memproses kamera: ${e.message}`
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// 11. Built-in Native Tools & Sub-Agent Orchestration
|
|
607
|
+
else if (checkTools(tool)) {
|
|
608
|
+
const approvalCheck = await window.api.checkToolApproval(tool, rawArgs)
|
|
609
|
+
|
|
610
|
+
if (approvalCheck.needsApproval && requestApproval) {
|
|
611
|
+
const userApproved = await requestApproval(approvalCheck.message, tool, rawArgs, {
|
|
612
|
+
sessionId: activeSessionNum,
|
|
613
|
+
targetSetChatData
|
|
614
|
+
})
|
|
615
|
+
if (!userApproved) {
|
|
616
|
+
resultString = `[DITOLAK] User menolak eksekusi "${tool}". Cari cara lain atau tanyakan user.`
|
|
617
|
+
return {
|
|
618
|
+
resultString,
|
|
619
|
+
rejected: true,
|
|
620
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString }
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const executionResult = await executeAgentTool({
|
|
626
|
+
tool,
|
|
627
|
+
rawArgs,
|
|
628
|
+
config,
|
|
629
|
+
context,
|
|
630
|
+
activeSessionNum,
|
|
631
|
+
activeTopic,
|
|
632
|
+
userInput,
|
|
633
|
+
durableTask,
|
|
634
|
+
durableActiveStep,
|
|
635
|
+
agenticProcessId,
|
|
636
|
+
targetPushProcess,
|
|
637
|
+
targetSetChatData,
|
|
638
|
+
currentSignal,
|
|
639
|
+
abortControllerRef,
|
|
640
|
+
getCurrentTimeInfo,
|
|
641
|
+
activeTaskObjectiveRef
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
const res = executionResult.res
|
|
645
|
+
|
|
646
|
+
if (res && res.success) {
|
|
647
|
+
if (res.data !== undefined) {
|
|
648
|
+
resultString = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
|
|
649
|
+
} else if (res.output !== undefined) {
|
|
650
|
+
resultString = typeof res.output === 'string' ? res.output : JSON.stringify(res.output)
|
|
651
|
+
} else if (res.result !== undefined) {
|
|
652
|
+
resultString = typeof res.result === 'string' ? res.result : JSON.stringify(res.result)
|
|
653
|
+
} else if (res.content !== undefined) {
|
|
654
|
+
resultString =
|
|
655
|
+
typeof res.content === 'string' ? res.content : JSON.stringify(res.content)
|
|
656
|
+
} else if (res.contents !== undefined) {
|
|
657
|
+
resultString =
|
|
658
|
+
typeof res.contents === 'string' ? res.contents : JSON.stringify(res.contents)
|
|
659
|
+
} else {
|
|
660
|
+
resultString = res.message || 'Success'
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Pemotongan isi dokumen jika terlalu panjang
|
|
664
|
+
if (tool === 'read-document') {
|
|
665
|
+
let fullText =
|
|
666
|
+
typeof res.data === 'object' && res.data !== null
|
|
667
|
+
? res.data.content || ''
|
|
668
|
+
: String(res.data || resultString || '')
|
|
669
|
+
if (fullText && fullText.length > 2500) {
|
|
670
|
+
resultString = `${fullText.slice(0, 2500)}\n\n[DOKUMEN DIPOTONG (Total: ${fullText.length} karakter). Gunakan read-document dengan keyword untuk pencarian spesifik]`
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Analisis visual untuk read-image
|
|
675
|
+
if (tool === 'read-image' && res.dataUrl) {
|
|
676
|
+
const promptText =
|
|
677
|
+
(typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
|
|
678
|
+
(typeof rawArgs === 'string' ? rawArgs : '') ||
|
|
679
|
+
'Jelaskan apa yang kamu lihat pada gambar ini secara rinci.'
|
|
680
|
+
try {
|
|
681
|
+
targetSetChatData((prev) => [
|
|
682
|
+
...prev.filter((item) => !item.isThinking),
|
|
683
|
+
{
|
|
684
|
+
role: 'ai',
|
|
685
|
+
content: `Menganalisis gambar ${res.filename || ''}...`,
|
|
686
|
+
isThinking: true
|
|
687
|
+
}
|
|
688
|
+
])
|
|
689
|
+
const visionResponse = await fetchAI(
|
|
690
|
+
[
|
|
691
|
+
{
|
|
692
|
+
role: 'user',
|
|
693
|
+
content: [
|
|
694
|
+
{ type: 'text', text: promptText },
|
|
695
|
+
{ type: 'image_url', image_url: { url: res.dataUrl } }
|
|
696
|
+
]
|
|
697
|
+
}
|
|
698
|
+
],
|
|
699
|
+
false,
|
|
700
|
+
{ signal: currentSignal, isSmallTask: true }
|
|
701
|
+
)
|
|
702
|
+
const textContent =
|
|
703
|
+
typeof visionResponse === 'object' && visionResponse.content
|
|
704
|
+
? visionResponse.content
|
|
705
|
+
: String(visionResponse)
|
|
706
|
+
resultString = `[Vision AI - read-image] Analisis berkas '${res.filename || 'gambar'}':\n${textContent}`
|
|
707
|
+
} catch (vErr) {
|
|
708
|
+
resultString = `Berkas gambar '${res.filename || 'gambar'}' berhasil dibaca. (Analisis teks awal dilewati: ${vErr.message}). Gambar visual diteruskan ke observasi.`
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Analisis visual untuk browser-screenshot jika query disertakan
|
|
713
|
+
if (tool === 'browser-screenshot' && res.dataUrl && (rawArgs?.query || res.query)) {
|
|
714
|
+
const promptText =
|
|
715
|
+
(typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
|
|
716
|
+
res.query ||
|
|
717
|
+
'Jelaskan tampilan visual halaman web ini.'
|
|
718
|
+
try {
|
|
719
|
+
targetSetChatData((prev) => [
|
|
720
|
+
...prev.filter((item) => !item.isThinking),
|
|
721
|
+
{ role: 'ai', content: 'Menganalisis tampilan web...', isThinking: true }
|
|
722
|
+
])
|
|
723
|
+
const visionResponse = await fetchAI(
|
|
724
|
+
[
|
|
725
|
+
{
|
|
726
|
+
role: 'user',
|
|
727
|
+
content: [
|
|
728
|
+
{ type: 'text', text: promptText },
|
|
729
|
+
{ type: 'image_url', image_url: { url: res.dataUrl } }
|
|
730
|
+
]
|
|
731
|
+
}
|
|
732
|
+
],
|
|
733
|
+
false,
|
|
734
|
+
{ signal: currentSignal, isSmallTask: true }
|
|
735
|
+
)
|
|
736
|
+
const textContent =
|
|
737
|
+
typeof visionResponse === 'object' && visionResponse.content
|
|
738
|
+
? visionResponse.content
|
|
739
|
+
: String(visionResponse)
|
|
740
|
+
resultString = `[Vision AI - browser-screenshot] ${res.message || 'Screenshot berhasil diambil.'}\nHasil analisis:\n${textContent}`
|
|
741
|
+
} catch (vErr) {
|
|
742
|
+
resultString = `${res.message || 'Screenshot berhasil diambil.'} (Analisis teks awal dilewati: ${vErr.message}). Gambar visual diteruskan ke observasi.`
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
resultString = `[ERROR] ${tool} gagal: ${(res && (res.message || res.error)) || 'Unknown error'}`
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const toolImageUrls = res?.dataUrl ? [res.dataUrl] : null
|
|
750
|
+
const toolPreviewUrl = res?.dataUrl || null
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
res,
|
|
754
|
+
success: Boolean(res?.success),
|
|
755
|
+
resultString,
|
|
756
|
+
rejected: false,
|
|
757
|
+
imageUrls: toolImageUrls,
|
|
758
|
+
previewUrl: toolPreviewUrl,
|
|
759
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString },
|
|
760
|
+
loadedGroup: res?.loaded_group || null,
|
|
761
|
+
durableTask: executionResult?.durableTask || durableTask || null,
|
|
762
|
+
durableActiveStep:
|
|
763
|
+
executionResult?.durableActiveStep !== undefined
|
|
764
|
+
? executionResult.durableActiveStep
|
|
765
|
+
: durableActiveStep || null
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// 12. Dynamic Plugin Execution
|
|
769
|
+
else {
|
|
770
|
+
targetPushProcess({
|
|
771
|
+
id: pluginProcessId,
|
|
772
|
+
type: 'plugin-execution',
|
|
773
|
+
status: 'active',
|
|
774
|
+
data: { action: tool, query: stringQuery }
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
const pluginPromise = window.api.executePlugin(tool, rawArgs)
|
|
778
|
+
const abortPromise = new Promise((_, reject) => {
|
|
779
|
+
const onAbort = () => reject(new Error('AbortError'))
|
|
780
|
+
if (currentSignal?.aborted) return onAbort()
|
|
781
|
+
currentSignal?.addEventListener('abort', onAbort)
|
|
782
|
+
})
|
|
783
|
+
const res = await Promise.race([pluginPromise, abortPromise])
|
|
784
|
+
|
|
785
|
+
resultString = res.success
|
|
786
|
+
? typeof res.data === 'string'
|
|
787
|
+
? res.data
|
|
788
|
+
: JSON.stringify(res.data)
|
|
789
|
+
: `[ERROR] Plugin ${tool} gagal: ${res.error}`
|
|
790
|
+
|
|
791
|
+
targetPushProcess({
|
|
792
|
+
id: pluginProcessId,
|
|
793
|
+
type: 'plugin-execution',
|
|
794
|
+
status: 'done',
|
|
795
|
+
data: { action: tool, query: stringQuery, result: resultString }
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
res,
|
|
800
|
+
success: Boolean(res?.success),
|
|
801
|
+
resultString,
|
|
802
|
+
rejected: false,
|
|
803
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString }
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch (toolError) {
|
|
807
|
+
if (toolError.name === 'AbortError' || toolError.message?.includes('AbortError')) {
|
|
808
|
+
throw toolError
|
|
809
|
+
}
|
|
810
|
+
resultString = `[ERROR] Tool ${tool} crash: ${toolError.message}`
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
success: !resultString.startsWith('[ERROR]'),
|
|
815
|
+
resultString,
|
|
816
|
+
rejected: false,
|
|
817
|
+
toolExecution: { action: tool, query: stringQuery, result: resultString }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ==========================================================================
|
|
822
|
+
// CORE HANDLER: handlePlanningCommand (Native ReAct Loop & Streaming Engine)
|
|
823
|
+
// ==========================================================================
|
|
824
|
+
const handlePlanningCommand = async (
|
|
825
|
+
userInput,
|
|
826
|
+
tgContextOrOptions = null,
|
|
827
|
+
isAutonomous = false,
|
|
828
|
+
autonomousInitialMessage = null,
|
|
829
|
+
options = {},
|
|
830
|
+
isSystem = false
|
|
831
|
+
) => {
|
|
832
|
+
let tgContext = tgContextOrOptions
|
|
833
|
+
let opts = options || {}
|
|
834
|
+
|
|
835
|
+
// Flexible options detection
|
|
836
|
+
if (
|
|
837
|
+
tgContextOrOptions &&
|
|
838
|
+
typeof tgContextOrOptions === 'object' &&
|
|
839
|
+
!tgContextOrOptions.chatId &&
|
|
840
|
+
!tgContextOrOptions.from
|
|
841
|
+
) {
|
|
842
|
+
opts = tgContextOrOptions
|
|
843
|
+
opts = { ...opts, ...tgContextOrOptions }
|
|
844
|
+
tgContext = null
|
|
845
|
+
} else if (
|
|
846
|
+
autonomousInitialMessage &&
|
|
847
|
+
typeof autonomousInitialMessage === 'object' &&
|
|
848
|
+
(autonomousInitialMessage.sessionId ||
|
|
849
|
+
autonomousInitialMessage.customChatData ||
|
|
850
|
+
autonomousInitialMessage.customSetChatData ||
|
|
851
|
+
autonomousInitialMessage.onSaveSession)
|
|
852
|
+
) {
|
|
853
|
+
opts = { ...opts, ...autonomousInitialMessage }
|
|
854
|
+
autonomousInitialMessage = null
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ------------------------------------------------------------------------
|
|
858
|
+
// FASE 1: VALIDASI INPUT & PER-SESSION LOCKING
|
|
859
|
+
// ------------------------------------------------------------------------
|
|
860
|
+
const activeSessionNum = opts.sessionId
|
|
861
|
+
? !isNaN(Number(opts.sessionId))
|
|
862
|
+
? Number(opts.sessionId)
|
|
863
|
+
: String(opts.sessionId)
|
|
864
|
+
: 1
|
|
865
|
+
activeRunningSessionIdRef.current = activeSessionNum
|
|
866
|
+
|
|
867
|
+
if (activeSessionsRef.current.has(activeSessionNum)) {
|
|
868
|
+
handleIntervention(userInput, activeSessionNum, { displayPrompt: opts.displayPrompt })
|
|
869
|
+
return
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const sessionAbortController = new AbortController()
|
|
873
|
+
const sessionRecord = {
|
|
874
|
+
abortController: sessionAbortController,
|
|
875
|
+
startTime: Date.now(),
|
|
876
|
+
prompt: userInput,
|
|
877
|
+
interventions: []
|
|
878
|
+
}
|
|
879
|
+
activeSessionsRef.current.set(activeSessionNum, sessionRecord)
|
|
880
|
+
|
|
881
|
+
if (abortControllerRef) {
|
|
882
|
+
abortControllerRef.current = sessionAbortController
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (addRunningSessionId) addRunningSessionId(activeSessionNum)
|
|
886
|
+
setIsAgentBusy(true)
|
|
887
|
+
|
|
888
|
+
let finalIsSpeak = opts.forceSpeak !== undefined ? opts.forceSpeak : isSpeak
|
|
889
|
+
if (userInput && typeof userInput === 'string') {
|
|
890
|
+
if (userInput.startsWith('<mic>') || userInput.startsWith('(Mikrofon)')) {
|
|
891
|
+
finalIsSpeak = true
|
|
892
|
+
} else if (!isAutonomous && !isSystem) {
|
|
893
|
+
finalIsSpeak = false
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Reset Speech Queue sebelum memulai turn baru
|
|
898
|
+
if (finalIsSpeak) {
|
|
899
|
+
speechQueue.reset()
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!userInput) {
|
|
903
|
+
activeSessionsRef.current.delete(activeSessionNum)
|
|
904
|
+
if (removeRunningSessionId) removeRunningSessionId(activeSessionNum)
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (!tgContext && !isAutonomous) {
|
|
909
|
+
if (activeSessionNum === 1) {
|
|
910
|
+
setIsLoading(true)
|
|
911
|
+
}
|
|
912
|
+
if (!isSystem && !opts.customSetChatData) {
|
|
913
|
+
lastUserPromptRef.current = userInput
|
|
914
|
+
setMessage('')
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const timestampStr = getCurrentTimeInfo()
|
|
919
|
+
|
|
920
|
+
// ------------------------------------------------------------------------
|
|
921
|
+
// FASE 2: FORMATTING PROMPT & VISION PAYLOAD
|
|
922
|
+
// ------------------------------------------------------------------------
|
|
923
|
+
let finalContent = userInput
|
|
924
|
+
if (userInput.startsWith('/')) {
|
|
925
|
+
const skillName = userInput.slice(1).split(' ')[0].trim()
|
|
926
|
+
try {
|
|
927
|
+
const skillData = await window.api.readSkill(skillName)
|
|
928
|
+
const skillContent = typeof skillData === 'string' ? skillData : skillData?.content
|
|
929
|
+
if (skillContent) {
|
|
930
|
+
finalContent = `[SYSTEM INSTRUCTION - SKILL ACTIVATED]: Kamu sekarang harus bertindak dan mengikuti seluruh instruksi dalam dokumen skill berikut ini secara ketat:\n\n=== SKILL: ${skillName} ===\n${skillContent}\n====================\n\nInstruksi dari user: ${userInput.replace('/' + skillName, '').trim() || 'Jalankan skill ini sekarang!'}`
|
|
931
|
+
} else {
|
|
932
|
+
finalContent = `Skill "${skillName}" tidak ditemukan di direktori Mark Skills.`
|
|
933
|
+
}
|
|
934
|
+
} catch (err) {
|
|
935
|
+
console.error('Error loading skill:', err)
|
|
936
|
+
}
|
|
937
|
+
} else if (isSystem) {
|
|
938
|
+
finalContent = `[SYSTEM INSTRUCTION]: ${userInput}`
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (isAutonomous) {
|
|
942
|
+
finalContent = `[SISTEM INTERNAL - INISIATIF OTONOM]: Otak bawah sadarmu berinisiatif untuk melakukan tindakan berikut: "${userInput}". LAKUKAN TUGAS INI! Bicaralah seolah-olah kamu yang memiliki inisiatif itu sendiri tanpa disuruh. PENTING: DILARANG KERAS menggunakan tool 'os-*' untuk interaksi PC secara otonom! Respons "answer"-mu HARUS SANGAT SINGKAT (1-2 kalimat pendek).`
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
let imageVisionPayloads = []
|
|
946
|
+
|
|
947
|
+
// 1. Ekstraksi langsung dari opts.attachedFiles jika tersedia
|
|
948
|
+
if (Array.isArray(opts.attachedFiles) && opts.attachedFiles.length > 0) {
|
|
949
|
+
for (const f of opts.attachedFiles) {
|
|
950
|
+
const isImg =
|
|
951
|
+
(f.type && f.type.startsWith('image/')) ||
|
|
952
|
+
(f.path && isImagePath(f.path)) ||
|
|
953
|
+
(f.name && isImagePath(f.name))
|
|
954
|
+
if (isImg) {
|
|
955
|
+
const imgUrl = f.previewUrl || (f.path ? await convertFilePathToBase64(f.path) : null)
|
|
956
|
+
if (imgUrl) {
|
|
957
|
+
imageVisionPayloads.push({ type: 'image_url', image_url: { url: imgUrl } })
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 2. Fallback parsing dari teks [FILE TERLAMPIR]:
|
|
964
|
+
if (imageVisionPayloads.length === 0 && userInput.includes('[FILE TERLAMPIR]:')) {
|
|
965
|
+
const matches = userInput.match(/"([^"]+)"/g)
|
|
966
|
+
if (matches && matches.length > 0) {
|
|
967
|
+
const paths = matches.map((m) => m.replace(/^"|"$/g, ''))
|
|
968
|
+
for (const p of paths) {
|
|
969
|
+
if (isImagePath(p)) {
|
|
970
|
+
const b64 = await convertFilePathToBase64(p)
|
|
971
|
+
if (b64) {
|
|
972
|
+
imageVisionPayloads.push({ type: 'image_url', image_url: { url: b64 } })
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
let payloadContent = finalContent
|
|
980
|
+
if (imageVisionPayloads.length > 0) {
|
|
981
|
+
payloadContent = [{ type: 'text', text: finalContent }, ...imageVisionPayloads]
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let uiDisplayContent = opts.displayPrompt !== undefined ? opts.displayPrompt : userInput
|
|
985
|
+
if (
|
|
986
|
+
typeof uiDisplayContent === 'string' &&
|
|
987
|
+
uiDisplayContent.includes('=== SYSTEM INSTRUCTION: SKILL DIAKTIFKAN ===')
|
|
988
|
+
) {
|
|
989
|
+
const cleanBeforeSkill = uiDisplayContent
|
|
990
|
+
.split('=== SYSTEM INSTRUCTION: SKILL DIAKTIFKAN ===')[0]
|
|
991
|
+
.trim()
|
|
992
|
+
uiDisplayContent = cleanBeforeSkill || 'Jalankan Skill'
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Jika ada gambar terlampir dan user tidak menulis teks manual, kosongkan teks display
|
|
996
|
+
if (imageVisionPayloads.length > 0 && typeof opts.displayPrompt === 'string') {
|
|
997
|
+
uiDisplayContent = opts.displayPrompt.trim()
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
let finalUserMessageContent = uiDisplayContent
|
|
1001
|
+
if (imageVisionPayloads.length > 0) {
|
|
1002
|
+
const textPart = typeof uiDisplayContent === 'string' ? uiDisplayContent.trim() : ''
|
|
1003
|
+
finalUserMessageContent = [
|
|
1004
|
+
...(textPart ? [{ type: 'text', text: textPart }] : []),
|
|
1005
|
+
...imageVisionPayloads
|
|
1006
|
+
]
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const userMessage = opts.customUserMessage
|
|
1010
|
+
? {
|
|
1011
|
+
...opts.customUserMessage,
|
|
1012
|
+
timestamp: timestampStr,
|
|
1013
|
+
created_at: Date.now()
|
|
1014
|
+
}
|
|
1015
|
+
: {
|
|
1016
|
+
role: 'user',
|
|
1017
|
+
content: finalUserMessageContent,
|
|
1018
|
+
timestamp: timestampStr,
|
|
1019
|
+
created_at: Date.now(),
|
|
1020
|
+
source: tgContext ? 'telegram' : 'pc',
|
|
1021
|
+
sender:
|
|
1022
|
+
tgContext?.from?.first_name ||
|
|
1023
|
+
tgContext?.from?.username ||
|
|
1024
|
+
(tgContext ? 'Telegram Admin' : undefined)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Penyiapan data sesi terisolasi (Database-First Persistent Pipeline)
|
|
1028
|
+
let inMemorySessionData = []
|
|
1029
|
+
if (activeSessionNum !== 1) {
|
|
1030
|
+
if (Array.isArray(opts.customChatData) && opts.customChatData.length > 0) {
|
|
1031
|
+
inMemorySessionData = [...opts.customChatData]
|
|
1032
|
+
} else {
|
|
1033
|
+
try {
|
|
1034
|
+
const existing = await getChatData(activeSessionNum)
|
|
1035
|
+
if (existing && Array.isArray(existing)) {
|
|
1036
|
+
inMemorySessionData = [...existing]
|
|
1037
|
+
}
|
|
1038
|
+
} catch (e) {}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Ambil workspaceRoot dari database jika belum disertakan di opts
|
|
1043
|
+
if (!opts.workspaceRoot) {
|
|
1044
|
+
try {
|
|
1045
|
+
const sessionRecord = await db.sessions.get(activeSessionNum)
|
|
1046
|
+
if (sessionRecord?.workspaceRoot) {
|
|
1047
|
+
opts.workspaceRoot = sessionRecord.workspaceRoot
|
|
1048
|
+
}
|
|
1049
|
+
} catch (e) {}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const targetSetChatData = (updater) => {
|
|
1053
|
+
if (activeSessionNum !== 1) {
|
|
1054
|
+
const next = typeof updater === 'function' ? updater(inMemorySessionData) : updater
|
|
1055
|
+
inMemorySessionData = next
|
|
1056
|
+
|
|
1057
|
+
// 1. Direct persistent DB write
|
|
1058
|
+
saveSession(activeSessionNum, next).catch((err) => {
|
|
1059
|
+
console.warn(`[useMarkPlan] Gagal auto-save session ${activeSessionNum}:`, err)
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
// 2. Broadcast reactive event to UI
|
|
1063
|
+
window.dispatchEvent(
|
|
1064
|
+
new CustomEvent('session-updated', {
|
|
1065
|
+
detail: { sessionId: activeSessionNum, data: next }
|
|
1066
|
+
})
|
|
1067
|
+
)
|
|
1068
|
+
} else {
|
|
1069
|
+
setChatData((prev) => {
|
|
1070
|
+
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
1071
|
+
chatDataRef.current = next
|
|
1072
|
+
return next
|
|
1073
|
+
})
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
activeSessionUpdatersRef.current.set(activeSessionNum, targetSetChatData)
|
|
1078
|
+
|
|
1079
|
+
// ------------------------------------------------------------------------
|
|
1080
|
+
// FASE 3: PENYIAPAN HISTORY CHAT & RETRIEVAL KONTEKS
|
|
1081
|
+
// ------------------------------------------------------------------------
|
|
1082
|
+
const sourceChatData =
|
|
1083
|
+
activeSessionNum === 1 ? chatDataRef.current || chatData : inMemorySessionData
|
|
1084
|
+
const validHistory = sourceChatData.filter(
|
|
1085
|
+
(m) =>
|
|
1086
|
+
m &&
|
|
1087
|
+
!m.isThinking &&
|
|
1088
|
+
!m.isSearching &&
|
|
1089
|
+
!m.isSummarizing &&
|
|
1090
|
+
m.role !== 'command' &&
|
|
1091
|
+
m.role !== 'system'
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
if (!isAutonomous && !isSystem) {
|
|
1095
|
+
targetSetChatData((prev) => [
|
|
1096
|
+
...prev,
|
|
1097
|
+
userMessage,
|
|
1098
|
+
{
|
|
1099
|
+
role: 'ai',
|
|
1100
|
+
content: 'Menganalisis instruksi...',
|
|
1101
|
+
isThinking: true,
|
|
1102
|
+
mood: 'neutral'
|
|
1103
|
+
}
|
|
1104
|
+
])
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const agenticProcessId = `agentic-${Date.now()}`
|
|
1108
|
+
let durableTaskForRecovery = null
|
|
1109
|
+
let execSteps = [{ task: 'Menganalisis Konteks...' }]
|
|
1110
|
+
let accumulatedThoughts = []
|
|
1111
|
+
let executedToolsList = []
|
|
1112
|
+
let currentInFlightTool = null
|
|
1113
|
+
let currentTurnReasoning = ''
|
|
1114
|
+
let finalContentAccumulator = ''
|
|
1115
|
+
|
|
1116
|
+
try {
|
|
1117
|
+
let durableTask = null
|
|
1118
|
+
let durableActiveStep = null
|
|
1119
|
+
|
|
1120
|
+
const allMemory = await getAllMemory()
|
|
1121
|
+
let searchQuery = userInput
|
|
1122
|
+
if (validHistory.length > 0) {
|
|
1123
|
+
const lastMsg = validHistory[validHistory.length - 1]
|
|
1124
|
+
if (lastMsg && (lastMsg.role === 'assistant' || lastMsg.role === 'ai') && lastMsg.content) {
|
|
1125
|
+
let lastAiText =
|
|
1126
|
+
typeof lastMsg.content === 'string' ? lastMsg.content : JSON.stringify(lastMsg.content)
|
|
1127
|
+
if (lastAiText.length > 600) {
|
|
1128
|
+
lastAiText = lastAiText.substring(0, 300) + ' ... ' + lastAiText.slice(-300)
|
|
1129
|
+
}
|
|
1130
|
+
searchQuery = `Konteks obrolan sebelumnya: "${lastAiText}". Pertanyaan user saat ini: "${userInput}"`
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const contextPromise = getUnifiedContext(searchQuery, allMemory)
|
|
1135
|
+
const abortPromise = new Promise((_, reject) => {
|
|
1136
|
+
const onAbort = () => reject(new Error('AbortError'))
|
|
1137
|
+
if (sessionAbortController.signal.aborted) return onAbort()
|
|
1138
|
+
sessionAbortController.signal.addEventListener('abort', onAbort)
|
|
1139
|
+
})
|
|
1140
|
+
const unifiedContext = await Promise.race([contextPromise, abortPromise])
|
|
1141
|
+
|
|
1142
|
+
let contextMsgStr = ''
|
|
1143
|
+
if (tgContext)
|
|
1144
|
+
contextMsgStr += `Permintaan ini berasal dari Telegram (Chat ID: ${tgContext.chatId}).\n`
|
|
1145
|
+
if (isSystem)
|
|
1146
|
+
contextMsgStr += `[SYSTEM INSTRUCTION]: Pesan ini adalah instruksi internal sistem.\n`
|
|
1147
|
+
if (isAutonomous) {
|
|
1148
|
+
contextMsgStr += `[AWARENESS MODE]: Ini adalah pemikiran autonom-mu sendiri. Buka topik secara proaktif.\n`
|
|
1149
|
+
}
|
|
1150
|
+
if (currentMusicTrack && currentMusicTrack.title) {
|
|
1151
|
+
contextMsgStr += `[STATUS SISTEM]: Sedang memutar "${currentMusicTrack.title}" oleh ${currentMusicTrack.artist}.\n`
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Inject 5 aktivitas OS terakhir dari window tracker
|
|
1155
|
+
try {
|
|
1156
|
+
const activityBuffer = await window.api.getActivityBuffer()
|
|
1157
|
+
if (activityBuffer && activityBuffer.length > 0) {
|
|
1158
|
+
const recent = activityBuffer.slice(-5)
|
|
1159
|
+
const activitySummary = recent
|
|
1160
|
+
.map((a) => `[${a.time || a.timestamp}] ${a.app}${a.title ? ` — ${a.title}` : ''}`)
|
|
1161
|
+
.join('\n')
|
|
1162
|
+
contextMsgStr += `[AKTIVITAS PC USER (terakhir)]\n${activitySummary}\n`
|
|
1163
|
+
}
|
|
1164
|
+
} catch (_) {}
|
|
1165
|
+
|
|
1166
|
+
// Ambil daftar sub-agent yang tersedia
|
|
1167
|
+
let existingSubagents = ''
|
|
1168
|
+
try {
|
|
1169
|
+
const { subagentStore } = await import('../../api/subagent/subagentStore.js')
|
|
1170
|
+
const allSubs = await subagentStore.listSubagents()
|
|
1171
|
+
if (allSubs && allSubs.length > 0) {
|
|
1172
|
+
existingSubagents = allSubs
|
|
1173
|
+
.slice(0, 10)
|
|
1174
|
+
.map(
|
|
1175
|
+
(s) =>
|
|
1176
|
+
`- [ID: ${s.id}] "${s.name}" (${s.role}) | Status: ${s.status} | Turns: ${s.turnCount || 0} | Goal: "${s.goal}"`
|
|
1177
|
+
)
|
|
1178
|
+
.join('\n')
|
|
1179
|
+
}
|
|
1180
|
+
} catch (e) {}
|
|
1181
|
+
|
|
1182
|
+
// Susun System Prompt Mark V5
|
|
1183
|
+
let systemTelemetry = null
|
|
1184
|
+
try {
|
|
1185
|
+
if (window.api && typeof window.api.getSystemTelemetry === 'function') {
|
|
1186
|
+
systemTelemetry = await window.api.getSystemTelemetry()
|
|
1187
|
+
}
|
|
1188
|
+
} catch (_) {}
|
|
1189
|
+
|
|
1190
|
+
const systemPrompt = await buildPlanningSystemPrompt(
|
|
1191
|
+
userInput,
|
|
1192
|
+
{
|
|
1193
|
+
...opts,
|
|
1194
|
+
tgContext,
|
|
1195
|
+
currentMusicTrack,
|
|
1196
|
+
systemTelemetry,
|
|
1197
|
+
activeTaskObjective: activeTaskObjectiveRef.current,
|
|
1198
|
+
existingSubagents
|
|
1199
|
+
},
|
|
1200
|
+
unifiedContext,
|
|
1201
|
+
contextMsgStr
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
// ------------------------------------------------------------------------
|
|
1205
|
+
// FASE 3.5: PRE-FLIGHT CONTEXT COMPACTION (Ambang Batas 256.000 Tokens)
|
|
1206
|
+
// ------------------------------------------------------------------------
|
|
1207
|
+
let activeSessionCompact = null
|
|
1208
|
+
try {
|
|
1209
|
+
activeSessionCompact = await getSessionCompact(String(activeSessionNum))
|
|
1210
|
+
} catch (_) {}
|
|
1211
|
+
|
|
1212
|
+
const activeSummaryBlock =
|
|
1213
|
+
activeSessionCompact?.summaryBlock || activeSessionCompact?.summary_block || ''
|
|
1214
|
+
const activeLastCompactedId =
|
|
1215
|
+
activeSessionCompact?.lastCompactedMessageId ||
|
|
1216
|
+
activeSessionCompact?.last_compacted_message_id ||
|
|
1217
|
+
null
|
|
1218
|
+
|
|
1219
|
+
const currentUserMsg = { ...userMessage, content: payloadContent }
|
|
1220
|
+
let effectiveSourceMessages = [...sourceChatData, currentUserMsg]
|
|
1221
|
+
|
|
1222
|
+
currentUserMsg.tokens = calculateMessageTokens(currentUserMsg)
|
|
1223
|
+
const currentEstimatedTokens = calculateSessionTokens(
|
|
1224
|
+
effectiveSourceMessages,
|
|
1225
|
+
activeSummaryBlock,
|
|
1226
|
+
activeLastCompactedId,
|
|
1227
|
+
systemPrompt,
|
|
1228
|
+
activeSessionCompact?.lastCompactedAt || activeSessionCompact?.last_compacted_at || null
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
// Bypass proses kompaksi berat jika instruksi internal (greeting sistem / awareness autonomous / disableTools)
|
|
1232
|
+
const isInternalTurn = Boolean(
|
|
1233
|
+
isSystem || isAutonomous || opts.skipCompaction || opts.disableTools
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
// Layer 1: Gateway Session Hygiene (Hermes 85% safety net)
|
|
1237
|
+
const gatewayHygieneTriggerTokens = MAX_CONTEXT_TOKENS * GATEWAY_HYGIENE_THRESHOLD
|
|
1238
|
+
if (!isInternalTurn && currentEstimatedTokens >= gatewayHygieneTriggerTokens) {
|
|
1239
|
+
const compactBannerId = `compact-banner-${Date.now()}`
|
|
1240
|
+
targetSetChatData((prev) => [
|
|
1241
|
+
...prev,
|
|
1242
|
+
{
|
|
1243
|
+
id: compactBannerId,
|
|
1244
|
+
role: 'system',
|
|
1245
|
+
isCompacting: true,
|
|
1246
|
+
compactProgress: 'Memangkas log tool di memori...'
|
|
1247
|
+
}
|
|
1248
|
+
])
|
|
1249
|
+
|
|
1250
|
+
try {
|
|
1251
|
+
const compactionResult = await executeSessionCompaction({
|
|
1252
|
+
sessionId: String(activeSessionNum),
|
|
1253
|
+
messages: effectiveSourceMessages,
|
|
1254
|
+
activeConfig: config[0] || {},
|
|
1255
|
+
systemPrompt,
|
|
1256
|
+
onProgress: (prog) => {
|
|
1257
|
+
targetSetChatData((prev) =>
|
|
1258
|
+
prev.map((item) =>
|
|
1259
|
+
item.id === compactBannerId ? { ...item, compactProgress: prog.text } : item
|
|
1260
|
+
)
|
|
1261
|
+
)
|
|
1262
|
+
}
|
|
1263
|
+
})
|
|
1264
|
+
|
|
1265
|
+
if (compactionResult?.isCompacted) {
|
|
1266
|
+
if (compactionResult.compactedMessages) {
|
|
1267
|
+
effectiveSourceMessages = compactionResult.compactedMessages
|
|
1268
|
+
// Sinkronkan riwayat pesan terpangkas ke state UI & persistent storage
|
|
1269
|
+
targetSetChatData(compactionResult.compactedMessages)
|
|
1270
|
+
}
|
|
1271
|
+
if (compactionResult.newSummaryBlock && compactionResult.lastCompactedMessageId) {
|
|
1272
|
+
activeSessionCompact = {
|
|
1273
|
+
summaryBlock: compactionResult.newSummaryBlock,
|
|
1274
|
+
lastCompactedMessageId: compactionResult.lastCompactedMessageId,
|
|
1275
|
+
lastCompactedAt: Date.now()
|
|
1276
|
+
}
|
|
1277
|
+
window.dispatchEvent(
|
|
1278
|
+
new CustomEvent('session-compact-updated', {
|
|
1279
|
+
detail: {
|
|
1280
|
+
sessionId: String(activeSessionNum),
|
|
1281
|
+
lastCompactedMessageId: compactionResult.lastCompactedMessageId,
|
|
1282
|
+
summaryBlock: compactionResult.newSummaryBlock
|
|
1283
|
+
}
|
|
1284
|
+
})
|
|
1285
|
+
)
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Segera update indikator context-tracker agar gauge langsung berwarna hijau
|
|
1289
|
+
const activeTokensAfterCompact = Number(
|
|
1290
|
+
compactionResult.currentTokens || compactionResult.currentChars || 0
|
|
1291
|
+
)
|
|
1292
|
+
window.dispatchEvent(
|
|
1293
|
+
new CustomEvent('context-tracker-updated', {
|
|
1294
|
+
detail: {
|
|
1295
|
+
sessionId: String(activeSessionNum),
|
|
1296
|
+
currentTokens: activeTokensAfterCompact,
|
|
1297
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
1298
|
+
percentage: Math.min(100, (activeTokensAfterCompact / MAX_CONTEXT_TOKENS) * 100),
|
|
1299
|
+
currentChars: activeTokensAfterCompact,
|
|
1300
|
+
maxChars: MAX_CONTEXT_TOKENS,
|
|
1301
|
+
lastCompactedAt: activeSessionCompact?.lastCompactedAt || Date.now()
|
|
1302
|
+
}
|
|
1303
|
+
})
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
} catch (compactErr) {
|
|
1307
|
+
console.error('[useMarkPlan] Gagal context compaction:', compactErr)
|
|
1308
|
+
} finally {
|
|
1309
|
+
targetSetChatData((prev) => (prev || []).filter((item) => item.id !== compactBannerId))
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ------------------------------------------------------------------------
|
|
1314
|
+
// FASE 4: AGENTIC REACT LOOP (Native Function Calling + SSE Token Stream)
|
|
1315
|
+
// ------------------------------------------------------------------------
|
|
1316
|
+
let loopMessages = []
|
|
1317
|
+
|
|
1318
|
+
if (isSystem) {
|
|
1319
|
+
// GREETING BOOT SEQUENCE: Sapaan awal startup hanya butuh systemPrompt + 1-2 pesan terakhir
|
|
1320
|
+
// Mencegah ledakan 1M+ token dari akumulasi ratusan riwayat masa lalu di database.
|
|
1321
|
+
const recentHistory = sourceChatData
|
|
1322
|
+
.filter(
|
|
1323
|
+
(m) =>
|
|
1324
|
+
m &&
|
|
1325
|
+
!m.isThinking &&
|
|
1326
|
+
!m.isSearching &&
|
|
1327
|
+
!m.isSummarizing &&
|
|
1328
|
+
m.role !== 'command' &&
|
|
1329
|
+
m.role !== 'system'
|
|
1330
|
+
)
|
|
1331
|
+
.slice(-2)
|
|
1332
|
+
|
|
1333
|
+
loopMessages = [
|
|
1334
|
+
{ role: 'system', content: systemPrompt },
|
|
1335
|
+
...recentHistory.map((m) => {
|
|
1336
|
+
const role = m.role === 'ai' || m.role === 'planSteps' ? 'assistant' : m.role
|
|
1337
|
+
let content = m.content || ''
|
|
1338
|
+
if (role === 'assistant' && m.mood && !/<mark\b/i.test(content)) {
|
|
1339
|
+
content = `<mark mood="${m.mood}" done="true" /> ${content}`
|
|
1340
|
+
}
|
|
1341
|
+
return { role, content, mood: m.mood || undefined }
|
|
1342
|
+
}),
|
|
1343
|
+
{ role: 'user', content: payloadContent }
|
|
1344
|
+
]
|
|
1345
|
+
} else if (isAutonomous) {
|
|
1346
|
+
// AWARENESS PROAKTIF: Cukup 3-4 pesan riwayat obrolan terkini
|
|
1347
|
+
const recentHistory = sourceChatData
|
|
1348
|
+
.filter(
|
|
1349
|
+
(m) =>
|
|
1350
|
+
m &&
|
|
1351
|
+
!m.isThinking &&
|
|
1352
|
+
!m.isSearching &&
|
|
1353
|
+
!m.isSummarizing &&
|
|
1354
|
+
m.role !== 'command' &&
|
|
1355
|
+
m.role !== 'system'
|
|
1356
|
+
)
|
|
1357
|
+
.slice(-4)
|
|
1358
|
+
|
|
1359
|
+
loopMessages = [
|
|
1360
|
+
{ role: 'system', content: systemPrompt },
|
|
1361
|
+
...recentHistory.map((m) => {
|
|
1362
|
+
const role = m.role === 'ai' || m.role === 'planSteps' ? 'assistant' : m.role
|
|
1363
|
+
let content = m.content || ''
|
|
1364
|
+
if (role === 'assistant' && m.mood && !/<mark\b/i.test(content)) {
|
|
1365
|
+
content = `<mark mood="${m.mood}" done="true" /> ${content}`
|
|
1366
|
+
}
|
|
1367
|
+
return { role, content, mood: m.mood || undefined }
|
|
1368
|
+
}),
|
|
1369
|
+
{ role: 'user', content: payloadContent }
|
|
1370
|
+
]
|
|
1371
|
+
} else {
|
|
1372
|
+
// TURN CHAT/CODING NORMAL:
|
|
1373
|
+
// Seluruh riwayat pesan & log tool dikirim 100% UTUH tanpa batasan turn (maxTurns)
|
|
1374
|
+
// selama masih berada dalam kapasitas 256.000 tokens (MAX_CONTEXT_TOKENS).
|
|
1375
|
+
loopMessages = assembleCompactedPayload({
|
|
1376
|
+
messages: effectiveSourceMessages,
|
|
1377
|
+
sessionCompact: activeSessionCompact,
|
|
1378
|
+
systemPrompt
|
|
1379
|
+
})
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
let isDone = false
|
|
1383
|
+
let stepCount = 0
|
|
1384
|
+
executedToolsList = []
|
|
1385
|
+
sessionRecord.executedToolsList = executedToolsList
|
|
1386
|
+
let lastToolExecution = null
|
|
1387
|
+
accumulatedThoughts = []
|
|
1388
|
+
let currentActiveMood = 'neutral'
|
|
1389
|
+
finalContentAccumulator = ''
|
|
1390
|
+
let savedTurnAiMsg = null
|
|
1391
|
+
let lastServerUsage = null
|
|
1392
|
+
execSteps = [{ task: 'Menganalisis Konteks...' }]
|
|
1393
|
+
const dynamicallyLoadedToolGroups = new Set()
|
|
1394
|
+
|
|
1395
|
+
// Deteksi Tool Groups yang di-mention (@group_name) di input pengguna
|
|
1396
|
+
if (typeof userInput === 'string' && userInput.includes('@')) {
|
|
1397
|
+
try {
|
|
1398
|
+
const groupToolsData = await webApi.getGroupTools()
|
|
1399
|
+
const validGroupNames = new Set(
|
|
1400
|
+
groupToolsData?.names || Object.keys(groupToolsData?.schema || {})
|
|
1401
|
+
)
|
|
1402
|
+
const mentionMatches = userInput.match(/@([a-zA-Z0-9_-]+)/g) || []
|
|
1403
|
+
const taggedGroups = []
|
|
1404
|
+
for (const rawMatch of mentionMatches) {
|
|
1405
|
+
const groupName = rawMatch.slice(1).toLowerCase()
|
|
1406
|
+
if (validGroupNames.has(groupName)) {
|
|
1407
|
+
dynamicallyLoadedToolGroups.add(groupName)
|
|
1408
|
+
taggedGroups.push(groupName)
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (taggedGroups.length > 0) {
|
|
1412
|
+
loopMessages.push({
|
|
1413
|
+
role: 'system',
|
|
1414
|
+
content: `[TOOL GROUPS AKTIF VIA MENTION USER]: Pengguna secara eksplisit menandai kelompok tool berikut: ${taggedGroups.map((g) => `@${g}`).join(', ')}. Seluruh kapabilitas tool dalam kelompok ini telah dibuka dan aktif. Utamakan penggunaan tool ini untuk menyelesaikan instruksi pengguna.`
|
|
1415
|
+
})
|
|
1416
|
+
}
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.error('[useMarkPlan] Gagal memproses mention tool group:', err)
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
let consecutiveErrors = 0
|
|
1423
|
+
const maxConsecutiveErrorRetries = 50
|
|
1424
|
+
|
|
1425
|
+
while (!isDone && !sessionAbortController.signal.aborted) {
|
|
1426
|
+
// Cek Abort Signal
|
|
1427
|
+
if (sessionAbortController.signal.aborted) {
|
|
1428
|
+
try {
|
|
1429
|
+
const runningTasks = await db.agentTasks.where('status').equals('running').toArray()
|
|
1430
|
+
for (const t of runningTasks) {
|
|
1431
|
+
await transitionAgentTask(t.id, 'cancelled', 'user_abort').catch(() => {})
|
|
1432
|
+
}
|
|
1433
|
+
} catch (_) {}
|
|
1434
|
+
targetSetChatData((prev) =>
|
|
1435
|
+
prev.map((msg) => {
|
|
1436
|
+
if (!msg.isPlanSteps || msg.taskStatus !== 'running') return msg
|
|
1437
|
+
return {
|
|
1438
|
+
...msg,
|
|
1439
|
+
taskStatus: 'stopped',
|
|
1440
|
+
plan: (msg.plan || []).map((s) => ({
|
|
1441
|
+
...s,
|
|
1442
|
+
status: s.status === 'running' ? 'stopped' : s.status
|
|
1443
|
+
}))
|
|
1444
|
+
}
|
|
1445
|
+
})
|
|
1446
|
+
)
|
|
1447
|
+
throw new Error('AbortError')
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Cek Intervensi User di tengah jalan
|
|
1451
|
+
if (sessionRecord.interventions?.length > 0) {
|
|
1452
|
+
const interventions = sessionRecord.interventions.splice(0).join('\n')
|
|
1453
|
+
loopMessages.push({ role: 'user', content: `[USER INTERVENTION]: ${interventions}` })
|
|
1454
|
+
|
|
1455
|
+
const alreadyInList = executedToolsList.some(
|
|
1456
|
+
(t) => t.type === 'intervention' && t.text === interventions
|
|
1457
|
+
)
|
|
1458
|
+
if (!alreadyInList) {
|
|
1459
|
+
executedToolsList.push({
|
|
1460
|
+
type: 'intervention',
|
|
1461
|
+
text: interventions,
|
|
1462
|
+
timestamp: Date.now()
|
|
1463
|
+
})
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
execSteps.push({ task: `Intervensi User: ${interventions}` })
|
|
1467
|
+
targetPushProcess({
|
|
1468
|
+
id: agenticProcessId,
|
|
1469
|
+
type: 'planning',
|
|
1470
|
+
status: 'active',
|
|
1471
|
+
data: {
|
|
1472
|
+
steps: [...execSteps],
|
|
1473
|
+
currentStep: execSteps.length - 1,
|
|
1474
|
+
reasoning: 'Menerima arahan baru dari user di tengah proses.'
|
|
1475
|
+
}
|
|
1476
|
+
})
|
|
1477
|
+
|
|
1478
|
+
targetSetChatData((prev) =>
|
|
1479
|
+
prev.map((msg) =>
|
|
1480
|
+
msg.isThinking
|
|
1481
|
+
? {
|
|
1482
|
+
...msg,
|
|
1483
|
+
executedTools: [...executedToolsList]
|
|
1484
|
+
}
|
|
1485
|
+
: msg
|
|
1486
|
+
)
|
|
1487
|
+
)
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
stepCount++
|
|
1491
|
+
|
|
1492
|
+
const isDurableTaskCompleted = Boolean(
|
|
1493
|
+
durableTask &&
|
|
1494
|
+
(durableTask.status === 'completed' ||
|
|
1495
|
+
(durableTask.steps &&
|
|
1496
|
+
durableTask.steps.length > 0 &&
|
|
1497
|
+
durableTask.steps.every((s) => s.status === 'completed')))
|
|
1498
|
+
)
|
|
1499
|
+
if (isDurableTaskCompleted) {
|
|
1500
|
+
durableActiveStep = null
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Ambil Tools OpenAPI Schema yang relevan dengan query/tugas saat ini + group yang sudah dimuat
|
|
1504
|
+
const activeTools =
|
|
1505
|
+
opts.disableTools || isDurableTaskCompleted
|
|
1506
|
+
? null
|
|
1507
|
+
: await getActiveToolsSchema(
|
|
1508
|
+
userInput + ' ' + (activeTaskObjectiveRef.current || ''),
|
|
1509
|
+
dynamicallyLoadedToolGroups
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
// Loading thinking indicator di awal turn (akumulasi semua pemikiran dari langkah sebelumnya)
|
|
1513
|
+
targetSetChatData((prev) => {
|
|
1514
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
1515
|
+
const loadingText =
|
|
1516
|
+
isAutonomous && autonomousInitialMessage ? autonomousInitialMessage : ''
|
|
1517
|
+
const allPriorThoughts = accumulatedThoughts
|
|
1518
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
1519
|
+
.filter(Boolean)
|
|
1520
|
+
const initialReasoning =
|
|
1521
|
+
allPriorThoughts.length > 0
|
|
1522
|
+
? Array.from(new Set(allPriorThoughts)).join('\n\n---\n\n')
|
|
1523
|
+
: undefined
|
|
1524
|
+
|
|
1525
|
+
return [
|
|
1526
|
+
...filtered,
|
|
1527
|
+
{
|
|
1528
|
+
role: 'ai',
|
|
1529
|
+
content: loadingText,
|
|
1530
|
+
isThinking: true,
|
|
1531
|
+
reasoning: initialReasoning,
|
|
1532
|
+
executedTools: executedToolsList.length > 0 ? [...executedToolsList] : undefined,
|
|
1533
|
+
mood: currentActiveMood
|
|
1534
|
+
}
|
|
1535
|
+
]
|
|
1536
|
+
})
|
|
1537
|
+
|
|
1538
|
+
currentTurnReasoning = ''
|
|
1539
|
+
let currentTurnContent = ''
|
|
1540
|
+
let sentenceBuffer = ''
|
|
1541
|
+
|
|
1542
|
+
// In-Flight Pruning: Jika akumulasi pesan tool di tengah loop mencapai batas kapasitas,
|
|
1543
|
+
// pangkas observasi tool terlama agar payload ReAct tetap berada di bawah 256K tokens.
|
|
1544
|
+
// Layer 2: In-Loop Agent ContextCompressor (Hermes 50% Threshold + O(n) Pruning)
|
|
1545
|
+
// Di setiap iterasi ReAct, evaluasi ukuran payload loopMessages.
|
|
1546
|
+
// Jika melebihi 50% kapasitas, lakukan pemangkasan Fase 1 O(n) dan jika perlu Fase 2-4 in-place.
|
|
1547
|
+
try {
|
|
1548
|
+
const inLoopResult = await checkAndCompressInLoop({
|
|
1549
|
+
loopMessages,
|
|
1550
|
+
sessionId: String(activeSessionNum || 1),
|
|
1551
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
1552
|
+
thresholdRatio: IN_LOOP_COMPACT_THRESHOLD,
|
|
1553
|
+
protectLastN: 6,
|
|
1554
|
+
protectFirstN: 2,
|
|
1555
|
+
activeConfig: config[0] || {},
|
|
1556
|
+
systemPrompt
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
if (inLoopResult?.compressed && inLoopResult.loopMessages) {
|
|
1560
|
+
loopMessages = inLoopResult.loopMessages
|
|
1561
|
+
if (inLoopResult.totalTokens || inLoopResult.totalChars) {
|
|
1562
|
+
const inLoopTokens = inLoopResult.totalTokens || inLoopResult.totalChars
|
|
1563
|
+
window.dispatchEvent(
|
|
1564
|
+
new CustomEvent('context-tracker-updated', {
|
|
1565
|
+
detail: {
|
|
1566
|
+
sessionId: String(activeSessionNum || 1),
|
|
1567
|
+
currentTokens: inLoopTokens,
|
|
1568
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
1569
|
+
percentage: Math.min(100, (inLoopTokens / MAX_CONTEXT_TOKENS) * 100),
|
|
1570
|
+
currentChars: inLoopTokens,
|
|
1571
|
+
maxChars: MAX_CONTEXT_TOKENS,
|
|
1572
|
+
lastCompactedAt: Date.now()
|
|
1573
|
+
}
|
|
1574
|
+
})
|
|
1575
|
+
)
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
} catch (inLoopErr) {
|
|
1579
|
+
console.warn('[useMarkPlan] In-loop compaction warning:', inLoopErr)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Safety Net Pruning
|
|
1583
|
+
loopMessages = pruneInFlightMessages(loopMessages, MAX_CONTEXT_TOKENS)
|
|
1584
|
+
|
|
1585
|
+
// Request streaming ke Backend AI Bridge
|
|
1586
|
+
const streamResult = await fetchAI(loopMessages, true, {
|
|
1587
|
+
sessionId: String(activeSessionNum || 1),
|
|
1588
|
+
tools: isDurableTaskCompleted ? null : activeTools,
|
|
1589
|
+
signal: sessionAbortController.signal,
|
|
1590
|
+
onReasoning: (chunk) => {
|
|
1591
|
+
currentTurnReasoning += chunk
|
|
1592
|
+
const currentCombined = [...accumulatedThoughts, currentTurnReasoning]
|
|
1593
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
1594
|
+
.filter(Boolean)
|
|
1595
|
+
const liveReasoning = Array.from(new Set(currentCombined)).join('\n\n---\n\n')
|
|
1596
|
+
|
|
1597
|
+
targetSetChatData((prev) => {
|
|
1598
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
1599
|
+
return [
|
|
1600
|
+
...filtered,
|
|
1601
|
+
{
|
|
1602
|
+
role: 'ai',
|
|
1603
|
+
content: currentTurnContent,
|
|
1604
|
+
isThinking: true,
|
|
1605
|
+
reasoning: liveReasoning || undefined,
|
|
1606
|
+
executedTools: executedToolsList.length > 0 ? [...executedToolsList] : undefined,
|
|
1607
|
+
mood: currentActiveMood
|
|
1608
|
+
}
|
|
1609
|
+
]
|
|
1610
|
+
})
|
|
1611
|
+
},
|
|
1612
|
+
onMood: (moodTag) => {
|
|
1613
|
+
currentActiveMood = moodTag
|
|
1614
|
+
targetSetChatData((prev) =>
|
|
1615
|
+
prev.map((msg) => (msg.isThinking ? { ...msg, mood: moodTag } : msg))
|
|
1616
|
+
)
|
|
1617
|
+
},
|
|
1618
|
+
onMeta: (meta) => {
|
|
1619
|
+
if (meta?.mood) {
|
|
1620
|
+
currentActiveMood = meta.mood
|
|
1621
|
+
targetSetChatData((prev) =>
|
|
1622
|
+
prev.map((msg) => (msg.isThinking ? { ...msg, mood: meta.mood } : msg))
|
|
1623
|
+
)
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
onToken: (token) => {
|
|
1627
|
+
currentTurnContent += token
|
|
1628
|
+
finalContentAccumulator = currentTurnContent
|
|
1629
|
+
|
|
1630
|
+
// Sentence-Level Streaming TTS: Deteksi kalimat lengkap secara real-time
|
|
1631
|
+
if (finalIsSpeak) {
|
|
1632
|
+
sentenceBuffer += token
|
|
1633
|
+
// Deteksi batas akhir kalimat (. ! ? atau newline ganda)
|
|
1634
|
+
let sentenceEndMatch
|
|
1635
|
+
while ((sentenceEndMatch = sentenceBuffer.match(/^(.*?[\.!\?\n]+)([\s\S]*)$/))) {
|
|
1636
|
+
const completeSentence = sentenceEndMatch[1].trim()
|
|
1637
|
+
sentenceBuffer = sentenceEndMatch[2] || ''
|
|
1638
|
+
if (completeSentence) {
|
|
1639
|
+
speechQueue.enqueue(completeSentence)
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
targetSetChatData((prev) => {
|
|
1645
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
1646
|
+
const currentCombined = [...accumulatedThoughts, currentTurnReasoning]
|
|
1647
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
1648
|
+
.filter(Boolean)
|
|
1649
|
+
const liveReasoning = Array.from(new Set(currentCombined)).join('\n\n---\n\n')
|
|
1650
|
+
|
|
1651
|
+
return [
|
|
1652
|
+
...filtered,
|
|
1653
|
+
{
|
|
1654
|
+
role: 'ai',
|
|
1655
|
+
content: currentTurnContent,
|
|
1656
|
+
isThinking: true,
|
|
1657
|
+
reasoning: liveReasoning || undefined,
|
|
1658
|
+
executedTools: executedToolsList.length > 0 ? [...executedToolsList] : undefined,
|
|
1659
|
+
mood: currentActiveMood
|
|
1660
|
+
}
|
|
1661
|
+
]
|
|
1662
|
+
})
|
|
1663
|
+
}
|
|
1664
|
+
})
|
|
1665
|
+
|
|
1666
|
+
if (
|
|
1667
|
+
currentTurnReasoning &&
|
|
1668
|
+
typeof currentTurnReasoning === 'string' &&
|
|
1669
|
+
currentTurnReasoning.trim()
|
|
1670
|
+
) {
|
|
1671
|
+
const trimmed = currentTurnReasoning.trim()
|
|
1672
|
+
if (!accumulatedThoughts.includes(trimmed)) {
|
|
1673
|
+
accumulatedThoughts.push(trimmed)
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (streamResult?.mood && streamResult.mood !== 'neutral') {
|
|
1678
|
+
currentActiveMood = streamResult.mood
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (streamResult?.usage) {
|
|
1682
|
+
lastServerUsage = streamResult.usage
|
|
1683
|
+
const liveTokens = Number(
|
|
1684
|
+
streamResult.usage.total_tokens ||
|
|
1685
|
+
(streamResult.usage.prompt_tokens || 0) +
|
|
1686
|
+
(streamResult.usage.completion_tokens || 0) ||
|
|
1687
|
+
0
|
|
1688
|
+
)
|
|
1689
|
+
if (liveTokens > 0) {
|
|
1690
|
+
window.dispatchEvent(
|
|
1691
|
+
new CustomEvent('context-tracker-updated', {
|
|
1692
|
+
detail: {
|
|
1693
|
+
sessionId: String(activeSessionNum || 1),
|
|
1694
|
+
currentTokens: liveTokens,
|
|
1695
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
1696
|
+
percentage: Math.min(100, (liveTokens / MAX_CONTEXT_TOKENS) * 100),
|
|
1697
|
+
currentChars: liveTokens,
|
|
1698
|
+
maxChars: MAX_CONTEXT_TOKENS,
|
|
1699
|
+
lastCompactedAt: activeSessionCompact?.lastCompactedAt || Date.now()
|
|
1700
|
+
}
|
|
1701
|
+
})
|
|
1702
|
+
)
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (streamResult?.finishReason === 'error') {
|
|
1707
|
+
throw new Error('Terjadi kesalahan pada respon stream AI.')
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Fallback Interceptor: Jika model mengembalikan teks JSON (tool_calls, mood, atau structured answer)
|
|
1711
|
+
let effectiveToolCalls = streamResult.toolCalls
|
|
1712
|
+
if ((!effectiveToolCalls || effectiveToolCalls.length === 0) && currentTurnContent) {
|
|
1713
|
+
const rawMatch = currentTurnContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/)
|
|
1714
|
+
let cand = (rawMatch ? rawMatch[1] : currentTurnContent).trim()
|
|
1715
|
+
const firstBrace = cand.indexOf('{')
|
|
1716
|
+
const lastBrace = cand.lastIndexOf('}')
|
|
1717
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
1718
|
+
cand = cand.substring(firstBrace, lastBrace + 1).trim()
|
|
1719
|
+
}
|
|
1720
|
+
if (
|
|
1721
|
+
cand.includes('"tool_calls"') ||
|
|
1722
|
+
cand.includes('"action"') ||
|
|
1723
|
+
cand.includes('"tool"') ||
|
|
1724
|
+
cand.includes('"mood"') ||
|
|
1725
|
+
cand.includes('"answer"')
|
|
1726
|
+
) {
|
|
1727
|
+
try {
|
|
1728
|
+
const { jsonrepair } = await import('jsonrepair')
|
|
1729
|
+
let pObj = null
|
|
1730
|
+
try {
|
|
1731
|
+
pObj = JSON.parse(cand)
|
|
1732
|
+
} catch (_) {
|
|
1733
|
+
pObj = JSON.parse(jsonrepair(cand))
|
|
1734
|
+
}
|
|
1735
|
+
if (pObj) {
|
|
1736
|
+
if (pObj.mood) {
|
|
1737
|
+
currentActiveMood = String(pObj.mood).toLowerCase().trim()
|
|
1738
|
+
}
|
|
1739
|
+
if (pObj.answer !== undefined || pObj.content !== undefined) {
|
|
1740
|
+
currentTurnContent = pObj.answer !== undefined ? pObj.answer : pObj.content
|
|
1741
|
+
finalContentAccumulator = currentTurnContent
|
|
1742
|
+
}
|
|
1743
|
+
if (Array.isArray(pObj.tool_calls) && pObj.tool_calls.length > 0) {
|
|
1744
|
+
effectiveToolCalls = pObj.tool_calls.map((tc, idx) => ({
|
|
1745
|
+
id: tc.id || `call_fallback_${Date.now()}_${idx}`,
|
|
1746
|
+
type: 'function',
|
|
1747
|
+
function: {
|
|
1748
|
+
name: tc.name || tc.function?.name,
|
|
1749
|
+
arguments:
|
|
1750
|
+
typeof tc.arguments === 'object'
|
|
1751
|
+
? JSON.stringify(tc.arguments)
|
|
1752
|
+
: String(tc.arguments || '{}')
|
|
1753
|
+
}
|
|
1754
|
+
}))
|
|
1755
|
+
currentTurnContent = ''
|
|
1756
|
+
finalContentAccumulator = ''
|
|
1757
|
+
} else if (Array.isArray(pObj.action) && pObj.action.length > 0) {
|
|
1758
|
+
// Format BATCH ACTIONS array V4: { "action": [ { "tool": "...", "query": "..." }, ... ] }
|
|
1759
|
+
effectiveToolCalls = pObj.action
|
|
1760
|
+
.filter((act) => act && (act.tool || act.name))
|
|
1761
|
+
.map((act, idx) => ({
|
|
1762
|
+
id: `call_fallback_${Date.now()}_${idx}`,
|
|
1763
|
+
type: 'function',
|
|
1764
|
+
function: {
|
|
1765
|
+
name: act.tool || act.name,
|
|
1766
|
+
arguments:
|
|
1767
|
+
typeof act.arguments === 'object'
|
|
1768
|
+
? JSON.stringify(act.arguments)
|
|
1769
|
+
: typeof act.query === 'object'
|
|
1770
|
+
? JSON.stringify(act.query)
|
|
1771
|
+
: JSON.stringify(
|
|
1772
|
+
act.query !== undefined
|
|
1773
|
+
? { query: act.query }
|
|
1774
|
+
: act.arguments !== undefined
|
|
1775
|
+
? { query: act.arguments }
|
|
1776
|
+
: {}
|
|
1777
|
+
)
|
|
1778
|
+
}
|
|
1779
|
+
}))
|
|
1780
|
+
currentTurnContent = ''
|
|
1781
|
+
finalContentAccumulator = ''
|
|
1782
|
+
} else if (Array.isArray(pObj) && pObj.length > 0) {
|
|
1783
|
+
// Format array tool calls langsung: [ { "name": "...", "arguments": ... }, ... ]
|
|
1784
|
+
effectiveToolCalls = pObj
|
|
1785
|
+
.filter((item) => item && (item.name || item.tool || item.function?.name))
|
|
1786
|
+
.map((item, idx) => ({
|
|
1787
|
+
id: item.id || `call_fallback_${Date.now()}_${idx}`,
|
|
1788
|
+
type: 'function',
|
|
1789
|
+
function: {
|
|
1790
|
+
name: item.name || item.tool || item.function?.name,
|
|
1791
|
+
arguments:
|
|
1792
|
+
typeof item.arguments === 'object'
|
|
1793
|
+
? JSON.stringify(item.arguments)
|
|
1794
|
+
: typeof item.query === 'object'
|
|
1795
|
+
? JSON.stringify(item.query)
|
|
1796
|
+
: String(
|
|
1797
|
+
item.arguments ||
|
|
1798
|
+
(item.query !== undefined
|
|
1799
|
+
? JSON.stringify({ query: item.query })
|
|
1800
|
+
: '{}')
|
|
1801
|
+
)
|
|
1802
|
+
}
|
|
1803
|
+
}))
|
|
1804
|
+
currentTurnContent = ''
|
|
1805
|
+
finalContentAccumulator = ''
|
|
1806
|
+
} else if (pObj.action && (pObj.action.tool || pObj.action.name)) {
|
|
1807
|
+
effectiveToolCalls = [
|
|
1808
|
+
{
|
|
1809
|
+
id: `call_fallback_${Date.now()}_0`,
|
|
1810
|
+
type: 'function',
|
|
1811
|
+
function: {
|
|
1812
|
+
name: pObj.action.tool || pObj.action.name,
|
|
1813
|
+
arguments:
|
|
1814
|
+
typeof pObj.action.arguments === 'object'
|
|
1815
|
+
? JSON.stringify(pObj.action.arguments)
|
|
1816
|
+
: typeof pObj.action.query === 'object'
|
|
1817
|
+
? JSON.stringify(pObj.action.query)
|
|
1818
|
+
: JSON.stringify(
|
|
1819
|
+
pObj.action.query !== undefined
|
|
1820
|
+
? { query: pObj.action.query }
|
|
1821
|
+
: {}
|
|
1822
|
+
)
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
]
|
|
1826
|
+
currentTurnContent = ''
|
|
1827
|
+
finalContentAccumulator = ''
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
} catch (_) {}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Jika alur kerja Durable Task telah tuntas 100%, abaikan seluruh tool calls dan paksa penyelesaian
|
|
1835
|
+
if (isDurableTaskCompleted) {
|
|
1836
|
+
effectiveToolCalls = []
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// ======================================================================
|
|
1840
|
+
// CABANG 1: MODEL MEMANGGIL NATIVE TOOL CALLS
|
|
1841
|
+
// ======================================================================
|
|
1842
|
+
if (effectiveToolCalls && effectiveToolCalls.length > 0) {
|
|
1843
|
+
sentenceBuffer = ''
|
|
1844
|
+
speechQueue.reset()
|
|
1845
|
+
const assistantMsg = {
|
|
1846
|
+
role: 'assistant',
|
|
1847
|
+
content: streamResult.content || null,
|
|
1848
|
+
tool_calls: effectiveToolCalls
|
|
1849
|
+
}
|
|
1850
|
+
loopMessages.push(assistantMsg)
|
|
1851
|
+
const turnVisualUrls = []
|
|
1852
|
+
|
|
1853
|
+
for (const tc of effectiveToolCalls) {
|
|
1854
|
+
const toolName = tc.function?.name
|
|
1855
|
+
let parsedArgs = {}
|
|
1856
|
+
try {
|
|
1857
|
+
parsedArgs = JSON.parse(tc.function?.arguments || '{}')
|
|
1858
|
+
} catch (_) {
|
|
1859
|
+
parsedArgs = { raw: tc.function?.arguments || '' }
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
if (!toolName) continue
|
|
1863
|
+
if (sessionAbortController.signal.aborted) throw new Error('AbortError')
|
|
1864
|
+
|
|
1865
|
+
execSteps.push({ task: `Eksekusi ${toolName}`, query: JSON.stringify(parsedArgs) })
|
|
1866
|
+
targetPushProcess({
|
|
1867
|
+
id: agenticProcessId,
|
|
1868
|
+
type: 'planning',
|
|
1869
|
+
status: 'active',
|
|
1870
|
+
data: {
|
|
1871
|
+
steps: [...execSteps],
|
|
1872
|
+
currentStep: execSteps.length - 1,
|
|
1873
|
+
reasoning: currentTurnReasoning || `Mengeksekusi ${toolName}`
|
|
1874
|
+
}
|
|
1875
|
+
})
|
|
1876
|
+
|
|
1877
|
+
const toolReason =
|
|
1878
|
+
parsedArgs.reason ||
|
|
1879
|
+
parsedArgs.description ||
|
|
1880
|
+
parsedArgs.task ||
|
|
1881
|
+
parsedArgs.title ||
|
|
1882
|
+
null
|
|
1883
|
+
|
|
1884
|
+
currentInFlightTool = {
|
|
1885
|
+
tool: toolName,
|
|
1886
|
+
query: JSON.stringify(parsedArgs),
|
|
1887
|
+
reason: toolReason,
|
|
1888
|
+
status: 'running'
|
|
1889
|
+
}
|
|
1890
|
+
const currentLiveTools = [...executedToolsList, currentInFlightTool]
|
|
1891
|
+
|
|
1892
|
+
const currentCombined = [...accumulatedThoughts, currentTurnReasoning]
|
|
1893
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
1894
|
+
.filter(Boolean)
|
|
1895
|
+
const liveReasoning = Array.from(new Set(currentCombined)).join('\n\n---\n\n')
|
|
1896
|
+
|
|
1897
|
+
targetSetChatData((prev) => {
|
|
1898
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
1899
|
+
return [
|
|
1900
|
+
...filtered,
|
|
1901
|
+
{
|
|
1902
|
+
role: 'ai',
|
|
1903
|
+
content: streamResult.content || `Mengeksekusi [${toolName}]...`,
|
|
1904
|
+
isThinking: true,
|
|
1905
|
+
reasoning: liveReasoning || undefined,
|
|
1906
|
+
executedTools: currentLiveTools,
|
|
1907
|
+
mood: currentActiveMood,
|
|
1908
|
+
usage: lastServerUsage || undefined,
|
|
1909
|
+
tokens: lastServerUsage?.completion_tokens || undefined
|
|
1910
|
+
}
|
|
1911
|
+
]
|
|
1912
|
+
})
|
|
1913
|
+
|
|
1914
|
+
// Eksekusi tool
|
|
1915
|
+
const pluginProcessId = `plugin-${Date.now()}`
|
|
1916
|
+
const currentSessionTitle =
|
|
1917
|
+
activeSessionNum === 1
|
|
1918
|
+
? activeTopic?.title || activeTopic?.name || 'Main Thread'
|
|
1919
|
+
: `Sesi #${activeSessionNum}`
|
|
1920
|
+
|
|
1921
|
+
// Jika alur kerja Durable Task sudah selesai, tolak pemanggilan task baru atau mark_done_task
|
|
1922
|
+
if (
|
|
1923
|
+
durableTask &&
|
|
1924
|
+
!durableActiveStep &&
|
|
1925
|
+
(toolName === 'create_agent_task' || toolName === 'mark_done_task')
|
|
1926
|
+
) {
|
|
1927
|
+
const rejectResult = `[TUGAS TELAH SELESAI]: Seluruh tahapan alur kerja '${durableTask.title}' telah tuntas 100%. DILARANG memanggil tool '${toolName}' lagi! SEGERA berikan jawaban akhir ringkasan menyeluruh kepada pengguna.`
|
|
1928
|
+
const toolObservation = {
|
|
1929
|
+
type: 'tool_result',
|
|
1930
|
+
tool_call_id: tc.id,
|
|
1931
|
+
tool: toolName,
|
|
1932
|
+
success: false,
|
|
1933
|
+
data: null,
|
|
1934
|
+
error: rejectResult
|
|
1935
|
+
}
|
|
1936
|
+
loopMessages.push({
|
|
1937
|
+
role: 'tool',
|
|
1938
|
+
tool_call_id: tc.id,
|
|
1939
|
+
name: toolName,
|
|
1940
|
+
content: JSON.stringify(toolObservation)
|
|
1941
|
+
})
|
|
1942
|
+
continue
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const execResult = await executeSingleTool(toolName, parsedArgs, {
|
|
1946
|
+
tgContext,
|
|
1947
|
+
isAutonomous,
|
|
1948
|
+
loopMessages,
|
|
1949
|
+
pluginProcessId,
|
|
1950
|
+
targetSetChatData,
|
|
1951
|
+
activeSessionNum,
|
|
1952
|
+
activeTopic,
|
|
1953
|
+
userInput,
|
|
1954
|
+
durableTask,
|
|
1955
|
+
durableActiveStep,
|
|
1956
|
+
agenticProcessId,
|
|
1957
|
+
workspaceRoot: opts.workspaceRoot,
|
|
1958
|
+
sessionId: String(activeSessionNum || 1),
|
|
1959
|
+
sessionTitle: currentSessionTitle,
|
|
1960
|
+
signal: sessionAbortController.signal
|
|
1961
|
+
})
|
|
1962
|
+
|
|
1963
|
+
if (execResult.durableTask) {
|
|
1964
|
+
durableTask = execResult.durableTask
|
|
1965
|
+
durableTaskForRecovery = execResult.durableTask
|
|
1966
|
+
}
|
|
1967
|
+
if (toolName === 'create_agent_task' || toolName === 'mark_done_task') {
|
|
1968
|
+
durableActiveStep = execResult.durableActiveStep || null
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
lastToolExecution = execResult.toolExecution
|
|
1972
|
+
if (execResult.loadedGroup) {
|
|
1973
|
+
dynamicallyLoadedToolGroups.add(execResult.loadedGroup)
|
|
1974
|
+
}
|
|
1975
|
+
const resStr = String(execResult.resultString || '')
|
|
1976
|
+
const executionSucceeded =
|
|
1977
|
+
execResult.success === true ||
|
|
1978
|
+
execResult.res?.success === true ||
|
|
1979
|
+
(!resStr.startsWith('[ERROR]') &&
|
|
1980
|
+
!resStr.includes(' crash:') &&
|
|
1981
|
+
!resStr.toLowerCase().includes(' gagal:'))
|
|
1982
|
+
if (executionSucceeded) {
|
|
1983
|
+
consecutiveErrors = 0
|
|
1984
|
+
} else {
|
|
1985
|
+
consecutiveErrors++
|
|
1986
|
+
if (consecutiveErrors >= maxConsecutiveErrorRetries) {
|
|
1987
|
+
const error = new Error(
|
|
1988
|
+
`Batas retry error berturut-turut tercapai (${maxConsecutiveErrorRetries}).`
|
|
1989
|
+
)
|
|
1990
|
+
error.code = 'LOOP_GUARD'
|
|
1991
|
+
throw error
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
executedToolsList.push({
|
|
1996
|
+
tool: toolName,
|
|
1997
|
+
query: JSON.stringify(parsedArgs),
|
|
1998
|
+
reason: toolReason,
|
|
1999
|
+
status: executionSucceeded ? 'done' : 'failed',
|
|
2000
|
+
preview: execResult.previewUrl || execResult.imageUrls?.[0] || null,
|
|
2001
|
+
fullResult:
|
|
2002
|
+
typeof execResult.resultString === 'string'
|
|
2003
|
+
? execResult.resultString.slice(0, 4000)
|
|
2004
|
+
: execResult.resultString,
|
|
2005
|
+
resultSummary:
|
|
2006
|
+
typeof execResult.resultString === 'string' && execResult.resultString.length > 250
|
|
2007
|
+
? execResult.resultString.slice(0, 250) + '...'
|
|
2008
|
+
: execResult.resultString
|
|
2009
|
+
})
|
|
2010
|
+
currentInFlightTool = null
|
|
2011
|
+
|
|
2012
|
+
if (Array.isArray(execResult.imageUrls) && execResult.imageUrls.length > 0) {
|
|
2013
|
+
for (const u of execResult.imageUrls) {
|
|
2014
|
+
if (u && !turnVisualUrls.includes(u)) {
|
|
2015
|
+
turnVisualUrls.push(u)
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
let obsStr = execResult.resultString
|
|
2021
|
+
if (
|
|
2022
|
+
typeof execResult.resultString === 'string' &&
|
|
2023
|
+
execResult.resultString.length > 3000
|
|
2024
|
+
) {
|
|
2025
|
+
obsStr = `${execResult.resultString.slice(0, 3000)}\n\n[SISA OUTPUT DIPOTONG (Total: ${execResult.resultString.length} karakter). Gunakan start_line/end_line atau grep-search untuk mencari bagian spesifik.]`
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const toolObservation = {
|
|
2029
|
+
type: 'tool_result',
|
|
2030
|
+
tool_call_id: tc.id,
|
|
2031
|
+
tool: toolName,
|
|
2032
|
+
success: executionSucceeded,
|
|
2033
|
+
data: executionSucceeded ? obsStr : null,
|
|
2034
|
+
error: executionSucceeded ? null : obsStr
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Push role: 'tool' observation ke ephemeral context
|
|
2038
|
+
loopMessages.push({
|
|
2039
|
+
role: 'tool',
|
|
2040
|
+
tool_call_id: tc.id,
|
|
2041
|
+
name: toolName,
|
|
2042
|
+
content: JSON.stringify(toolObservation)
|
|
2043
|
+
})
|
|
2044
|
+
|
|
2045
|
+
// Jika mark_done_task berhasil dan ada tahap berikutnya, otomatis majukan loop
|
|
2046
|
+
if (
|
|
2047
|
+
toolName === 'mark_done_task' &&
|
|
2048
|
+
executionSucceeded &&
|
|
2049
|
+
execResult?.durableActiveStep &&
|
|
2050
|
+
execResult?.durableTask
|
|
2051
|
+
) {
|
|
2052
|
+
const nextStep = execResult.durableActiveStep
|
|
2053
|
+
const nextIdx = (nextStep.stepIndex ?? nextStep.index ?? 0) + 1
|
|
2054
|
+
const totalSteps = execResult.durableTask?.steps?.length || 0
|
|
2055
|
+
const isNextLastStep = nextIdx === totalSteps
|
|
2056
|
+
consecutiveErrors = 0
|
|
2057
|
+
loopMessages.push({
|
|
2058
|
+
role: 'user',
|
|
2059
|
+
content: `[TAHAP SELESAI & PINDAH KE TAHAP ${nextIdx}${isNextLastStep ? ' - TAHAP TERAKHIR' : ''}]: Artefak tahap sebelumnya telah disimpan ke disk.\n\n>>> SEKARANG KERJAKAN TAHAP ${nextIdx}: "${nextStep.title}"\n- Sasaran: ${nextStep.objective}\n- Target Deliverable: ${nextStep.deliverable}\n${nextStep.acceptanceCriteria?.length ? `- Kriteria Sukses: ${nextStep.acceptanceCriteria.join(', ')}` : ''}\nKerjakan tugas tahap ini di workspace pengguna (buat file/tulis kode). DILARANG memanggil 'read_task' untuk membaca artefak sebelumnya (kamu sudah tahu apa yang kamu buat). WAJIB UJI & VERIFIKASI HASIL terlebih dahulu. DILARANG memanggil 'mark_done_task' sebelum pengujian berhasil! Setelah teruji, barulah PANGGIL TOOL 'mark_done_task' TEPAT 1 KALI dengan parameter stepIndex: ${nextIdx}, artifactContent, dan verificationProof (bukti hasil uji)!${isNextLastStep ? ' Karena ini adalah TAHAP TERAKHIR, saat deliverable tahap ini selesai dan diuji, panggil tool "mark_done_task" dengan parameter "summary" yang memuat rangkuman menyeluruh seluruh alur kerja proyek dari awal hingga akhir!' : ''}`
|
|
2060
|
+
})
|
|
2061
|
+
await startAgentTaskStep(execResult.durableTask.id, nextStep.id)
|
|
2062
|
+
targetPushProcess({
|
|
2063
|
+
id: agenticProcessId,
|
|
2064
|
+
type: 'planning',
|
|
2065
|
+
status: 'active',
|
|
2066
|
+
data: {
|
|
2067
|
+
steps: (execResult.durableTask.steps || []).map((s) => ({ task: s.title })),
|
|
2068
|
+
currentStep: nextStep.stepIndex ?? nextStep.index ?? 0,
|
|
2069
|
+
reasoning: `Melanjutkan ke Tahap ${nextIdx}: "${nextStep.title}"...`
|
|
2070
|
+
}
|
|
2071
|
+
})
|
|
2072
|
+
} else if (
|
|
2073
|
+
toolName === 'mark_done_task' &&
|
|
2074
|
+
executionSucceeded &&
|
|
2075
|
+
!execResult?.durableActiveStep &&
|
|
2076
|
+
execResult?.durableTask
|
|
2077
|
+
) {
|
|
2078
|
+
// Seluruh tahapan alur kerja telah tuntas 100%!
|
|
2079
|
+
consecutiveErrors = 0
|
|
2080
|
+
durableActiveStep = null
|
|
2081
|
+
|
|
2082
|
+
const completedTask = execResult.durableTask
|
|
2083
|
+
const steps = completedTask.steps || []
|
|
2084
|
+
const taskTitle = completedTask.title || 'Task Workflow'
|
|
2085
|
+
|
|
2086
|
+
let rawSummary =
|
|
2087
|
+
(typeof parsedArgs.summary === 'string' && parsedArgs.summary.trim()) ||
|
|
2088
|
+
(typeof parsedArgs.artifactContent === 'string' &&
|
|
2089
|
+
parsedArgs.artifactContent.trim()) ||
|
|
2090
|
+
''
|
|
2091
|
+
|
|
2092
|
+
let finalReport = ''
|
|
2093
|
+
if (rawSummary && rawSummary.length > 150) {
|
|
2094
|
+
finalReport = rawSummary
|
|
2095
|
+
} else {
|
|
2096
|
+
const stepsSummary = steps
|
|
2097
|
+
.map((s, i) => {
|
|
2098
|
+
const num = (s.stepIndex ?? s.index ?? i) + 1
|
|
2099
|
+
const title = s.title || `Tahap ${num}`
|
|
2100
|
+
const out = s.outputSummary || s.deliverable || 'Selesai dan terverifikasi'
|
|
2101
|
+
return `${num}. **${title}**: ${out}`
|
|
2102
|
+
})
|
|
2103
|
+
.join('\n')
|
|
2104
|
+
|
|
2105
|
+
finalReport = `Seluruh tahapan alur kerja **${taskTitle}** telah berhasil diselesaikan dan terverifikasi 100%.\n\n### Rangkuman Hasil Pekerjaan:\n${stepsSummary}${rawSummary ? `\n\n**Catatan Tambahan:**\n${rawSummary}` : ''}`
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// Pastikan nol emoji
|
|
2109
|
+
finalReport = finalReport
|
|
2110
|
+
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, '')
|
|
2111
|
+
.trim()
|
|
2112
|
+
|
|
2113
|
+
isDone = true
|
|
2114
|
+
finalContentAccumulator = finalReport
|
|
2115
|
+
|
|
2116
|
+
execSteps.push({ task: 'Selesai' })
|
|
2117
|
+
targetPushProcess({
|
|
2118
|
+
id: agenticProcessId,
|
|
2119
|
+
type: 'planning',
|
|
2120
|
+
status: 'done',
|
|
2121
|
+
data: {
|
|
2122
|
+
steps: [...execSteps],
|
|
2123
|
+
currentStep: execSteps.length,
|
|
2124
|
+
reasoning: 'Seluruh tahapan tugas telah diselesaikan dan terverifikasi.'
|
|
2125
|
+
}
|
|
2126
|
+
})
|
|
2127
|
+
|
|
2128
|
+
targetSetChatData((prev) => {
|
|
2129
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
2130
|
+
const finalAllThoughts = [...accumulatedThoughts, currentTurnReasoning]
|
|
2131
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
2132
|
+
.filter(Boolean)
|
|
2133
|
+
const mergedReasoning =
|
|
2134
|
+
finalAllThoughts.length > 0
|
|
2135
|
+
? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
|
|
2136
|
+
: null
|
|
2137
|
+
|
|
2138
|
+
const { meta: reportMeta, cleanContent: cleanFinalOutput } =
|
|
2139
|
+
parseMarkTag(finalReport)
|
|
2140
|
+
if (reportMeta?.mood && reportMeta.mood !== 'neutral') {
|
|
2141
|
+
currentActiveMood = reportMeta.mood
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
const aiMsg = {
|
|
2145
|
+
role: 'ai',
|
|
2146
|
+
content: cleanFinalOutput,
|
|
2147
|
+
executedTools: executedToolsList.length > 0 ? executedToolsList : null,
|
|
2148
|
+
isTaskDone: true,
|
|
2149
|
+
reasoning: mergedReasoning,
|
|
2150
|
+
mood: currentActiveMood || 'neutral',
|
|
2151
|
+
pluginExecution: lastToolExecution,
|
|
2152
|
+
isProactive: isAutonomous,
|
|
2153
|
+
timestamp: getCurrentTimeInfo(),
|
|
2154
|
+
created_at: Date.now(),
|
|
2155
|
+
source: tgContext ? 'telegram' : 'pc',
|
|
2156
|
+
usage: lastServerUsage || null,
|
|
2157
|
+
tokens:
|
|
2158
|
+
lastServerUsage?.completion_tokens ||
|
|
2159
|
+
calculateMessageTokens({
|
|
2160
|
+
content: cleanFinalOutput,
|
|
2161
|
+
reasoning: mergedReasoning
|
|
2162
|
+
})
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
savedTurnAiMsg = aiMsg
|
|
2166
|
+
|
|
2167
|
+
return filtered
|
|
2168
|
+
.map((msg) => {
|
|
2169
|
+
if (!msg.isPlanSteps || msg.taskId !== completedTask.id) return msg
|
|
2170
|
+
return {
|
|
2171
|
+
...msg,
|
|
2172
|
+
taskStatus: 'completed',
|
|
2173
|
+
currentStep: (msg.plan || []).length,
|
|
2174
|
+
plan: (msg.plan || []).map((s) => ({
|
|
2175
|
+
...s,
|
|
2176
|
+
status: s.status === 'failed' ? 'failed' : 'completed'
|
|
2177
|
+
}))
|
|
2178
|
+
}
|
|
2179
|
+
})
|
|
2180
|
+
.concat(aiMsg)
|
|
2181
|
+
})
|
|
2182
|
+
|
|
2183
|
+
if (finalIsSpeak && finalReport) {
|
|
2184
|
+
playVoice(finalReport).catch(() => {})
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
if (window.api?.showNotification && !document.hasFocus() && finalReport) {
|
|
2188
|
+
window.api.showNotification('Mark', `Alur kerja "${taskTitle}" tuntas!`)
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
break
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// Jika ada tool yang menghasilkan gambar visual, sertakan observasi multimodal
|
|
2196
|
+
if (turnVisualUrls.length > 0) {
|
|
2197
|
+
loopMessages.push({
|
|
2198
|
+
role: 'user',
|
|
2199
|
+
content: [
|
|
2200
|
+
{
|
|
2201
|
+
type: 'text',
|
|
2202
|
+
text: '[Visual Observation]: Berikut adalah gambar visual aktual beresolusi penuh dari eksekusi tool di atas untuk kamu analisis secara langsung:'
|
|
2203
|
+
},
|
|
2204
|
+
...turnVisualUrls.map((url) => ({
|
|
2205
|
+
type: 'image_url',
|
|
2206
|
+
image_url: { url }
|
|
2207
|
+
}))
|
|
2208
|
+
]
|
|
2209
|
+
})
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// Lanjut ke giliran berikutnya untuk membiarkan model menganalisis observasi tool
|
|
2213
|
+
if (sessionAbortController.signal.aborted) {
|
|
2214
|
+
throw new Error('AbortError')
|
|
2215
|
+
}
|
|
2216
|
+
if (isDone) {
|
|
2217
|
+
break
|
|
2218
|
+
}
|
|
2219
|
+
continue
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// ======================================================================
|
|
2223
|
+
// CABANG 2: SELESAI / DIRECT TEXT RESPONSE (Stop / Selesai)
|
|
2224
|
+
// ======================================================================
|
|
2225
|
+
const rawTurnAnswer = streamResult.content || currentTurnContent || ''
|
|
2226
|
+
const { meta: turnMeta, cleanContent: turnAnswer } = parseMarkTag(rawTurnAnswer)
|
|
2227
|
+
if (turnMeta?.mood && turnMeta.mood !== 'neutral') {
|
|
2228
|
+
currentActiveMood = turnMeta.mood
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Jika alur kerja Durable Task sedang aktif tapi belum seluruh tahap selesai
|
|
2232
|
+
if (durableTask && !isDurableTaskCompleted) {
|
|
2233
|
+
const activeStep =
|
|
2234
|
+
durableActiveStep ||
|
|
2235
|
+
durableTask.steps?.find((s) => s.status === 'running') ||
|
|
2236
|
+
durableTask.steps?.find((s) => s.status !== 'completed') ||
|
|
2237
|
+
durableTask.steps?.[0]
|
|
2238
|
+
const curIdx = activeStep ? (activeStep.stepIndex ?? activeStep.index ?? 0) + 1 : 1
|
|
2239
|
+
const curTitle = activeStep?.title || 'Tahap Aktif'
|
|
2240
|
+
const curObj = activeStep?.objective || durableTask.objective || ''
|
|
2241
|
+
const curDeliv = activeStep?.deliverable || ''
|
|
2242
|
+
|
|
2243
|
+
loopMessages.push({
|
|
2244
|
+
role: 'assistant',
|
|
2245
|
+
content: rawTurnAnswer
|
|
2246
|
+
})
|
|
2247
|
+
loopMessages.push({
|
|
2248
|
+
role: 'user',
|
|
2249
|
+
content: `[PENGINGAT TAHAP AKTIF]: Kamu saat ini sedang berada di Tahap ${curIdx}: "${curTitle}".\n- Sasaran: ${curObj}\n- Target Deliverable: ${curDeliv}\n\nTahap ini BELUM selesai karena kamu belum memverifikasi hasil dan belum memanggil tool 'mark_done_task'. Kerjakan sasaran ini dengan tools yang relevan di workspace pengguna, lakukan pengujian/verifikasi hasil, lalu PANGGIL TOOL 'mark_done_task' dengan parameter stepIndex: ${curIdx}, artifactContent, dan verificationProof agar alur kerja dapat beralih ke tahap berikutnya!`
|
|
2250
|
+
})
|
|
2251
|
+
continue
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// Jika seluruh tahapan Durable Task telah tuntas diselesaikan via mark_done_task
|
|
2255
|
+
if (durableTask && isDurableTaskCompleted) {
|
|
2256
|
+
targetSetChatData((prev) =>
|
|
2257
|
+
prev.map((msg) => {
|
|
2258
|
+
if (!msg.isPlanSteps || msg.taskId !== durableTask.id) return msg
|
|
2259
|
+
return {
|
|
2260
|
+
...msg,
|
|
2261
|
+
taskStatus: 'completed',
|
|
2262
|
+
currentStep: (msg.plan || []).length,
|
|
2263
|
+
plan: (msg.plan || []).map((s) => ({
|
|
2264
|
+
...s,
|
|
2265
|
+
status: s.status === 'failed' ? 'failed' : 'completed'
|
|
2266
|
+
}))
|
|
2267
|
+
}
|
|
2268
|
+
})
|
|
2269
|
+
)
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// Cek apakah ada intervensi user yang masuk saat streaming giliran ini
|
|
2273
|
+
if (sessionRecord.interventions?.length > 0) {
|
|
2274
|
+
const interventions = sessionRecord.interventions.splice(0).join('\n')
|
|
2275
|
+
if (rawTurnAnswer && rawTurnAnswer.trim()) {
|
|
2276
|
+
loopMessages.push({ role: 'assistant', content: rawTurnAnswer })
|
|
2277
|
+
}
|
|
2278
|
+
loopMessages.push({ role: 'user', content: `[USER INTERVENTION]: ${interventions}` })
|
|
2279
|
+
|
|
2280
|
+
const alreadyInList = executedToolsList.some(
|
|
2281
|
+
(t) => t.type === 'intervention' && t.text === interventions
|
|
2282
|
+
)
|
|
2283
|
+
if (!alreadyInList) {
|
|
2284
|
+
executedToolsList.push({
|
|
2285
|
+
type: 'intervention',
|
|
2286
|
+
text: interventions,
|
|
2287
|
+
timestamp: Date.now()
|
|
2288
|
+
})
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
execSteps.push({ task: `Intervensi User: ${interventions}` })
|
|
2292
|
+
targetPushProcess({
|
|
2293
|
+
id: agenticProcessId,
|
|
2294
|
+
type: 'planning',
|
|
2295
|
+
status: 'active',
|
|
2296
|
+
data: {
|
|
2297
|
+
steps: [...execSteps],
|
|
2298
|
+
currentStep: execSteps.length - 1,
|
|
2299
|
+
reasoning: 'Menerima arahan baru dari user saat penyelesaian giliran.'
|
|
2300
|
+
}
|
|
2301
|
+
})
|
|
2302
|
+
|
|
2303
|
+
targetSetChatData((prev) =>
|
|
2304
|
+
prev.map((msg) =>
|
|
2305
|
+
msg.isThinking
|
|
2306
|
+
? {
|
|
2307
|
+
...msg,
|
|
2308
|
+
executedTools: [...executedToolsList]
|
|
2309
|
+
}
|
|
2310
|
+
: msg
|
|
2311
|
+
)
|
|
2312
|
+
)
|
|
2313
|
+
|
|
2314
|
+
continue
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// Strict Explicit Done: AI harus menuliskan done="true" untuk mengakhiri loop.
|
|
2318
|
+
// Jika done === false, teruskan loop ke turn berikutnya agar AI bisa mengeksekusi langkah lanjutan.
|
|
2319
|
+
if (!turnMeta.done) {
|
|
2320
|
+
if (rawTurnAnswer && rawTurnAnswer.trim()) {
|
|
2321
|
+
loopMessages.push({ role: 'assistant', content: rawTurnAnswer })
|
|
2322
|
+
}
|
|
2323
|
+
loopMessages.push({
|
|
2324
|
+
role: 'user',
|
|
2325
|
+
content: '[Catatan Sistem]: Lanjutkan analisis dan eksekusi tugas berikutnya.'
|
|
2326
|
+
})
|
|
2327
|
+
|
|
2328
|
+
execSteps.push({ task: 'Melanjutkan langkah...' })
|
|
2329
|
+
targetPushProcess({
|
|
2330
|
+
id: agenticProcessId,
|
|
2331
|
+
type: 'planning',
|
|
2332
|
+
status: 'active',
|
|
2333
|
+
data: {
|
|
2334
|
+
steps: [...execSteps],
|
|
2335
|
+
currentStep: execSteps.length - 1,
|
|
2336
|
+
reasoning: currentTurnReasoning || 'Melanjutkan langkah berikutnya...'
|
|
2337
|
+
}
|
|
2338
|
+
})
|
|
2339
|
+
|
|
2340
|
+
continue
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
isDone = true
|
|
2344
|
+
finalContentAccumulator = turnAnswer || 'Selesai.'
|
|
2345
|
+
|
|
2346
|
+
execSteps.push({ task: 'Selesai' })
|
|
2347
|
+
targetPushProcess({
|
|
2348
|
+
id: agenticProcessId,
|
|
2349
|
+
type: 'planning',
|
|
2350
|
+
status: 'done',
|
|
2351
|
+
data: {
|
|
2352
|
+
steps: [...execSteps],
|
|
2353
|
+
currentStep: execSteps.length,
|
|
2354
|
+
reasoning: currentTurnReasoning || 'Selesai'
|
|
2355
|
+
}
|
|
2356
|
+
})
|
|
2357
|
+
|
|
2358
|
+
// TTS Lisan: Kirimkan sisa buffer kalimat yang belum ter-enqueue
|
|
2359
|
+
if (finalIsSpeak) {
|
|
2360
|
+
if (sentenceBuffer && sentenceBuffer.trim()) {
|
|
2361
|
+
speechQueue.enqueue(sentenceBuffer.trim())
|
|
2362
|
+
sentenceBuffer = ''
|
|
2363
|
+
} else if (
|
|
2364
|
+
!speechQueue.isPlaying &&
|
|
2365
|
+
speechQueue.queue.length === 0 &&
|
|
2366
|
+
finalContentAccumulator
|
|
2367
|
+
) {
|
|
2368
|
+
// Fallback jika tidak ada tanda baca di output model sama sekali
|
|
2369
|
+
playVoice(finalContentAccumulator).catch(() => {})
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// OS Notification
|
|
2374
|
+
if (window.api.showNotification && !document.hasFocus() && finalContentAccumulator) {
|
|
2375
|
+
window.api.showNotification('Mark', finalContentAccumulator)
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// Tampilkan balasan final di chat UI
|
|
2379
|
+
targetSetChatData((prev) => {
|
|
2380
|
+
const filtered = prev.filter((item) => {
|
|
2381
|
+
if (item.isThinking) return false
|
|
2382
|
+
if (isAutonomous && item.isProactive && item.content === autonomousInitialMessage)
|
|
2383
|
+
return false
|
|
2384
|
+
return true
|
|
2385
|
+
})
|
|
2386
|
+
|
|
2387
|
+
const { meta: endMeta, cleanContent: parsedEndOutput } = parseMarkTag(
|
|
2388
|
+
finalContentAccumulator || ''
|
|
2389
|
+
)
|
|
2390
|
+
if (endMeta?.mood && endMeta.mood !== 'neutral') {
|
|
2391
|
+
currentActiveMood = endMeta.mood
|
|
2392
|
+
}
|
|
2393
|
+
let finalOutput = parsedEndOutput
|
|
2394
|
+
if (isAutonomous && autonomousInitialMessage) {
|
|
2395
|
+
finalOutput = `**${autonomousInitialMessage}**\n\n${finalOutput}`
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const finalAllThoughts = [...accumulatedThoughts, currentTurnReasoning]
|
|
2399
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
2400
|
+
.filter(Boolean)
|
|
2401
|
+
const mergedReasoning =
|
|
2402
|
+
finalAllThoughts.length > 0
|
|
2403
|
+
? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
|
|
2404
|
+
: null
|
|
2405
|
+
|
|
2406
|
+
const aiMsg = {
|
|
2407
|
+
role: 'ai',
|
|
2408
|
+
content: finalOutput,
|
|
2409
|
+
executedTools: executedToolsList.length > 0 ? executedToolsList : null,
|
|
2410
|
+
isTaskDone: true,
|
|
2411
|
+
reasoning: mergedReasoning,
|
|
2412
|
+
mood: currentActiveMood || 'neutral',
|
|
2413
|
+
pluginExecution: lastToolExecution,
|
|
2414
|
+
isProactive: isAutonomous,
|
|
2415
|
+
timestamp: getCurrentTimeInfo(),
|
|
2416
|
+
created_at: Date.now(),
|
|
2417
|
+
source: tgContext ? 'telegram' : 'pc',
|
|
2418
|
+
usage: lastServerUsage || null,
|
|
2419
|
+
tokens:
|
|
2420
|
+
lastServerUsage?.completion_tokens ||
|
|
2421
|
+
calculateMessageTokens({ content: finalOutput, reasoning: mergedReasoning })
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
savedTurnAiMsg = aiMsg
|
|
2425
|
+
return [...filtered, aiMsg]
|
|
2426
|
+
})
|
|
2427
|
+
|
|
2428
|
+
// Meta-Learning: Sintesis skill otomatis jika turn berhasil mengeksekusi tool bermakna
|
|
2429
|
+
if (
|
|
2430
|
+
!isAutonomous &&
|
|
2431
|
+
!isSystem &&
|
|
2432
|
+
executedToolsList &&
|
|
2433
|
+
executedToolsList.length > 0 &&
|
|
2434
|
+
finalContentAccumulator
|
|
2435
|
+
) {
|
|
2436
|
+
const finalAllThoughts = [...accumulatedThoughts, currentTurnReasoning]
|
|
2437
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
2438
|
+
.filter(Boolean)
|
|
2439
|
+
const mergedReasoning =
|
|
2440
|
+
finalAllThoughts.length > 0
|
|
2441
|
+
? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
|
|
2442
|
+
: ''
|
|
2443
|
+
|
|
2444
|
+
synthesizeSkillAndSave({
|
|
2445
|
+
userPrompt: lastUserPromptRef.current || userInput,
|
|
2446
|
+
executedTools: executedToolsList,
|
|
2447
|
+
finalAnswer: finalContentAccumulator,
|
|
2448
|
+
thought: mergedReasoning
|
|
2449
|
+
}).catch((err) => {
|
|
2450
|
+
console.warn('[useMarkPlan] Background Meta-Learning error:', err)
|
|
2451
|
+
})
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
break
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
if (sessionAbortController.signal.aborted) {
|
|
2458
|
+
throw new Error('AbortError')
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// Pastikan sisa thinking indicator selalu dibersihkan jika loop selesai
|
|
2462
|
+
targetSetChatData((prev) => {
|
|
2463
|
+
const hasThinking = prev.some((item) => item.isThinking)
|
|
2464
|
+
if (!hasThinking) return prev
|
|
2465
|
+
const filtered = prev.filter((item) => !item.isThinking)
|
|
2466
|
+
if (finalContentAccumulator) return filtered
|
|
2467
|
+
|
|
2468
|
+
const finalAllThoughts = [...accumulatedThoughts]
|
|
2469
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
2470
|
+
.filter(Boolean)
|
|
2471
|
+
const mergedReasoning =
|
|
2472
|
+
finalAllThoughts.length > 0
|
|
2473
|
+
? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
|
|
2474
|
+
: null
|
|
2475
|
+
|
|
2476
|
+
return [
|
|
2477
|
+
...filtered,
|
|
2478
|
+
{
|
|
2479
|
+
role: 'ai',
|
|
2480
|
+
content: 'Tugas telah selesai diproses.',
|
|
2481
|
+
executedTools: executedToolsList.length > 0 ? executedToolsList : null,
|
|
2482
|
+
isTaskDone: true,
|
|
2483
|
+
reasoning: mergedReasoning,
|
|
2484
|
+
mood: currentActiveMood || 'neutral',
|
|
2485
|
+
timestamp: getCurrentTimeInfo(),
|
|
2486
|
+
created_at: Date.now(),
|
|
2487
|
+
usage: lastServerUsage || null,
|
|
2488
|
+
tokens:
|
|
2489
|
+
lastServerUsage?.completion_tokens ||
|
|
2490
|
+
calculateMessageTokens({
|
|
2491
|
+
content: 'Tugas telah selesai diproses.',
|
|
2492
|
+
reasoning: mergedReasoning
|
|
2493
|
+
})
|
|
2494
|
+
}
|
|
2495
|
+
]
|
|
2496
|
+
})
|
|
2497
|
+
|
|
2498
|
+
// ------------------------------------------------------------------------
|
|
2499
|
+
// FASE 5: CLEANUP & CLOSING
|
|
2500
|
+
// ------------------------------------------------------------------------
|
|
2501
|
+
targetPushProcess({
|
|
2502
|
+
id: agenticProcessId,
|
|
2503
|
+
type: 'planning',
|
|
2504
|
+
status: 'done',
|
|
2505
|
+
data: {
|
|
2506
|
+
steps: [...execSteps],
|
|
2507
|
+
currentStep: execSteps.length,
|
|
2508
|
+
reasoning: accumulatedThoughts[accumulatedThoughts.length - 1] || 'Selesai'
|
|
2509
|
+
}
|
|
2510
|
+
})
|
|
2511
|
+
setTimeout(() => {
|
|
2512
|
+
dismissProcess(agenticProcessId)
|
|
2513
|
+
}, 1500)
|
|
2514
|
+
|
|
2515
|
+
if (!tgContext && !isAutonomous) {
|
|
2516
|
+
if (activeSessionNum === 1) {
|
|
2517
|
+
setIsLoading(false)
|
|
2518
|
+
}
|
|
2519
|
+
lastUserPromptRef.current = ''
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// Post-Turn Context Sync: Hitung total token terkini dan trigger event ke UI
|
|
2523
|
+
try {
|
|
2524
|
+
let latestSessionData =
|
|
2525
|
+
activeSessionNum === 1 ? chatDataRef.current || chatData : inMemorySessionData
|
|
2526
|
+
if (savedTurnAiMsg) {
|
|
2527
|
+
const hasAi = (latestSessionData || []).some(
|
|
2528
|
+
(m) => m.created_at === savedTurnAiMsg.created_at
|
|
2529
|
+
)
|
|
2530
|
+
if (!hasAi) {
|
|
2531
|
+
latestSessionData = [...(latestSessionData || []), savedTurnAiMsg]
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const latestTokens = calculateSessionTokens(
|
|
2535
|
+
latestSessionData,
|
|
2536
|
+
activeSessionCompact?.summaryBlock || activeSessionCompact?.summary_block || '',
|
|
2537
|
+
activeSessionCompact?.lastCompactedMessageId ||
|
|
2538
|
+
activeSessionCompact?.last_compacted_message_id ||
|
|
2539
|
+
null,
|
|
2540
|
+
systemPrompt,
|
|
2541
|
+
activeSessionCompact?.lastCompactedAt || activeSessionCompact?.last_compacted_at || null
|
|
2542
|
+
)
|
|
2543
|
+
window.dispatchEvent(
|
|
2544
|
+
new CustomEvent('context-tracker-updated', {
|
|
2545
|
+
detail: {
|
|
2546
|
+
sessionId: String(activeSessionNum),
|
|
2547
|
+
currentTokens: latestTokens,
|
|
2548
|
+
maxTokens: MAX_CONTEXT_TOKENS,
|
|
2549
|
+
percentage: Math.min(100, (latestTokens / MAX_CONTEXT_TOKENS) * 100),
|
|
2550
|
+
currentChars: latestTokens,
|
|
2551
|
+
maxChars: MAX_CONTEXT_TOKENS,
|
|
2552
|
+
lastCompactedAt: activeSessionCompact?.lastCompactedAt || null
|
|
2553
|
+
}
|
|
2554
|
+
})
|
|
2555
|
+
)
|
|
2556
|
+
} catch {
|
|
2557
|
+
/* ignore */
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
try {
|
|
2561
|
+
if (window.api && window.api.executeNativeTool) {
|
|
2562
|
+
window.api.executeNativeTool('os-control-close').catch(() => {})
|
|
2563
|
+
}
|
|
2564
|
+
} catch {
|
|
2565
|
+
/* ignore */
|
|
2566
|
+
}
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
const isAbort =
|
|
2569
|
+
error.name === 'AbortError' ||
|
|
2570
|
+
error.message?.includes('AbortError') ||
|
|
2571
|
+
Boolean(sessionAbortController?.signal?.aborted)
|
|
2572
|
+
|
|
2573
|
+
if (!isAbort) {
|
|
2574
|
+
console.error('[useMarkPlan] Critical ReAct Loop Error:', error)
|
|
2575
|
+
} else {
|
|
2576
|
+
console.log('[useMarkPlan] ReAct loop dihentikan oleh pengguna.')
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
if (!isAbort) {
|
|
2580
|
+
targetPushProcess({
|
|
2581
|
+
id: agenticProcessId,
|
|
2582
|
+
type: 'planning',
|
|
2583
|
+
status: 'failed',
|
|
2584
|
+
data: {
|
|
2585
|
+
steps: [...execSteps],
|
|
2586
|
+
currentStep: execSteps.length,
|
|
2587
|
+
reasoning: `Error: ${error.message}`
|
|
2588
|
+
}
|
|
2589
|
+
})
|
|
2590
|
+
setTimeout(() => {
|
|
2591
|
+
dismissProcess(agenticProcessId)
|
|
2592
|
+
}, 3000)
|
|
2593
|
+
|
|
2594
|
+
if (durableTaskForRecovery && durableTaskForRecovery.status === 'running') {
|
|
2595
|
+
transitionAgentTask(
|
|
2596
|
+
durableTaskForRecovery.id,
|
|
2597
|
+
'failed',
|
|
2598
|
+
`Uncaught exception: ${error.message}`
|
|
2599
|
+
).catch(() => {})
|
|
2600
|
+
}
|
|
2601
|
+
} else {
|
|
2602
|
+
dismissProcess(agenticProcessId)
|
|
2603
|
+
try {
|
|
2604
|
+
speechQueue.reset()
|
|
2605
|
+
} catch (_) {}
|
|
2606
|
+
try {
|
|
2607
|
+
const runningTasks = await db.agentTasks.where('status').equals('running').toArray()
|
|
2608
|
+
for (const t of runningTasks) {
|
|
2609
|
+
await transitionAgentTask(
|
|
2610
|
+
t.id,
|
|
2611
|
+
'cancelled',
|
|
2612
|
+
'Eksekusi dibatalkan atas permintaan pengguna.'
|
|
2613
|
+
).catch(() => {})
|
|
2614
|
+
}
|
|
2615
|
+
} catch (_) {}
|
|
2616
|
+
if (durableTaskForRecovery) {
|
|
2617
|
+
transitionAgentTask(
|
|
2618
|
+
durableTaskForRecovery.id,
|
|
2619
|
+
'cancelled',
|
|
2620
|
+
'Eksekusi dibatalkan atas permintaan pengguna.'
|
|
2621
|
+
).catch(() => {})
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
targetSetChatData((prev) => {
|
|
2626
|
+
const thinkingItem = prev.find((item) => item.isThinking)
|
|
2627
|
+
let updated = prev.filter((item) => !item.isThinking)
|
|
2628
|
+
|
|
2629
|
+
if (isAbort) {
|
|
2630
|
+
updated = updated.map((msg) => {
|
|
2631
|
+
if (!msg.isPlanSteps) return msg
|
|
2632
|
+
if (durableTaskForRecovery && msg.taskId && msg.taskId !== durableTaskForRecovery.id)
|
|
2633
|
+
return msg
|
|
2634
|
+
return {
|
|
2635
|
+
...msg,
|
|
2636
|
+
taskStatus: 'stopped',
|
|
2637
|
+
plan: (msg.plan || []).map((s) => ({
|
|
2638
|
+
...s,
|
|
2639
|
+
status: s.status === 'running' ? 'stopped' : s.status
|
|
2640
|
+
}))
|
|
2641
|
+
}
|
|
2642
|
+
})
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// Ambil riwayat tool yang sempat dieksekusi sebelum di-abort
|
|
2646
|
+
let rawExecutedTools =
|
|
2647
|
+
thinkingItem?.executedTools && thinkingItem.executedTools.length > 0
|
|
2648
|
+
? [...thinkingItem.executedTools]
|
|
2649
|
+
: [...(executedToolsList || [])]
|
|
2650
|
+
|
|
2651
|
+
// Pastikan in-flight tool yang sedang dieksekusi saat abort tidak hilang
|
|
2652
|
+
if (
|
|
2653
|
+
currentInFlightTool &&
|
|
2654
|
+
!rawExecutedTools.some(
|
|
2655
|
+
(t) =>
|
|
2656
|
+
t.tool === currentInFlightTool.tool &&
|
|
2657
|
+
(t.status === 'running' || t.status === 'stopped')
|
|
2658
|
+
)
|
|
2659
|
+
) {
|
|
2660
|
+
rawExecutedTools.push(currentInFlightTool)
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// Tandai tool yang sedang 'running' saat abort menjadi 'stopped'
|
|
2664
|
+
const preservedExecutedTools = rawExecutedTools.map((t) => ({
|
|
2665
|
+
...t,
|
|
2666
|
+
status: t.status === 'running' ? 'stopped' : t.status
|
|
2667
|
+
}))
|
|
2668
|
+
|
|
2669
|
+
// Ambil riwayat pemikiran (reasoning) yang sempat digenerate
|
|
2670
|
+
const allAccumulated = [...accumulatedThoughts, currentTurnReasoning]
|
|
2671
|
+
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
2672
|
+
.filter(Boolean)
|
|
2673
|
+
const fallbackReasoning =
|
|
2674
|
+
allAccumulated.length > 0
|
|
2675
|
+
? Array.from(new Set(allAccumulated)).join('\n\n---\n\n')
|
|
2676
|
+
: undefined
|
|
2677
|
+
const preservedReasoning = thinkingItem?.reasoning || fallbackReasoning
|
|
2678
|
+
|
|
2679
|
+
// Ambil konten parsial yang sempat digenerate
|
|
2680
|
+
const rawContent =
|
|
2681
|
+
finalContentAccumulator ||
|
|
2682
|
+
(thinkingItem?.content && !thinkingItem.content.startsWith('Mengeksekusi [')
|
|
2683
|
+
? thinkingItem.content
|
|
2684
|
+
: '')
|
|
2685
|
+
const partialContent = typeof rawContent === 'string' ? rawContent.trim() : ''
|
|
2686
|
+
|
|
2687
|
+
let abortContent = ''
|
|
2688
|
+
if (isAbort) {
|
|
2689
|
+
abortContent = partialContent
|
|
2690
|
+
? `${partialContent}\n\n[Eksekusi dihentikan oleh pengguna]`
|
|
2691
|
+
: 'Eksekusi dibatalkan atas permintaan pengguna.'
|
|
2692
|
+
} else {
|
|
2693
|
+
abortContent = partialContent
|
|
2694
|
+
? `${partialContent}\n\n[Terjadi kendala saat memproses: ${error.message}]`
|
|
2695
|
+
: `Terjadi kendala saat memproses: ${error.message}`
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
const finalUpdated = [
|
|
2699
|
+
...updated,
|
|
2700
|
+
{
|
|
2701
|
+
role: 'ai',
|
|
2702
|
+
content: abortContent,
|
|
2703
|
+
reasoning: preservedReasoning,
|
|
2704
|
+
executedTools: preservedExecutedTools.length > 0 ? preservedExecutedTools : undefined,
|
|
2705
|
+
mood: isAbort ? 'neutral' : 'sadness',
|
|
2706
|
+
timestamp: getCurrentTimeInfo(),
|
|
2707
|
+
created_at: Date.now()
|
|
2708
|
+
}
|
|
2709
|
+
]
|
|
2710
|
+
|
|
2711
|
+
if (activeSessionNum === 1) {
|
|
2712
|
+
saveSession('1', finalUpdated).catch((err) => {
|
|
2713
|
+
console.warn('[useMarkPlan] Gagal auto-save abort main thread session 1:', err)
|
|
2714
|
+
})
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
return finalUpdated
|
|
2718
|
+
})
|
|
2719
|
+
} finally {
|
|
2720
|
+
activeSessionsRef.current.delete(activeSessionNum)
|
|
2721
|
+
activeSessionUpdatersRef.current.delete(activeSessionNum)
|
|
2722
|
+
if (removeRunningSessionId) removeRunningSessionId(activeSessionNum)
|
|
2723
|
+
if (activeSessionsRef.current.size === 0) {
|
|
2724
|
+
setIsAgentBusy(false)
|
|
2725
|
+
if (setRunningSessionId) setRunningSessionId(null)
|
|
2726
|
+
}
|
|
2727
|
+
if (activeSessionNum === 1) {
|
|
2728
|
+
setIsLoading(false)
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
return {
|
|
2734
|
+
handlePlanningCommand,
|
|
2735
|
+
handleIntervention,
|
|
2736
|
+
handleStop,
|
|
2737
|
+
activeRunningSessionIdRef
|
|
2738
|
+
}
|
|
2739
|
+
}
|