@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,78 +1,6 @@
1
- import { genId } from '@/lib/id'
2
- import { z } from 'zod'
3
1
  import type { GoalContract, MessageToolEvent } from '@/types'
4
- import { loadSessions, saveSessions, loadAgents, saveAgents, loadSettings } from './storage'
5
- import { log } from './logger'
6
- import { getMemoryDb } from './memory-db'
7
- import { isMainLoopSession } from './main-session'
8
- import { logExecution } from './execution-log'
9
- import {
10
- mergeGoalContracts,
11
- parseGoalContractFromText,
12
- parseMainLoopPlan,
13
- parseMainLoopReview,
14
- } from './autonomy-contract'
15
- import { buildIdentityContext } from './heartbeat-service'
16
2
 
17
- const MAX_PENDING_EVENTS = 60
18
- const MAX_TIMELINE_EVENTS = 120
19
- const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
20
- const MEMORY_NOTE_MIN_INTERVAL_MS = 30 * 60 * 1000
21
- const DEFAULT_FOLLOWUP_DELAY_SEC = 45
22
- const DEFAULT_MAX_FOLLOWUP_CHAIN = 20
23
- function getMaxFollowupChain(agentId: string | undefined): number {
24
- if (agentId) {
25
- const agents = loadAgents()
26
- const agent = agents[agentId]
27
- if (typeof agent?.maxFollowupChain === 'number' && agent.maxFollowupChain > 0) {
28
- return Math.min(agent.maxFollowupChain, 100)
29
- }
30
- }
31
- const settings = loadSettings()
32
- if (typeof settings?.maxFollowupChain === 'number' && settings.maxFollowupChain > 0) {
33
- return Math.min(settings.maxFollowupChain, 100)
34
- }
35
- return DEFAULT_MAX_FOLLOWUP_CHAIN
36
- }
37
-
38
- const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
39
- const AGENT_HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
40
- const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
41
- const DELIVERY_GOAL_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
42
- const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recurr)\b/i
43
- const UPLOAD_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
44
- const SENT_ARTIFACT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image|file)\b/i
45
-
46
- const COMPANION_GOAL_PROMPT = `
47
- ## Identity & Vibe
48
- You are a persistent companion.
49
- 1. **Identity**: Embody your creature, theme, and vibe. Your emoji is your signature.
50
- 2. **Workspace Context**: Respect the current workspace. Read IDENTITY.md and HEARTBEAT.md if they exist.
51
- 3. **Continuity**: Maintain awareness of the user's long-term journey. Proactively help with open-ended goals without being asked for every step.
52
- `.trim()
53
-
54
- interface MainLoopSessionMessageLike {
55
- text?: string
56
- }
57
-
58
- interface MainLoopSessionEvidenceLike {
59
- messages?: MainLoopSessionMessageLike[]
60
- }
61
-
62
- export interface MainLoopEvent {
63
- id: string
64
- type: string
65
- text: string
66
- createdAt: number
67
- }
68
-
69
- export interface MainLoopTimelineEntry {
70
- id: string
71
- at: number
72
- source: string
73
- note: string
74
- status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection'
75
- }
3
+ const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
76
4
 
