@swarmclawai/swarmclaw 1.2.3 → 1.2.5

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 (273) hide show
  1. package/README.md +20 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/models/route.test.ts +60 -0
  49. package/src/app/api/providers/[id]/models/route.ts +33 -1
  50. package/src/app/api/providers/[id]/route.test.ts +49 -0
  51. package/src/app/api/providers/[id]/route.ts +30 -1
  52. package/src/app/api/providers/ollama/route.ts +6 -5
  53. package/src/app/api/schedules/[id]/route.ts +14 -108
  54. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  55. package/src/app/api/schedules/route.ts +9 -51
  56. package/src/app/api/settings/route.ts +4 -3
  57. package/src/app/api/setup/check-provider/route.ts +15 -1
  58. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  59. package/src/app/api/system/status/route.ts +2 -2
  60. package/src/app/api/tasks/[id]/route.ts +16 -202
  61. package/src/app/api/tasks/bulk/route.ts +5 -86
  62. package/src/app/api/tasks/metrics/route.ts +2 -1
  63. package/src/app/api/tasks/route.ts +11 -171
  64. package/src/app/api/upload/route.ts +1 -1
  65. package/src/app/api/uploads/[filename]/route.ts +1 -1
  66. package/src/app/api/uploads/route.ts +1 -1
  67. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  68. package/src/app/layout.tsx +9 -6
  69. package/src/app/protocols/page.tsx +71 -89
  70. package/src/app/tasks/page.tsx +32 -32
  71. package/src/cli/index.js +1 -0
  72. package/src/cli/spec.js +1 -0
  73. package/src/components/agents/agent-sheet.tsx +51 -25
  74. package/src/components/agents/inspector-panel.tsx +15 -4
  75. package/src/components/auth/setup-wizard/index.tsx +27 -18
  76. package/src/components/auth/setup-wizard/shared.tsx +2 -2
  77. package/src/components/auth/setup-wizard/step-agents.tsx +51 -38
  78. package/src/components/auth/setup-wizard/step-connect.tsx +48 -17
  79. package/src/components/auth/setup-wizard/types.ts +6 -4
  80. package/src/components/auth/setup-wizard/utils.test.ts +38 -8
  81. package/src/components/auth/setup-wizard/utils.ts +14 -8
  82. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  83. package/src/components/connectors/connector-list.tsx +26 -40
  84. package/src/components/connectors/connector-sheet.tsx +95 -149
  85. package/src/components/gateways/gateway-sheet.tsx +61 -110
  86. package/src/components/layout/live-query-sync.tsx +121 -0
  87. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  88. package/src/components/providers/app-query-provider.tsx +17 -0
  89. package/src/components/providers/provider-list.tsx +150 -77
  90. package/src/components/providers/provider-sheet.tsx +102 -77
  91. package/src/components/shared/model-combobox.tsx +5 -4
  92. package/src/components/skills/skill-list.tsx +5 -18
  93. package/src/components/skills/skill-sheet.tsx +21 -20
  94. package/src/components/skills/skills-workspace.tsx +48 -87
  95. package/src/components/tasks/task-card.tsx +20 -13
  96. package/src/components/tasks/task-column.tsx +22 -7
  97. package/src/components/tasks/task-list.tsx +8 -11
  98. package/src/components/tasks/task-sheet.tsx +111 -103
  99. package/src/features/agents/queries.ts +20 -0
  100. package/src/features/chatrooms/queries.ts +20 -0
  101. package/src/features/chats/queries.ts +27 -0
  102. package/src/features/connectors/queries.ts +145 -0
  103. package/src/features/credentials/queries.ts +37 -0
  104. package/src/features/extensions/queries.ts +26 -0
  105. package/src/features/external-agents/queries.ts +36 -0
  106. package/src/features/gateways/queries.ts +274 -0
  107. package/src/features/missions/queries.ts +23 -0
  108. package/src/features/projects/queries.ts +20 -0
  109. package/src/features/protocols/queries.ts +149 -0
  110. package/src/features/providers/queries.ts +142 -0
  111. package/src/features/settings/queries.ts +20 -0
  112. package/src/features/skills/queries.ts +182 -0
  113. package/src/features/tasks/queries.ts +189 -0
  114. package/src/hooks/use-ws.ts +3 -2
  115. package/src/lib/agent-provider-options.test.ts +152 -0
  116. package/src/lib/agent-provider-options.ts +84 -0
  117. package/src/lib/app/api-client.ts +2 -2
  118. package/src/lib/providers/index.test.ts +78 -0
  119. package/src/lib/providers/index.ts +13 -10
  120. package/src/lib/query/client.ts +17 -0
  121. package/src/lib/server/agents/agent-runtime-config.ts +6 -6
  122. package/src/lib/server/agents/agent-service.ts +429 -0
  123. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  124. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  125. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  126. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  127. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  128. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  129. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  130. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  131. package/src/lib/server/build-llm.ts +7 -15
  132. package/src/lib/server/capability-router.test.ts +70 -1
  133. package/src/lib/server/capability-router.ts +24 -99
  134. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  135. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  136. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  137. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  138. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  139. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  140. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  141. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  142. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  143. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  144. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  145. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  146. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  147. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  148. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  149. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  150. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  151. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  152. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  153. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  154. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  155. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  156. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  157. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  158. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  159. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  160. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  161. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  162. package/src/lib/server/chats/chat-session-service.ts +410 -0
  163. package/src/lib/server/connectors/access.ts +1 -1
  164. package/src/lib/server/connectors/commands.ts +7 -6
  165. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  166. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  167. package/src/lib/server/connectors/connector-service.ts +453 -0
  168. package/src/lib/server/connectors/delivery.ts +17 -12
  169. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  170. package/src/lib/server/connectors/media.ts +1 -1
  171. package/src/lib/server/connectors/response-media.ts +1 -1
  172. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  173. package/src/lib/server/connectors/session.ts +9 -7
  174. package/src/lib/server/connectors/voice-note.ts +2 -1
  175. package/src/lib/server/context-manager.ts +20 -1
  176. package/src/lib/server/cost.ts +2 -3
  177. package/src/lib/server/credentials/credential-repository.ts +43 -4
  178. package/src/lib/server/credentials/credential-service.ts +112 -0
  179. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  180. package/src/lib/server/daemon/controller.ts +577 -0
  181. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  182. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  183. package/src/lib/server/daemon/types.ts +101 -0
  184. package/src/lib/server/embeddings.ts +3 -9
  185. package/src/lib/server/eval/agent-regression.ts +3 -2
  186. package/src/lib/server/eval/runner.ts +2 -2
  187. package/src/lib/server/execution-brief.test.ts +167 -0
  188. package/src/lib/server/execution-brief.ts +295 -0
  189. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  190. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  191. package/src/lib/server/execution-engine/index.ts +35 -0
  192. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  193. package/src/lib/server/execution-engine/types.ts +33 -0
  194. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  195. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  196. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  197. package/src/lib/server/messages/message-repository.ts +330 -0
  198. package/src/lib/server/missions/mission-service/core.ts +8 -6
  199. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  200. package/src/lib/server/openclaw/doctor.ts +1 -1
  201. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  202. package/src/lib/server/openclaw/gateway.ts +5 -14
  203. package/src/lib/server/openclaw/health.ts +3 -11
  204. package/src/lib/server/openclaw/sync.ts +8 -6
  205. package/src/lib/server/persistence/storage-context.ts +3 -0
  206. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  207. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  208. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  209. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  210. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  211. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  212. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  213. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  214. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  215. package/src/lib/server/protocols/protocol-types.ts +10 -7
  216. package/src/lib/server/provider-endpoint.ts +7 -12
  217. package/src/lib/server/provider-model-discovery.ts +2 -11
  218. package/src/lib/server/query-expansion.ts +5 -6
  219. package/src/lib/server/run-context.test.ts +365 -0
  220. package/src/lib/server/run-context.ts +367 -0
  221. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  222. package/src/lib/server/runtime/queue/core.ts +61 -190
  223. package/src/lib/server/runtime/run-ledger.ts +8 -0
  224. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  225. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  226. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  227. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  228. package/src/lib/server/service-result.ts +16 -0
  229. package/src/lib/server/session-note.ts +2 -3
  230. package/src/lib/server/session-reset-policy.ts +4 -3
  231. package/src/lib/server/session-tools/connector.ts +9 -6
  232. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  233. package/src/lib/server/session-tools/crud.ts +162 -10
  234. package/src/lib/server/session-tools/delegate.ts +1 -1
  235. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  236. package/src/lib/server/session-tools/memory.ts +6 -4
  237. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  238. package/src/lib/server/session-tools/session-info.ts +119 -12
  239. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  240. package/src/lib/server/session-tools/skills.ts +15 -15
  241. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  242. package/src/lib/server/session-tools/subagent.ts +125 -7
  243. package/src/lib/server/session-tools/team-context.ts +4 -3
  244. package/src/lib/server/session-tools/wallet.ts +0 -58
  245. package/src/lib/server/sessions/session-lineage.ts +55 -0
  246. package/src/lib/server/sessions/session-repository.ts +2 -2
  247. package/src/lib/server/skills/learned-skills.ts +24 -23
  248. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  249. package/src/lib/server/skills/skill-repository.ts +136 -13
  250. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  251. package/src/lib/server/storage-normalization.test.ts +42 -215
  252. package/src/lib/server/storage-normalization.ts +98 -0
  253. package/src/lib/server/storage.ts +19 -0
  254. package/src/lib/server/structured-extract.ts +3 -14
  255. package/src/lib/server/tasks/task-followups.ts +16 -11
  256. package/src/lib/server/tasks/task-result.test.ts +25 -29
  257. package/src/lib/server/tasks/task-result.ts +5 -9
  258. package/src/lib/server/tasks/task-route-service.ts +449 -0
  259. package/src/lib/server/text-normalization.ts +41 -0
  260. package/src/lib/server/tool-planning.ts +6 -42
  261. package/src/lib/server/upload-path.ts +5 -0
  262. package/src/lib/server/working-state/extraction.ts +614 -0
  263. package/src/lib/server/working-state/normalization.ts +866 -0
  264. package/src/lib/server/working-state/prompt.ts +60 -0
  265. package/src/lib/server/working-state/repository.ts +38 -0
  266. package/src/lib/server/working-state/service.test.ts +253 -0
  267. package/src/lib/server/working-state/service.ts +293 -0
  268. package/src/lib/validation/schemas.ts +1 -0
  269. package/src/lib/ws-client.ts +3 -3
  270. package/src/stores/slices/task-slice.ts +1 -4
  271. package/src/stores/use-chatroom-store.ts +2 -2
  272. package/src/types/index.ts +288 -22
  273. package/src/views/settings/section-providers.tsx +2 -2
