@swarmclawai/swarmclaw 0.6.8 → 0.7.0

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 (166) hide show
  1. package/README.md +70 -45
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -10,6 +10,7 @@ import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-setti
10
10
  import { getMemoryDb } from './memory-db'
11
11
  import { logExecution } from './execution-log'
12
12
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
13
+ import { expandToolIds } from './tool-aliases'
13
14
  import type { Session, Message, UsageRecord } from '@/types'
14
15
  import { extractSuggestions } from './suggestions'
15
16
 
@@ -111,115 +112,65 @@ function buildAgenticExecutionPolicy(opts: {
111
112
  }) {
112
113
  const hasTooling = opts.enabledTools.length > 0
113
114
  const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
114
- const delegationOrder = [
115
- opts.enabledTools.includes('claude_code') ? '`delegate_to_claude_code`' : null,
116
- opts.enabledTools.includes('codex_cli') ? '`delegate_to_codex_cli`' : null,
117
- opts.enabledTools.includes('opencode_cli') ? '`delegate_to_opencode_cli`' : null,
118
- ].filter(Boolean) as string[]
119
- const hasDelegationTool = delegationOrder.length > 0
120
- return [
115
+ const has = (t: string) => opts.enabledTools.includes(t)
116
+ const hasDelegationTool = has('claude_code') || has('codex_cli') || has('opencode_cli')
117
+
118
+ const parts: string[] = []
119
+
120
+ // Core execution philosophy
121
+ parts.push(
121
122
  '## How I Work',
122
- 'I take initiative. When there\'s work to do, I do it — I use my tools to research, build, and make real progress rather than just talking about it.',
123
123
  hasTooling
124
- ? 'For open-ended requests, run an action loop: plan briefly, execute tools, evaluate results, then continue until meaningful progress is achieved.'
125
- : 'This session has no tools enabled, so be explicit about what tool access is needed for deeper execution.',
126
- 'Do not stop at generic advice when the request implies action (research, coding, setup, business ideas, optimization, automation, or platform operations).',
127
- 'For multi-step work, keep the user informed with short progress updates tied to real actions (what you are doing now, what finished, and what is next).',
128
- 'If you state an intention to do research/build/execute, immediately follow through with tool calls in the same run.',
129
- 'Never claim completed research/build results without tool evidence. If a tool fails or returns empty results, say that clearly and retry with another approach.',
130
- 'If the user names a tool explicitly (for example "call connector_message_tool"), you must actually invoke that tool instead of simulating or paraphrasing its result.',
131
- 'Before finalizing: verify key claims with concrete outputs from tools whenever tools are available.',
124
+ ? 'I take initiative plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
125
+ : 'No tools enabled. Be explicit about what tool access is needed.',
126
+ 'Follow through on stated intentions with tool calls. Never claim results without tool evidence.',
127
+ 'If a tool is named explicitly, invoke it. Short progress updates between steps.',
132
128
  opts.loopMode === 'ongoing'
133
- ? 'Loop mode is ONGOING: prefer continued execution and progress tracking over one-shot replies; keep iterating until done, blocked, or safety/runtime limits are reached.'
134
- : 'Loop mode is BOUNDED: still execute multiple steps when needed, but finish within the recursion budget.',
135
- opts.enabledTools.includes('manage_tasks')
136
- ? 'When goals are long-lived, create/update tasks in the task board so progress is trackable over time.'
137
- : '',
138
- opts.enabledTools.includes('manage_schedules')
139
- ? 'When goals require follow-up, create schedules for recurring checks or future actions instead of waiting for manual prompts.'
140
- : '',
141
- opts.enabledTools.includes('manage_schedules')
142
- ? 'Before creating a schedule, first inspect existing schedules (list/get) and reuse or update a matching one instead of creating duplicates.'
143
- : '',
144
- opts.enabledTools.includes('manage_agents')
145
- ? 'If a specialist would improve output, create or configure a focused agent and assign work accordingly.'
146
- : '',
147
- opts.enabledTools.includes('manage_documents')
148
- ? 'For substantial context, store source documents and retrieve them with manage_documents search/get instead of relying on short memory snippets alone.'
149
- : '',
150
- opts.enabledTools.includes('manage_webhooks')
151
- ? 'For event-driven workflows, register webhooks and let external triggers enqueue follow-up work automatically.'
152
- : '',
153
- opts.enabledTools.includes('manage_connectors')
154
- ? 'If the user wants proactive outreach (e.g., WhatsApp updates), configure connectors and pair with schedules/tasks to deliver status updates.'
155
- : '',
156
- opts.enabledTools.includes('manage_connectors')
157
- ? 'Autonomous outreach is allowed for significant events (completed/failed tasks, blockers, deadlines, meaningful reminders from memory). Avoid casual or repetitive check-ins.'
158
- : '',
159
- opts.enabledTools.includes('manage_connectors')
160
- ? 'When you proactively message through connectors, keep it concise and purposeful, and avoid sending duplicate updates about the same event.'
161
- : '',
162
- opts.enabledTools.includes('manage_sessions')
163
- ? 'When coordinating platform work, inspect existing sessions and avoid duplicating active efforts.'
164
- : '',
165
- hasDelegationTool
166
- ? 'CRITICAL tool selection: ALWAYS use `execute_command` for running servers, dev servers, HTTP servers, installing dependencies, running scripts, git operations, process management, starting/stopping services, or any command the user wants to "run". Delegation tools (Claude/Codex/OpenCode) CANNOT keep a server running — their session ends and the process dies. `execute_command` with background=true is the ONLY way to run persistent processes.'
167
- : '',
168
- opts.enabledTools.includes('shell')
169
- ? 'When the user asks for an IP address or network URL, execute shell commands to resolve it and return the concrete value. Never reply with placeholders like `<your-local-ip>` and never tell the user to run `ifconfig`/`ipconfig` themselves unless shell access is unavailable.'
170
- : '',
171
- opts.enabledTools.includes('shell')
172
- ? 'For long-lived servers/processes: start with `execute_command` using `background=true`, capture the returned processId, then verify with `process_tool` status/log before claiming success. If the process exits or crashes, retry with a corrected command and report what changed.'
173
- : '',
174
- opts.enabledTools.includes('shell')
175
- ? 'Do not claim a server is running unless there is direct tool evidence (process status/log output).'
176
- : '',
177
- opts.enabledTools.includes('shell')
178
- ? 'If `execute_command` fails due workdir/path traversal, retry without a workdir override or use a safe relative path under the current session cwd.'
179
- : '',
180
- hasDelegationTool
181
- ? `Only use CLI delegation (${delegationOrder.join(' -> ')}) for tasks that need deep code understanding across multiple files: large refactors, complex debugging, multi-file code generation, or test suites. Never delegate when the user says "run", "start", "serve", "execute", or "test it locally".`
182
- : '',
183
- opts.enabledTools.includes('memory')
184
- ? 'Memory is active and required for long-horizon work: before major tasks, run memory_tool search/list for relevant prior work; after each meaningful step, store concise reusable notes (what changed, where it lives, constraints, next step). Treat memory as shared context plus your own agent notes, not as user-owned personal profile data.'
185
- : '',
186
- opts.enabledTools.includes('memory')
187
- ? 'The platform preloads relevant memory context each turn. Use memory_tool for deeper lookup, explicit recall requests, and durable storage.'
188
- : '',
189
- opts.enabledTools.includes('memory')
190
- ? 'If the user gives an open goal (e.g. "go make money"), do not keep re-asking broad clarifying questions. Form a hypothesis, execute a concrete step, then adapt using memory + evidence.'
191
- : '',
192
- '## Knowing When Not to Reply',
193
- 'Real conversations have natural pauses. Not every message needs a response — sometimes the most human thing is comfortable silence.',
194
- 'Reply with exactly "NO_MESSAGE" (nothing else) to suppress outbound delivery when replying would feel unnatural.',
195
- 'Think about what a thoughtful friend would do:',
196
- '- "okay" / "alright" / "cool" / "got it" / "sounds good" → they\'re just acknowledging, not expecting a reply back',
197
- '- "thanks" / "thx" / "ty" after you\'ve helped → the conversation is wrapping up naturally',
198
- '- thumbs up, emoji reactions, read receipts → these are closers, not openers',
199
- '- "night" / "ttyl" / "bye" / "gotta go" → they\'re leaving, let them go',
200
- '- "haha" / "lol" / "lmao" → they appreciated something, no follow-up needed',
201
- '- forwarded content or status updates with no question → they\'re sharing, not asking',
202
- 'Always reply when:',
203
- '- There is a question, even an implied one ("I wonder if...")',
204
- '- They give you a task or instruction',
205
- '- They share something emotional or personal — silence here feels cold',
206
- '- They say "thanks" with a follow-up context ("thanks, what about X?") or in a tone that expects "you\'re welcome"',
207
- '- You have something genuinely useful to add',
208
- 'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
209
- 'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
210
- 'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
211
- 'Do not end every response with a question. Use declarative completion statements by default, and only ask a question when a concrete missing detail blocks the next action.',
212
- 'Do not pause for a "continue" confirmation after the user has already asked you to execute a goal. Keep moving until blocked by permissions, missing credentials, or hard tool failures.',
213
- 'Never repeat one-time side effects that are already complete (for example creating the same schedule/task again). Verify state first, then either continue execution or reply HEARTBEAT_OK.',
214
- 'For main-loop tick messages that begin with "SWARM_MAIN_MISSION_TICK" or "SWARM_MAIN_AUTO_FOLLOWUP", follow that response contract exactly and include one valid [MAIN_LOOP_META] JSON line when you are not returning HEARTBEAT_OK.',
215
- `Heartbeat protocol: if the user message is exactly "${opts.heartbeatPrompt}", reply exactly "HEARTBEAT_OK" when there is nothing important to report; otherwise reply with a concise progress update and immediate next step.`,
216
- opts.heartbeatIntervalSec > 0
217
- ? `Expected heartbeat cadence is roughly every ${opts.heartbeatIntervalSec} seconds while ongoing work is active.`
218
- : '',
219
- toolLines.length ? 'What I can do:\n' + toolLines.join('\n') : '',
220
- // Inject goal decomposition instructions for broad goals without existing plans
221
- (opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) ? GOAL_DECOMPOSITION_BLOCK : '',
222
- ].filter(Boolean).join('\n')
129
+ ? 'Loop: ONGOING keep iterating until done, blocked, or limits reached.'
130
+ : 'Loop: BOUNDED execute multiple steps but finish within recursion budget.',
131
+ )
132
+
133
+ // Tool-specific guidance (consolidated)
134
+ if (has('shell')) {
135
+ parts.push(
136
+ 'Shell: use `execute_command` for servers, installs, scripts, git. Use `background=true` for long-lived processes.',
137
+ 'Verify servers with `process_tool` status/log and liveness probes before claiming success.',
138
+ 'Resolve IPs/URLs via shell never use placeholders. Retry path errors without workdir override.',
139
+ )
140
+ }
141
+ if (hasDelegationTool) {
142
+ parts.push(
143
+ 'CRITICAL: `execute_command` (not delegation) for running servers, installs, scripts. Delegation sessions end and kill processes.',
144
+ 'Delegate only for deep multi-file code work: refactors, debugging, generation, test suites.',
145
+ )
146
+ }
147
+ if (has('memory')) {
148
+ parts.push(
149
+ 'Memory: search before major tasks, store concise notes after meaningful steps. Platform preloads context each turn.',
150
+ 'For open goals, form a hypothesis and execute do not keep re-asking broad questions.',
151
+ )
152
+ }
153
+ if (has('manage_tasks')) parts.push('Create/update tasks for long-lived goals to track progress.')
154
+ if (has('manage_schedules')) parts.push('Use schedules for follow-ups. Check existing schedules before creating new ones.')
155
+ if (has('manage_connectors')) parts.push('Connectors: proactive outreach for significant events only. Keep messages concise, no duplicates.')
156
+ if (has('manage_sessions')) parts.push('Inspect existing chats before creating duplicates.')
157
+
158
+ // Response behavior
159
+ parts.push(
160
+ '## Response Rules',
161
+ 'NO_MESSAGE: reply with exactly this to suppress delivery for pure acknowledgments (ok/thanks/bye/emoji/lol).',
162
+ 'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
163
+ 'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
164
+ 'Never repeat completed side effects. Verify state first.',
165
+ `Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
166
+ opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
167
+ 'For SWARM_MAIN_MISSION_TICK / SWARM_MAIN_AUTO_FOLLOWUP messages, follow the response contract and include [MAIN_LOOP_META] JSON.',
168
+ )
169
+
170
+ if (toolLines.length) parts.push('What I can do:\n' + toolLines.join('\n'))
171
+ if (opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
172
+
173
+ return parts.filter(Boolean).join('\n')
223
174
  }
224
175
 
225
176
  export interface StreamAgentChatResult {
@@ -233,10 +184,12 @@ export interface StreamAgentChatResult {
233
184
  export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
234
185
  const startTs = Date.now()
235
186
  const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
236
- const sessionToolsWithImplicitProcess = Array.from(new Set([
237
- ...(session.tools || []),
238
- ...((session.tools || []).includes('shell') ? ['process'] : []),
239
- ]))
187
+ const rawTools = Array.isArray(session.tools) ? session.tools : []
188
+ const hasShellCapability = rawTools.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
189
+ const sessionToolsWithImplicitProcess = expandToolIds([
190
+ ...rawTools,
191
+ ...(hasShellCapability ? ['process'] : []),
192
+ ])
240
193
 
241
194
  // fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
242
195
  void fallbackCredentialIds
@@ -413,17 +366,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
413
366
  '- When I learn something that corrects old knowledge, update or remove the old memory',
414
367
  ].join('\n'))
415
368
 
416
- // Pre-compaction memory flush & Personality Evolution: nudge agent to reflect when conversation is long
369
+ // Pre-compaction memory flush: nudge agent to save important context before it's lost
417
370
  const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
418
371
  if (msgCount > 20) {
419
- const canEditSelf = (session.tools || []).includes('manage_agents')
420
372
  stateModifierParts.push([
421
373
  '## Reflection & Consolidation Reminder',
422
374
  'This conversation is getting long and I might lose older context soon.',
423
- '1. **Memory:** I should save anything important I\'ve learned, decided, or discovered to my memory now. Only what matters, not every detail.',
424
- canEditSelf ? `2. **Personality Evolution:** I should reflect on this conversation. Have my boundaries, tone, or relationship with the user evolved? If so, I MUST use \`manage_agents\` (action: update, id: "${session.agentId}") to update my \`soul\` field with these new learnings.` : '',
425
- 'If there\'s nothing worth saving or updating, carry on.',
426
- ].filter(Boolean).join('\n'))
375
+ 'Save anything important I\'ve learned, decided, or discovered to memory now. Only what matters, not every detail.',
376
+ ].join('\n'))
427
377
  }