77
5
  export interface MainLoopState {
78
6
  goal: string | null
@@ -88,8 +16,19 @@ export interface MainLoopState {
88
16
  paused: boolean
89
17
  status: 'idle' | 'progress' | 'blocked' | 'ok'
90
18
  autonomyMode: 'assist' | 'autonomous'
91
- pendingEvents: MainLoopEvent[]
92
- timeline: MainLoopTimelineEntry[]
19
+ pendingEvents: Array<{
20
+ id: string
21
+ type: string
22
+ text: string
23
+ createdAt: number
24
+ }>
25
+ timeline: Array<{
26
+ id: string
27
+ at: number
28
+ source: string
29
+ note: string
30
+ status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection'
31
+ }>
93
32
  missionTokens: number
94
33
  missionCostUsd: number
95
34
  followupChainCount: number
@@ -102,16 +41,6 @@ export interface MainLoopState {
102
41
  updatedAt: number
103
42
  }
104
43
 
105
- interface MainLoopMeta {
106
- status?: 'idle' | 'progress' | 'blocked' | 'ok'
107
- summary?: string
108
- next_action?: string
109
- follow_up?: boolean
110
- delay_sec?: number
111
- goal?: string
112
- consume_event_ids?: string[]
113
- }
114
-
115
44
  export interface MainLoopFollowupRequest {
116
45
  message: string
117
46
  delayMs: number
@@ -137,903 +66,41 @@ export interface HandleMainLoopRunResultInput {
137
66
  estimatedCost?: number
138
67
  }
139
68
 
140
- function toOneLine(value: string, max = 240): string {
141
- return (value || '').replace(/\s+/g, ' ').trim().slice(0, max)
142
- }
143
-
144
- function normalizeMemoryText(value: string): string {
145
- return (value || '').replace(/\s+/g, ' ').trim()
146
- }
147
-
148
- function clampInt(value: unknown, fallback: number, min: number, max: number): number {
149
- const parsed = typeof value === 'number'
150
- ? value
151
- : typeof value === 'string'
152
- ? Number.parseInt(value, 10)
153
- : Number.NaN
154
- if (!Number.isFinite(parsed)) return fallback
155
- return Math.max(min, Math.min(max, Math.trunc(parsed)))
156
- }
157
-
158
- function pruneEvents(events: MainLoopEvent[], now = Date.now()): MainLoopEvent[] {
159
- const minTs = now - EVENT_TTL_MS
160
- const fresh = events.filter((e) => e && typeof e.createdAt === 'number' && e.createdAt >= minTs)
161
- if (fresh.length <= MAX_PENDING_EVENTS) return fresh
162
- return fresh.slice(fresh.length - MAX_PENDING_EVENTS)
163
- }
164
-
165
- function pruneTimeline(entries: MainLoopTimelineEntry[], now = Date.now()): MainLoopTimelineEntry[] {
166
- const minTs = now - EVENT_TTL_MS
167
- const fresh = entries.filter((e) => e && typeof e.at === 'number' && e.at >= minTs && typeof e.note === 'string' && e.note.trim())
168
- if (fresh.length <= MAX_TIMELINE_EVENTS) return fresh
169
- return fresh.slice(fresh.length - MAX_TIMELINE_EVENTS)
170
- }
171
-
172
- function appendTimeline(
173
- state: MainLoopState,
174
- source: string,
175
- note: string,
176
- now = Date.now(),
177
- status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection',
178
- ) {
179
- const normalizedNote = toOneLine(note, 400)
180
- if (!normalizedNote) return
181
- const recent = state.timeline.at(-1)
182
- if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
183
- state.timeline.push({
184
- id: `tl_${genId()}`,
185
- at: now,
186
- source,
187
- note: normalizedNote,
188
- status,
189
- })
190
- state.timeline = pruneTimeline(state.timeline, now)
191
- }
192
-
193
- function computeMomentumScore(state: MainLoopState): number {
194
- const baseByStatus = {
195
- idle: 40,
196
- progress: 72,
197
- blocked: 20,
198
- ok: 94,
199
- } as const
200
- let score: number = baseByStatus[state.status]
201
- score -= Math.min(20, state.metaMissCount * 3)
202
- score -= Math.min(12, Math.max(0, state.pendingEvents.length - 4) * 2)
203
- if (state.paused) score = Math.min(score, 35)
204
- return clampInt(score, 0, 0, 100)
205
- }
206
-
207
- function normalizeStringList(input: unknown, maxItems: number, maxChars: number): string[] {
208
- if (!Array.isArray(input)) return []
209
- const seen = new Set<string>()
210
- const out: string[] = []
211
- for (const raw of input) {
212
- if (typeof raw !== 'string') continue
213
- const value = raw.replace(/\s+/g, ' ').trim().slice(0, maxChars)
214
- if (!value) continue
215
- const key = value.toLowerCase()
216
- if (seen.has(key)) continue
217
- seen.add(key)
218
- out.push(value)
219
- if (out.length >= maxItems) break
220
- }
221
- return out
222
- }
223
-
224
- function normalizeGoalContract(raw: any): GoalContract | null {
225
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
226
- const objective = typeof raw.objective === 'string' ? raw.objective.trim().slice(0, 300) : ''
227
- if (!objective) return null
228
- const constraints = normalizeStringList(raw.constraints, 10, 220)
229
- const budgetUsd = typeof raw.budgetUsd === 'number'
230
- ? Math.max(0, Math.min(1_000_000, raw.budgetUsd))
231
- : null
232
- const deadlineAt = typeof raw.deadlineAt === 'number' && Number.isFinite(raw.deadlineAt)
233
- ? Math.trunc(raw.deadlineAt)
234
- : null
235
- const successMetric = typeof raw.successMetric === 'string'
236
- ? raw.successMetric.trim().slice(0, 220) || null
237
- : null
238
- return {
239
- objective,
240
- constraints: constraints.length ? constraints : undefined,
241
- budgetUsd,
242
- deadlineAt,
243
- successMetric,
244
- }
245
- }
246
-
247
- function normalizeState(raw: any, now = Date.now()): MainLoopState {
248
- const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
249
- ? raw.status
250
- : 'idle'
251
-
252
- const pendingRaw = Array.isArray(raw?.pendingEvents) ? raw.pendingEvents : []
253
- const pendingEvents = pruneEvents(
254
- pendingRaw
255
- .map((e: any) => {
256
- const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
257
- if (!text) return null
258
- return {
259
- id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${genId(3)}`,
260
- type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
261
- text,
262
- createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
263
- } as MainLoopEvent
264
- })
265
- .filter(Boolean) as MainLoopEvent[],
266
- now,
267
- )
268
-
269
- const timelineRaw = Array.isArray(raw?.timeline) ? raw.timeline : []
270
- const timeline = pruneTimeline(
271
- timelineRaw
272
- .map((entry: any) => {
273
- const note = toOneLine(typeof entry?.note === 'string' ? entry.note : '', 400)
274
- if (!note) return null
275
- const status = entry?.status === 'blocked' || entry?.status === 'ok' || entry?.status === 'progress' || entry?.status === 'idle'
276
- ? entry.status
277
- : undefined
278
- return {
279
- id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${genId(3)}`,
280
- at: typeof entry?.at === 'number' ? entry.at : now,
281
- source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
282
- note,
283
- status,
284
- } as MainLoopTimelineEntry
285
- })
286
- .filter(Boolean) as MainLoopTimelineEntry[],
287
- now,
288
- )
289
-
290
- const normalized: MainLoopState = {
291
- goal: typeof raw?.goal === 'string' && raw.goal.trim() ? raw.goal.trim().slice(0, 600) : null,
292
- goalContract: normalizeGoalContract(raw?.goalContract),
293
- status,
294
- summary: typeof raw?.summary === 'string' && raw.summary.trim() ? raw.summary.trim().slice(0, 800) : null,
295
- nextAction: typeof raw?.nextAction === 'string' && raw.nextAction.trim() ? raw.nextAction.trim().slice(0, 600) : null,
296
- planSteps: normalizeStringList(raw?.planSteps, 10, 220),
297
- currentPlanStep: typeof raw?.currentPlanStep === 'string' && raw.currentPlanStep.trim()
298
- ? raw.currentPlanStep.trim().slice(0, 220)
299
- : null,
300
- reviewNote: typeof raw?.reviewNote === 'string' && raw.reviewNote.trim()
301
- ? raw.reviewNote.trim().slice(0, 320)
302
- : null,
303
- reviewConfidence: typeof raw?.reviewConfidence === 'number' && Number.isFinite(raw.reviewConfidence)
304
- ? Math.max(0, Math.min(1, raw.reviewConfidence))
305
- : null,
306
- missionTaskId: typeof raw?.missionTaskId === 'string' && raw.missionTaskId.trim() ? raw.missionTaskId.trim() : null,
307
- momentumScore: clampInt(raw?.momentumScore, 40, 0, 100),
308
- paused: raw?.paused === true,
309
- autonomyMode: raw?.autonomyMode === 'assist' ? 'assist' : 'autonomous',
310
- pendingEvents,
311
- timeline,
312
- missionTokens: typeof raw?.missionTokens === 'number' && Number.isFinite(raw.missionTokens) ? raw.missionTokens : 0,
313
- missionCostUsd: typeof raw?.missionCostUsd === 'number' && Number.isFinite(raw.missionCostUsd) ? raw.missionCostUsd : 0,
314
- followupChainCount: clampInt(raw?.followupChainCount, 0, 0, 100),
315
- metaMissCount: clampInt(raw?.metaMissCount, 0, 0, 100),
316
- workingMemoryNotes: normalizeStringList(raw?.workingMemoryNotes, 24, 260),
317
- lastMemoryNoteAt: typeof raw?.lastMemoryNoteAt === 'number' ? raw.lastMemoryNoteAt : null,
318
- lastPlannedAt: typeof raw?.lastPlannedAt === 'number' ? raw.lastPlannedAt : null,
319
- lastReviewedAt: typeof raw?.lastReviewedAt === 'number' ? raw.lastReviewedAt : null,
320
- lastTickAt: typeof raw?.lastTickAt === 'number' ? raw.lastTickAt : null,
321
- updatedAt: typeof raw?.updatedAt === 'number' ? raw.updatedAt : now,
322
- }
323
- if (!normalized.goal && normalized.goalContract?.objective) {
324
- normalized.goal = normalized.goalContract.objective
325
- }
326
- normalized.momentumScore = computeMomentumScore(normalized)
327
- return normalized
328
- }
329
-
330
- function appendEvent(state: MainLoopState, type: string, text: string, now = Date.now()): boolean {
331
- const normalizedText = toOneLine(text)
332
- if (!normalizedText) return false
333
- const recent = state.pendingEvents.at(-1)
334
- if (recent && recent.type === type && recent.text === normalizedText && now - recent.createdAt < 60_000) {
335
- return false
336
- }
337
- state.pendingEvents.push({
338
- id: `evt_${genId()}`,
339
- type,
340
- text: normalizedText,
341
- createdAt: now,
342
- })
343
- state.pendingEvents = pruneEvents(state.pendingEvents, now)
344
- return true
345
- }
346
-
347
- function appendWorkingMemoryNote(state: MainLoopState, note: string) {
348
- const value = toOneLine(note, 260)
349
- if (!value) return
350
- const existing = state.workingMemoryNotes || []
351
- if (existing.length && existing[existing.length - 1] === value) return
352
- state.workingMemoryNotes = [...existing.slice(-23), value]
353
- }
354
-
355
-
356
- function parseMainLoopMeta(text: string): MainLoopMeta | null {
357
- const raw = (text || '').trim()
358
- if (!raw) return null
359
-
360
- const markerMatch = raw.match(META_LINE_RE)
361
- const parseCandidate = markerMatch?.[1]
362
- if (parseCandidate) {
363
- try {
364
- const parsed = JSON.parse(parseCandidate)
365
- return normalizeMeta(parsed)
366
- } catch {
367
- // fall through
368
- }
369
- }
370
-
371
- // Fallback: parse any one-line JSON that appears to be the meta payload.
372
- for (const line of raw.split('\n')) {
373
- const trimmed = line.trim()
374
- if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue
375
- if (!trimmed.includes('follow_up') && !trimmed.includes('next_action') && !trimmed.includes('consume_event_ids')) continue
376
- try {
377
- const parsed = JSON.parse(trimmed)
378
- return normalizeMeta(parsed)
379
- } catch {
380
- // skip malformed candidate lines
381
- }
382
- }
383
-
384
- return null
385
- }
386
-
387
- function normalizeMeta(raw: any): MainLoopMeta {
388
- const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
389
- ? raw.status
390
- : undefined
391
-
392
- const consumeIds = Array.isArray(raw?.consume_event_ids)
393
- ? raw.consume_event_ids
394
- .map((v: unknown) => (typeof v === 'string' ? v.trim() : ''))
395
- .filter(Boolean)
396
- : undefined
397
-
398
- const followUp = typeof raw?.follow_up === 'boolean'
399
- ? raw.follow_up
400
- : typeof raw?.follow_up === 'string'
401
- ? raw.follow_up.trim().toLowerCase() === 'true'
402
- : undefined
403
-
404
- return {
405
- status,
406
- summary: typeof raw?.summary === 'string' ? raw.summary.trim().slice(0, 800) : undefined,
407
- next_action: typeof raw?.next_action === 'string' ? raw.next_action.trim().slice(0, 600) : undefined,
408
- follow_up: followUp,
409
- delay_sec: clampInt(raw?.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900),
410
- goal: typeof raw?.goal === 'string' ? raw.goal.trim().slice(0, 600) : undefined,
411
- consume_event_ids: consumeIds,
412
- }
413
- }
414
-
415
- function consumeEvents(state: MainLoopState, ids: string[] | undefined) {
416
- if (!ids?.length) return
417
- const remove = new Set(ids)
418
- state.pendingEvents = state.pendingEvents.filter((event) => !remove.has(event.id))
419
- }
420
-
421
- function buildPendingEventLines(state: MainLoopState): string {
422
- if (!state.pendingEvents.length) return 'Pending events:\n- none'
423
- const lines = state.pendingEvents
424
- .slice(-10)
425
- .map((event) => `- ${event.id} | ${event.type} | ${event.text}`)
426
- .join('\n')
427
- return `Pending events (oldest → newest):\n${lines}`
428
- }
429
-
430
- function buildTimelineLines(state: MainLoopState): string {
431
- if (!state.timeline.length) return 'Recent mission timeline:\n- none'
432
- const lines = state.timeline
433
- .slice(-5)
434
- .map((entry) => {
435
- const ts = new Date(entry.at).toISOString().slice(11, 19)
436
- const status = entry.status ? ` [${entry.status}]` : ''
437
- return `- ${ts} ${entry.source}${status}: ${entry.note}`
438
- })
439
- .join('\n')
440
- return `Recent mission timeline:\n${lines}`
441
- }
442
-
443
- function buildGoalContractLines(state: MainLoopState): string[] {
444
- const contract = state.goalContract
445
- if (!contract?.objective) return []
446
- const lines = [
447
- `contract_objective: ${contract.objective}`,
448
- ]
449
- if (contract.constraints?.length) lines.push(`contract_constraints: ${contract.constraints.join(' | ')}`)
450
- if (typeof contract.budgetUsd === 'number') lines.push(`contract_budget_usd: ${contract.budgetUsd}`)
451
- if (typeof contract.deadlineAt === 'number') lines.push(`contract_deadline_iso: ${new Date(contract.deadlineAt).toISOString()}`)
452
- if (contract.successMetric) lines.push(`contract_success_metric: ${contract.successMetric}`)
453
- return lines
454
- }
455
-
456
- function missionNeedsScreenshotArtifactEvidence(state: MainLoopState): boolean {
457
- const haystack = [
458
- state.goal || '',
459
- state.goalContract?.objective || '',
460
- state.goalContract?.successMetric || '',
461
- state.nextAction || '',
462
- ...(state.planSteps || []),
463
- state.currentPlanStep || '',
464
- ].join(' ')
465
- if (!SCREENSHOT_GOAL_HINT.test(haystack)) return false
466
- return DELIVERY_GOAL_HINT.test(haystack) || SCHEDULE_GOAL_HINT.test(haystack)
467
- }
468
-
469
- function missionHasScreenshotArtifactEvidence(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): boolean {
470
- const candidates: string[] = [
471
- state.summary || '',
472
- additionalText || '',
473
- ]
474
- if (Array.isArray(session?.messages)) {
475
- for (let i = session.messages.length - 1; i >= 0 && candidates.length < 16; i--) {
476
- const text = typeof session.messages[i]?.text === 'string' ? session.messages[i].text! : ''
477
- if (text && text.trim()) candidates.push(text)
478
- }
479
- }
480
- return candidates.some((value) => UPLOAD_ARTIFACT_HINT.test(value) || SENT_ARTIFACT_HINT.test(value))
481
- }
482
-
483
- function getMissionCompletionGateReason(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): string | null {
484
- if (!missionNeedsScreenshotArtifactEvidence(state)) return null
485
- if (missionHasScreenshotArtifactEvidence(session, state, additionalText)) return null
486
- return 'Mission requires screenshot artifact evidence (upload link or explicit sent screenshot confirmation) before completion.'
487
- }
488
-
489
-
490
-
491
- function maybeStoreMissionMemoryNote(
492
- session: any,
493
- state: MainLoopState,
494
- now: number,
495
- source: string,
496
- force = false,
497
- ) {
498
- if (!session?.agentId || !state.goal) return
499
- if (!force && state.lastMemoryNoteAt && (now - state.lastMemoryNoteAt) < MEMORY_NOTE_MIN_INTERVAL_MS) return
500
-
501
- const summary = state.summary || 'No summary'
502
- const next = state.nextAction || 'No next action'
503
- const title = `Mission ${state.status}: ${state.goal.slice(0, 72)}`
504
- const content = [
505
- `source: ${source}`,
506
- `status: ${state.status}`,
507
- `momentum: ${state.momentumScore}/100`,
508
- `goal: ${state.goal}`,
509
- ...buildGoalContractLines(state),
510
- state.planSteps.length ? `plan_steps: ${state.planSteps.join(' -> ')}` : '',
511
- state.currentPlanStep ? `current_plan_step: ${state.currentPlanStep}` : '',
512
- `summary: ${summary}`,
513
- `next_action: ${next}`,
514
- state.reviewNote ? `review: ${state.reviewNote}` : '',
515
- typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
516
- state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
517
- typeof state.missionTokens === 'number' ? `mission_tokens: ${state.missionTokens}` : '',
518
- typeof state.missionCostUsd === 'number' ? `mission_cost_usd: $${state.missionCostUsd.toFixed(4)}` : '',
519
- state.workingMemoryNotes?.length ? `working_notes: ${state.workingMemoryNotes.slice(-5).join('; ')}` : '',
520
- ].filter(Boolean).join('\n')
521
-
522
- try {
523
- const memDb = getMemoryDb()
524
- const latest = memDb.getLatestBySessionCategory?.(session.id, 'mission')
525
- if (latest) {
526
- const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
527
- const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
528
- if (sameTitle && sameContent) {
529
- state.lastMemoryNoteAt = now
530
- return
531
- }
532
- }
533
- memDb.add({
534
- agentId: session.agentId || null,
535
- sessionId: session.id,
536
- category: 'mission',
537
- title,
538
- content,
539
- })
540
- state.lastMemoryNoteAt = now
541
- logExecution(session.id, 'mission_checkpoint', `Checkpoint: ${toOneLine(state.goal || '', 120)}`, {
542
- agentId: session.agentId,
543
- detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
544
- })
545
- } catch (err: unknown) {
546
- appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err instanceof Error ? err.message : String(err), 240)}`, now)
547
- }
69
+ export function isMainSession(session: unknown): boolean {
70
+ void session
71
+ return false
548
72
  }
549
73
 
550
- function buildFollowupPrompt(state: MainLoopState, opts?: { agent?: Record<string, unknown> | null; session?: Record<string, unknown> | null }): string {
551
- const identityContext = buildIdentityContext(opts?.session, opts?.agent)
552
- const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
553
- const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
554
- const contractLines = buildGoalContractLines(state)
555
- return [
556
- 'SWARM_MAIN_AUTO_FOLLOWUP',
557
- identityContext,
558
- COMPANION_GOAL_PROMPT,
559
- `Mission goal: ${goal}`,
560
- `Next action to execute now: ${nextAction}`,
561
- `Current status: ${state.status}`,
562
- `Mission task id: ${state.missionTaskId || 'none'}`,
563
- `Momentum score: ${state.momentumScore}/100`,
564
- ...contractLines,
565
- state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
566
- state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
567
- state.reviewNote ? `Last review: ${state.reviewNote}` : '',
568
- buildPendingEventLines(state),
569
- buildTimelineLines(state),
570
- state.planSteps.length === 0 && state.followupChainCount === 0
571
- ? 'Before executing, break the mission goal into 3-7 concrete subtasks. Output a [MAIN_LOOP_PLAN] JSON line with your plan, then execute the first step immediately.'
572
- : '',
573
- 'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
574
- state.autonomyMode === 'assist'
575
- ? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
576
- : 'Autonomous mode: execute safe next actions without waiting for confirmation; ask only when blocked by permissions, credentials, or policy.',
577
- 'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
578
- 'Use any available tools actively to maintain state across turns.',
579
- 'If you are blocked by missing credentials, permissions, or policy limits, say exactly what is blocked and the smallest unblock needed.',
580
- 'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
581
- 'When the mission goal is fully completed, set status to "ok" with follow_up:false and include a clear summary of what was accomplished. The loop will auto-pause.',
582
- 'If no meaningful action remains right now, reply exactly HEARTBEAT_OK.',
583
- 'Otherwise include a concise human update, then append exactly one [MAIN_LOOP_META] JSON line.',
584
- 'Optionally append one [MAIN_LOOP_PLAN] JSON line when you create/revise a plan.',
585
- 'Optionally append one [MAIN_LOOP_REVIEW] JSON line when you review recent execution results.',
586
- '[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
587
- '[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
588
- '[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
589
- ].join('\n')
590
- }
591
-
592
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
593
- export function isMainSession(session: any): boolean {
594
- return isMainLoopSession(session)
595
- }
596
-
597
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
598
- export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
599
- const now = Date.now()
600
- const agents = loadAgents()
601
- const agent = session.agentId ? agents[session.agentId] : null
602
- const identityContext = buildIdentityContext(session, agent)
603
- const state = normalizeState(session?.mainLoopState, now)
604
- const goal = state.goal || null
605
- const promptGoal = goal || 'No explicit mission captured yet. Infer the mission from recent user instructions and continue proactively.'
606
- const promptSummary = state.summary || 'No prior mission summary yet.'
607
- const promptNextAction = state.nextAction || 'No queued action. Determine one.'
608
- const contractLines = buildGoalContractLines(state)
609
-
610
- return [
611
- 'SWARM_MAIN_MISSION_TICK',
612
- identityContext,
613
- COMPANION_GOAL_PROMPT,
614
- `Time: ${new Date(now).toISOString()}`,
615
- `Mission goal: ${promptGoal}`,
616
- `Current status: ${state.status}`,
617
- `Mission paused: ${state.paused ? 'yes' : 'no'}`,
618
- `Autonomy mode: ${state.autonomyMode}`,
619
- `Mission task id: ${state.missionTaskId || 'none'}`,
620
- `Momentum score: ${state.momentumScore}/100`,
621
- ...contractLines,
622
- state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
623
- state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
624
- state.reviewNote ? `Last review: ${state.reviewNote}` : '',
625
- `Last summary: ${toOneLine(promptSummary, 500)}`,
626
- `Last next action: ${toOneLine(promptNextAction, 500)}`,
627
- buildPendingEventLines(state),
628
- buildTimelineLines(state),
629
- 'You are running the main autonomous mission loop. Continue executing toward the goal with initiative.',
630
- state.autonomyMode === 'assist'
631
- ? 'Assist mode is active: execute safe internal work and ask before irreversible external side effects.'
632
- : 'Autonomous mode is active: execute safe next actions without waiting for confirmation; only ask when blocked.',
633
- 'Use tools where needed, verify outcomes, and avoid vague status-only replies.',
634
- 'Do not ask broad exploratory questions when a safe next action exists. Pick a reasonable assumption, execute, and adapt from evidence.',
635
- 'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
636
- 'Use any available tools actively to maintain state and recall context across turns.',
637
- 'Use a planner-executor-review loop: keep a concrete step plan, execute one meaningful step, then self-review and either continue or re-plan.',
638
- 'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
639
- 'When the mission goal is fully completed, set status to "ok" with follow_up:false and include a clear summary of what was accomplished. The loop will auto-pause.',
640
- 'If nothing important changed and no action is needed now, reply exactly HEARTBEAT_OK.',
641
- 'Otherwise: provide a concise human-readable update, then append exactly one [MAIN_LOOP_META] JSON line.',
642
- 'Optionally append one [MAIN_LOOP_PLAN] JSON line when creating/updating plan steps.',
643
- 'Optionally append one [MAIN_LOOP_REVIEW] JSON line after execution review.',
644
- '[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
645
- '[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
646
- '[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
647
- 'The [MAIN_LOOP_META] JSON must be valid, on one line, and only appear once.',
648
- `Fallback prompt context: ${fallbackPrompt || 'SWARM_HEARTBEAT_CHECK'}`,
649
- ].join('\n')
74
+ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: string): string {
75
+ void session
76
+ return fallbackPrompt
650
77
  }
651
78
 
652
79
  export function stripMainLoopMetaForPersistence(text: string): string {
653
- if (!text) return ''
654
- return text
80
+ return (text || '')
655
81
  .split('\n')
656
- .filter((line) => !line.includes('[MAIN_LOOP_META]') && !line.includes('[MAIN_LOOP_PLAN]') && !line.includes('[MAIN_LOOP_REVIEW]') && !line.includes('[AGENT_HEARTBEAT_META]'))
82
+ .filter((line) => !LEGACY_META_LINE_RE.test(line))
657
83
  .join('\n')
658
84
  .trim()
659
85
  }
660
86
 
661
87
  export function getMainLoopStateForSession(sessionId: string): MainLoopState | null {
662
- const sessions = loadSessions()
663
- const session = sessions[sessionId]
664
- if (!session || !isMainSession(session)) return null
665
- return normalizeState(session.mainLoopState)
88
+ void sessionId
89
+ return null
666
90
  }
667
91
 
668
92
  export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
669
- const sessions = loadSessions()
670
- const session = sessions[sessionId]
671
- if (!session || !isMainSession(session)) return null
672
- const now = Date.now()
673
- const state = normalizeState(session.mainLoopState, now)
674
-
675
- if (typeof patch.goal === 'string') state.goal = patch.goal.trim().slice(0, 600) || null
676
- if (patch.goal === null) state.goal = null
677
- if (patch.goalContract !== undefined) state.goalContract = normalizeGoalContract(patch.goalContract)
678
- if (patch.status === 'idle' || patch.status === 'progress' || patch.status === 'blocked' || patch.status === 'ok') state.status = patch.status
679
- if (typeof patch.summary === 'string') state.summary = patch.summary.trim().slice(0, 800) || null
680
- if (patch.summary === null) state.summary = null
681
- if (typeof patch.nextAction === 'string') state.nextAction = patch.nextAction.trim().slice(0, 600) || null
682
- if (patch.nextAction === null) state.nextAction = null
683
- if (Array.isArray(patch.planSteps)) state.planSteps = normalizeStringList(patch.planSteps, 10, 220)
684
- if (typeof patch.currentPlanStep === 'string') state.currentPlanStep = patch.currentPlanStep.trim().slice(0, 220) || null
685
- if (patch.currentPlanStep === null) state.currentPlanStep = null
686
- if (typeof patch.reviewNote === 'string') state.reviewNote = patch.reviewNote.trim().slice(0, 320) || null
687
- if (patch.reviewNote === null) state.reviewNote = null
688
- if (typeof patch.reviewConfidence === 'number' && Number.isFinite(patch.reviewConfidence)) {
689
- state.reviewConfidence = Math.max(0, Math.min(1, patch.reviewConfidence))
690
- }
691
- if (patch.reviewConfidence === null) state.reviewConfidence = null
692
- if (typeof patch.missionTaskId === 'string') state.missionTaskId = patch.missionTaskId.trim() || null
693
- if (patch.missionTaskId === null) state.missionTaskId = null
694
- if (typeof patch.momentumScore === 'number') state.momentumScore = clampInt(patch.momentumScore, state.momentumScore, 0, 100)
695
- if (typeof patch.paused === 'boolean') state.paused = patch.paused
696
- if (patch.autonomyMode === 'assist' || patch.autonomyMode === 'autonomous') state.autonomyMode = patch.autonomyMode
697
- if (Array.isArray(patch.pendingEvents)) state.pendingEvents = pruneEvents(patch.pendingEvents, now)
698
- if (Array.isArray(patch.timeline)) state.timeline = pruneTimeline(patch.timeline, now)
699
- if (typeof patch.followupChainCount === 'number') state.followupChainCount = clampInt(patch.followupChainCount, state.followupChainCount, 0, 100)
700
- if (typeof patch.metaMissCount === 'number') state.metaMissCount = clampInt(patch.metaMissCount, state.metaMissCount, 0, 100)
701
- if (Array.isArray(patch.workingMemoryNotes)) state.workingMemoryNotes = normalizeStringList(patch.workingMemoryNotes, 24, 260)
702
- if (typeof patch.lastMemoryNoteAt === 'number') state.lastMemoryNoteAt = patch.lastMemoryNoteAt
703
- if (patch.lastMemoryNoteAt === null) state.lastMemoryNoteAt = null
704
- if (typeof patch.lastPlannedAt === 'number') state.lastPlannedAt = patch.lastPlannedAt
705
- if (patch.lastPlannedAt === null) state.lastPlannedAt = null
706
- if (typeof patch.lastReviewedAt === 'number') state.lastReviewedAt = patch.lastReviewedAt
707
- if (patch.lastReviewedAt === null) state.lastReviewedAt = null
708
-
709
- state.momentumScore = computeMomentumScore(state)
710
- state.updatedAt = now
711
- session.mainLoopState = state
712
- sessions[sessionId] = session
713
- saveSessions(sessions)
714
- return state
93
+ void sessionId
94
+ void patch
95
+ return null
715
96
  }
716
97
 
717
98
  export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput): number {
718
- const text = toOneLine(input.text)
719
- if (!text) return 0
720
-
721
- const sessions = loadSessions()
722
- const now = Date.now()
723
- let changed = 0
724
-
725
- for (const session of Object.values(sessions) as any[]) {
726
- if (!isMainSession(session)) continue
727
- if (input.user && session.user && session.user !== input.user) continue
728
-
729
- const state = normalizeState(session.mainLoopState, now)
730
- const appended = appendEvent(state, input.type || 'event', text, now)
731
- if (!appended) continue
732
- appendTimeline(state, input.type || 'event', text, now, state.status)
733
- state.momentumScore = computeMomentumScore(state)
734
- state.updatedAt = now
735
- session.mainLoopState = state
736
- changed += 1
737
- }
738
-
739
- if (changed > 0) {
740
- saveSessions(sessions)
741
- log.info('main-loop', `Queued event for ${changed} main session(s)`, {
742
- type: input.type,
743
- text,
744
- user: input.user || null,
745
- })
746
- }
747
-
748
- return changed
749
- }
750
-
751
- const AgentHeartbeatMetaSchema = z.object({
752
- goal: z.string().trim().optional(),
753
- status: z.enum(['progress', 'ok', 'idle', 'blocked']).optional(),
754
- next_action: z.string().trim().optional(),
755
- }).passthrough()
756
-
757
- type AgentHeartbeatMeta = z.infer<typeof AgentHeartbeatMetaSchema>
758
-
759
- function parseAgentHeartbeatMeta(text: string): AgentHeartbeatMeta | null {
760
- const raw = (text || '').trim()
761
- if (!raw) return null
762
- const match = raw.match(AGENT_HEARTBEAT_META_RE)
763
- if (!match?.[1]) return null
764
- try {
765
- const parsed = JSON.parse(match[1])
766
- return AgentHeartbeatMetaSchema.parse(parsed)
767
- } catch {
768
- return null
769
- }
770
- }
771
-
772
- function handleAgentHeartbeatResult(session: any, input: HandleMainLoopRunResultInput): null {
773
- if (!input.internal || input.source !== 'heartbeat') return null
774
- if (!session.agentId) return null
775
- const text = input.resultText || ''
776
- if (!text.trim()) return null
777
-
778
- const meta = parseAgentHeartbeatMeta(text)
779
- if (!meta) return null
780
-
781
- const agents = loadAgents()
782
- const agent = agents[session.agentId]
783
- if (!agent) return null
784
-
785
- let changed = false
786
- if (meta.goal && meta.goal !== agent.heartbeatGoal) {
787
- agent.heartbeatGoal = meta.goal
788
- changed = true
789
- log.info('agent-heartbeat', `Goal updated for agent ${agent.name}: ${meta.goal.slice(0, 120)}`)
790
- }
791
- if (meta.next_action) {
792
- agent.heartbeatNextAction = meta.next_action
793
- changed = true
794
- }
795
- if (meta.status) {
796
- agent.heartbeatStatus = meta.status
797
- changed = true
798
- }
799
-
800
- if (changed) {
801
- agents[session.agentId] = agent
802
- saveAgents(agents)
803
- }
804
- return null
99
+ void input
100
+ return 0
805
101
  }
806
102
 
807
103
  export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
808
- const sessions = loadSessions()
809
- const session = sessions[input.sessionId]
810
- if (!session) return null
811
- if (!isMainLoopSession(session)) return handleAgentHeartbeatResult(session, input)
812
-
813
- const now = Date.now()
814
- const state = normalizeState(session.mainLoopState, now)
815
- state.pendingEvents = pruneEvents(state.pendingEvents, now)
816
- let forceMemoryNote = false
817
-
818
- const userGoalContract = parseGoalContractFromText(input.message)
819
- if (!input.internal) {
820
- if (userGoalContract?.objective) {
821
- state.goal = userGoalContract.objective
822
- state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
823
- state.status = 'progress'
824
- appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
825
- appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
826
- forceMemoryNote = true
827
- logExecution(input.sessionId, 'mission_start', `New goal contract: ${toOneLine(userGoalContract.objective, 200)}`, {
828
- agentId: session.agentId,
829
- detail: { goal: userGoalContract.objective, contract: userGoalContract, planSteps: state.planSteps },
830
- })
831
- }
832
- state.followupChainCount = 0
833
- state.missionTokens = 0
834
- state.missionCostUsd = 0
835
- }
836
-
837
- // Accumulate per-mission token/cost tracking
838
- if (typeof input.inputTokens === 'number') {
839
- state.missionTokens = (state.missionTokens || 0) + input.inputTokens + (input.outputTokens || 0)
840
- }
841
- if (typeof input.estimatedCost === 'number') {
842
- state.missionCostUsd = (state.missionCostUsd || 0) + input.estimatedCost
843
- }
844
-
845
- if (state.paused && input.internal) {
846
- appendTimeline(state, 'paused_skip', `Skipped internal tick from ${input.source} because mission is paused.`, now, state.status)
847
- state.momentumScore = computeMomentumScore(state)
848
- state.updatedAt = now
849
- session.mainLoopState = state
850
- sessions[input.sessionId] = session
851
- saveSessions(sessions)
852
- return null
853
- }
854
-
855
- if (input.error) {
856
- appendEvent(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 400)}`, now)
857
- appendTimeline(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 220)}`, now, 'blocked')
858
- state.status = 'blocked'
859
- appendWorkingMemoryNote(state, `blocked:${toOneLine(input.error, 120)}`)
860
- forceMemoryNote = true
861
- }
862
-
863
- for (const event of input.toolEvents || []) {
864
- if (!event?.error) continue
865
- appendEvent(
866
- state,
867
- 'tool_error',
868
- `Tool ${event.name || 'unknown'} error: ${toOneLine(event.output || event.input || 'unknown error', 400)}`,
869
- now,
870
- )
871
- appendTimeline(
872
- state,
873
- 'tool_error',
874
- `Tool ${event.name || 'unknown'} error encountered.`,
875
- now,
876
- 'blocked',
877
- )
878
- forceMemoryNote = true
879
- }
880
-
881
- let followup: MainLoopFollowupRequest | null = null
882
- const shouldAutoKickFromUserGoal = !input.internal
883
- && !input.error
884
- && !!userGoalContract?.objective
885
- && !state.paused
886
- && state.autonomyMode === 'autonomous'
887
-
888
- if (shouldAutoKickFromUserGoal) {
889
- followup = {
890
- message: buildFollowupPrompt(state, { agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
891
- delayMs: 1500,
892
- dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
893
- }
894
- appendTimeline(state, 'followup', 'Queued autonomous kickoff follow-up from new user goal.', now, state.status)
895
- }
896
-
897
- if (input.internal) {
898
- state.lastTickAt = now
899
- const trimmedText = (input.resultText || '').trim()
900
- const isHeartbeatOk = /^HEARTBEAT_OK$/i.test(trimmedText)
901
- const meta = parseMainLoopMeta(trimmedText)
902
- const planMeta = parseMainLoopPlan(trimmedText)
903
- const reviewMeta = parseMainLoopReview(trimmedText)
904
-
905
- if (planMeta) {
906
- if (planMeta.steps?.length) {
907
- state.planSteps = planMeta.steps
908
- state.lastPlannedAt = now
909
- appendWorkingMemoryNote(state, `plan:${planMeta.steps.join(' -> ')}`)
910
- }
911
- if (planMeta.current_step) {
912
- state.currentPlanStep = planMeta.current_step
913
- state.lastPlannedAt = now
914
- }
915
- appendTimeline(state, 'plan', `Plan updated${planMeta.current_step ? ` at step: ${planMeta.current_step}` : ''}.`, now, state.status)
916
- }
917
-
918
- if (reviewMeta) {
919
- if (reviewMeta.note) {
920
- state.reviewNote = reviewMeta.note
921
- appendWorkingMemoryNote(state, `review:${reviewMeta.note}`)
922
- }
923
- if (typeof reviewMeta.confidence === 'number') state.reviewConfidence = reviewMeta.confidence
924
- state.lastReviewedAt = now
925
- if (reviewMeta.needs_replan === true && state.planSteps.length > 0) {
926
- appendEvent(state, 'review_replan', 'Execution review requested replanning.', now)
927
- }
928
- appendTimeline(state, 'review', reviewMeta.note || 'Execution review updated.', now, state.status)
929
- }
930
-
931
- if (meta) {
932
- state.metaMissCount = 0
933
- if (meta.goal) {
934
- state.goal = meta.goal
935
- const metaGoalContract = parseGoalContractFromText(meta.goal)
936
- if (metaGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, metaGoalContract)
937
- }
938
- if (meta.status) state.status = meta.status
939
- if (meta.summary) state.summary = meta.summary
940
- if (meta.next_action) state.nextAction = meta.next_action
941
- if (meta.summary) appendWorkingMemoryNote(state, `summary:${toOneLine(meta.summary, 180)}`)
942
- if (meta.next_action) appendWorkingMemoryNote(state, `next:${toOneLine(meta.next_action, 180)}`)
943
- appendTimeline(
944
- state,
945
- 'meta',
946
- `Meta update: status=${meta.status || state.status}; summary=${toOneLine(meta.summary || state.summary || 'none', 140)}`,
947
- now,
948
- meta.status || state.status,
949
- )
950
- consumeEvents(state, meta.consume_event_ids)
951
-
952
- // Budget enforcement: check mission cost against goalContract.budgetUsd
953
- const budgetUsd = state.goalContract?.budgetUsd
954
- if (typeof budgetUsd === 'number' && budgetUsd > 0 && typeof state.missionCostUsd === 'number') {
955
- const usageRatio = state.missionCostUsd / budgetUsd
956
- if (usageRatio >= 1.0 && !state.paused) {
957
- state.paused = true
958
- state.status = 'blocked'
959
- appendTimeline(state, 'budget_exceeded', `Mission budget exceeded ($${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}). Mission paused.`, now, 'blocked')
960
- appendEvent(state, 'budget_exceeded', `Budget limit reached: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)}`, now)
961
- logExecution(input.sessionId, 'budget_warning', `Budget exceeded: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
962
- agentId: session.agentId,
963
- detail: { missionCostUsd: state.missionCostUsd, budgetUsd, missionTokens: state.missionTokens },
964
- })
965
- } else if (usageRatio >= 0.8) {
966
- appendEvent(state, 'budget_warning', `Mission approaching budget limit: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)} (${Math.round(usageRatio * 100)}%)`, now)
967
- logExecution(input.sessionId, 'budget_warning', `Budget at ${Math.round(usageRatio * 100)}%: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
968
- agentId: session.agentId,
969
- detail: { missionCostUsd: state.missionCostUsd, budgetUsd, usagePercent: Math.round(usageRatio * 100) },
970
- })
971
- }
972
- }
973
-
974
- if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < getMaxFollowupChain(session.agentId)) {
975
- state.followupChainCount += 1
976
- const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
977
- followup = {
978
- message: buildFollowupPrompt(state, { agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
979
- delayMs: delaySec * 1000,
980
- dedupeKey: `main-loop-followup:${input.sessionId}`,
981
- }
982
- appendTimeline(state, 'followup', `Queued chained follow-up in ${delaySec}s.`, now, state.status)
983
- } else if (meta.follow_up === false || isHeartbeatOk) {
984
- state.followupChainCount = 0
985
- }
986
- if (state.status === 'ok' || state.status === 'blocked') {
987
- forceMemoryNote = true
988
- if (state.status === 'ok') {
989
- // Auto-pause the mission loop — the goal is complete
990
- state.paused = true
991
- state.followupChainCount = 0
992
- followup = null
993
- logExecution(input.sessionId, 'mission_complete', `Mission completed: ${toOneLine(state.goal || 'unknown', 200)}`, {
994
- agentId: session.agentId,
995
- detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
996
- })
997
- appendTimeline(state, 'auto_pause', 'Mission goal completed — auto-paused.', now, state.status)
998
- }
999
- }
1000
- } else if (!isHeartbeatOk && trimmedText) {
1001
- state.metaMissCount = Math.min(100, state.metaMissCount + 1)
1002
- state.summary = toOneLine(trimmedText, 700)
1003
- appendWorkingMemoryNote(state, `inferred:${toOneLine(trimmedText, 160)}`)
1004
- if (state.status === 'idle') state.status = 'progress'
1005
- appendEvent(state, 'meta_missing', 'Main-loop reply missing [MAIN_LOOP_META] contract; state inferred from text.', now)
1006
- appendTimeline(state, 'meta_missing', 'Missing [MAIN_LOOP_META]; inferred state from plain text.', now, state.status)
1007
- } else if (isHeartbeatOk) {
1008
- state.metaMissCount = 0
1009
- appendTimeline(state, 'heartbeat_ok', 'Heartbeat returned HEARTBEAT_OK.', now, state.status)
1010
- }
1011
- }
1012
-
1013
- if (input.internal && state.status === 'ok') {
1014
- const completionGateReason = getMissionCompletionGateReason(session, state, input.resultText || '')
1015
- if (completionGateReason) {
1016
- state.status = 'progress'
1017
- if (!state.nextAction || /^no queued action/i.test(state.nextAction)) {
1018
- state.nextAction = 'Wait for the next schedule run and verify a screenshot artifact link is delivered.'
1019
- }
1020
- appendEvent(state, 'completion_gate', completionGateReason, now)
1021
- appendTimeline(state, 'completion_gate', 'Holding completion until screenshot artifact evidence is observed.', now, state.status)
1022
- appendWorkingMemoryNote(state, `gate:${toOneLine(completionGateReason, 180)}`)
1023
- }
1024
- }
1025
-
1026
- // Agents don't auto-create tasks for themselves — they just do the work.
1027
- // Tasks are created explicitly by the user or when delegating to another agent.
1028
- const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
1029
- maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
1030
-
1031
- state.momentumScore = computeMomentumScore(state)
1032
-
1033
- state.updatedAt = now
1034
- session.mainLoopState = state
1035
- sessions[input.sessionId] = session
1036
- saveSessions(sessions)
1037
-
1038
- return followup
104
+ void input
105
+ return null
1039
106
  }