@swarmclawai/swarmclaw 0.7.3 → 0.7.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 (152) hide show
  1. package/README.md +47 -40
  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 +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +4 -87
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  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]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/agent-thread-session.test.ts +85 -0
  88. package/src/lib/server/agent-thread-session.ts +123 -0
  89. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  90. package/src/lib/server/build-llm.test.ts +13 -5
  91. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  92. package/src/lib/server/chat-execution.ts +159 -71
  93. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  94. package/src/lib/server/chatroom-helpers.ts +99 -6
  95. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  96. package/src/lib/server/connectors/manager.ts +89 -61
  97. package/src/lib/server/connectors/slack.ts +1 -1
  98. package/src/lib/server/daemon-state.ts +3 -2
  99. package/src/lib/server/data-dir.test.ts +56 -0
  100. package/src/lib/server/data-dir.ts +15 -9
  101. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  102. package/src/lib/server/eval/agent-regression.ts +1742 -0
  103. package/src/lib/server/eval/runner.ts +11 -1
  104. package/src/lib/server/eval/store.ts +2 -1
  105. package/src/lib/server/heartbeat-service.ts +23 -8
  106. package/src/lib/server/heartbeat-wake.ts +6 -2
  107. package/src/lib/server/main-agent-loop.ts +13 -6
  108. package/src/lib/server/openclaw-exec-config.ts +4 -2
  109. package/src/lib/server/openclaw-gateway.ts +123 -36
  110. package/src/lib/server/orchestrator-lg.ts +1 -2
  111. package/src/lib/server/orchestrator.ts +3 -2
  112. package/src/lib/server/plugins.test.ts +9 -1
  113. package/src/lib/server/plugins.ts +12 -2
  114. package/src/lib/server/provider-model-discovery.ts +481 -0
  115. package/src/lib/server/queue.ts +1 -1
  116. package/src/lib/server/runtime-settings.test.ts +119 -0
  117. package/src/lib/server/runtime-settings.ts +12 -92
  118. package/src/lib/server/schedule-normalization.ts +187 -0
  119. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  120. package/src/lib/server/session-tools/crud.ts +27 -3
  121. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  122. package/src/lib/server/session-tools/discovery.ts +18 -8
  123. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  124. package/src/lib/server/session-tools/file.ts +8 -2
  125. package/src/lib/server/session-tools/http.ts +9 -3
  126. package/src/lib/server/session-tools/index.ts +31 -1
  127. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  128. package/src/lib/server/session-tools/monitor.ts +14 -7
  129. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  130. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  131. package/src/lib/server/session-tools/platform.ts +1 -1
  132. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  133. package/src/lib/server/session-tools/sandbox.ts +51 -92
  134. package/src/lib/server/session-tools/session-info.ts +22 -1
  135. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  136. package/src/lib/server/session-tools/shell.ts +2 -2
  137. package/src/lib/server/session-tools/subagent.ts +3 -1
  138. package/src/lib/server/session-tools/web.ts +73 -30
  139. package/src/lib/server/storage.ts +29 -3
  140. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  141. package/src/lib/server/stream-agent-chat.ts +139 -4
  142. package/src/lib/server/structured-extract.ts +1 -1
  143. package/src/lib/server/task-mention.ts +0 -1
  144. package/src/lib/server/tool-aliases.ts +37 -6
  145. package/src/lib/server/tool-capability-policy.ts +1 -1
  146. package/src/lib/setup-defaults.ts +352 -11
  147. package/src/lib/tool-definitions.ts +3 -4
  148. package/src/lib/validation/schemas.ts +55 -1
  149. package/src/stores/use-app-store.ts +43 -1
  150. package/src/stores/use-chatroom-store.ts +153 -26
  151. package/src/types/index.ts +189 -6
  152. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { describe, it } from 'node:test'