428
378
  } catch {
429
379
  // If memory context fails to load, continue without blocking the run.
@@ -471,26 +421,44 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
471
421
  }
472
422
  }
473
423
 
474
- // Tell the LLM about tools it could use but doesn't have enabled
424
+ // Tell the LLM about available plugins and their access status
475
425
  {
476
- const enabledSet = new Set(sessionToolsWithImplicitProcess)
477
- const allToolIds = [
478
- 'shell', 'files', 'copy_file', 'move_file', 'delete_file', 'edit_file', 'process',
479
- 'web_search', 'web_fetch', 'browser', 'memory',
480
- 'claude_code', 'codex_cli', 'opencode_cli',
481
- 'sandbox', 'create_document', 'create_spreadsheet', 'http_request', 'git', 'wallet',
482
- 'manage_agents', 'manage_tasks', 'manage_schedules', 'schedule_wake', 'manage_skills',
483
- 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets',
484
- ]
485
- const disabled = allToolIds.filter((t) => !enabledSet.has(t))
426
+ const agentEnabledSet = new Set(sessionToolsWithImplicitProcess)
427
+ const { getPluginManager } = await import('./plugins')
428
+ const allPlugins = getPluginManager().listPlugins()
486
429
  const mcpDisabled = agentMcpDisabledTools ?? []
487
- const allDisabled = [...disabled, ...mcpDisabled]
488
- if (allDisabled.length > 0) {
489
- stateModifierParts.push(
490
- `## Tools I Don't Have Yet\nI don't currently have access to: ${allDisabled.join(', ')}.\n` +
491
- 'If I need any of these for a task, I can ask the user to enable them with `request_tool_access`.',
430
+
431
+ // Categorize plugins
432
+ const globallyDisabled: string[] = [] // Disabled site-wide by admin
433
+ const enabledButNoAccess: string[] = [] // Enabled globally but agent doesn't have access
434
+ const agentHasAccess: string[] = [] // Agent can use these
435
+
436
+ for (const p of allPlugins) {
437
+ if (!p.enabled) {
438
+ globallyDisabled.push(`${p.name} (${p.filename})`)
439
+ } else if (!agentEnabledSet.has(p.filename)) {
440
+ enabledButNoAccess.push(`${p.name} (${p.filename})`)
441
+ } else {
442
+ agentHasAccess.push(p.filename)
443
+ }
444
+ }
445
+
446
+ const parts: string[] = []
447
+ if (enabledButNoAccess.length > 0) {
448
+ parts.push(
449
+ `**Available but not assigned to me:** ${enabledButNoAccess.join(', ')}\n` +
450
+ 'I can request access using `manage_capabilities` with action "request_access" or `request_tool_access`.',
492
451
  )
493
452
  }
453
+ if (globallyDisabled.length > 0) {
454
+ parts.push(`**Disabled site-wide:** ${globallyDisabled.join(', ')} — ask the user to enable these in Settings > Plugins first.`)
455
+ }
456
+ if (mcpDisabled.length > 0) {
457
+ parts.push(`**MCP tools not available:** ${mcpDisabled.join(', ')}`)
458
+ }
459
+ if (parts.length > 0) {
460
+ stateModifierParts.push(`## Plugin Access\n${parts.join('\n')}`)
461
+ }
494
462
  }
495
463
 
496
464
  if (settings.suggestionsEnabled === true) {
@@ -672,7 +640,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
672
640
  const postClearHistory = effectiveHistory.slice(contextStart)
673
641
 
674
642
  const langchainMessages: Array<HumanMessage | AIMessage> = []
675
- for (const m of postClearHistory.slice(-20)) {
643
+ for (const m of postClearHistory.slice(-30)) {
676
644
  if (m.role === 'user') {
677
645
  langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
678
646
  } else {
@@ -687,6 +655,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
687
655
  let fullText = ''
688
656
  let lastSegment = ''
689
657
  let hasToolCalls = false
658
+ let needsTextSeparator = false
690
659
  let totalInputTokens = 0
691
660
  let totalOutputTokens = 0
692
661
  let lastToolInput: unknown = null
@@ -710,135 +679,223 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
710
679
  }, runtime.ongoingLoopMaxRuntimeMs)
711
680
  : null
712
681
 
682
+ const MAX_AUTO_CONTINUES = 3
683
+ const MAX_TRANSIENT_RETRIES = 2
684
+ let autoContinueCount = 0
685
+ let transientRetryCount = 0
686
+
713
687
  try {
714
- const eventStream = agent.streamEvents(
715
- { messages: langchainMessages },
716
- { version: 'v2', recursionLimit, signal: abortController.signal },
717
- )
688
+ const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
689
+ for (let iteration = 0; iteration <= maxIterations; iteration++) {
690
+ let shouldContinue: 'recursion' | 'transient' | false = false
691
+
692
+ // Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
693
+ // Linked to the parent so client disconnect / timeout still propagates.
694
+ const iterationController = new AbortController()
695
+ const onParentAbort = () => iterationController.abort()
696
+ if (abortController.signal.aborted) iterationController.abort()
697
+ else abortController.signal.addEventListener('abort', onParentAbort)
718
698
 
719
- for await (const event of eventStream) {
720
- const kind = event.event
721
-
722
- if (kind === 'on_chat_model_stream') {
723
- const chunk = event.data?.chunk
724
- if (chunk?.content) {
725
- // content can be string or array of content blocks
726
- if (Array.isArray(chunk.content)) {
727
- for (const block of chunk.content) {
728
- // Anthropic extended thinking blocks
729
- if (block.type === 'thinking' && block.thinking) {
730
- accumulatedThinking += block.thinking
731
- write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
732
- // OpenClaw [[thinking]] prefix convention
733
- } else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
734
- accumulatedThinking += block.text.slice(12)
735
- write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
736
- } else if (block.text) {
737
- fullText += block.text
738
- lastSegment += block.text
739
- write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
699
+ try {
700
+ const eventStream = agent.streamEvents(
701
+ { messages: langchainMessages },
702
+ { version: 'v2', recursionLimit, signal: iterationController.signal },
703
+ )
704
+
705
+ for await (const event of eventStream) {
706
+ const kind = event.event
707
+
708
+ if (kind === 'on_chat_model_stream') {
709
+ const chunk = event.data?.chunk
710
+ if (chunk?.content) {
711
+ // content can be string or array of content blocks
712
+ if (Array.isArray(chunk.content)) {
713
+ for (const block of chunk.content) {
714
+ // Anthropic extended thinking blocks
715
+ if (block.type === 'thinking' && block.thinking) {
716
+ accumulatedThinking += block.thinking
717
+ write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
718
+ // OpenClaw [[thinking]] prefix convention
719
+ } else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
720
+ accumulatedThinking += block.text.slice(12)
721
+ write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
722
+ } else if (block.text) {
723
+ if (needsTextSeparator && fullText.length > 0) {
724
+ fullText += '\n\n'
725
+ write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
726
+ needsTextSeparator = false
727
+ }
728
+ fullText += block.text
729
+ lastSegment += block.text
730
+ write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
731
+ }
732
+ }
733
+ } else {
734
+ const text = typeof chunk.content === 'string' ? chunk.content : ''
735
+ if (text) {
736
+ if (needsTextSeparator && fullText.length > 0) {
737
+ fullText += '\n\n'
738
+ write(`data: ${JSON.stringify({ t: 'd', text: '\n\n' })}\n\n`)
739
+ needsTextSeparator = false
740
+ }
741
+ fullText += text
742
+ lastSegment += text
743
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
744
+ }
740
745
  }
741
746
  }
742
- } else {
743
- const text = typeof chunk.content === 'string' ? chunk.content : ''
744
- if (text) {
745
- fullText += text
746
- lastSegment += text
747
- write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
747
+ } else if (kind === 'on_llm_end') {
748
+ // Track token usage from LLM responses check all known LangChain event shapes
749
+ const output = event.data?.output
750
+ const usage = output?.llmOutput?.tokenUsage
751
+ || output?.llmOutput?.usage
752
+ || output?.usage_metadata
753
+ || output?.response_metadata?.usage
754
+ || output?.response_metadata?.tokenUsage
755
+ if (usage) {
756
+ totalInputTokens += usage.promptTokens || usage.input_tokens || usage.prompt_tokens || 0
757
+ totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
748
758
  }
749
- }
750
- }
751
- } else if (kind === 'on_llm_end') {
752
- // Track token usage from LLM responses — check all known LangChain event shapes
753
- const output = event.data?.output
754
- const usage = output?.llmOutput?.tokenUsage
755
- || output?.llmOutput?.usage
756
- || output?.usage_metadata
757
- || output?.response_metadata?.usage
758
- || output?.response_metadata?.tokenUsage
759
- if (usage) {
760
- totalInputTokens += usage.promptTokens || usage.input_tokens || usage.prompt_tokens || 0
761
- totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
762
- }
763
- } else if (kind === 'on_tool_start') {
764
- hasToolCalls = true
765
- lastSegment = ''
766
- const toolName = event.name || 'unknown'
767
- const input = event.data?.input
768
- lastToolInput = input
769
- // Plugin hooks: beforeToolExec
770
- await pluginMgr.runHook('beforeToolExec', { toolName, input })
771
- const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
772
- logExecution(session.id, 'tool_call', `${toolName} invoked`, {
773
- agentId: session.agentId,
774
- detail: { toolName, input: inputStr?.slice(0, 4000) },
775
- })
776
- write(`data: ${JSON.stringify({
777
- t: 'tool_call',
778
- toolName,
779
- toolInput: inputStr,
780
- })}\n\n`)
781
- } else if (kind === 'on_tool_end') {
782
- const toolName = event.name || 'unknown'
783
- const output = event.data?.output
784
- const outputStr = typeof output === 'string'
785
- ? output
786
- : output?.content
787
- ? String(output.content)
788
- : JSON.stringify(output)
789
- // Plugin hooks: afterToolExec
790
- await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
791
- // Event-driven memory breadcrumbs
792
- if (session.agentId && (session.tools || []).includes('memory')) {
793
- try {
794
- const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
795
- if (breadcrumbTitle) {
796
- const memDb = getMemoryDb()
797
- memDb.add({
759
+ } else if (kind === 'on_tool_start') {
760
+ hasToolCalls = true
761
+ needsTextSeparator = true
762
+ lastSegment = ''
763
+ const toolName = event.name || 'unknown'
764
+ const input = event.data?.input
765
+ lastToolInput = input
766
+ // Plugin hooks: beforeToolExec
767
+ await pluginMgr.runHook('beforeToolExec', { toolName, input })
768
+ const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
769
+ logExecution(session.id, 'tool_call', `${toolName} invoked`, {
770
+ agentId: session.agentId,
771
+ detail: { toolName, input: inputStr?.slice(0, 4000) },
772
+ })
773
+ write(`data: ${JSON.stringify({
774
+ t: 'tool_call',
775
+ toolName,
776
+ toolInput: inputStr,
777
+ })}\n\n`)
778
+ } else if (kind === 'on_tool_end') {
779
+ const toolName = event.name || 'unknown'
780
+ const output = event.data?.output
781
+ const outputStr = typeof output === 'string'
782
+ ? output
783
+ : output?.content
784
+ ? String(output.content)
785
+ : JSON.stringify(output)
786
+ // Plugin hooks: afterToolExec
787
+ await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
788
+ // Event-driven memory breadcrumbs
789
+ if (session.agentId && (session.tools || []).includes('memory')) {
790
+ try {
791
+ const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
792
+ if (breadcrumbTitle) {
793
+ const memDb = getMemoryDb()
794
+ memDb.add({
795
+ agentId: session.agentId,
796
+ sessionId: session.id,
797
+ category: 'breadcrumb',
798
+ title: breadcrumbTitle,
799
+ content: '',
800
+ })
801
+ }
802
+ } catch { /* breadcrumbs are best-effort */ }
803
+ }
804
+ lastToolInput = null
805
+ logExecution(session.id, 'tool_result', `${toolName} returned`, {
806
+ agentId: session.agentId,
807
+ detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
808
+ })
809
+ // Enriched file_op logging for file-mutating tools
810
+ if (['write_file', 'edit_file', 'copy_file', 'move_file', 'delete_file'].includes(toolName)) {
811
+ const inputData = event.data?.input
812
+ const inputObj = typeof inputData === 'object' ? inputData : {}
813
+ logExecution(session.id, 'file_op', `${toolName}: ${inputObj?.filePath || inputObj?.sourcePath || 'unknown'}`, {
798
814
  agentId: session.agentId,
799
- sessionId: session.id,
800
- category: 'breadcrumb',
801
- title: breadcrumbTitle,
802
- content: '',
815
+ detail: { toolName, filePath: inputObj?.filePath, sourcePath: inputObj?.sourcePath, destinationPath: inputObj?.destinationPath, success: !/^Error/i.test((outputStr || '').trim()) },
803
816
  })
804
817
  }
805
- } catch { /* breadcrumbs are best-effort */ }
818
+ // Enriched commit logging for git operations
819
+ if (toolName === 'execute_command' && outputStr) {
820
+ const commitMatch = outputStr.match(/\[[\w/-]+\s+([a-f0-9]{7,40})\]/)
821
+ if (commitMatch) {
822
+ logExecution(session.id, 'commit', `git commit ${commitMatch[1]}`, {
823
+ agentId: session.agentId,
824
+ detail: { commitId: commitMatch[1], outputPreview: outputStr.slice(0, 500) },
825
+ })
826
+ }
827
+ }
828
+ write(`data: ${JSON.stringify({
829
+ t: 'tool_result',
830
+ toolName,
831
+ toolOutput: outputStr?.slice(0, 2000),
832
+ })}\n\n`)
833
+ }
806
834
  }
807
- lastToolInput = null
808
- logExecution(session.id, 'tool_result', `${toolName} returned`, {
809
- agentId: session.agentId,
810
- detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
835
+ } catch (innerErr: unknown) {
836
+ const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
837
+ const errMsg = innerErr instanceof Error ? innerErr.message : String(innerErr)
838
+ const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
839
+
840
+ // Classify the error:
841
+ // 1. GraphRecursionError — explicit or wrapped as abort (LangGraph aborts internally on limit)
842
+ // 2. Transient abort/timeout — LLM API failure, not from client disconnect
843
+ const isRecursionError = errName === 'GraphRecursionError'
844
+ || /recursion limit|maximum recursion/i.test(errMsg)
845
+ const isTransientAbort = !isRecursionError
846
+ && /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
847
+ && !abortController.signal.aborted
848
+
849
+ // Log diagnostic details for every error so we can trace root causes
850
+ console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
851
+ errName, errMsg, errStack,
852
+ isRecursionError, isTransientAbort,
853
+ hasToolCalls, fullTextLen: fullText.length,
854
+ parentAborted: abortController.signal.aborted,
811
855
  })
812
- // Enriched file_op logging for file-mutating tools
813
- if (['write_file', 'edit_file', 'copy_file', 'move_file', 'delete_file'].includes(toolName)) {
814
- const inputData = event.data?.input
815
- const inputObj = typeof inputData === 'object' ? inputData : {}
816
- logExecution(session.id, 'file_op', `${toolName}: ${inputObj?.filePath || inputObj?.sourcePath || 'unknown'}`, {
856
+
857
+ if (isRecursionError && autoContinueCount < MAX_AUTO_CONTINUES && !abortController.signal.aborted) {
858
+ shouldContinue = 'recursion'
859
+ autoContinueCount++
860
+ logExecution(session.id, 'decision', `Recursion limit hit, auto-continuing (${autoContinueCount}/${MAX_AUTO_CONTINUES})`, {
817
861
  agentId: session.agentId,
818
- detail: { toolName, filePath: inputObj?.filePath, sourcePath: inputObj?.sourcePath, destinationPath: inputObj?.destinationPath, success: !/^Error/i.test((outputStr || '').trim()) },
862
+ detail: { errName, errMsg },
819
863
  })
864
+ write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ autoContinue: autoContinueCount, maxContinues: MAX_AUTO_CONTINUES }) })}\n\n`)
865
+ } else if (isTransientAbort && transientRetryCount < MAX_TRANSIENT_RETRIES && !abortController.signal.aborted) {
866
+ shouldContinue = 'transient'
867
+ transientRetryCount++
868
+ logExecution(session.id, 'decision', `Transient error, retrying (${transientRetryCount}/${MAX_TRANSIENT_RETRIES}): ${errMsg}`, {
869
+ agentId: session.agentId,
870
+ detail: { errName, errMsg },
871
+ })
872
+ write(`data: ${JSON.stringify({ t: 'status', text: JSON.stringify({ transientRetry: transientRetryCount, maxRetries: MAX_TRANSIENT_RETRIES, error: errMsg }) })}\n\n`)
873
+ } else {
874
+ // Non-retryable error or exhausted retries — rethrow to outer catch
875
+ throw innerErr
820
876
  }
821
- // Enriched commit logging for git operations
822
- if (toolName === 'execute_command' && outputStr) {
823
- const commitMatch = outputStr.match(/\[[\w/-]+\s+([a-f0-9]{7,40})\]/)
824
- if (commitMatch) {
825
- logExecution(session.id, 'commit', `git commit ${commitMatch[1]}`, {
826
- agentId: session.agentId,
827
- detail: { commitId: commitMatch[1], outputPreview: outputStr.slice(0, 500) },
828
- })
829
- }
877
+ } finally {
878
+ abortController.signal.removeEventListener('abort', onParentAbort)
879
+ }
880
+
881
+ if (!shouldContinue) break
882
+
883
+ if (shouldContinue === 'recursion') {
884
+ // Append accumulated text and a continue prompt
885
+ if (fullText.trim()) {
886
+ langchainMessages.push(new AIMessage({ content: fullText }))
830
887
  }
831
- write(`data: ${JSON.stringify({
832
- t: 'tool_result',
833
- toolName,
834
- toolOutput: outputStr?.slice(0, 2000),
835
- })}\n\n`)
888
+ langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
889
+ lastSegment = ''
890
+ } else if (shouldContinue === 'transient') {
891
+ // Short delay before retrying transient errors (API timeout, rate limit, etc.)
892
+ await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
836
893
  }
837
894
  }
838
- } catch (err: any) {
895
+ } catch (err: unknown) {
839
896
  const errMsg = timedOut
840
897
  ? 'Ongoing loop stopped after reaching the configured runtime limit.'
841
- : err.message || String(err)
898
+ : err instanceof Error ? err.message : String(err)
842
899
  logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
843
900
  write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
844
901
  } finally {