@swarmclawai/swarmclaw 1.2.4 → 1.2.6

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 (262) hide show
  1. package/README.md +14 -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]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +23 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -0,0 +1,614 @@
1
+ import { HumanMessage } from '@langchain/core/messages'
2
+
3
+ import { genId } from '@/lib/id'
4
+ import { buildLLM } from '@/lib/server/build-llm'
5
+ import { log } from '@/lib/server/logger'
6
+ import { cleanText, cleanMultiline, normalizeList } from '@/lib/server/text-normalization'
7
+ import type {
8
+ EvidenceRef,
9
+ MessageToolEvent,
10
+ SessionWorkingState,
11
+ WorkingArtifact,
12
+ WorkingArtifactPatch,
13
+ WorkingBlockerPatch,
14
+ WorkingDecisionPatch,
15
+ WorkingFactPatch,
16
+ WorkingHypothesisPatch,
17
+ WorkingPlanStepPatch,
18
+ WorkingQuestionPatch,
19
+ WorkingStatePatch,
20
+ } from '@/types'
21
+
22
+ import {
23
+ EXTRACTION_TIMEOUT_MS,
24
+ WorkingStatePatchSchema,
25
+ missionStatusToWorkingStateStatus,
26
+ normalizeEvidenceIds,
27
+ now,
28
+ } from '@/lib/server/working-state/normalization'
29
+ import type {
30
+ SynchronizeWorkingStateForTurnInput,
31
+ WorkingStateDeterministicUpdateInput,
32
+ WorkingStateExtractionInput,
33
+ } from '@/lib/server/working-state/normalization'
34
+
35
+ const TAG = 'working-state'
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Parsing helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export function parseStructuredObject(raw: string): Record<string, unknown> | null {
42
+ const text = cleanMultiline(raw, 20_000)
43
+ if (!text || (!text.startsWith('{') && !text.startsWith('['))) return null
44
+ try {
45
+ const parsed = JSON.parse(text)
46
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
47
+ ? parsed as Record<string, unknown>
48
+ : null
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ export function extractFirstJsonObject(text: string): string | null {
55
+ const source = String(text || '').trim()
56
+ if (!source) return null
57
+ let start = -1
58
+ let depth = 0
59
+ let inString = false
60
+ let escaped = false
61
+ for (let index = 0; index < source.length; index += 1) {
62
+ const char = source[index]
63
+ if (start === -1) {
64
+ if (char === '{') {
65
+ start = index
66
+ depth = 1
67
+ }
68
+ continue
69
+ }
70
+ if (inString) {
71
+ if (escaped) escaped = false
72
+ else if (char === '\\') escaped = true
73
+ else if (char === '"') inString = false
74
+ continue
75
+ }
76
+ if (char === '"') {
77
+ inString = true
78
+ continue
79
+ }
80
+ if (char === '{') depth += 1
81
+ else if (char === '}') depth -= 1
82
+ if (depth === 0) return source.slice(start, index + 1)
83
+ }
84
+ return null
85
+ }
86
+
87
+ export function parseWorkingStatePatchResponse(text: string): WorkingStatePatch | null {
88
+ const jsonText = extractFirstJsonObject(text)
89
+ if (!jsonText) return null
90
+ let raw: unknown = null
91
+ try {
92
+ raw = JSON.parse(jsonText)
93
+ } catch {
94
+ return null
95
+ }
96
+ const parsed = WorkingStatePatchSchema.safeParse(raw)
97
+ if (!parsed.success) return null
98
+ const data = parsed.data
99
+ return {
100
+ objective: cleanMultiline(data.objective, 900) || null,
101
+ summary: cleanMultiline(data.summary, 600) || null,
102
+ constraints: normalizeList(data.constraints, 12, 240),
103
+ successCriteria: normalizeList(data.successCriteria, 12, 240),
104
+ status: data.status || null,
105
+ nextAction: cleanText(data.nextAction, 240) || null,
106
+ planSteps: Array.isArray(data.planSteps)
107
+ ? data.planSteps
108
+ .map((step) => {
109
+ const text = cleanText(step.text, 240)
110
+ if (!text) return null
111
+ return {
112
+ id: cleanText(step.id, 120) || null,
113
+ text,
114
+ status: step.status || undefined,
115
+ } satisfies WorkingPlanStepPatch
116
+ })
117
+ .filter(Boolean) as WorkingPlanStepPatch[]
118
+ : undefined,
119
+ factsUpsert: Array.isArray(data.factsUpsert)
120
+ ? data.factsUpsert
121
+ .map((fact) => {
122
+ const statement = cleanText(fact.statement, 280)
123
+ if (!statement) return null
124
+ return {
125
+ id: cleanText(fact.id, 120) || null,
126
+ statement,
127
+ source: fact.source || undefined,
128
+ status: fact.status || undefined,
129
+ evidenceIds: normalizeEvidenceIds(fact.evidenceIds),
130
+ } satisfies WorkingFactPatch
131
+ })
132
+ .filter(Boolean) as WorkingFactPatch[]
133
+ : undefined,
134
+ artifactsUpsert: Array.isArray(data.artifactsUpsert)
135
+ ? data.artifactsUpsert
136
+ .map((artifact) => {
137
+ const label = cleanText(artifact.label, 240)
138
+ if (!label) return null
139
+ return {
140
+ id: cleanText(artifact.id, 120) || null,
141
+ label,
142
+ kind: artifact.kind || undefined,
143
+ path: cleanText(artifact.path, 320) || null,
144
+ url: cleanText(artifact.url, 320) || null,
145
+ sourceTool: cleanText(artifact.sourceTool, 120) || null,
146
+ status: artifact.status || undefined,
147
+ evidenceIds: normalizeEvidenceIds(artifact.evidenceIds),
148
+ } satisfies WorkingArtifactPatch
149
+ })
150
+ .filter(Boolean) as WorkingArtifactPatch[]
151
+ : undefined,
152
+ decisionsAppend: Array.isArray(data.decisionsAppend)
153
+ ? data.decisionsAppend
154
+ .map((decision) => {
155
+ const summary = cleanText(decision.summary, 280)
156
+ if (!summary) return null
157
+ return {
158
+ id: cleanText(decision.id, 120) || null,
159
+ summary,
160
+ rationale: cleanText(decision.rationale, 320) || null,
161
+ status: decision.status || undefined,
162
+ evidenceIds: normalizeEvidenceIds(decision.evidenceIds),
163
+ } satisfies WorkingDecisionPatch
164
+ })
165
+ .filter(Boolean) as WorkingDecisionPatch[]
166
+ : undefined,
167
+ blockersUpsert: Array.isArray(data.blockersUpsert)
168
+ ? data.blockersUpsert
169
+ .map((blocker) => {
170
+ const summary = cleanText(blocker.summary, 280)
171
+ if (!summary) return null
172
+ return {
173
+ id: cleanText(blocker.id, 120) || null,
174
+ summary,
175
+ kind: blocker.kind || undefined,
176
+ nextAction: cleanText(blocker.nextAction, 240) || null,
177
+ status: blocker.status || undefined,
178
+ evidenceIds: normalizeEvidenceIds(blocker.evidenceIds),
179
+ } satisfies WorkingBlockerPatch
180
+ })
181
+ .filter(Boolean) as WorkingBlockerPatch[]
182
+ : undefined,
183
+ questionsUpsert: Array.isArray(data.questionsUpsert)
184
+ ? data.questionsUpsert
185
+ .map((question) => {
186
+ const value = cleanText(question.question, 280)
187
+ if (!value) return null
188
+ return {
189
+ id: cleanText(question.id, 120) || null,
190
+ question: value,
191
+ status: question.status || undefined,
192
+ evidenceIds: normalizeEvidenceIds(question.evidenceIds),
193
+ } satisfies WorkingQuestionPatch
194
+ })
195
+ .filter(Boolean) as WorkingQuestionPatch[]
196
+ : undefined,
197
+ hypothesesUpsert: Array.isArray(data.hypothesesUpsert)
198
+ ? data.hypothesesUpsert
199
+ .map((hypothesis) => {
200
+ const statement = cleanText(hypothesis.statement, 280)
201
+ if (!statement) return null
202
+ return {
203
+ id: cleanText(hypothesis.id, 120) || null,
204
+ statement,
205
+ confidence: hypothesis.confidence || undefined,
206
+ status: hypothesis.status || undefined,
207
+ evidenceIds: normalizeEvidenceIds(hypothesis.evidenceIds),
208
+ } satisfies WorkingHypothesisPatch
209
+ })
210
+ .filter(Boolean) as WorkingHypothesisPatch[]
211
+ : undefined,
212
+ supersedeIds: normalizeList(data.supersedeIds, 24, 120),
213
+ }
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Rendering helpers for extraction prompt
218
+ // ---------------------------------------------------------------------------
219
+
220
+ export function renderStateForExtraction(state: SessionWorkingState | null | undefined): string {
221
+ if (!state) return '(none)'
222
+ const activePlan = state.planSteps.filter((item) => item.status === 'active').map((item) => item.text).slice(0, 8)
223
+ const facts = state.confirmedFacts.filter((item) => item.status === 'active').map((item) => item.statement).slice(0, 8)
224
+ const blockers = state.blockers.filter((item) => item.status === 'active').map((item) => item.summary).slice(0, 6)
225
+ const questions = state.openQuestions.filter((item) => item.status === 'active').map((item) => item.question).slice(0, 6)
226
+ const hypotheses = state.hypotheses.filter((item) => item.status === 'active').map((item) => item.statement).slice(0, 6)
227
+ const artifacts = state.artifacts.filter((item) => item.status === 'active').map((item) => cleanText(item.path || item.url || item.label, 180)).slice(0, 6)
228
+ return [
229
+ `objective: ${JSON.stringify(state.objective || null)}`,
230
+ `summary: ${JSON.stringify(state.summary || null)}`,
231
+ `status: ${JSON.stringify(state.status)}`,
232
+ `nextAction: ${JSON.stringify(state.nextAction || null)}`,
233
+ `constraints: ${JSON.stringify(state.constraints || [])}`,
234
+ `successCriteria: ${JSON.stringify(state.successCriteria || [])}`,
235
+ `activePlan: ${JSON.stringify(activePlan)}`,
236
+ `facts: ${JSON.stringify(facts)}`,
237
+ `blockers: ${JSON.stringify(blockers)}`,
238
+ `openQuestions: ${JSON.stringify(questions)}`,
239
+ `hypotheses: ${JSON.stringify(hypotheses)}`,
240
+ `artifacts: ${JSON.stringify(artifacts)}`,
241
+ ].join('\n')
242
+ }
243
+
244
+ export function summarizeToolEvents(toolEvents: MessageToolEvent[] | undefined): string {
245
+ if (!Array.isArray(toolEvents) || toolEvents.length === 0) return '(none)'
246
+ return toolEvents
247
+ .slice(-8)
248
+ .map((event) => {
249
+ const name = cleanText(event.name, 80) || 'unknown'
250
+ const input = cleanText(event.input, 160)
251
+ const output = cleanText(event.output, 200)
252
+ const parts = [name]
253
+ if (input) parts.push(`input=${JSON.stringify(input)}`)
254
+ if (output) parts.push(`output=${JSON.stringify(output)}`)
255
+ if (event.error === true) parts.push('error=true')
256
+ if (event.toolCallId) parts.push(`toolCallId=${JSON.stringify(event.toolCallId)}`)
257
+ return parts.join(' ')
258
+ })
259
+ .join('\n')
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Prompt building for extraction LLM call
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export function buildWorkingStatePatchPrompt(input: WorkingStateExtractionInput): string {
267
+ return [
268
+ 'You maintain a structured working-state object for an autonomous agent.',
269
+ 'Return JSON only.',
270
+ '',
271
+ 'Update the state using only evidence from the latest turn, tool results, and mission snapshot.',
272
+ 'Rules:',
273
+ '- Facts must be confirmed by explicit user text, mission state, or tool evidence. Do not turn guesses into facts.',
274
+ '- Put uncertain leads into hypotheses, not facts.',
275
+ '- Use blockers for approvals, credentials, human input, external waits, and explicit execution failures.',
276
+ '- nextAction must be one concrete immediate action, not a broad plan.',
277
+ '- Keep entries concise and avoid duplicates with the current state.',
278
+ '- If newer evidence invalidates an existing live item, include its id in supersedeIds.',
279
+ '- Do not repeat the entire state. Only emit useful deltas.',
280
+ '- If nothing material changed, return {}.',
281
+ '',
282
+ 'Output shape:',
283
+ JSON.stringify({
284
+ objective: 'optional',
285
+ summary: 'optional',
286
+ constraints: ['optional'],
287
+ successCriteria: ['optional'],
288
+ status: 'idle|progress|blocked|waiting|completed',
289
+ nextAction: 'optional',
290
+ planSteps: [{ id: 'optional', text: 'step', status: 'active|resolved|superseded' }],
291
+ factsUpsert: [{ id: 'optional', statement: 'confirmed fact', source: 'user|tool|assistant|mission|system', status: 'active|resolved|superseded', evidenceIds: ['optional'] }],
292
+ artifactsUpsert: [{ id: 'optional', label: 'artifact', kind: 'file|url|approval|message|other', path: 'optional', url: 'optional', sourceTool: 'optional', status: 'active|resolved|superseded', evidenceIds: ['optional'] }],
293
+ decisionsAppend: [{ summary: 'decision', rationale: 'optional', status: 'active|resolved|superseded', evidenceIds: ['optional'] }],
294
+ blockersUpsert: [{ summary: 'blocker', kind: 'approval|credential|human_input|external_dependency|error|other', nextAction: 'optional', status: 'active|resolved|superseded', evidenceIds: ['optional'] }],
295
+ questionsUpsert: [{ question: 'open question', status: 'active|resolved|superseded', evidenceIds: ['optional'] }],
296
+ hypothesesUpsert: [{ statement: 'possible lead', confidence: 'low|medium|high', status: 'active|resolved|superseded', evidenceIds: ['optional'] }],
297
+ supersedeIds: ['optional item id'],
298
+ }),
299
+ '',
300
+ `source: ${JSON.stringify(cleanText(input.source, 80) || 'chat')}`,
301
+ `mission: ${JSON.stringify(input.mission ? {
302
+ id: input.mission.id,
303
+ objective: cleanMultiline(input.mission.objective, 600),
304
+ status: input.mission.status,
305
+ phase: input.mission.phase,
306
+ currentStep: cleanText(input.mission.currentStep, 240) || null,
307
+ plannerSummary: cleanText(input.mission.plannerSummary, 280) || null,
308
+ verifierSummary: cleanText(input.mission.verifierSummary, 280) || null,
309
+ blockerSummary: cleanText(input.mission.blockerSummary, 240) || null,
310
+ waitState: input.mission.waitState ? {
311
+ kind: input.mission.waitState.kind,
312
+ reason: cleanText(input.mission.waitState.reason, 240),
313
+ approvalId: cleanText(input.mission.waitState.approvalId, 120) || null,
314
+ } : null,
315
+ } : null)}`,
316
+ `current_state:\n${renderStateForExtraction(input.currentState)}`,
317
+ `user_message: ${JSON.stringify(cleanMultiline(input.message, 1200) || null)}`,
318
+ `assistant_text: ${JSON.stringify(cleanMultiline(input.assistantText, 1200) || null)}`,
319
+ `assistant_error: ${JSON.stringify(cleanText(input.error, 320) || null)}`,
320
+ `tool_evidence:\n${summarizeToolEvents(input.toolEvents)}`,
321
+ ].join('\n')
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Plain-text artifact extraction helpers
326
+ // ---------------------------------------------------------------------------
327
+
328
+ export function collectJsonCandidates(value: unknown, pathLabel = '', out?: Array<{ key: string; value: string }>): Array<{ key: string; value: string }> {
329
+ const results = out || []
330
+ if (!value) return results
331
+ if (typeof value === 'string') {
332
+ const cleaned = cleanText(value, 400)
333
+ if (cleaned) results.push({ key: pathLabel, value: cleaned })
334
+ return results
335
+ }
336
+ if (Array.isArray(value)) {
337
+ for (const entry of value) collectJsonCandidates(entry, pathLabel, results)
338
+ return results
339
+ }
340
+ if (typeof value === 'object') {
341
+ for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
342
+ collectJsonCandidates(nested, key, results)
343
+ }
344
+ }
345
+ return results
346
+ }
347
+
348
+ export function uniqueByKey<T>(items: T[], keyFn: (item: T) => string): T[] {
349
+ const seen = new Set<string>()
350
+ const out: T[] = []
351
+ for (const item of items) {
352
+ const key = keyFn(item)
353
+ if (!key || seen.has(key)) continue
354
+ seen.add(key)
355
+ out.push(item)
356
+ }
357
+ return out
358
+ }
359
+
360
+ export function looksLikeUrl(value: string): boolean {
361
+ return /^https?:\/\//i.test(value) || /^\/api\/uploads\//.test(value) || /^sandbox:\/\//i.test(value)
362
+ }
363
+
364
+ export function looksLikeFilePath(value: string): boolean {
365
+ return /^(?:\.{1,2}\/|\/|[A-Za-z0-9_-]+\/)/.test(value)
366
+ || /^sandbox:\//.test(value)
367
+ }
368
+
369
+ export function extractPlainTextArtifacts(text: string): Array<{ kind: WorkingArtifact['kind']; value: string }> {
370
+ const out: Array<{ kind: WorkingArtifact['kind']; value: string }> = []
371
+ if (!text) return out
372
+ const urlMatches = text.match(/(?:https?:\/\/|\/api\/uploads\/|sandbox:\/\/)[^\s)\]}>,"]+/g) || []
373
+ for (const match of urlMatches) out.push({ kind: 'url', value: match })
374
+ const pathMatches = text.match(/(?:^|[\s("'`])((?:\.{1,2}\/|\/|sandbox:\/)[A-Za-z0-9._\-\/]+(?:\.[A-Za-z0-9]{1,8})?)/g) || []
375
+ for (const raw of pathMatches) {
376
+ const match = raw.trim().replace(/^[(\s"'`]+/, '')
377
+ if (match) out.push({ kind: 'file', value: match })
378
+ }
379
+ return uniqueByKey(out, (item) => `${item.kind}:${item.value}`)
380
+ }
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // deterministicEvidencePatch
384
+ // ---------------------------------------------------------------------------
385
+
386
+ export function deterministicEvidencePatch(input: WorkingStateDeterministicUpdateInput): WorkingStatePatch {
387
+ const nowTs = now()
388
+ const evidenceAppend: EvidenceRef[] = []
389
+ const artifactsUpsert: WorkingArtifactPatch[] = []
390
+ const blockersUpsert: WorkingBlockerPatch[] = []
391
+ const factsUpsert: WorkingFactPatch[] = []
392
+
393
+ if (input.runId) {
394
+ evidenceAppend.push({
395
+ id: genId(12),
396
+ type: 'message',
397
+ summary: `Run ${input.runId} completed on ${cleanText(input.source, 80) || 'chat'}.`,
398
+ value: input.runId,
399
+ runId: input.runId,
400
+ sessionId: input.sessionId,
401
+ missionId: input.mission?.id || null,
402
+ createdAt: nowTs,
403
+ })
404
+ }
405
+
406
+ if (Array.isArray(input.toolEvents)) {
407
+ input.toolEvents.forEach((event, index) => {
408
+ const toolName = cleanText(event.name, 80) || 'unknown'
409
+ const output = cleanText(event.output, 240)
410
+ const summary = event.error === true
411
+ ? `Tool ${toolName} returned an explicit error.`
412
+ : `Tool ${toolName} produced new execution evidence.`
413
+ const evidenceId = `${event.toolCallId || `${toolName}-${index}`}-${genId(6)}`
414
+ evidenceAppend.push({
415
+ id: evidenceId,
416
+ type: event.error === true ? 'error' : 'tool',
417
+ summary,
418
+ value: output || cleanText(event.input, 240) || null,
419
+ toolName,
420
+ toolCallId: cleanText(event.toolCallId, 120) || null,
421
+ runId: input.runId || null,
422
+ sessionId: input.sessionId,
423
+ missionId: input.mission?.id || null,
424
+ createdAt: nowTs + index,
425
+ })
426
+
427
+ if (event.error === true) {
428
+ blockersUpsert.push({
429
+ summary: output || `Tool ${toolName} failed.`,
430
+ kind: 'error',
431
+ nextAction: cleanText(input.mission?.currentStep, 240) || null,
432
+ status: 'active',
433
+ evidenceIds: [evidenceId],
434
+ })
435
+ }
436
+
437
+ const structuredInput = parseStructuredObject(event.input)
438
+ const structuredOutput = parseStructuredObject(event.output || '')
439
+ const candidates = [
440
+ ...collectJsonCandidates(structuredInput),
441
+ ...collectJsonCandidates(structuredOutput),
442
+ ...extractPlainTextArtifacts(event.output || ''),
443
+ ].map((entry) => {
444
+ if ('kind' in entry) return entry
445
+ const value = entry.value
446
+ if (looksLikeUrl(value)) return { kind: 'url' as const, value }
447
+ if (looksLikeFilePath(value)) return { kind: 'file' as const, value }
448
+ return null
449
+ }).filter(Boolean) as Array<{ kind: WorkingArtifact['kind']; value: string }>
450
+
451
+ for (const candidate of uniqueByKey(candidates, (item) => `${item.kind}:${item.value}`)) {
452
+ artifactsUpsert.push({
453
+ label: candidate.value,
454
+ kind: candidate.kind,
455
+ path: candidate.kind === 'file' ? candidate.value : null,
456
+ url: candidate.kind === 'url' ? candidate.value : null,
457
+ sourceTool: toolName,
458
+ status: 'active',
459
+ evidenceIds: [evidenceId],
460
+ })
461
+ }
462
+
463
+ const approvalRecord = structuredOutput || structuredInput
464
+ const approvalId = cleanText(
465
+ approvalRecord?.approvalId
466
+ || (approvalRecord?.approval && typeof approvalRecord.approval === 'object'
467
+ ? (approvalRecord.approval as Record<string, unknown>).id
468
+ : null),
469
+ 120,
470
+ )
471
+ const requiresApproval = approvalRecord?.requiresApproval === true || Boolean(approvalId)
472
+ if (requiresApproval) {
473
+ const approvalLabel = approvalId ? `Approval ${approvalId}` : `Approval required for ${toolName}`
474
+ artifactsUpsert.push({
475
+ label: approvalLabel,
476
+ kind: 'approval',
477
+ sourceTool: toolName,
478
+ status: 'active',
479
+ evidenceIds: [evidenceId],
480
+ })
481
+ blockersUpsert.push({
482
+ summary: approvalId
483
+ ? `Approval ${approvalId} is required before continuing.`
484
+ : `Approval is required before continuing ${toolName}.`,
485
+ kind: 'approval',
486
+ status: 'active',
487
+ evidenceIds: [evidenceId],
488
+ })
489
+ if (approvalId) {
490
+ factsUpsert.push({
491
+ statement: `Pending approval id: ${approvalId}`,
492
+ source: 'tool',
493
+ status: 'active',
494
+ evidenceIds: [evidenceId],
495
+ })
496
+ }
497
+ }
498
+
499
+ const taskId = cleanText(
500
+ structuredOutput?.taskId
501
+ || structuredInput?.taskId
502
+ || (Array.isArray(structuredOutput?.taskIds) ? structuredOutput?.taskIds[0] : null),
503
+ 120,
504
+ )
505
+ if (taskId) {
506
+ factsUpsert.push({
507
+ statement: `Task id in play: ${taskId}`,
508
+ source: 'tool',
509
+ status: 'active',
510
+ evidenceIds: [evidenceId],
511
+ })
512
+ }
513
+ })
514
+ }
515
+
516
+ if (input.error) {
517
+ evidenceAppend.push({
518
+ id: genId(12),
519
+ type: 'error',
520
+ summary: `Assistant run ended with an explicit error.`,
521
+ value: cleanText(input.error, 240) || null,
522
+ runId: input.runId || null,
523
+ sessionId: input.sessionId,
524
+ missionId: input.mission?.id || null,
525
+ createdAt: nowTs + 100,
526
+ })
527
+ blockersUpsert.push({
528
+ summary: cleanText(input.error, 280),
529
+ kind: 'error',
530
+ status: 'active',
531
+ })
532
+ }
533
+
534
+ if (input.mission) {
535
+ evidenceAppend.push({
536
+ id: genId(12),
537
+ type: 'mission',
538
+ summary: `Mission state updated: ${input.mission.status}/${input.mission.phase}.`,
539
+ value: cleanText(input.mission.currentStep || input.mission.objective, 240) || null,
540
+ runId: input.runId || null,
541
+ sessionId: input.sessionId,
542
+ missionId: input.mission.id,
543
+ createdAt: nowTs + 200,
544
+ })
545
+ }
546
+
547
+ return {
548
+ status: input.error
549
+ ? 'blocked'
550
+ : input.mission
551
+ ? missionStatusToWorkingStateStatus(input.mission)
552
+ : undefined,
553
+ nextAction: cleanText(input.mission?.currentStep, 240) || undefined,
554
+ factsUpsert: factsUpsert.length > 0 ? factsUpsert : undefined,
555
+ artifactsUpsert: artifactsUpsert.length > 0 ? artifactsUpsert : undefined,
556
+ blockersUpsert: blockersUpsert.length > 0 ? blockersUpsert : undefined,
557
+ evidenceAppend: evidenceAppend.length > 0 ? evidenceAppend : undefined,
558
+ }
559
+ }
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // extractWorkingStatePatch — async LLM call
563
+ // ---------------------------------------------------------------------------
564
+
565
+ export async function extractWorkingStatePatch(
566
+ input: WorkingStateExtractionInput,
567
+ options?: {
568
+ generateText?: (prompt: string) => Promise<string>
569
+ },
570
+ ): Promise<WorkingStatePatch | null> {
571
+ const prompt = buildWorkingStatePatchPrompt(input)
572
+ try {
573
+ const responseText = options?.generateText
574
+ ? await options.generateText(prompt)
575
+ : await (async () => {
576
+ const { llm } = await buildLLM({
577
+ sessionId: input.sessionId,
578
+ agentId: input.agentId || null,
579
+ })
580
+ const response = await Promise.race([
581
+ llm.invoke([new HumanMessage(prompt)]),
582
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error('working-state-extraction-timeout')), EXTRACTION_TIMEOUT_MS)),
583
+ ])
584
+ const content = response.content
585
+ if (typeof content === 'string') return content
586
+ if (!Array.isArray(content)) return ''
587
+ return content
588
+ .map((part) => (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') ? part.text : '')
589
+ .join('')
590
+ })()
591
+ return parseWorkingStatePatchResponse(responseText)
592
+ } catch (error: unknown) {
593
+ log.warn(TAG, 'Working-state extraction failed', {
594
+ sessionId: input.sessionId,
595
+ error: error instanceof Error ? error.message : String(error),
596
+ })
597
+ return null
598
+ }
599
+ }
600
+
601
+ // ---------------------------------------------------------------------------
602
+ // shouldExtractStructuredPatch
603
+ // ---------------------------------------------------------------------------
604
+
605
+ export function shouldExtractStructuredPatch(input: SynchronizeWorkingStateForTurnInput): boolean {
606
+ const hasToolEvents = Array.isArray(input.toolEvents) && input.toolEvents.length > 0
607
+ const hasMessage = cleanMultiline(input.message, 400).length > 0
608
+ const hasAssistantText = cleanMultiline(input.assistantText, 400).length > 0
609
+ const hasError = cleanText(input.error, 120).length > 0
610
+ if (cleanText(input.source, 80) === 'heartbeat') {
611
+ return hasToolEvents || hasError || Boolean(input.mission)
612
+ }
613
+ return hasToolEvents || hasAssistantText || hasMessage || hasError || Boolean(input.mission)
614
+ }