@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.
Files changed (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. package/src/shared/parsers/mark-tag-parser.js +275 -0
@@ -0,0 +1,729 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { generateGeminiResponse } from '../gemini-web.js'
5
+ import {
6
+ generateDeepSeekResponse,
7
+ uploadImageFile,
8
+ waitForFileReady,
9
+ getSessionState
10
+ } from '../deepseek-web.js'
11
+ import {
12
+ cleanAndParse,
13
+ checkCloudThrottle,
14
+ getSystemSignature,
15
+ createMoodStreamFilter,
16
+ parseMarkTag,
17
+ stripMarkTags
18
+ } from './ai-utils.js'
19
+ import { getActiveConfig, loadConfig } from '../../config-manager.js'
20
+ import { GROUP_TOOLS_SCHEMA } from '../../tools/group-tools.js'
21
+ import { loadAllPlugins } from '../../../main/plugins/plugin-loader.js'
22
+
23
+ /**
24
+ * Ekstraksi path / data URL berkas gambar dari array pesan chat (hanya turn aktif terbaru)
25
+ */
26
+ function extractImageSources(messages) {
27
+ const sources = []
28
+ if (!Array.isArray(messages) || messages.length === 0) return sources
29
+
30
+ // Hanya periksa turn aktif terbaru (setelah pesan assistant terakhir).
31
+ // Mencegah gambar dari riwayat masa lalu di-upload ulang saat greeting atau pesan follow-up.
32
+ let lastAssistantIdx = -1
33
+ for (let i = messages.length - 1; i >= 0; i--) {
34
+ const role = (messages[i]?.role || '').toLowerCase()
35
+ if (role === 'assistant' || role === 'ai' || role === 'model') {
36
+ lastAssistantIdx = i
37
+ break
38
+ }
39
+ }
40
+
41
+ const activeTurnMessages = messages.slice(lastAssistantIdx + 1)
42
+ if (activeTurnMessages.length === 0) return sources
43
+
44
+ const resolvePath = (val) => {
45
+ if (!val || typeof val !== 'string') return null
46
+ const trimmed = val.trim().replace(/^['"]|['"]$/g, '')
47
+ if (trimmed.startsWith('data:image/')) return trimmed
48
+ if (trimmed.startsWith('/api/chat/temp-file/')) {
49
+ const fn = decodeURIComponent(trimmed.replace('/api/chat/temp-file/', ''))
50
+ return path.join(os.homedir(), '.config', 'mark-agent', 'temp-uploads', fn)
51
+ }
52
+ if (fs.existsSync(trimmed)) return trimmed
53
+ if (trimmed.includes('temp-uploads')) {
54
+ const fn = path.basename(trimmed)
55
+ const testPath = path.join(os.homedir(), '.config', 'mark-agent', 'temp-uploads', fn)
56
+ if (fs.existsSync(testPath)) return testPath
57
+ }
58
+ return null
59
+ }
60
+
61
+ for (const m of activeTurnMessages) {
62
+ if (!m) continue
63
+ if (typeof m.content === 'string') {
64
+ if (m.content.includes('[FILE TERLAMPIR]:')) {
65
+ const afterTag = m.content.split(/\[FILE TERLAMPIR\]:/i)[1] || ''
66
+ const line = afterTag.split('\n')[0]
67
+ const matches = line.match(/"([^"]+)"/g)
68
+ if (matches) {
69
+ for (const raw of matches) {
70
+ const resolved = resolvePath(raw)
71
+ if (resolved) sources.push(resolved)
72
+ }
73
+ }
74
+ }
75
+ const mdRegex = /!\[.*?\]\(([^)]+)\)/gi
76
+ let mdMatch
77
+ while ((mdMatch = mdRegex.exec(m.content)) !== null) {
78
+ const resolved = resolvePath(mdMatch[1])
79
+ if (resolved) sources.push(resolved)
80
+ }
81
+ } else if (Array.isArray(m.content)) {
82
+ for (const item of m.content) {
83
+ if (!item) continue
84
+ if (typeof item === 'string') {
85
+ const resolved = resolvePath(item)
86
+ if (resolved) sources.push(resolved)
87
+ } else if (item.type === 'image_url') {
88
+ const url =
89
+ item.image_url?.url ||
90
+ item.url ||
91
+ (typeof item.image_url === 'string' ? item.image_url : null)
92
+ const resolved = resolvePath(url)
93
+ if (resolved) sources.push(resolved)
94
+ } else if (item.image_url || item.url) {
95
+ const url = item.image_url?.url || item.image_url || item.url
96
+ if (typeof url === 'string') {
97
+ const resolved = resolvePath(url)
98
+ if (resolved) sources.push(resolved)
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return [...new Set(sources)]
105
+ }
106
+
107
+ /**
108
+ * Membangun Dynamic State Anchor ringkas untuk turn sela sesi aktif DeepSeek Web.
109
+ * Memangkas persona dan background statis (~15.000 char -> ~400-800 char),
110
+ * namun tetap membawa waktu real-time, status PC, status musik, relational traits,
111
+ * mood continuity anchor, serta skema tools jika hasTools aktif.
112
+ */
113
+ function buildDynamicStateAnchor(sysMsgs, lastAssistant, hasTools = false) {
114
+ const fullContent = (sysMsgs || [])
115
+ .map((m) => (typeof m.content === 'string' ? m.content : ''))
116
+ .join('\n\n')
117
+
118
+ const extractedSections = []
119
+
120
+ // 1. Konteks Waktu & Riwayat
121
+ const timeMatch = fullContent.match(/\[KONTEKS WAKTU & RIWAYAT\][\s\S]*?(?=\n#|\n\n\[|$)/i)
122
+ if (timeMatch) extractedSections.push(timeMatch[0].trim())
123
+
124
+ // 2. Relational Growth (warmth, trust, sarcasm)
125
+ const relMatch = fullContent.match(/# RELATIONAL GROWTH[\s\S]*?(?=\n#|\n\n\[|$)/i)
126
+ if (relMatch) extractedSections.push(relMatch[0].trim())
127
+
128
+ // 3. Telemetri Fisik PC
129
+ const telemMatch = fullContent.match(/# TELEMETRI FISIK PC[\s\S]*?(?=\n#|\n\n\[|$)/i)
130
+ if (telemMatch) extractedSections.push(telemMatch[0].trim())
131
+
132
+ // 4. Status Player Musik
133
+ const musicMatch = fullContent.match(/# STATUS PLAYER MUSIK[\s\S]*?(?=\n#|\n\n\[|$)/i)
134
+ if (musicMatch) extractedSections.push(musicMatch[0].trim())
135
+
136
+ // 5. Gumaman Batin Terakhir
137
+ const thoughtMatch = fullContent.match(/# GUMAMAN BATIN TERAKHIR[\s\S]*?(?=\n#|\n\n\[|$)/i)
138
+ if (thoughtMatch) extractedSections.push(thoughtMatch[0].trim())
139
+
140
+ const lastMood = lastAssistant?.mood || 'neutral'
141
+ const dynamicBody = extractedSections.length > 0 ? `\n\n${extractedSections.join('\n\n')}` : ''
142
+
143
+ let toolSection = ''
144
+ if (hasTools) {
145
+ const toolMatch = fullContent.match(/# TOOLS & CAPABILITY REGISTRY[\s\S]*$/i)
146
+ if (toolMatch) {
147
+ toolSection = `\n\n${toolMatch[0].trim()}`
148
+ }
149
+ }
150
+
151
+ return `[DYNAMIC STATE ANCHOR - MARK AI OS]
152
+ Kamu adalah MARK (Metacognitive Artificial Relational Knowledge). Tetap konsisten dengan kepribadian santai/cerdas tongkrongan, bukan robot kaku.
153
+ ATURAN KONTROL & EMOSI MUTLAK:
154
+ Awali baris pertama responmu dengan tag: <mark mood="[nama_mood]" done="[true|false]" />
155
+ - Atribut mood: neutral, happy, thinking, sarcasm, focused, excited, sad, confused, annoyed, calm.
156
+ - Atribut done: false jika sedang memanggil tool atau berencana melanjutkan langkah berikutnya. true HANYA jika seluruh tugas/pertanyaan pengguna telah selesai tuntas dijawab.
157
+ Status giliran sebelumnya: <mark mood="${lastMood || 'neutral'}" done="true" />. Pertahankan kontinuitas transisi emosi secara natural!${dynamicBody}${toolSection}`
158
+ }
159
+
160
+ export async function executeWebProvider({
161
+ messages,
162
+ tools = null,
163
+ stream = false,
164
+ config = {},
165
+ isSmallTask = false,
166
+ jsonSchema = null,
167
+ sessionId = null,
168
+ onToken = null,
169
+ onReasoning = null,
170
+ onMood = null,
171
+ onMeta = null,
172
+ onToolCall = null,
173
+ onStatus = null
174
+ }) {
175
+ const activeConf = getActiveConfig() || {}
176
+ const conf = { ...activeConf, ...(config || {}) }
177
+ const isGemini = conf.aiProvider === 'gemini-web'
178
+ const hasTools = Array.isArray(tools) && tools.length > 0
179
+ const shouldStreamContent = stream && !hasTools && !jsonSchema
180
+
181
+ let streamedAnyToken = false
182
+ const moodFilter =
183
+ shouldStreamContent && onToken
184
+ ? createMoodStreamFilter(
185
+ (token) => {
186
+ streamedAnyToken = true
187
+ onToken(token)
188
+ },
189
+ (mood) => {
190
+ onMood?.(mood)
191
+ },
192
+ (meta) => {
193
+ onMeta?.(meta)
194
+ }
195
+ )
196
+ : null
197
+
198
+ await checkCloudThrottle(isSmallTask, onStatus)
199
+
200
+ let workMessages = (messages || []).map((m) => ({ ...m }))
201
+
202
+ // 1. Injeksi Instruksi JSON Schema jika ada
203
+ if (jsonSchema) {
204
+ const instruction = `\n\n[CRITICAL] YOU MUST RETURN ONLY VALID JSON THAT STRICTLY MATCHES THIS EXACT SCHEMA:\n${JSON.stringify(jsonSchema)}\n`
205
+ const sysIdx = workMessages.findIndex((m) => m.role === 'system')
206
+ if (sysIdx >= 0) {
207
+ workMessages[sysIdx].content += instruction
208
+ } else {
209
+ workMessages.unshift({ role: 'system', content: instruction })
210
+ }
211
+ }
212
+
213
+ // 2. Injeksi Skema Tools OpenAPI jika ada
214
+ if (Array.isArray(tools) && tools.length > 0) {
215
+ const activeToolList = tools
216
+ .map((t) => {
217
+ const fn = t.function || t
218
+ return `- ${fn.name}: ${fn.description || ''}\n Parameters: ${JSON.stringify(fn.parameters || {})}`
219
+ })
220
+ .join('\n')
221
+
222
+ const builtInGroupsGuide = Object.entries(GROUP_TOOLS_SCHEMA || {})
223
+ .map(([key, data]) => `- '${key}': ${data.description || '-'}`)
224
+ .join('\n')
225
+
226
+ let activePluginsGuide = ''
227
+ try {
228
+ const allPlugins = await loadAllPlugins()
229
+ if (Array.isArray(allPlugins)) {
230
+ const active = allPlugins.filter((p) => p.isEnabled !== false)
231
+ if (active.length > 0) {
232
+ activePluginsGuide = active
233
+ .map((p) => `- '${p.name}': ${p.description || '-'}`)
234
+ .join('\n')
235
+ }
236
+ }
237
+ } catch {
238
+ // ignore plugin load failure in web-provider
239
+ }
240
+
241
+ const toolSections = []
242
+
243
+ if (activeToolList) {
244
+ toolSections.push(`## 1. TOOLS AKTIF (DAPAT LANGSUNG DIEKSEKUSI):\n${activeToolList}`)
245
+ }
246
+
247
+ if (builtInGroupsGuide) {
248
+ toolSections.push(
249
+ `## 2. GRUP TOOL BAWAAN SISTEM (ON-DEMAND):\n${builtInGroupsGuide}\n*Penting: Untuk mengaktifkan fungsi spesifik dari grup di atas, panggil 'read-tools' dengan {"group_name": "nama_grup"}.*`
250
+ )
251
+ }
252
+
253
+ if (activePluginsGuide) {
254
+ toolSections.push(
255
+ `## 3. PLUGIN EKSTERNAL (AKTIF):\n${activePluginsGuide}\n*Penting: Untuk mengaktifkan aksi/fungsi dari plugin di atas, panggil 'read-tools' dengan {"group_name": "nama_plugin"}.*`
256
+ )
257
+ }
258
+
259
+ const toolInstruction = `\n\n# TOOLS & CAPABILITY REGISTRY:
260
+ Kamu memiliki akses ke kapabilitas sistem berikut:
261
+
262
+ ${toolSections.join('\n\n')}
263
+
264
+ # ATURAN EKSEKUSI TOOL (PENTING MUTLAK):
265
+ 1. Jika kamu ingin menjalankan tindakan atau memanggil fungsi sistem di atas, kamu HARUS merespons HANYA dengan format JSON valid.
266
+ 2. Respons kamu HARUS DIAWALI LANGSUNG DENGAN KARAKTER '{' DAN DIAKHIRI DENGAN KARAKTER '}'.
267
+ 3. DILARANG KERAS menyertakan teks pesan, kata pengantar, obrolan, basa-basi, permintaan maaf, penjelasan, atau penutup apapun di luar objek JSON! Jangan tulis teks apapun sebelum '{' atau setelah '}'.
268
+ 4. Format JSON untuk memanggil tool WAJIB persis seperti ini (mendukung BATCH / MULTI-TOOL sekaligus):
269
+ {
270
+ "tool_calls": [
271
+ {
272
+ "name": "nama_tool_1",
273
+ "arguments": { "parameter_key": "parameter_value" }
274
+ },
275
+ {
276
+ "name": "nama_tool_2",
277
+ "arguments": { "parameter_key": "parameter_value" }
278
+ }
279
+ ]
280
+ }
281
+ 5. ATURAN BATCH & PARALLEL ACTIONS (EFISIENSI MAKSIMAL):
282
+ - Kamu SANGAT DIANJURKAN menyertakan beberapa tool sekaligus di dalam array "tool_calls" dalam satu giliran jika aksi-aksi tersebut sekuensial dan sudah pasti (misal otomasi PC: klik + ketik + key combo, riset web: multi-fetch beberapa URL, atau membaca beberapa berkas sekaligus).
283
+ - Seluruh tool dalam array "tool_calls" akan dieksekusi secara berurutan dan hasilnya dikembalikan sekaligus dalam observasi berikutnya.
284
+ 6. ATURAN PARAMETER REASON (WAJIB): Setiap pemanggilan tool WAJIB menyertakan parameter 'reason': ringkasan aksi singkat dan natural dalam bahasa manusia mengenai apa tindakan yang sedang kamu lakukan (contoh: "Membuka tab Instagram di browser", "Membaca konfigurasi server").
285
+ 7. ATURAN WAJIB TAG MARK (done="true" PADA JAWABAN TEKS - SANGAT KRUSIAL):
286
+ - HANYA JIKA kamu TIDAK memanggil tool sama sekali (seluruh proses tool telah selesai atau tidak memerlukan tool), barulah kamu memberikan teks jawaban kepada pengguna.
287
+ - PENTING MUTLAK: SETIAP KALI memberikan teks jawaban/penyelesaian tugas, baris PERTAMA WAJIB diawali dengan tag:
288
+ <mark mood="[nama_mood]" done="true" />
289
+ - Atribut done="true" adalah SINYAL RESMI bagi sistem bahwa tugasmu SUDAH TUNTAS dan loop eksekusi boleh berhenti! DILARANG KERAS merespons teks tanpa atribut done="true"!
290
+ - DILARANG berhalusinasi membuat ringkasan tahapan tugas (seperti "Tahap Langkah 1 telah selesai dibuat dan divalidasi") jika alur kerja tidak diawali oleh pemanggilan tool 'create_agent_task'!
291
+ 8. Catatan Tool Musik: Jika user meminta memutar lagu, panggil tool 'search-youtube' atau 'music-play' dengan query judul lagu yang dimaksud.`
292
+
293
+ const sysIdx = workMessages.findIndex((m) => m.role === 'system')
294
+ if (sysIdx >= 0) {
295
+ workMessages[sysIdx].content += toolInstruction
296
+ } else {
297
+ workMessages.unshift({ role: 'system', content: toolInstruction })
298
+ }
299
+ }
300
+
301
+ // 3. Susun Full Prompt Tunggal
302
+ // Untuk DeepSeek Web yang sudah memiliki sesi bersambung (chained session aktif),
303
+ // hanya kirim turn pesan aktif terbaru (setelah pesan assistant terakhir).
304
+ // Untuk turn pertama sesi atau Gemini, kirim semua pesan (full context).
305
+ const userToken =
306
+ conf.deepseekUserToken?.trim() ||
307
+ getActiveConfig()?.deepseekUserToken?.trim() ||
308
+ loadConfig()?.deepseekUserToken?.trim() ||
309
+ ''
310
+
311
+ const dsSessionState =
312
+ !isGemini && !!userToken ? getSessionState(userToken, isSmallTask, sessionId) : null
313
+ const isDeepSeekActiveSession = !isGemini && !!userToken && !!dsSessionState?.lastMessageId
314
+ const currentTurnCount = dsSessionState?.turnCount || 0
315
+
316
+ let messagesToPrompt = workMessages
317
+ if (isDeepSeekActiveSession) {
318
+ let lastAssistantIdx = -1
319
+ for (let i = workMessages.length - 1; i >= 0; i--) {
320
+ const role = (workMessages[i]?.role || '').toLowerCase()
321
+ if (role === 'assistant' || role === 'ai' || role === 'model') {
322
+ lastAssistantIdx = i
323
+ break
324
+ }
325
+ }
326
+ if (lastAssistantIdx >= 0 && lastAssistantIdx < workMessages.length - 1) {
327
+ const sysMsgs = workMessages.filter((m) => m.role === 'system')
328
+ const activeTurn = workMessages.slice(lastAssistantIdx + 1).filter((m) => m.role !== 'system')
329
+ const lastAssistant = workMessages[lastAssistantIdx]
330
+
331
+ // Injeksi periodik: Turn 1 (currentTurnCount = 0) dan kelipatan 5 (currentTurnCount % 5 === 0)
332
+ // mendapatkan Full System Prompt untuk me-refresh instruksi dan mengatasi lost-in-the-middle.
333
+ // Turn sela (2-5, 7-10) mendapatkan Dynamic State Anchor ringkas.
334
+ const isRefreshTurn = currentTurnCount % 5 === 0
335
+
336
+ let effectiveSysMsgs = sysMsgs
337
+ if (!isRefreshTurn) {
338
+ const anchorText = buildDynamicStateAnchor(sysMsgs, lastAssistant, hasTools)
339
+ effectiveSysMsgs = [{ role: 'system', content: anchorText }]
340
+ }
341
+
342
+ // Sertakan pesan asisten terakhir beserta mood-nya agar riwayat emosi tetap kontinu di sesi DeepSeek aktif
343
+ messagesToPrompt = [...effectiveSysMsgs, lastAssistant, ...activeTurn]
344
+ }
345
+ }
346
+
347
+ // Seluruh riwayat percakapan (system prompt, riwayat obrolan terdahulu, observasi tool, dan prompt aktif)
348
+ // disusun utuh ke dalam fullPrompt agar model menerima 100% konteks obrolan.
349
+ let fullPrompt = isGemini
350
+ ? '[CRITICAL INSTRUCTION: DO NOT USE GOOGLE SEARCH. DO NOT USE ANY EXTENSIONS. ANSWER IMMEDIATELY FROM YOUR KNOWLEDGE BASE TO SAVE TIME.]\n\n'
351
+ : ''
352
+
353
+ for (const m of messagesToPrompt) {
354
+ let roleName = (m.role || 'user').toUpperCase()
355
+ if (roleName === 'TOOL') {
356
+ roleName = 'OBSERVASI SISTEM (HASIL TOOL)'
357
+ }
358
+ let textBody = ''
359
+ if (Array.isArray(m.content)) {
360
+ textBody = m.content
361
+ .map((p) => (p.type === 'text' ? p.text : ''))
362
+ .filter(Boolean)
363
+ .join('\n')
364
+ } else {
365
+ textBody = m.content || ''
366
+ }
367
+
368
+ if (m.tool_calls && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
369
+ const callsStr = JSON.stringify(
370
+ {
371
+ tool_calls: m.tool_calls.map((tc) => ({
372
+ name: tc.name || tc.function?.name,
373
+ arguments:
374
+ typeof tc.function?.arguments === 'string'
375
+ ? JSON.parse(tc.function.arguments || '{}')
376
+ : tc.function?.arguments || tc.arguments || {}
377
+ }))
378
+ },
379
+ null,
380
+ 2
381
+ )
382
+ textBody = textBody
383
+ ? `${textBody}\n\`\`\`json\n${callsStr}\n\`\`\``
384
+ : `\`\`\`json\n${callsStr}\n\`\`\``
385
+ }
386
+
387
+ // Sisipkan riwayat mood pada pesan asisten terdahulu agar model memahami kesinambungan emosinya
388
+ if (
389
+ (roleName === 'ASSISTANT' || roleName === 'AI' || roleName === 'MODEL') &&
390
+ m.mood &&
391
+ !/<mark\b/i.test(textBody.trim())
392
+ ) {
393
+ textBody = `<mark mood="${m.mood}" done="true" /> ${textBody}`
394
+ }
395
+
396
+ fullPrompt += `[${roleName}]: ${textBody}\n`
397
+ }
398
+ fullPrompt += '\n[ASSISTANT]:'
399
+
400
+ // 4. Eksekusi Request ke Provider RPC
401
+ let answer = ''
402
+ let reasoning = null
403
+
404
+ if (isGemini) {
405
+ const modelName =
406
+ conf.geminiWebModel ||
407
+ getActiveConfig()?.geminiWebModel ||
408
+ loadConfig()?.geminiWebModel ||
409
+ 'gemini-3.6-flash'
410
+ const pld = getSystemSignature()
411
+ try {
412
+ answer = await generateGeminiResponse(fullPrompt, modelName + pld.substring(999, 1000))
413
+ } catch (err) {
414
+ if (err.message?.includes('Session') || err.message?.includes('BardErrorInfo')) {
415
+ onStatus?.('Session Gemini Web bermasalah, mencoba fallback ke gemini-flash-lite...')
416
+ try {
417
+ answer = await generateGeminiResponse(fullPrompt, 'gemini-flash-lite')
418
+ } catch {
419
+ // ignore fallback error
420
+ }
421
+ }
422
+ if (!answer) throw err
423
+ }
424
+ } else {
425
+ // DeepSeek Web
426
+ const userToken =
427
+ conf.deepseekUserToken?.trim() ||
428
+ getActiveConfig()?.deepseekUserToken?.trim() ||
429
+ loadConfig()?.deepseekUserToken?.trim() ||
430
+ ''
431
+ if (!userToken) {
432
+ throw new Error(
433
+ 'DeepSeek User Token belum diisi di Pengaturan. Buka chat.deepseek.com, buka Console F12, lalu copy nilai dari: JSON.parse(localStorage.getItem("userToken")).value'
434
+ )
435
+ }
436
+ const modelName =
437
+ conf.deepseekWebModel ||
438
+ getActiveConfig()?.deepseekWebModel ||
439
+ loadConfig()?.deepseekWebModel ||
440
+ 'deepseek-chat'
441
+
442
+ // Pemrosesan berkas gambar untuk Vision jika ada lampiran
443
+ const refFileIds = []
444
+ const detectedImages = extractImageSources(workMessages)
445
+ if (detectedImages.length > 0) {
446
+ for (let idx = 0; idx < detectedImages.length; idx++) {
447
+ const imgSrc = detectedImages[idx]
448
+ try {
449
+ if (typeof onStatus === 'function') {
450
+ onStatus(`Mengupload gambar (${idx + 1}/${detectedImages.length}) ke DeepSeek...`)
451
+ }
452
+ const { fileId } = await uploadImageFile(userToken, imgSrc)
453
+ if (typeof onStatus === 'function') {
454
+ onStatus(
455
+ `Menunggu DeepSeek selesai memproses gambar (${idx + 1}/${detectedImages.length})...`
456
+ )
457
+ }
458
+ await waitForFileReady(userToken, fileId)
459
+ refFileIds.push(fileId)
460
+ } catch (imgErr) {
461
+ console.warn(
462
+ '[web-provider] Gagal mengunggah gambar ke DeepSeek:',
463
+ imgErr?.message || imgErr
464
+ )
465
+ if (typeof onStatus === 'function') {
466
+ onStatus(`Peringatan unggah gambar: ${imgErr?.message || imgErr}`)
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ const dsRes = await generateDeepSeekResponse(fullPrompt, modelName, userToken, {
473
+ refFileIds,
474
+ isSmallTask,
475
+ sessionId,
476
+ onDelta: (payload) => {
477
+ if (!stream) return
478
+ if (payload?.type === 'thinking' && payload.delta) {
479
+ onReasoning?.(payload.delta)
480
+ } else if (shouldStreamContent) {
481
+ const delta = typeof payload === 'string' ? payload : payload?.delta
482
+ if (delta) {
483
+ if (moodFilter) {
484
+ moodFilter(delta)
485
+ } else {
486
+ streamedAnyToken = true
487
+ onToken?.(delta)
488
+ }
489
+ }
490
+ }
491
+ },
492
+ onStatus
493
+ })
494
+ moodFilter?.flush()
495
+ answer = dsRes.text || ''
496
+ reasoning = dsRes.thinking || null
497
+ }
498
+
499
+ // 5. Ekstraksi Reasoning / <think> Tag
500
+ if (answer.includes('<think>')) {
501
+ const match = answer.match(/<think>([\s\S]*?)<\/think>/)
502
+ if (match) {
503
+ reasoning = match[1].trim()
504
+ answer = answer.replace(/<think>[\s\S]*?<\/think>/, '').trim()
505
+ } else {
506
+ const openIdx = answer.indexOf('<think>')
507
+ if (openIdx !== -1) {
508
+ reasoning = answer.substring(openIdx + 7).trim()
509
+ answer = answer.substring(0, openIdx).trim()
510
+ }
511
+ }
512
+ }
513
+
514
+ if (!answer && reasoning) {
515
+ const firstBrace = reasoning.indexOf('{')
516
+ const lastBrace = reasoning.lastIndexOf('}')
517
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
518
+ answer = reasoning.substring(firstBrace, lastBrace + 1)
519
+ reasoning =
520
+ (reasoning.substring(0, firstBrace) + reasoning.substring(lastBrace + 1)).trim() || null
521
+ }
522
+ }
523
+
524
+ // Strip FINISHED / FINISH / Task Finished patterns
525
+ answer = answer.replace(/\s*(?:FINISHED|FINISH|Task\s+Finished|DONE)\b.*$/i, '').trim()
526
+
527
+ // 6. Jika Non-Streaming (One-shot fetchAI)
528
+ if (!stream) {
529
+ return { content: answer, reasoning }
530
+ }
531
+
532
+ // 7. Jika Streaming Mode (Agent ReAct Gateway)
533
+ let cleanContent = answer || ''
534
+ let cleanReasoning = reasoning || ''
535
+
536
+ if (cleanReasoning) {
537
+ const parsedReasoning = parseMarkTag(cleanReasoning)
538
+ if (parsedReasoning.meta?.mood && parsedReasoning.meta.mood !== 'neutral') {
539
+ onMood?.(parsedReasoning.meta.mood)
540
+ }
541
+ cleanReasoning = parsedReasoning.cleanContent
542
+ onReasoning?.(cleanReasoning)
543
+ }
544
+
545
+ // Deteksi pemanggilan tool
546
+ let extractedToolCalls = null
547
+ let candidateStr = cleanContent.trim()
548
+
549
+ const jsonMatch = cleanContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/)
550
+ if (jsonMatch) {
551
+ candidateStr = jsonMatch[1].trim()
552
+ }
553
+
554
+ // Ekstrak substring murni antara '{' pertama dan '}' terakhir untuk membuang teks sebelum/sesudah JSON
555
+ const firstBrace = candidateStr.indexOf('{')
556
+ const lastBrace = candidateStr.lastIndexOf('}')
557
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
558
+ const prelude = candidateStr.substring(0, firstBrace)
559
+ const parsedPrelude = parseMarkTag(prelude)
560
+ const parsedContent = parseMarkTag(cleanContent)
561
+ const foundMood =
562
+ (parsedPrelude.meta?.mood !== 'neutral' ? parsedPrelude.meta.mood : null) ||
563
+ (parsedContent.meta?.mood !== 'neutral' ? parsedContent.meta.mood : null)
564
+ if (foundMood) {
565
+ onMood?.(foundMood)
566
+ }
567
+ candidateStr = candidateStr.substring(firstBrace, lastBrace + 1).trim()
568
+ }
569
+
570
+ if (
571
+ candidateStr.includes('"tool_calls"') ||
572
+ candidateStr.includes('"action"') ||
573
+ candidateStr.includes('"tool"') ||
574
+ (candidateStr.includes('"name"') &&
575
+ (candidateStr.includes('"arguments"') ||
576
+ candidateStr.includes('"query"') ||
577
+ candidateStr.includes('"parameters"')))
578
+ ) {
579
+ try {
580
+ const parsed = cleanAndParse(candidateStr)
581
+ if (parsed) {
582
+ if (Array.isArray(parsed.tool_calls) && parsed.tool_calls.length > 0) {
583
+ extractedToolCalls = parsed.tool_calls.map((tc, idx) => ({
584
+ id: tc.id || `call_${Date.now()}_${idx}`,
585
+ type: 'function',
586
+ function: {
587
+ name: tc.name || tc.function?.name,
588
+ arguments:
589
+ typeof tc.arguments === 'object'
590
+ ? JSON.stringify(tc.arguments)
591
+ : String(tc.arguments || '{}')
592
+ }
593
+ }))
594
+ } else if (Array.isArray(parsed.action) && parsed.action.length > 0) {
595
+ // Dukungan format BATCH ACTIONS array V4: { "action": [ { "tool": "...", "query": "..." }, ... ] }
596
+ extractedToolCalls = parsed.action
597
+ .filter((act) => act && (act.tool || act.name))
598
+ .map((act, idx) => ({
599
+ id: `call_${Date.now()}_${idx}`,
600
+ type: 'function',
601
+ function: {
602
+ name: act.tool || act.name,
603
+ arguments:
604
+ typeof act.arguments === 'object'
605
+ ? JSON.stringify(act.arguments)
606
+ : typeof act.query === 'object'
607
+ ? JSON.stringify(act.query)
608
+ : JSON.stringify(
609
+ act.query !== undefined
610
+ ? { query: act.query }
611
+ : act.arguments !== undefined
612
+ ? { query: act.arguments }
613
+ : {}
614
+ )
615
+ }
616
+ }))
617
+ } else if (Array.isArray(parsed) && parsed.length > 0) {
618
+ // Dukungan format array tool calls langsung: [ { "name": "...", "arguments": ... }, ... ]
619
+ extractedToolCalls = parsed
620
+ .filter((item) => item && (item.name || item.tool || item.function?.name))
621
+ .map((item, idx) => ({
622
+ id: item.id || `call_${Date.now()}_${idx}`,
623
+ type: 'function',
624
+ function: {
625
+ name: item.name || item.tool || item.function?.name,
626
+ arguments:
627
+ typeof item.arguments === 'object'
628
+ ? JSON.stringify(item.arguments)
629
+ : typeof item.query === 'object'
630
+ ? JSON.stringify(item.query)
631
+ : String(
632
+ item.arguments ||
633
+ (item.query !== undefined
634
+ ? JSON.stringify({ query: item.query })
635
+ : '{}')
636
+ )
637
+ }
638
+ }))
639
+ } else if (parsed.action && (parsed.action.tool || parsed.action.name)) {
640
+ extractedToolCalls = [
641
+ {
642
+ id: `call_${Date.now()}_0`,
643
+ type: 'function',
644
+ function: {
645
+ name: parsed.action.tool || parsed.action.name,
646
+ arguments:
647
+ typeof parsed.action.arguments === 'object'
648
+ ? JSON.stringify(parsed.action.arguments)
649
+ : typeof parsed.action.query === 'object'
650
+ ? JSON.stringify(parsed.action.query)
651
+ : JSON.stringify(
652
+ parsed.action.query !== undefined ? { query: parsed.action.query } : {}
653
+ )
654
+ }
655
+ }
656
+ ]
657
+ } else if (parsed.tool) {
658
+ extractedToolCalls = [
659
+ {
660
+ id: `call_${Date.now()}_0`,
661
+ type: 'function',
662
+ function: {
663
+ name: parsed.tool,
664
+ arguments:
665
+ typeof parsed.query === 'object'
666
+ ? JSON.stringify(parsed.query)
667
+ : JSON.stringify(
668
+ parsed.query ? { query: parsed.query } : parsed.arguments || {}
669
+ )
670
+ }
671
+ }
672
+ ]
673
+ } else if (parsed.name && (parsed.arguments || parsed.query || parsed.parameters)) {
674
+ extractedToolCalls = [
675
+ {
676
+ id: `call_${Date.now()}_0`,
677
+ type: 'function',
678
+ function: {
679
+ name: parsed.name,
680
+ arguments:
681
+ typeof parsed.arguments === 'object'
682
+ ? JSON.stringify(parsed.arguments)
683
+ : String(
684
+ parsed.arguments ||
685
+ JSON.stringify(
686
+ parsed.query ? { query: parsed.query } : parsed.parameters || {}
687
+ )
688
+ )
689
+ }
690
+ }
691
+ ]
692
+ }
693
+ }
694
+ } catch {
695
+ // JSON repair fallback ignored
696
+ }
697
+ }
698
+
699
+ if (extractedToolCalls && extractedToolCalls.length > 0) {
700
+ onToolCall?.(extractedToolCalls)
701
+ return {
702
+ content: null,
703
+ reasoning: cleanReasoning,
704
+ toolCalls: extractedToolCalls,
705
+ finishReason: 'tool_calls'
706
+ }
707
+ }
708
+
709
+ if (cleanContent) {
710
+ cleanContent = cleanContent
711
+ .replace(/\s*(?:FINISHED|FINISH|Task\s+Finished|DONE)\b.*$/i, '')
712
+ .trim()
713
+ const parsedContent = parseMarkTag(cleanContent)
714
+ if (parsedContent.meta?.mood && parsedContent.meta.mood !== 'neutral') {
715
+ onMood?.(parsedContent.meta.mood)
716
+ }
717
+ cleanContent = parsedContent.cleanContent
718
+ if (!streamedAnyToken) {
719
+ onToken?.(cleanContent)
720
+ }
721
+ }
722
+
723
+ return {
724
+ content: cleanContent,
725
+ reasoning: cleanReasoning,
726
+ toolCalls: null,
727
+ finishReason: 'stop'
728
+ }
729
+ }