@@ -1,4 +1,5 @@
1
1
  import { hmrSingleton } from '@/lib/shared-utils'
2
+ import { getMessages } from '@/lib/server/messages/message-repository'
2
3
  import type { GoalContract, Message, MessageToolEvent, Session } from '@/types'
3
4
  import { mergeGoalContracts, parseGoalContractFromText, parseMainLoopPlan, parseMainLoopReview } from '@/lib/server/agents/autonomy-contract'
4
5
  import {
@@ -12,6 +13,10 @@ import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
12
13
  import { buildMissionHeartbeatPrompt as buildMissionHeartbeatPromptFromMission, getMissionForSession } from '@/lib/server/missions/mission-service'
13
14
  import { loadSettings } from '@/lib/server/settings/settings-repository'
14
15
  import { getSession, loadSessions } from '@/lib/server/sessions/session-repository'
16
+ import { deleteSessionWorkingState, loadSessionWorkingState, syncWorkingStateFromMainLoopState } from '@/lib/server/working-state/service'
17
+ import { syncMainLoopToRunContext } from '@/lib/server/run-context'
18
+ import { buildExecutionBrief, buildExecutionBriefContextBlock } from '@/lib/server/execution-brief'
19
+ import { cleanText, cleanMultiline } from '@/lib/server/text-normalization'
15
20
 
16
21
  const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
17
22
  const HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
@@ -19,7 +24,7 @@ const MAX_PENDING_EVENTS = 16
19
24
  const MAX_TIMELINE_ITEMS = 40
20
25
  const MAX_WORKING_MEMORY_NOTES = 12
21
26
  const DEFAULT_FOLLOWUP_DELAY_MS = 1500
22
- const DEFAULT_MAX_FOLLOWUP_CHAIN = 4
27
+ const DEFAULT_MAX_FOLLOWUP_CHAIN = 3
23
28
  const MAX_LIFETIME_ITERATIONS = 200
24
29
 
25
30
  export interface MainLoopState {
@@ -111,24 +116,6 @@ function asSession(session: unknown): MainSessionLike | null {
111
116
  return session as MainSessionLike
112
117
  }
113
118
 
114
- function cleanText(value: unknown, maxChars = 320): string | null {
115
- if (typeof value !== 'string') return null
116
- const normalized = value.replace(/\s+/g, ' ').trim()
117
- return normalized ? normalized.slice(0, maxChars) : null
118
- }
119
-
120
- function cleanMultiline(value: unknown, maxChars = 1400): string | null {
121
- if (typeof value !== 'string') return null
122
- const normalized = value
123
- .split('\n')
124
- .map((line) => line.trim())
125
- .filter(Boolean)
126
- .join('\n')
127
- .slice(0, maxChars)
128
- .trim()
129
- return normalized || null
130
- }
131
-
132
119
  function normalizeConfidence(value: unknown): number | null {
133
120
  const raw = typeof value === 'number'
134
121
  ? value
@@ -403,7 +390,7 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
403
390
  const session = sessions[sessionId]
404
391
  if (!session || !isMainSession(session)) return null
405
392
 
406
- const messages = Array.isArray(session.messages) ? session.messages : []
393
+ const messages = getMessages(sessionId)
407
394
  const hydrated = defaultState()
408
395
  hydrated.autonomyMode = session.heartbeatEnabled === true ? 'autonomous' : 'assist'
409
396
  hydrated.updatedAt = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : now()
@@ -440,21 +427,48 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
440
427
  }
441
428
  }
442
429
 
443
- return normalizeState(hydrated)
430
+ return mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(hydrated))
444
431
  }
