@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,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import { createReactAgent } from '@langchain/langgraph/prebuilt'
3
3
  import { HumanMessage, AIMessage } from '@langchain/core/messages'
4
+ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
4
5
  import { buildSessionTools } from './session-tools'
5
6
  import { buildChatModel } from './build-llm'
6
7
  import { loadSettings, loadAgents, loadSkills, appendUsage } from './storage'
@@ -13,6 +14,8 @@ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
13
14
  import { expandPluginIds } from './tool-aliases'
14
15
  import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
15
16
  import { extractSuggestions } from './suggestions'
17
+ import { buildIdentityContinuityContext } from './identity-continuity'
18
+ import { enqueueSystemEvent } from './system-events'
16
19
 
17
20
  /** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
18
21
  interface StreamAgentChatOpts {
@@ -42,6 +45,85 @@ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformA
42
45
  return lines
43
46
  }
44
47
 
48
+ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
49
+ const uniqueTools = Array.from(new Set(enabledPlugins.filter(Boolean))).sort()
50
+ if (uniqueTools.length === 0) return []
51
+
52
+ const lines = [
53
+ `Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
54
+ 'Only call tools from this enabled list or tools explicitly returned by the runtime.',
55
+ ]
56
+
57
+ const directPlatformTools = uniqueTools.filter((toolId) => toolId.startsWith('manage_') && toolId !== 'manage_platform')
58
+ if (directPlatformTools.length > 0 && !uniqueTools.includes('manage_platform')) {
59
+ lines.push(`Use direct platform tools exactly as named (${directPlatformTools.map((toolId) => `\`${toolId}\``).join(', ')}). Do not substitute \`manage_platform\` unless it is explicitly enabled.`)
60
+ }
61
+
62
+ if (uniqueTools.includes('files')) {
63
+ lines.push('For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.')
64
+ }
65
+
66
+ if (uniqueTools.includes('shell')) {
67
+ lines.push('For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.')
68
+ }
69
+
70
+ if (uniqueTools.includes('web')) {
71
+ lines.push('For `web`, use `{"action":"search","query":"..."}` to research and `{"action":"fetch","url":"https://..."}` to read a specific page.')
72
+ }
73
+
74
+ if (uniqueTools.includes('browser')) {
75
+ lines.push('For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.')
76
+ lines.push('For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.')
77
+ }
78
+
79
+ if (uniqueTools.includes('http_request')) {
80
+ lines.push('For `http_request`, send exact literal URLs from the task or from prior tool results. Keep JSON request bodies as raw JSON strings.')
81
+ }
82
+
83
+ if (uniqueTools.includes('email')) {
84
+ lines.push('For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`. If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.')
85
+ }
86
+
87
+ if (uniqueTools.includes('ask_human')) {
88
+ lines.push('For `ask_human`, when a workflow needs a code, approval, or out-of-band value from a person, do not guess or keep re-submitting blank forms. Use `{"action":"request_input","question":"..."}` and, for durable pauses, `{"action":"wait_for_reply","correlationId":"..."}`.')
89
+ }
90
+
91
+ return lines
92
+ }
93
+
94
+ export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
95
+ const normalized = text.toLowerCase()
96
+ if (!normalized.trim()) return false
97
+ if (/```|package\.json|tsconfig|tsx?\b|jsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
98
+ if (/\b(revise|revision|iterate|iteration|draft|deliverable|deliverables|offer|brief|copy|proposal|landing|outreach|plan|strategy|report|memo|document|docs?)\b/.test(normalized)) return true
99
+ return isBroadGoal(text) && /(\.md\b|\.txt\b|copy|brief|proposal|plan|report|draft|document)/.test(normalized)
100
+ }
101
+
102
+ function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
103
+ const normalized = userMessage.toLowerCase()
104
+ const required: string[] = []
105
+
106
+ if (enabledPlugins.includes('ask_human')
107
+ && (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
108
+ required.push('ask_human')
109
+ }
110
+
111
+ if (enabledPlugins.includes('email')
112
+ && (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
113
+ required.push('email')
114
+ }
115
+
116
+ return required
117
+ }
118
+
119
+ const OPEN_ENDED_REVISION_BLOCK = [
120
+ '## Revision Loop',
121
+ 'For open-ended deliverable work, do a real two-pass loop before declaring success: create the draft artifacts, critique them against the objective, then modify at least one artifact based on that critique.',
122
+ 'A critique by itself does not count as iteration. Iteration requires an actual changed artifact.',
123
+ 'When resuming in an existing workspace, inspect the current files first, then update them. Do not assume you lost access to the workspace without an explicit tool attempt.',
124
+ 'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
125
+ ].join('\n')
126
+
45
127
  /** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
46
128
  function isBroadGoal(text: string): boolean {
47
129
  if (text.length < 50) return false
@@ -59,7 +141,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
59
141
  'When you receive a broad, open-ended goal:',
60
142
  '1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
61
143
  '2. If manage_tasks is available, create a task for each subtask to track progress.',
62
- '3. Output your plan in a [MAIN_LOOP_PLAN] JSON line: {"steps":["step1","step2",...],"current_step":"step1"}',
144
+ '3. Present the plan as a short checklist or numbered list in plain language.',
63
145
  '4. Execute the first subtask immediately — do not stop after planning.',
64
146
  '5. After each subtask, update progress and move to the next.',
65
147
  ].join('\n')
@@ -71,10 +153,10 @@ function buildAgenticExecutionPolicy(opts: {
71
153
  heartbeatIntervalSec: number
72
154
  platformAssignScope?: 'self' | 'all'
73
155
  userMessage?: string
74
- hasExistingPlan?: boolean
75
156
  }) {
76
157
  const hasTooling = opts.enabledPlugins.length > 0
77
158
  const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
159
+ const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
78
160
 
79
161
  const parts: string[] = []
80
162
 
@@ -85,7 +167,8 @@ function buildAgenticExecutionPolicy(opts: {
85
167
  ? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
86
168
  : 'No tools enabled. Be explicit about what tool access is needed.',
87
169
  'Follow through on stated intentions with tool calls. Never claim results without tool evidence.',
88
- 'If a tool is named explicitly, invoke it. Short progress updates between steps.',
170
+ 'If a task explicitly names an enabled tool, use that tool before declaring success. A prose request is not a substitute for `ask_human`, and browser work is not a substitute for `email` delivery.',
171
+ 'When `ask_human` is enabled, collect required human input through the tool instead of asking for it only in plain assistant text.',
89
172
  opts.loopMode === 'ongoing'
90
173
  ? 'Loop: ONGOING — keep iterating until done, blocked, or limits reached.'
91
174
  : 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
@@ -102,13 +185,22 @@ function buildAgenticExecutionPolicy(opts: {
102
185
  'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
103
186
  'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
104
187
  'Never repeat completed side effects. Verify state first.',
188
+ 'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
189
+ 'Prefer the most specific tool you already have. Example: use `manage_schedules` for schedules and `manage_tasks` for tasks; treat `manage_platform` as a fallback umbrella only when a specific `manage_*` tool is unavailable.',
190
+ 'For recurring, cron, interval, or follow-up automation requests, use `manage_schedules` directly when it is available.',
191
+ 'Delegation is optional, not a stopping condition. If one delegate backend is unavailable or unauthenticated, try another delegate backend or continue with your other tools.',
192
+ 'If a required tool is missing, request access by name with `manage_capabilities` action `request_access` (for example `shell` or `manage_schedules`).',
193
+ 'Only mention files, screenshots, URLs, or download links that were actually returned by tools. Copy returned links exactly; do not rewrite them or prepend extra prefixes like "sandbox:".',
105
194
  `Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
106
195
  opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
107
- 'For SWARM_MAIN_MISSION_TICK / SWARM_MAIN_AUTO_FOLLOWUP messages, follow the response contract and include [MAIN_LOOP_META] JSON.',
108
196
  )
109
197
 
198
+ if (toolDisciplineLines.length) parts.push('## Tool Discipline', ...toolDisciplineLines)
110
199
  if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
111
- if (opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
200
+ if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
201
+ if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
202
+ parts.push(OPEN_ENDED_REVISION_BLOCK)
203
+ }
112
204
 
113
205
  return parts.filter(Boolean).join('\n')
114
206
  }
@@ -136,7 +228,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
136
228
 
137
229
  // Resolve agent's thinking level for provider-native params
138
230
  let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
139
- if (session.agentId) {
231
+ if (session.thinkingLevel) {
232
+ agentThinkingLevel = session.thinkingLevel
233
+ } else if (session.agentId) {
140
234
  const agentsForThinking = loadAgents()
141
235
  agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
142
236
  }
@@ -162,7 +256,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
162
256
  : typeof raw === 'string'
163
257
  ? Number.parseInt(raw, 10)
164
258
  : Number.NaN
165
- if (!Number.isFinite(parsed)) return 120
259
+ if (!Number.isFinite(parsed)) return DEFAULT_HEARTBEAT_INTERVAL_SEC
166
260
  return Math.max(0, Math.min(3600, Math.trunc(parsed)))
167
261
  })()
168
262
 
@@ -180,18 +274,22 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
180
274
  let agentPlatformAssignScope: 'self' | 'all' = 'self'
181
275
  let agentMcpServerIds: string[] | undefined
182
276
  let agentMcpDisabledTools: string[] | undefined
277
+ let agentHeartbeatEnabled = false
183
278
  if (session.agentId) {
184
279
  const agents = loadAgents()
185
280
  const agent = agents[session.agentId]
186
281
  agentPlatformAssignScope = agent?.platformAssignScope || 'self'
187
282
  agentMcpServerIds = agent?.mcpServerIds
188
283
  agentMcpDisabledTools = agent?.mcpDisabledTools
284
+ agentHeartbeatEnabled = agent?.heartbeatEnabled === true
189
285
  if (!hasProvidedSystemPrompt) {
190
286
  // Identity block — make sure the agent knows who it is
191
287
  const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
192
288
  if (agent?.description) identityLines.push(agent.description)
193
289
  identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
194
290
  stateModifierParts.push(identityLines.join(' '))
291
+ const continuityBlock = buildIdentityContinuityContext(session, agent)
292
+ if (continuityBlock) stateModifierParts.push(continuityBlock)
195
293
  if (agent?.soul) stateModifierParts.push(agent.soul)
196
294
  if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
197
295
  if (agent?.skillIds?.length) {
@@ -290,9 +388,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
290
388
  )
291
389
  }
292
390
 
293
- // Check for existing plan in mainLoopState to skip decomposition injection
294
- const hasExistingPlan = Array.isArray(session.mainLoopState?.planSteps) && session.mainLoopState.planSteps.length > 0
295
-
296
391
  stateModifierParts.push(
297
392
  buildAgenticExecutionPolicy({
298
393
  enabledPlugins: sessionPlugins,
@@ -301,7 +396,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
301
396
  heartbeatIntervalSec,
302
397
  platformAssignScope: agentPlatformAssignScope,
303
398
  userMessage: message,
304
- hasExistingPlan,
305
399
  }),
306
400
  )
307
401
 
@@ -480,14 +574,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
480
574
  let needsTextSeparator = false
481
575
  let totalInputTokens = 0
482
576
  let totalOutputTokens = 0
483
- let lastToolInput: unknown = null
484
577
  let accumulatedThinking = ''
485
578
  const pluginInvocations: PluginInvocationRecord[] = []
486
579
  let currentToolInputTokens = 0
487
580
 
488
581
  // Plugin hooks: beforeAgentStart
489
582
  const pluginMgr = getPluginManager()
490
- await pluginMgr.runHook('beforeAgentStart', { session, message })
583
+ await pluginMgr.runHook('beforeAgentStart', { session, message }, { enabledIds: sessionPlugins })
491
584
 
492
585
  const abortController = new AbortController()
493
586
  const abortFromSignal = () => abortController.abort()
@@ -505,13 +598,21 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
505
598
 
506
599
  const MAX_AUTO_CONTINUES = 3
507
600
  const MAX_TRANSIENT_RETRIES = 2
601
+ const MAX_REQUIRED_TOOL_CONTINUES = 2
508
602
  let autoContinueCount = 0
509
603
  let transientRetryCount = 0
604
+ let requiredToolContinueCount = 0
605
+ const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
606
+ const usedToolNames = new Set<string>()
510
607
 
511
608
  try {
512
- const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
609
+ const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES
513
610
  for (let iteration = 0; iteration <= maxIterations; iteration++) {
514
- let shouldContinue: 'recursion' | 'transient' | false = false
611
+ let shouldContinue: 'recursion' | 'transient' | 'required_tool' | false = false
612
+ let requiredToolReminderNames: string[] = []
613
+ let waitingForToolResult = false
614
+ let idleTimedOut = false
615
+ let idleTimer: ReturnType<typeof setTimeout> | null = null
515
616
 
516
617
  // Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
517
618
  // Linked to the parent so client disconnect / timeout still propagates.
@@ -520,7 +621,24 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
520
621
  if (abortController.signal.aborted) iterationController.abort()
521
622
  else abortController.signal.addEventListener('abort', onParentAbort)
522
623
 
624
+ const clearIdleWatchdog = () => {
625
+ if (idleTimer) {
626
+ clearTimeout(idleTimer)
627
+ idleTimer = null
628
+ }
629
+ }
630
+
631
+ const armIdleWatchdog = () => {
632
+ clearIdleWatchdog()
633
+ if (waitingForToolResult || iterationController.signal.aborted) return
634
+ idleTimer = setTimeout(() => {
635
+ idleTimedOut = true
636
+ iterationController.abort()
637
+ }, 90_000)
638
+ }
639
+
523
640
  try {
641
+ armIdleWatchdog()
524
642
  const eventStream = agent.streamEvents(
525
643
  { messages: langchainMessages },
526
644
  { version: 'v2', recursionLimit, signal: iterationController.signal },
@@ -530,6 +648,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
530
648
  const kind = event.event
531
649
 
532
650
  if (kind === 'on_chat_model_stream') {
651
+ armIdleWatchdog()
533
652
  const chunk = event.data?.chunk
534
653
  if (chunk?.content) {
535
654
  // content can be string or array of content blocks
@@ -569,6 +688,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
569
688
  }
570
689
  }
571
690
  } else if (kind === 'on_llm_end') {
691
+ armIdleWatchdog()
572
692
  // Track token usage from LLM responses — check all known LangChain event shapes
573
693
  const output = event.data?.output
574
694
  const usage = output?.llmOutput?.tokenUsage
@@ -581,17 +701,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
581
701
  totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
582
702
  }
583
703
  } else if (kind === 'on_tool_start') {
704
+ clearIdleWatchdog()
705
+ waitingForToolResult = true
584
706
  hasToolCalls = true
585
707
  needsTextSeparator = true
586
708
  lastSegment = ''
587
709
  const toolName = event.name || 'unknown'
710
+ usedToolNames.add(toolName)
588
711
  const input = event.data?.input
589
- lastToolInput = input
590
712
  // Estimate input tokens for plugin invocation tracking
591
713
  const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
592
714
  currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
593
- // Plugin hooks: beforeToolExec
594
- await pluginMgr.runHook('beforeToolExec', { toolName, input })
595
715
  logExecution(session.id, 'tool_call', `${toolName} invoked`, {
596
716
  agentId: session.agentId,
597
717
  detail: { toolName, input: inputStr?.slice(0, 4000) },
@@ -602,6 +722,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
602
722
  toolInput: inputStr,
603
723
  })}\n\n`)
604
724
  } else if (kind === 'on_tool_end') {
725
+ waitingForToolResult = false
726
+ armIdleWatchdog()
605
727
  const toolName = event.name || 'unknown'
606
728
  const output = event.data?.output
607
729
  const outputStr = typeof output === 'string'
@@ -609,9 +731,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
609
731
  : output?.content
610
732
  ? String(output.content)
611
733
  : JSON.stringify(output)
612
- // Plugin hooks: afterToolExec
613
- await pluginMgr.runHook('afterToolExec', { session, toolName, input: lastToolInput as Record<string, unknown> | null, output: outputStr })
614
- lastToolInput = null
615
734
  logExecution(session.id, 'tool_result', `${toolName} returned`, {
616
735
  agentId: session.agentId,
617
736
  detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
@@ -654,7 +773,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
654
773
  }
655
774
  } catch (innerErr: unknown) {
656
775
  const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
657
- const errMsg = innerErr instanceof Error ? innerErr.message : String(innerErr)
776
+ const errMsg = idleTimedOut
777
+ ? 'Model stream stalled without emitting text or tool results for 90 seconds.'
778
+ : innerErr instanceof Error ? innerErr.message : String(innerErr)
658
779
  const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
659
780
 
660
781
  // Classify the error:
@@ -662,9 +783,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
662
783
  // 2. Transient abort/timeout — LLM API failure, not from client disconnect
663
784
  const isRecursionError = errName === 'GraphRecursionError'
664
785
  || /recursion limit|maximum recursion/i.test(errMsg)
665
- const isTransientAbort = !isRecursionError
786
+ const isTransientAbort = (!isRecursionError && idleTimedOut)
787
+ || (!isRecursionError
666
788
  && /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
667
- && !abortController.signal.aborted
789
+ && !abortController.signal.aborted)
668
790
 
669
791
  // Log diagnostic details for every error so we can trace root causes
670
792
  console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
@@ -695,9 +817,26 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
695
817
  throw innerErr
696
818
  }
697
819
  } finally {
820
+ clearIdleWatchdog()
698
821
  abortController.signal.removeEventListener('abort', onParentAbort)
699
822
  }
700
823
 
824
+ if (!shouldContinue && explicitRequiredToolNames.length > 0 && requiredToolContinueCount < MAX_REQUIRED_TOOL_CONTINUES) {
825
+ requiredToolReminderNames = explicitRequiredToolNames.filter((toolName) => !usedToolNames.has(toolName))
826
+ if (requiredToolReminderNames.length > 0) {
827
+ shouldContinue = 'required_tool'
828
+ requiredToolContinueCount++
829
+ write(`data: ${JSON.stringify({
830
+ t: 'status',
831
+ text: JSON.stringify({
832
+ requiredToolsPending: requiredToolReminderNames,
833
+ reminderCount: requiredToolContinueCount,
834
+ maxReminders: MAX_REQUIRED_TOOL_CONTINUES,
835
+ }),
836
+ })}\n\n`)
837
+ }
838
+ }
839
+
701
840
  if (!shouldContinue) break
702
841
 
703
842
  if (shouldContinue === 'recursion') {
@@ -707,6 +846,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
707
846
  }
708
847
  langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
709
848
  lastSegment = ''
849
+ } else if (shouldContinue === 'required_tool') {
850
+ if (fullText.trim()) {
851
+ langchainMessages.push(new AIMessage({ content: fullText }))
852
+ }
853
+ langchainMessages.push(new HumanMessage({
854
+ content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request, and do not replace email delivery with browser work or prose.`,
855
+ }))
856
+ lastSegment = ''
710
857
  } else if (shouldContinue === 'transient') {
711
858
  // Short delay before retrying transient errors (API timeout, rate limit, etc.)
712
859
  await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
@@ -716,6 +863,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
716
863
  const errMsg = timedOut
717
864
  ? 'Ongoing loop stopped after reaching the configured runtime limit.'
718
865
  : err instanceof Error ? err.message : String(err)
866
+ const heartbeatEligible = runtime.loopMode === 'ongoing' || session.heartbeatEnabled === true || agentHeartbeatEnabled
867
+ const budgetLimited = timedOut || /recursion limit|maximum recursion/i.test(errMsg)
868
+ if (heartbeatEligible && budgetLimited) {
869
+ enqueueSystemEvent(
870
+ session.id,
871
+ '[Loop Budget Reached] The previous autonomous run stopped after hitting its loop budget. On the next heartbeat, resume carefully from the current state, verify completed work before repeating it, and focus only on the remaining objective.',
872
+ 'loop_budget_reached',
873
+ )
874
+ logExecution(session.id, 'decision', 'Queued a deferred resume cue for the next heartbeat after loop budget exhaustion.', {
875
+ agentId: session.agentId,
876
+ detail: { timedOut, heartbeatEligible },
877
+ })
878
+ }
719
879
  logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
720
880
  write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
721
881
  } finally {
@@ -775,7 +935,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
775
935
  }
776
936
 
777
937
  // Plugin hooks: afterAgentComplete
778
- await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
938
+ await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
779
939
 
780
940
  // OpenClaw auto-sync: push memory if enabled
781
941
  try {
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict'
2
+ import { afterEach, describe, it } from 'node:test'
3
+ import { PROVIDERS } from '../providers'
4
+ import { runStructuredExtraction } from './structured-extract'
5
+
6
+ const originalOllamaHandler = PROVIDERS.ollama.handler.streamChat
7
+
8
+ afterEach(() => {
9
+ PROVIDERS.ollama.handler.streamChat = originalOllamaHandler
10
+ })
11
+
12
+ describe('runStructuredExtraction', () => {
13
+ it('parses fenced JSON output from the current provider', async () => {
14
+ PROVIDERS.ollama.handler.streamChat = async () => '```json\n{"name":"Ada","score":10}\n```'
15
+
16
+ const result = await runStructuredExtraction({
17
+ session: {
18
+ id: 'session-1',
19
+ provider: 'ollama',
20
+ model: 'qwen3.5',
21
+ credentialId: null,
22
+ fallbackCredentialIds: [],
23
+ apiEndpoint: 'http://localhost:11434',
24
+ },
25
+ text: 'Ada scored 10.',
26
+ schema: {
27
+ type: 'object',
28
+ properties: {
29
+ name: { type: 'string' },
30
+ score: { type: 'number' },
31
+ },
32
+ required: ['name', 'score'],
33
+ },
34
+ instruction: 'Extract the person and score.',
35
+ })
36
+
37
+ assert.deepEqual(result.object, { name: 'Ada', score: 10 })
38
+ assert.deepEqual(result.validationErrors, [])
39
+ })
40
+
41
+ it('repairs invalid JSON with a second pass', async () => {
42
+ let callCount = 0
43
+ PROVIDERS.ollama.handler.streamChat = async () => {
44
+ callCount += 1
45
+ return callCount === 1 ? 'name: Ada' : '{"name":"Ada"}'
46
+ }
47
+
48
+ const result = await runStructuredExtraction({
49
+ session: {
50
+ id: 'session-2',
51
+ provider: 'ollama',
52
+ model: 'qwen3.5',
53
+ credentialId: null,
54
+ fallbackCredentialIds: [],
55
+ apiEndpoint: 'http://localhost:11434',
56
+ },
57
+ text: 'Ada',
58
+ schema: {
59
+ type: 'object',
60
+ properties: {
61
+ name: { type: 'string' },
62
+ },
63
+ required: ['name'],
64
+ },
65
+ instruction: 'Extract the name.',
66
+ })
67
+
68
+ assert.equal(callCount, 2)
69
+ assert.deepEqual(result.object, { name: 'Ada' })
70
+ assert.deepEqual(result.validationErrors, [])
71
+ })
72
+ })