5
+ import { buildToolDisciplineLines, looksLikeOpenEndedDeliverableTask } from './stream-agent-chat'
6
+
7
+ const streamAgentChatSource = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'stream-agent-chat.ts'), 'utf-8')
8
+
9
+ describe('buildToolDisciplineLines', () => {
10
+ it('tells the agent to use direct platform tools when manage_platform is absent', () => {
11
+ const lines = buildToolDisciplineLines(['files', 'manage_schedules'])
12
+
13
+ assert.equal(lines[0], 'Enabled tools in this session: `files`, `manage_schedules`.')
14
+ assert.ok(lines.some((line) => line.includes('Do not substitute `manage_platform`')))
15
+ })
16
+
17
+ it('omits the manage_platform warning when the umbrella tool is enabled', () => {
18
+ const lines = buildToolDisciplineLines(['manage_platform', 'manage_schedules'])
19
+
20
+ assert.ok(lines.every((line) => !line.includes('Do not substitute `manage_platform`')))
21
+ })
22
+
23
+ it('includes concrete files-tool examples for revision work', () => {
24
+ const lines = buildToolDisciplineLines(['files'])
25
+
26
+ assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
27
+ })
28
+
29
+ it('warns browser tasks to use literal urls and the supported form schema', () => {
30
+ const lines = buildToolDisciplineLines(['browser', 'http_request', 'email', 'ask_human'])
31
+
32
+ assert.ok(lines.some((line) => line.includes('Do not invent placeholder URLs')))
33
+ assert.ok(lines.some((line) => line.includes('A shorthand `form` object keyed by input id/name also works')))
34
+ assert.ok(lines.some((line) => line.includes('Keep JSON request bodies as raw JSON strings')))
35
+ assert.ok(lines.some((line) => line.includes('{"action":"send","to":"user@example.com","subject":"...","body":"..."}')))
36
+ assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
37
+ })
38
+
39
+ it('tells the agent that named enabled tools are completion requirements', () => {
40
+ assert.ok(streamAgentChatSource.includes('If a task explicitly names an enabled tool, use that tool before declaring success.'))
41
+ assert.ok(streamAgentChatSource.includes('collect required human input through the tool'))
42
+ assert.ok(streamAgentChatSource.includes('You have not yet completed the required explicit tool step(s):'))
43
+ assert.ok(streamAgentChatSource.includes('[Loop Budget Reached]'))
44
+ })
45
+ })
46
+
47
+ describe('looksLikeOpenEndedDeliverableTask', () => {
48
+ it('detects open-ended deliverable prompts', () => {
49
+ assert.equal(
50
+ looksLikeOpenEndedDeliverableTask('Revise the landing copy and update the proposal draft with a stronger second pass.'),
51
+ true,
52
+ )
53
+ })
54
+
55
+ it('does not misclassify explicit coding tasks', () => {
56
+ assert.equal(
57
+ looksLikeOpenEndedDeliverableTask('Fix the React bug in src/components/chat/chat-area.tsx and run npm run build.'),
58
+ false,
59
+ )
60
+ })
61
+ })
@@ -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'
@@ -14,6 +15,7 @@ import { expandPluginIds } from './tool-aliases'
14
15
  import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
15
16
  import { extractSuggestions } from './suggestions'
16
17
  import { buildIdentityContinuityContext } from './identity-continuity'
18
+ import { enqueueSystemEvent } from './system-events'
17
19
 