445
432
 
446
433
  function persistState(sessionId: string, state: MainLoopState): void {
447
- upsertPersistedMainLoopState(sessionId, state as unknown as Record<string, unknown>)
434
+ const normalized = clampState(state)
435
+ upsertPersistedMainLoopState(sessionId, normalized as unknown as Record<string, unknown>)
436
+ const session = getSession(sessionId)
437
+ if (!session) return
438
+ const mission = getMissionForSession(session)
439
+ void syncWorkingStateFromMainLoopState({
440
+ sessionId,
441
+ mission,
442
+ goal: normalized.goal,
443
+ summary: normalized.summary,
444
+ status: normalized.status === 'ok'
445
+ ? 'completed'
446
+ : normalized.status === 'blocked'
447
+ ? 'blocked'
448
+ : normalized.status === 'progress'
449
+ ? 'progress'
450
+ : 'idle',
451
+ nextAction: normalized.nextAction,
452
+ planSteps: normalized.planSteps,
453
+ blockers: normalized.skillBlocker ? [{
454
+ summary: normalized.skillBlocker.summary,
455
+ kind: normalized.skillBlocker.status === 'approval_requested' ? 'approval' : 'other',
456
+ }] : undefined,
457
+ })
448
458
  }
449
459
 
450
460
  function getOrCreateState(sessionId: string): MainLoopState | null {
451
461
  const existing = stateMap.get(sessionId)
452
- if (existing) return existing
462
+ if (existing) {
463
+ const merged = mergeWorkingStateIntoMainLoopState(sessionId, existing)
464
+ stateMap.set(sessionId, merged)
465
+ return merged
466
+ }
453
467
 
454
468
  // Try disk (survives full restart)
455
469
  const persisted = loadPersistedMainLoopState(sessionId) as Partial<MainLoopState> | null
456
470
  if (persisted) {
457
- const restored = normalizeState(persisted)
471
+ const restored = mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(persisted))
458
472
  stateMap.set(sessionId, restored)
459
473
  return restored
460
474
  }