18
20
  /** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
19
21
  interface StreamAgentChatOpts {
@@ -43,6 +45,85 @@ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformA
43
45
  return lines
44
46
  }
45
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
+
46
127
  /** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
47
128
  function isBroadGoal(text: string): boolean {
48
129
  if (text.length < 50) return false
@@ -75,6 +156,7 @@ function buildAgenticExecutionPolicy(opts: {
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.',
@@ -103,14 +186,21 @@ function buildAgenticExecutionPolicy(opts: {
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.',
105
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.',
106
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`).',
107
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:".',
108
194
  `Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
109
195
  opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
110
196
  )
111
197
 
198
+ if (toolDisciplineLines.length) parts.push('## Tool Discipline', ...toolDisciplineLines)
112
199
  if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
113
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
+ }
114
204
 
115
205
  return parts.filter(Boolean).join('\n')
116
206
  }
@@ -166,7 +256,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
166
256
  : typeof raw === 'string'
167
257
  ? Number.parseInt(raw, 10)
168
258
  : Number.NaN
169
- if (!Number.isFinite(parsed)) return 120
259
+ if (!Number.isFinite(parsed)) return DEFAULT_HEARTBEAT_INTERVAL_SEC
170
260
  return Math.max(0, Math.min(3600, Math.trunc(parsed)))
171
261
  })()
172
262
 
@@ -184,12 +274,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
184
274
  let agentPlatformAssignScope: 'self' | 'all' = 'self'
185
275
  let agentMcpServerIds: string[] | undefined
186
276
  let agentMcpDisabledTools: string[] | undefined
277
+ let agentHeartbeatEnabled = false
187
278
  if (session.agentId) {
188
279
  const agents = loadAgents()
189
280
  const agent = agents[session.agentId]
190
281
  agentPlatformAssignScope = agent?.platformAssignScope || 'self'
191
282
  agentMcpServerIds = agent?.mcpServerIds
192
283
  agentMcpDisabledTools = agent?.mcpDisabledTools
284
+ agentHeartbeatEnabled = agent?.heartbeatEnabled === true
193
285
  if (!hasProvidedSystemPrompt) {
194
286
  // Identity block — make sure the agent knows who it is
195
287
  const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
@@ -506,13 +598,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
506
598
 
507
599
  const MAX_AUTO_CONTINUES = 3
508
600
  const MAX_TRANSIENT_RETRIES = 2
601
+ const MAX_REQUIRED_TOOL_CONTINUES = 2
509
602
  let autoContinueCount = 0
510
603
  let transientRetryCount = 0
604
+ let requiredToolContinueCount = 0
605
+ const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
606
+ const usedToolNames = new Set<string>()
511
607
 
512
608
  try {
513
- const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
609
+ const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES
514
610
  for (let iteration = 0; iteration <= maxIterations; iteration++) {
515
- let shouldContinue: 'recursion' | 'transient' | false = false
611
+ let shouldContinue: 'recursion' | 'transient' | 'required_tool' | false = false
612
+ let requiredToolReminderNames: string[] = []
516
613
  let waitingForToolResult = false
517
614
  let idleTimedOut = false
518
615
  let idleTimer: ReturnType<typeof setTimeout> | null = null
@@ -610,6 +707,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
610
707
  needsTextSeparator = true
611
708
  lastSegment = ''
612
709
  const toolName = event.name || 'unknown'
710
+ usedToolNames.add(toolName)
613
711
  const input = event.data?.input
614
712
  // Estimate input tokens for plugin invocation tracking
615
713
  const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
@@ -723,6 +821,22 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
723
821
  abortController.signal.removeEventListener('abort', onParentAbort)
724
822
  }
725
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
+
726
840
  if (!shouldContinue) break
727
841
 
728
842
  if (shouldContinue === 'recursion') {
@@ -732,6 +846,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
732
846
  }
733
847
  langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
734
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 = ''
735
857
  } else if (shouldContinue === 'transient') {
736
858
  // Short delay before retrying transient errors (API timeout, rate limit, etc.)
737
859
  await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
@@ -741,6 +863,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
741
863
  const errMsg = timedOut
742
864
  ? 'Ongoing loop stopped after reaching the configured runtime limit.'
743
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
+ }
744
879
  logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
745
880
  write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
746
881
  } finally {
@@ -348,7 +348,7 @@ export async function runStructuredExtraction(params: {
348
348
  let parsed: unknown
349
349
  try {
350
350
  parsed = parseModelJson(raw)
351
- } catch (error) {
351
+ } catch {
352
352
  raw = await callExtractionModel({
353
353
  session: params.session,
354
354
  prompt: [
@@ -43,7 +43,6 @@ export function parseMentionedAgentId(
43
43
  agents: Record<string, Agent>,
44
44
  ): string | null {
45
45
  const mentionRegex = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
46
- const agentList = Object.values(agents)
47
46
  let match: RegExpExecArray | null
48
47
 
49
48
  while ((match = mentionRegex.exec(description)) !== null) {
@@ -1,11 +1,18 @@
1
1
  const PLUGIN_ALIAS_GROUPS: string[][] = [
2
- ['shell', 'execute_command', 'process_tool', 'process'],
2
+ ['shell', 'execute_command', 'process_tool'],
3
3
  ['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
4
4
  ['edit_file'],
5
5
  ['web', 'web_search', 'web_fetch'],
6
6
  ['browser', 'openclaw_browser'],
7
7
  ['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'],
8
- ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_secrets', 'manage_sessions'],
8
+ ['manage_platform'],
9
+ ['manage_agents'],
10
+ ['manage_tasks'],
11
+ ['manage_schedules'],
12
+ ['manage_skills'],
13
+ ['manage_documents'],
14
+ ['manage_webhooks'],
15
+ ['manage_secrets'],
9
16
  ['manage_connectors', 'connectors', 'connector_message_tool'],
10
17
  ['manage_chatrooms', 'chatroom'],
11
18
  ['spawn_subagent', 'subagent', 'delegate_to_agent'],
@@ -32,6 +39,21 @@ const PLUGIN_ALIAS_GROUPS: string[][] = [
32
39
  ['crawl', 'site_crawler'],
33
40
  ]
34
41
 
42
+ const PLUGIN_IMPLICATIONS: Record<string, string[]> = {
43
+ shell: ['process'],
44
+ manage_platform: [
45
+ 'manage_agents',
46
+ 'manage_tasks',
47
+ 'manage_schedules',
48
+ 'manage_skills',
49
+ 'manage_documents',
50
+ 'manage_webhooks',
51
+ 'manage_connectors',
52
+ 'manage_sessions',
53
+ 'manage_secrets',
54
+ ],
55
+ }
56
+
35
57
  const PLUGIN_CANONICAL_MAP = (() => {
36
58
  const map = new Map<string, string>()
37
59
  for (const group of PLUGIN_ALIAS_GROUPS) {
@@ -85,13 +107,22 @@ export function expandPluginIds(values: string[] | null | undefined): string[] {
85
107
  while (queue.length > 0) {
86
108
  const next = queue.shift()!
87
109
  const normalized = normalizePluginId(next)
110
+ const canonical = canonicalizePluginId(next)
88
111
  const aliases = PLUGIN_ALIAS_MAP.get(normalized)
89
- const key = aliases ? normalized : next
112
+ const key = aliases ? normalized : (canonical || next)
90
113
  if (expanded.has(key)) continue
91
114
  expanded.add(key)
92
- if (!aliases) continue
93
- for (const alias of aliases) {
94
- if (!expanded.has(alias)) queue.push(alias)
115
+ if (aliases) {
116
+ for (const alias of aliases) {
117
+ if (!expanded.has(alias)) queue.push(alias)
118
+ }
119
+ }
120
+ const implicationSources = [key, normalized, normalizePluginId(canonical)]
121
+ for (const source of implicationSources) {
122
+ if (!source) continue
123
+ for (const implied of PLUGIN_IMPLICATIONS[source] || []) {
124
+ if (!expanded.has(implied)) queue.push(implied)
125
+ }
95
126
  }
96
127
  }
97
128
 
@@ -56,7 +56,7 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
56
56
  opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
57
57
  gemini_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_gemini_cli'] },
58
58
  memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
59
- sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes', 'openclaw_sandbox'] },
59
+ sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'] },
60
60
  git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
61
61
  http_request: { categories: ['network'], concreteTools: ['http_request'] },
62
62
  canvas: { categories: ['filesystem'], concreteTools: ['canvas'] },