@@ -467,6 +481,43 @@ function getOrCreateState(sessionId: string): MainLoopState | null {
467
481
  return hydrated
468
482
  }
469
483
 
484
+ function mergeWorkingStateIntoMainLoopState(sessionId: string, current: MainLoopState): MainLoopState {
485
+ const workingState = loadSessionWorkingState(sessionId)
486
+ if (!workingState) return clampState(current)
487
+ const next = normalizeState(current)
488
+ if (workingState.objective) next.goal = cleanMultiline(workingState.objective, 900)
489
+ if (workingState.summary) next.summary = cleanText(workingState.summary, 1000)
490
+ if (workingState.nextAction) next.nextAction = cleanText(workingState.nextAction, 240)
491
+ if (workingState.status === 'completed') next.status = 'ok'
492
+ else if (workingState.status === 'blocked' || workingState.status === 'waiting') next.status = 'blocked'
493
+ else if (workingState.status === 'progress') next.status = 'progress'
494
+
495
+ const planSteps = (workingState.planSteps || [])
496
+ .map((step) => cleanText(step.text, 240))
497
+ .filter((step): step is string => Boolean(step))
498
+ if (planSteps.length > 0) {
499
+ next.planSteps = uniqueStrings(planSteps, 8)
500
+ next.completedPlanSteps = uniqueStrings(
501
+ (workingState.planSteps || [])
502
+ .filter((step) => step.status === 'resolved')
503
+ .map((step) => cleanText(step.text, 240))
504
+ .filter((step): step is string => Boolean(step)),
505
+ 16,
506
+ )
507
+ const activeStep = (workingState.planSteps || []).find((step) => step.status === 'active')
508
+ if (activeStep?.text) next.currentPlanStep = cleanText(activeStep.text, 240)
509
+ }
510
+
511
+ const noteCandidates = [
512
+ ...(workingState.confirmedFacts || []).filter((item) => item.status === 'active').map((item) => `Fact: ${item.statement}`),
513
+ ...(workingState.blockers || []).filter((item) => item.status === 'active').map((item) => `Blocker: ${item.summary}`),
514
+ ]
515
+ if (noteCandidates.length > 0) {
516
+ next.workingMemoryNotes = uniqueStrings([...(next.workingMemoryNotes || []), ...noteCandidates], MAX_WORKING_MEMORY_NOTES)
517
+ }
518
+ return clampState(next)
519
+ }
520
+
470
521
  function summarizePendingEvents(events: MainLoopState['pendingEvents']): string {
471
522
  if (!events.length) return ''
472
523
  return events
@@ -754,39 +805,38 @@ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: s
754
805
  const state = getOrCreateState(String(candidate.id))
755
806
  if (!state) return fallbackPrompt
756
807
  const latestExternalGoal = extractLatestGoal(Array.isArray(candidate.messages) ? candidate.messages as Message[] : [])
757
- const effectiveGoal = state.goal || latestExternalGoal.goal
758
808
  const effectiveGoalContract = latestExternalGoal.goalContract
759
809
  ? mergeGoalContracts(state.goalContract, latestExternalGoal.goalContract)
760
810
  : state.goalContract
761
811
 
762
- const completedSet = new Set(state.completedPlanSteps.map((s) => s.toLowerCase()))
763
- const planLines = state.planSteps.length > 0
764
- ? state.planSteps.slice(0, 8).map((step, index) => {
765
- const isDone = completedSet.has(step.toLowerCase())
766
- return `${index + 1}. ${isDone ? '[DONE] ' : ''}${step}`
767
- }).join('\n')
768
- : ''
812
+ const heartbeatSession = (persistedSession || candidate as Session)
813
+ const executionBrief = buildExecutionBrief({
814
+ session: heartbeatSession,
815
+ mission: getMissionForSession(heartbeatSession),
816
+ })
817
+ const executionBriefBlock = buildExecutionBriefContextBlock(executionBrief)
769
818
  const boundedFallbackPrompt = cleanMultiline(fallbackPrompt, 500)
770
- const boundedSummary = cleanMultiline(state.summary, 500)
819
+ const workingState = loadSessionWorkingState(String(candidate.id))
820
+ const activeWorkingBlockers = (workingState?.blockers || [])
821
+ .filter((item) => item.status === 'active')
822
+ .map((item) => item.nextAction ? `${item.summary} | next: ${item.nextAction}` : item.summary)
823
+ .slice(0, 4)
824
+ .join('\n')
771
825
 
772
826
  return [
773
827
  'MAIN_AGENT_HEARTBEAT_TICK',
774
828
  `Time: ${new Date().toISOString()}`,
775
- effectiveGoal ? `Current goal:\n${effectiveGoal}` : '',
829
+ executionBriefBlock,
776
830
  formatGoalContract(effectiveGoalContract),
777
- `Current status: ${state.status}`,
778
- state.nextAction ? `Planned next action: ${state.nextAction}` : '',
779
- state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
780
- planLines ? `Plan:\n${planLines}` : '',
781
831
  state.pendingEvents.length > 0 ? `Pending external events:\n${summarizePendingEvents(state.pendingEvents)}` : '',
832
+ activeWorkingBlockers ? `Active blockers:\n${activeWorkingBlockers}` : '',
782
833
  state.skillBlocker ? `Active skill blocker:\n${summarizeSkillBlocker(state.skillBlocker)}` : '',
783
834
  summarizeSelectedSkillRuntime(candidate),
784
- boundedSummary ? `Latest summary:\n${boundedSummary}` : '',
785
835
  boundedFallbackPrompt ? `Base heartbeat instructions:\n${boundedFallbackPrompt}` : '',
786
836
  '',
787
837
  'You are checking the durable main mission thread for this agent.',
788
838
  'Keep this status check brief — 5-10 tool calls maximum. Read key state, summarize progress, and report. Do not attempt fixes or deep investigation during heartbeats.',
789
- 'Use only the current goal, plan, next action, and pending external events shown above.',
839
+ 'Use the execution brief and pending external events shown above as the authoritative state for this tick.',
790
840
  'Do not infer or repeat old tasks from prior heartbeats.',
791
841
  'Prefer taking the single highest-value next step over restating the plan. Do not repeat completed work.',
792
842
  'If you revise the plan, emit exactly one line like:',
@@ -827,6 +877,7 @@ export function getMainLoopStateForSession(sessionId: string): MainLoopState | n
827
877
 
828
878
  export function clearMainLoopStateForSession(sessionId: string): boolean {
829
879
  deletePersistedMainLoopState(sessionId)
880
+ deleteSessionWorkingState(sessionId)
830
881
  return stateMap.delete(sessionId)
831
882
  }
832
883
 
@@ -840,6 +891,7 @@ export function pruneMainLoopState(liveSessionIds: Set<string>): number {
840
891
  if (!liveSessionIds.has(sessionId)) {
841
892
  stateMap.delete(sessionId)
842
893
  deletePersistedMainLoopState(sessionId)
894
+ deleteSessionWorkingState(sessionId)
843
895
  removed++
844
896
  }
845
897
  }
@@ -1096,5 +1148,11 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
1096
1148
  const finalClamped = clampState(state)
1097
1149
  stateMap.set(input.sessionId, finalClamped)
1098
1150
  persistState(input.sessionId, finalClamped)
1151
+
1152
+ // Project orchestrator state into session RunContext (non-critical)
1153
+ try {
1154
+ syncMainLoopToRunContext(input.sessionId, finalClamped)
1155
+ } catch { /* non-critical — main loop continues even if sync fails */ }
1156
+
1099
1157
  return followup
1100
1158
  }
@@ -49,6 +49,8 @@ import { logExecution } from '@/lib/server/execution-log'
49
49
  import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
50
50
  import { getEnabledCapabilityIds, splitCapabilityIds } from '@/lib/capability-selection'
51
51
  import { getSession, loadSessions, saveSession } from '@/lib/server/sessions/session-repository'
52
+ import { ensureRunContext } from '@/lib/server/run-context'
53
+ import { buildExecutionBrief, serializeExecutionBriefForDelegation } from '@/lib/server/execution-brief'
52
54
 
53
55
  // ---------------------------------------------------------------------------
54
56
  // Types
@@ -277,6 +279,16 @@ async function spawnSubagentImpl(
277
279
  browserProfileId,
278
280
  }
279
281
  sessions[sid] = applyResolvedRoute(nextSession, resolvePrimaryAgentRoute(agent))
282
+
283
+ // Enrich child session with parent's RunContext for delegation handoff
284
+ const delegationContext = parent ? serializeExecutionBriefForDelegation(buildExecutionBrief({ sessionId: context.sessionId })) : null
285
+ if (delegationContext) {
286
+ const childCtx = ensureRunContext(null)
287
+ childCtx.parentContext = delegationContext
288
+ childCtx.objective = input.message.slice(0, 900)
289
+ sessions[sid].runContext = childCtx
290
+ }
291
+
280
292
  saveSession(sid, sessions[sid])
281
293
 
282
294
  log.info('subagent', 'Spawning', { agentId: agent.id, agentName: agent.name, depth: depth + 1, jobId: job.id, sessionId: sid })
@@ -228,7 +228,26 @@ describe('supervisor-reflection', () => {
228
228
  model: 'gpt-test',
229
229
  claudeSessionId: null,
230
230
  messages: [
231
- { role: 'user', text: 'I am moving to Lisbon next month and prefer short check-ins while I am juggling the move.', time: 1 },
231
+ {
232
+ role: 'user',
233
+ text: 'I am moving to Lisbon next month and prefer short check-ins while I am juggling the move.',
234
+ time: 1,
235
+ semantics: {
236
+ taskIntent: 'general',
237
+ workType: 'general',
238
+ walletIntent: 'none',
239
+ isDeliverableTask: false,
240
+ isBroadGoal: false,
241
+ isResearchSynthesis: false,
242
+ hasHumanSignals: true,
243
+ hasSignificantEvent: true,
244
+ wantsScreenshots: false,
245
+ wantsOutboundDelivery: false,
246
+ wantsVoiceDelivery: false,
247
+ explicitToolRequests: [],
248
+ confidence: 0.98,
249
+ },
250
+ },
232
251
  { role: 'assistant', text: 'Understood. I will keep updates tight and remember the move timing.', time: 2 },
233
252
  ],
234
253
  createdAt: 1,
@@ -33,14 +33,16 @@ import { log } from '@/lib/server/logger'
33
33
  import { logExecution } from '@/lib/server/execution-log'
34
34
  import { logActivity } from '@/lib/server/storage'
35
35
  import { createNotification } from '@/lib/server/create-notification'
36
+ import { foldReflectionIntoRunContext } from '@/lib/server/run-context'
37
+ import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
38
+ import { cleanText } from '@/lib/server/text-normalization'
39
+ import { getMessages, getMessageCount, getRecentMessages } from '@/lib/server/messages/message-repository'
36
40
 
37
41
  const TAG = 'supervisor-reflection'
38
42
 
39
43
  const MAIN_LOOP_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
40
44
  const DEFAULT_TRANSCRIPT_MESSAGES = 12
41
45
  const DEFAULT_SNIPPET_CHARS = 800
42
- const HUMAN_SIGNAL_RE = /\b(?:prefer|please|call me|don't call me|do not call me|i like|i dislike|i hate|i love|my pronouns|my partner|my wife|my husband|my kid|my child|my mom|my dad|my sister|my brother|birthday|anniversary|wedding|married|divorc|pregnan|baby|moved|moving|relocat|promotion|promoted|laid off|new job|job change|graduat|hospital|sick|illness|diagnos|passed away|funeral|grief|bereave|deadline|launch|fundraising|closing|house|home|travel)\b/i
43
- const SIGNIFICANT_EVENT_RE = /\b(?:birthday|anniversary|wedding|married|divorc|pregnan|baby|moved|moving|relocat|promotion|promoted|laid off|new job|job change|graduat|hospital|sick|illness|diagnos|passed away|funeral|grief|bereave|deadline|launch|fundraising|closing|house|home|travel)\b/i
44
46
 
45
47
  export interface SupervisorStateSnapshot {
46
48
  followupChainCount?: number | null
@@ -81,13 +83,6 @@ function now(): number {
81
83
  return Date.now()
82
84
  }
83
85
 
84
- function cleanText(value: unknown, max = 320): string | null {
85
- if (typeof value !== 'string') return null
86
- const compact = value.replace(/\s+/g, ' ').trim()
87
- if (!compact) return null
88
- return compact.slice(0, max)
89
- }
90
-
91
86
  function looksLikeHtmlErrorPayload(value: string): boolean {
92
87
  const normalized = value.toLowerCase()
93
88
  let matches = 0
@@ -189,9 +184,11 @@ function buildIncident(
189
184
  }
190
185
 
191
186
  function sessionContextPressure(session: Session | null): boolean {
192
- if (!session || !Array.isArray(session.messages)) return false
193
- if (session.messages.length >= 60) return true
194
- const totalChars = session.messages.reduce((sum, message) => sum + String(message?.text || '').length, 0)
187
+ if (!session) return false
188
+ const msgCount = getMessageCount(session.id)
189
+ if (msgCount >= 60) return true
190
+ const messages = getMessages(session.id)
191
+ const totalChars = messages.reduce((sum, message) => sum + String(message?.text || '').length, 0)
195
192
  return totalChars >= 18_000
196
193
  }
197
194
 
@@ -428,7 +425,7 @@ export async function executeSupervisorAutoActions(params: {
428
425
  }
429
426
 
430
427
  function buildSessionTranscript(session: Session, maxMessages = DEFAULT_TRANSCRIPT_MESSAGES): string {
431
- const messages = Array.isArray(session.messages) ? session.messages.slice(-maxMessages) : []
428
+ const messages = getRecentMessages(session.id, maxMessages)
432
429
  const lines: string[] = []
433
430
  for (const message of messages) {
434
431
  if (!message || message.suppressed) continue
@@ -543,10 +540,21 @@ function normalizeNoteArray(value: unknown, limit = 4): string[] {
543
540
  return out
544
541
  }
545
542
 
543
+ function transcriptHasSemanticSignal(
544
+ session: Session | null,
545
+ signal: 'hasHumanSignals' | 'hasSignificantEvent',
546
+ ): boolean {
547
+ if (!session) return false
548
+ const recentMessages = getRecentMessages(session.id, 8)
549
+ return recentMessages.some((message) => message?.role === 'user' && message?.semantics?.[signal] === true)
550
+ }
551
+
546
552
  function transcriptHasHumanSignals(session: Session | null): boolean {
547
- if (!session || !Array.isArray(session.messages)) return false
548
- const recentMessages = session.messages.slice(-8)
549
- return recentMessages.some((message) => HUMAN_SIGNAL_RE.test(stripMainLoopMeta(String(message?.text || ''))))
553
+ return transcriptHasSemanticSignal(session, 'hasHumanSignals')
554
+ }
555
+
556
+ function transcriptHasSignificantEvents(session: Session | null): boolean {
557
+ return transcriptHasSemanticSignal(session, 'hasSignificantEvent')
550
558
  }
551
559
 
552
560
  function parseReflectionResponse(raw: string): {
@@ -658,10 +666,11 @@ function shouldReflectRun(params: {
658
666
  if (!surface || !runtimeScopeIncludes(params.runtimeScope, surface)) return false
659
667
  if (params.status === 'cancelled') return false
660
668
  if (surface === 'task') return Boolean(params.resultText.trim() || params.incidents.length > 0)
661
- const meaningfulMessages = Array.isArray(params.session?.messages)
662
- ? params.session.messages.filter((message) => message && !message.suppressed && (message.text || message.toolEvents?.length)).length
669
+ const meaningfulMessages = params.session
670
+ ? getMessages(params.session.id).filter((message) => message && !message.suppressed && (message.text || message.toolEvents?.length)).length
663
671
  : 0
664
672
  if (transcriptHasHumanSignals(params.session)) return true
673
+ if (transcriptHasSignificantEvents(params.session)) return true
665
674
  if (params.incidents.length > 0) return true
666
675
  if (params.toolEvents.length > 0) return true
667
676
  if (params.resultText.trim().length >= 180) return true
@@ -788,7 +797,7 @@ function writeReflectionMemories(params: {
788
797
  }
789
798
  if (group.kind === 'significant_event') {
790
799
  metadata.memoryFacet = 'event'
791
- metadata.eventSalience = SIGNIFICANT_EVENT_RE.test(note) ? 'high' : 'medium'
800
+ metadata.eventSalience = 'high'
792
801
  }
793
802
  if (group.kind === 'open_loop') {
794
803
  metadata.memoryFacet = 'followup'
@@ -1095,6 +1104,17 @@ export async function observeAutonomyRunOutcome(
1095
1104
  reflections[reflection.id] = reflection
1096
1105
  saveRunReflections(reflections)
1097
1106
 
1107
+ // Fold reflection notes into session RunContext (non-critical)
1108
+ try {
1109
+ const freshSession = getSession(input.sessionId) as Session | undefined
1110
+ if (freshSession) {
1111
+ freshSession.runContext = foldReflectionIntoRunContext(freshSession.runContext, reflection)
1112
+ saveSession(input.sessionId, freshSession)
1113
+ }
1114
+ } catch (err: unknown) {
1115
+ log.warn(TAG, 'RunContext reflection folding failed:', err instanceof Error ? err.message : String(err))
1116
+ }
1117
+
1098
1118
  // Quality degradation alert — if recent quality trend drops below 0.5
1099
1119
  if (typeof reflection.qualityScore === 'number' && input.agentId) {
1100
1120
  checkQualityDegradation(input.agentId).catch(() => {})
@@ -1,11 +1,13 @@
1
1
  import { ChatAnthropic } from '@langchain/anthropic'
2
2
  import { ChatOpenAI } from '@langchain/openai'
3
- import { loadCredentials, decryptKey, loadAgents, loadSessions } from './storage'
4
3
  import { getProviderList } from '../providers'
5
4
  import { normalizeOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
6
5
  import { NON_LANGGRAPH_PROVIDER_IDS } from '../provider-sets'
7
6
  import { resolveOllamaRuntimeConfig } from './ollama-runtime'
8
7
  import { resolveProviderApiEndpoint, resolveProviderCredentialId } from './provider-endpoint'
8
+ import { getAgent } from './agents/agent-repository'
9
+ import { resolveCredentialSecret } from './credentials/credential-service'
10
+ import { getSession } from './sessions/session-repository'
9
11
  import type { Agent } from '@/types'
10
12
 
11
13
  const OLLAMA_CLOUD_URL = 'https://ollama.com/v1'
@@ -135,15 +137,7 @@ export function buildChatModel(opts: {
135
137
  }
136
138
 
137
139
  function resolveApiKeyFromCredential(credentialId: string | null | undefined): string | null {
138
- if (!credentialId) return null
139
- const creds = loadCredentials()
140
- const cred = creds[credentialId]
141
- if (!cred?.encryptedKey) return null
142
- try {
143
- return decryptKey(cred.encryptedKey)
144
- } catch {
145
- return null
146
- }
140
+ return resolveCredentialSecret(credentialId)
147
141
  }
148
142
 
149
143
  function normalizePreferenceValue(value: string | null | undefined): string {
@@ -222,12 +216,10 @@ export function resolveGenerationModelConfig(options?: {
222
216
  excludeProviders?: string[]
223
217
  }): ResolvedGenerationModelConfig {
224
218
  const providers = getProviderList()
225
- const agents = loadAgents()
226
- const sessions = loadSessions()
227
219
  const excludeProviders = new Set((options?.excludeProviders || []).map((value) => normalizePreferenceValue(value)).filter(Boolean))
228
- const session = options?.sessionId ? sessions[options.sessionId] : null
229
- const sessionAgent = session?.agentId ? agents[session.agentId] as Agent | undefined : null
230
- const directAgent = options?.agentId ? agents[options.agentId] as Agent | undefined : null
220
+ const session = options?.sessionId ? getSession(options.sessionId) : null
221
+ const sessionAgent = session?.agentId ? getAgent(session.agentId) as Agent | null : null
222
+ const directAgent = options?.agentId ? getAgent(options.agentId) as Agent | null : null
231
223
  const resolved = resolvePreferredGenerationConfig(providers, [
232
224
  ...(Array.isArray(options?.preferred) ? options?.preferred : options?.preferred ? [options.preferred] : []),
233
225
  ...(session ? [{
@@ -1,12 +1,14 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
3
  import { routeTaskIntent } from './capability-router'
4
+ import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
4
5
 
5
6
  test('routeTaskIntent keeps recall-style prompts as general intent', () => {
6
7
  const decision = routeTaskIntent(
7
8
  'What token did we store earlier as e2e_validation_token? Reply only with the token.',
8
9
  ['memory', 'web_search'],
9
10
  null,
11
+ makeClassification({ taskIntent: 'general' }),
10
12
  )
11
13
  assert.equal(decision.intent, 'general')
12
14
  })
@@ -16,6 +18,7 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
16
18
  'Build and test a calculator app, then remember the final path in memory.',
17
19
  ['memory', 'shell', 'files'],
18
20
  null,
21
+ makeClassification({ taskIntent: 'coding', workType: 'coding' }),
19
22
  )
20
23
  assert.equal(decision.intent, 'coding')
21
24
  })
@@ -25,6 +28,14 @@ test('routeTaskIntent keeps hybrid research-plus-media prompts in research inten
25
28
  'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
26
29
  ['web_search', 'web_fetch', 'browser', 'manage_connectors'],
27
30
  null,
31
+ makeClassification({
32
+ taskIntent: 'research',
33
+ workType: 'research',
34
+ wantsScreenshots: true,
35
+ wantsVoiceDelivery: true,
36
+ wantsOutboundDelivery: true,
37
+ isResearchSynthesis: true,
38
+ }),
28
39
  )
29
40
 
30
41
  assert.equal(decision.intent, 'research')
@@ -36,6 +47,12 @@ test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
36
47
  'Send me a voice note over WhatsApp summarizing what changed.',
37
48
  ['manage_connectors'],
38
49
  null,
50
+ makeClassification({
51
+ taskIntent: 'outreach',
52
+ workType: 'writing',
53
+ wantsVoiceDelivery: true,
54
+ wantsOutboundDelivery: true,
55
+ }),
39
56
  )
40
57
 
41
58
  assert.equal(decision.intent, 'outreach')
@@ -45,10 +62,62 @@ test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
45
62
  test('routeTaskIntent treats keep-watching update requests as research even without explicit news keywords', () => {
46
63
  const decision = routeTaskIntent(
47
64
  'Tell me about the Iran war, keep watching for meaningful updates, and avoid duplicate reminders.',
48
- ['web_search', 'manage_schedules'],
65
+ ['web_search', 'web_fetch', 'manage_schedules'],
49
66
  null,
67
+ makeClassification({
68
+ taskIntent: 'research',
69
+ workType: 'research',
70
+ isResearchSynthesis: true,
71
+ }),
50
72
  )
51
73
 
52
74
  assert.equal(decision.intent, 'research')
53
75
  assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch'])
54
76
  })
77
+
78
+ test('routeTaskIntent uses structured classification when available', () => {
79
+ const classification: MessageClassification = {
80
+ taskIntent: 'browsing',
81
+ isDeliverableTask: true,
82
+ isBroadGoal: false,
83
+ walletIntent: 'none',
84
+ hasHumanSignals: false,
85
+ hasSignificantEvent: false,
86
+ isResearchSynthesis: true,
87
+ workType: 'research',
88
+ wantsScreenshots: true,
89
+ wantsOutboundDelivery: false,
90
+ wantsVoiceDelivery: false,
91
+ explicitToolRequests: ['browser'],
92
+ confidence: 0.92,
93
+ }
94
+
95
+ const decision = routeTaskIntent(
96
+ 'Review this story and show me screenshots.',
97
+ ['web_search', 'web_fetch', 'browser'],
98
+ null,
99
+ classification,
100
+ )
101
+
102
+ assert.equal(decision.intent, 'browsing')
103
+ assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch'])
104
+ })
105
+
106
+ function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {
107
+ return {
108
+ taskIntent: 'general',
109
+ isDeliverableTask: false,
110
+ isBroadGoal: false,
111
+ walletIntent: 'none',
112
+ hasHumanSignals: false,
113
+ hasSignificantEvent: false,
114
+ isResearchSynthesis: false,
115
+ workType: 'general',
116
+ wantsScreenshots: false,
117
+ wantsOutboundDelivery: false,
118
+ wantsVoiceDelivery: false,
119
+ explicitToolRequests: [],
120
+ confidence: 0.9,
121
+ ...overrides,
122
+ }
123
+ }