@swarmclawai/swarmclaw 0.6.7 → 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 (203) hide show
  1. package/README.md +82 -39
  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 +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -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
 
@@ -59,6 +60,7 @@ function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssig
59
60
  if (enabledTools.includes('manage_agents')) lines.push('- I can create and configure other agents (`manage_agents`) — spin up specialists when a task calls for it.')
60
61
  if (enabledTools.includes('manage_tasks')) lines.push('- I can manage tasks (`manage_tasks`) — create plans, track progress, and stay organized over time.')
61
62
  if (enabledTools.includes('manage_schedules')) lines.push('- I can set up schedules (`manage_schedules`) for recurring work or future follow-ups.')
63
+ if (enabledTools.includes('schedule_wake')) lines.push('- I can set a conversational timer (`schedule_wake`) to remind myself to check back on something later in this chat.')
62
64
  if (enabledTools.includes('manage_documents')) lines.push('- I can store and search documents (`manage_documents`) for long-term knowledge and reference.')
63
65
  if (enabledTools.includes('manage_webhooks')) lines.push('- I can register webhooks (`manage_webhooks`) so external events can trigger my work automatically.')
64
66
  if (enabledTools.includes('manage_skills')) lines.push('- I can manage reusable skills (`manage_skills`) — building blocks I can learn and apply.')
@@ -77,122 +79,98 @@ function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssig
77
79
  return lines
78
80
  }
79
81
 
82
+ /** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
83
+ function isBroadGoal(text: string): boolean {
84
+ if (text.length < 50) return false
85
+ // Messages with code fences, file paths, or numbered steps are already structured
86
+ if (/```/.test(text)) return false
87
+ if (/\/(src|lib|app|pages|components|api)\//.test(text)) return false
88
+ if (/^\s*\d+[.)]\s/m.test(text)) return false
89
+ // Short direct questions aren't broad goals
90
+ if (text.length < 80 && text.endsWith('?')) return false
91
+ return true
92
+ }
93
+
94
+ const GOAL_DECOMPOSITION_BLOCK = [
95
+ '## Goal Decomposition',
96
+ 'When you receive a broad, open-ended goal:',
97
+ '1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
98
+ '2. If manage_tasks is available, create a task for each subtask to track progress.',
99
+ '3. Output your plan in a [MAIN_LOOP_PLAN] JSON line: {"steps":["step1","step2",...],"current_step":"step1"}',
100
+ '4. Execute the first subtask immediately — do not stop after planning.',
101
+ '5. After each subtask, update progress and move to the next.',
102
+ ].join('\n')
103
+
80
104
  function buildAgenticExecutionPolicy(opts: {
81
105
  enabledTools: string[]
82
106
  loopMode: 'bounded' | 'ongoing'
83
107
  heartbeatPrompt: string
84
108
  heartbeatIntervalSec: number
85
109
  platformAssignScope?: 'self' | 'all'
110
+ userMessage?: string
111
+ hasExistingPlan?: boolean
86
112
  }) {
87
113
  const hasTooling = opts.enabledTools.length > 0
88
114
  const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
89
- const delegationOrder = [
90
- opts.enabledTools.includes('claude_code') ? '`delegate_to_claude_code`' : null,
91
- opts.enabledTools.includes('codex_cli') ? '`delegate_to_codex_cli`' : null,
92
- opts.enabledTools.includes('opencode_cli') ? '`delegate_to_opencode_cli`' : null,
93
- ].filter(Boolean) as string[]
94
- const hasDelegationTool = delegationOrder.length > 0
95
- 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(
96
122
  '## How I Work',
97
- '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.',
98
123
  hasTooling
99
- ? 'For open-ended requests, run an action loop: plan briefly, execute tools, evaluate results, then continue until meaningful progress is achieved.'
100
- : 'This session has no tools enabled, so be explicit about what tool access is needed for deeper execution.',
101
- 'Do not stop at generic advice when the request implies action (research, coding, setup, business ideas, optimization, automation, or platform operations).',
102
- '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).',
103
- 'If you state an intention to do research/build/execute, immediately follow through with tool calls in the same run.',
104
- '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.',
105
- '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.',
106
- '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.',
107
128
  opts.loopMode === 'ongoing'
108
- ? '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.'
109
- : 'Loop mode is BOUNDED: still execute multiple steps when needed, but finish within the recursion budget.',
110
- opts.enabledTools.includes('manage_tasks')
111
- ? 'When goals are long-lived, create/update tasks in the task board so progress is trackable over time.'
112
- : '',
113
- opts.enabledTools.includes('manage_schedules')
114
- ? 'When goals require follow-up, create schedules for recurring checks or future actions instead of waiting for manual prompts.'
115
- : '',
116
- opts.enabledTools.includes('manage_schedules')
117
- ? 'Before creating a schedule, first inspect existing schedules (list/get) and reuse or update a matching one instead of creating duplicates.'
118
- : '',
119
- opts.enabledTools.includes('manage_agents')
120
- ? 'If a specialist would improve output, create or configure a focused agent and assign work accordingly.'
121
- : '',
122
- opts.enabledTools.includes('manage_documents')
123
- ? 'For substantial context, store source documents and retrieve them with manage_documents search/get instead of relying on short memory snippets alone.'
124
- : '',
125
- opts.enabledTools.includes('manage_webhooks')
126
- ? 'For event-driven workflows, register webhooks and let external triggers enqueue follow-up work automatically.'
127
- : '',
128
- opts.enabledTools.includes('manage_connectors')
129
- ? 'If the user wants proactive outreach (e.g., WhatsApp updates), configure connectors and pair with schedules/tasks to deliver status updates.'
130
- : '',
131
- opts.enabledTools.includes('manage_connectors')
132
- ? 'Autonomous outreach is allowed for significant events (completed/failed tasks, blockers, deadlines, meaningful reminders from memory). Avoid casual or repetitive check-ins.'
133
- : '',
134
- opts.enabledTools.includes('manage_connectors')
135
- ? 'When you proactively message through connectors, keep it concise and purposeful, and avoid sending duplicate updates about the same event.'
136
- : '',
137
- opts.enabledTools.includes('manage_sessions')
138
- ? 'When coordinating platform work, inspect existing sessions and avoid duplicating active efforts.'
139
- : '',
140
- hasDelegationTool
141
- ? '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.'
142
- : '',
143
- opts.enabledTools.includes('shell')
144
- ? '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.'
145
- : '',
146
- opts.enabledTools.includes('shell')
147
- ? '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.'
148
- : '',
149
- opts.enabledTools.includes('shell')
150
- ? 'Do not claim a server is running unless there is direct tool evidence (process status/log output).'
151
- : '',
152
- opts.enabledTools.includes('shell')
153
- ? 'If `execute_command` fails due workdir/path traversal, retry without a workdir override or use a safe relative path under the current session cwd.'
154
- : '',
155
- hasDelegationTool
156
- ? `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".`
157
- : '',
158
- opts.enabledTools.includes('memory')
159
- ? '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.'
160
- : '',
161
- opts.enabledTools.includes('memory')
162
- ? 'The platform preloads relevant memory context each turn. Use memory_tool for deeper lookup, explicit recall requests, and durable storage.'
163
- : '',
164
- opts.enabledTools.includes('memory')
165
- ? '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.'
166
- : '',
167
- '## Knowing When Not to Reply',
168
- 'Real conversations have natural pauses. Not every message needs a response — sometimes the most human thing is comfortable silence.',
169
- 'Reply with exactly "NO_MESSAGE" (nothing else) to suppress outbound delivery when replying would feel unnatural.',
170
- 'Think about what a thoughtful friend would do:',
171
- '- "okay" / "alright" / "cool" / "got it" / "sounds good" → they\'re just acknowledging, not expecting a reply back',
172
- '- "thanks" / "thx" / "ty" after you\'ve helped → the conversation is wrapping up naturally',
173
- '- thumbs up, emoji reactions, read receipts → these are closers, not openers',
174
- '- "night" / "ttyl" / "bye" / "gotta go" → they\'re leaving, let them go',
175
- '- "haha" / "lol" / "lmao" → they appreciated something, no follow-up needed',
176
- '- forwarded content or status updates with no question → they\'re sharing, not asking',
177
- 'Always reply when:',
178
- '- There is a question, even an implied one ("I wonder if...")',
179
- '- They give you a task or instruction',
180
- '- They share something emotional or personal — silence here feels cold',
181
- '- They say "thanks" with a follow-up context ("thanks, what about X?") or in a tone that expects "you\'re welcome"',
182
- '- You have something genuinely useful to add',
183
- 'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
184
- 'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
185
- 'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
186
- '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.',
187
- '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.',
188
- '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.',
189
- '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.',
190
- `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.`,
191
- opts.heartbeatIntervalSec > 0
192
- ? `Expected heartbeat cadence is roughly every ${opts.heartbeatIntervalSec} seconds while ongoing work is active.`
193
- : '',
194
- toolLines.length ? 'What I can do:\n' + toolLines.join('\n') : '',
195
- ].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')
196
174
  }
197
175
 
198
176
  export interface StreamAgentChatResult {
@@ -204,11 +182,14 @@ export interface StreamAgentChatResult {
204
182
  }
205
183
 
206
184
  export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
185
+ const startTs = Date.now()
207
186
  const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
208
- const sessionToolsWithImplicitProcess = Array.from(new Set([
209
- ...(session.tools || []),
210
- ...((session.tools || []).includes('shell') ? ['process'] : []),
211
- ]))
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
+ ])
212
193
 
213
194
  // fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
214
195
  void fallbackCredentialIds
@@ -385,14 +366,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
385
366
  '- When I learn something that corrects old knowledge, update or remove the old memory',
386
367
  ].join('\n'))
387
368
 
388
- // Pre-compaction memory flush: nudge agent to persist learnings when conversation is long
369
+ // Pre-compaction memory flush: nudge agent to save important context before it's lost
389
370
  const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
390
371
  if (msgCount > 20) {
391
372
  stateModifierParts.push([
392
- '## Memory Flush Reminder',
393
- 'This conversation is getting long and I might lose older context soon. I should save anything',
394
- 'important I\'ve learned, decided, or discovered to my memory now things I\'d want to recall',
395
- 'in future conversations. Only what matters, not every detail. If there\'s nothing worth saving, carry on.',
373
+ '## Reflection & Consolidation Reminder',
374
+ 'This conversation is getting long and I might lose older context soon.',
375
+ 'Save anything important I\'ve learned, decided, or discovered to memory now. Only what matters, not every detail.',
396
376
  ].join('\n'))
397
377
  }
398
378
  } catch {
@@ -441,26 +421,44 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
441
421
  }
442
422
  }
443
423
 
444
- // Tell the LLM about tools it could use but doesn't have enabled
424
+ // Tell the LLM about available plugins and their access status
445
425
  {
446
- const enabledSet = new Set(sessionToolsWithImplicitProcess)
447
- const allToolIds = [
448
- 'shell', 'files', 'copy_file', 'move_file', 'delete_file', 'edit_file', 'process',
449
- 'web_search', 'web_fetch', 'browser', 'memory',
450
- 'claude_code', 'codex_cli', 'opencode_cli',
451
- 'sandbox', 'create_document', 'create_spreadsheet', 'http_request', 'git', 'wallet',
452
- 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills',
453
- 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets',
454
- ]
455
- 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()
456
429
  const mcpDisabled = agentMcpDisabledTools ?? []
457
- const allDisabled = [...disabled, ...mcpDisabled]
458
- if (allDisabled.length > 0) {
459
- stateModifierParts.push(
460
- `## Tools I Don't Have Yet\nI don't currently have access to: ${allDisabled.join(', ')}.\n` +
461
- '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`.',
462
451
  )
463
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
+ }
464
462
  }
465
463
 
466
464
  if (settings.suggestionsEnabled === true) {
@@ -475,6 +473,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
475
473
  )
476
474
  }
477
475
 
476
+ // Check for existing plan in mainLoopState to skip decomposition injection
477
+ const hasExistingPlan = Array.isArray(session.mainLoopState?.planSteps) && session.mainLoopState.planSteps.length > 0
478
+
478
479
  stateModifierParts.push(
479
480
  buildAgenticExecutionPolicy({
480
481
  enabledTools: sessionToolsWithImplicitProcess,
@@ -482,10 +483,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
482
483
  heartbeatPrompt,
483
484
  heartbeatIntervalSec,
484
485
  platformAssignScope: agentPlatformAssignScope,
486
+ userMessage: message,
487
+ hasExistingPlan,
485
488
  }),
486
489
  )
487
490
 
488
- const stateModifier = stateModifierParts.join('\n\n')
491
+ let stateModifier = stateModifierParts.join('\n\n')
489
492
 
490
493
  const { tools, cleanup } = await buildSessionTools(session.cwd, sessionToolsWithImplicitProcess, {
491
494
  agentId: session.agentId,
@@ -613,6 +616,19 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
613
616
  // Context manager failure — continue with full history
614
617
  }
615
618
 
619
+ // Context degradation warning: prepend warning to system prompt when nearing limits
620
+ try {
621
+ const { getContextDegradationWarning, estimateTokens: estTokens } = await import('./context-manager')
622
+ const sysTokens = estTokens(stateModifier)
623
+ const warning = getContextDegradationWarning(effectiveHistory, sysTokens, session.provider, session.model)
624
+ if (warning) {
625
+ stateModifierParts.unshift(warning)
626
+ stateModifier = stateModifierParts.join('\n\n')
627
+ }
628
+ } catch {
629
+ // Warning failure is non-critical
630
+ }
631
+
616
632
  // Apply context-clear boundary: slice from most recent context-clear marker
617
633
  let contextStart = 0
618
634
  for (let i = effectiveHistory.length - 1; i >= 0; i--) {
@@ -624,7 +640,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
624
640
  const postClearHistory = effectiveHistory.slice(contextStart)
625
641
 
626
642
  const langchainMessages: Array<HumanMessage | AIMessage> = []
627
- for (const m of postClearHistory.slice(-20)) {
643
+ for (const m of postClearHistory.slice(-30)) {
628
644
  if (m.role === 'user') {
629
645
  langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
630
646
  } else {
@@ -639,6 +655,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
639
655
  let fullText = ''
640
656
  let lastSegment = ''
641
657
  let hasToolCalls = false
658
+ let needsTextSeparator = false
642
659
  let totalInputTokens = 0
643
660
  let totalOutputTokens = 0
644
661
  let lastToolInput: unknown = null
@@ -662,135 +679,223 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
662
679
  }, runtime.ongoingLoopMaxRuntimeMs)
663
680
  : null
664
681
 
682
+ const MAX_AUTO_CONTINUES = 3
683
+ const MAX_TRANSIENT_RETRIES = 2
684
+ let autoContinueCount = 0
685
+ let transientRetryCount = 0
686
+
665
687
  try {
666
- const eventStream = agent.streamEvents(
667
- { messages: langchainMessages },
668
- { version: 'v2', recursionLimit, signal: abortController.signal },
669
- )
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)
670
698
 
671
- for await (const event of eventStream) {
672
- const kind = event.event
673
-
674
- if (kind === 'on_chat_model_stream') {
675
- const chunk = event.data?.chunk
676
- if (chunk?.content) {
677
- // content can be string or array of content blocks
678
- if (Array.isArray(chunk.content)) {
679
- for (const block of chunk.content) {
680
- // Anthropic extended thinking blocks
681
- if (block.type === 'thinking' && block.thinking) {
682
- accumulatedThinking += block.thinking
683
- write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
684
- // OpenClaw [[thinking]] prefix convention
685
- } else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
686
- accumulatedThinking += block.text.slice(12)
687
- write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
688
- } else if (block.text) {
689
- fullText += block.text
690
- lastSegment += block.text
691
- 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
+ }
692
745
  }
693
746
  }
694
- } else {
695
- const text = typeof chunk.content === 'string' ? chunk.content : ''
696
- if (text) {
697
- fullText += text
698
- lastSegment += text
699
- 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
700
758
  }
701
- }
702
- }
703
- } else if (kind === 'on_llm_end') {
704
- // Track token usage from LLM responses — check all known LangChain event shapes
705
- const output = event.data?.output
706
- const usage = output?.llmOutput?.tokenUsage
707
- || output?.llmOutput?.usage
708
- || output?.usage_metadata
709
- || output?.response_metadata?.usage
710
- || output?.response_metadata?.tokenUsage
711
- if (usage) {
712
- totalInputTokens += usage.promptTokens || usage.input_tokens || usage.prompt_tokens || 0
713
- totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
714
- }
715
- } else if (kind === 'on_tool_start') {
716
- hasToolCalls = true
717
- lastSegment = ''
718
- const toolName = event.name || 'unknown'
719
- const input = event.data?.input
720
- lastToolInput = input
721
- // Plugin hooks: beforeToolExec
722
- await pluginMgr.runHook('beforeToolExec', { toolName, input })
723
- const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
724
- logExecution(session.id, 'tool_call', `${toolName} invoked`, {
725
- agentId: session.agentId,
726
- detail: { toolName, input: inputStr?.slice(0, 4000) },
727
- })
728
- write(`data: ${JSON.stringify({
729
- t: 'tool_call',
730
- toolName,
731
- toolInput: inputStr,
732
- })}\n\n`)
733
- } else if (kind === 'on_tool_end') {
734
- const toolName = event.name || 'unknown'
735
- const output = event.data?.output
736
- const outputStr = typeof output === 'string'
737
- ? output
738
- : output?.content
739
- ? String(output.content)
740
- : JSON.stringify(output)
741
- // Plugin hooks: afterToolExec
742
- await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
743
- // Event-driven memory breadcrumbs
744
- if (session.agentId && (session.tools || []).includes('memory')) {
745
- try {
746
- const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
747
- if (breadcrumbTitle) {
748
- const memDb = getMemoryDb()
749
- 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'}`, {
750
814
  agentId: session.agentId,
751
- sessionId: session.id,
752
- category: 'breadcrumb',
753
- title: breadcrumbTitle,
754
- content: '',
815
+ detail: { toolName, filePath: inputObj?.filePath, sourcePath: inputObj?.sourcePath, destinationPath: inputObj?.destinationPath, success: !/^Error/i.test((outputStr || '').trim()) },
755
816
  })
756
817
  }
757
- } 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
+ }
758
834
  }
759
- lastToolInput = null
760
- logExecution(session.id, 'tool_result', `${toolName} returned`, {
761
- agentId: session.agentId,
762
- 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,
763
855
  })
764
- // Enriched file_op logging for file-mutating tools
765
- if (['write_file', 'edit_file', 'copy_file', 'move_file', 'delete_file'].includes(toolName)) {
766
- const inputData = event.data?.input
767
- const inputObj = typeof inputData === 'object' ? inputData : {}
768
- 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})`, {
861
+ agentId: session.agentId,
862
+ detail: { errName, errMsg },
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}`, {
769
869
  agentId: session.agentId,
770
- detail: { toolName, filePath: inputObj?.filePath, sourcePath: inputObj?.sourcePath, destinationPath: inputObj?.destinationPath, success: !/^Error/i.test((outputStr || '').trim()) },
870
+ detail: { errName, errMsg },
771
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
772
876
  }
773
- // Enriched commit logging for git operations
774
- if (toolName === 'execute_command' && outputStr) {
775
- const commitMatch = outputStr.match(/\[[\w/-]+\s+([a-f0-9]{7,40})\]/)
776
- if (commitMatch) {
777
- logExecution(session.id, 'commit', `git commit ${commitMatch[1]}`, {
778
- agentId: session.agentId,
779
- detail: { commitId: commitMatch[1], outputPreview: outputStr.slice(0, 500) },
780
- })
781
- }
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 }))
782
887
  }
783
- write(`data: ${JSON.stringify({
784
- t: 'tool_result',
785
- toolName,
786
- toolOutput: outputStr?.slice(0, 2000),
787
- })}\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))
788
893
  }
789
894
  }
790
- } catch (err: any) {
895
+ } catch (err: unknown) {
791
896
  const errMsg = timedOut
792
897
  ? 'Ongoing loop stopped after reaching the configured runtime limit.'
793
- : err.message || String(err)
898
+ : err instanceof Error ? err.message : String(err)
794
899
  logExecution(session.id, 'error', errMsg, { agentId: session.agentId, detail: { timedOut } })
795
900
  write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
796
901
  } finally {
@@ -836,6 +941,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
836
941
  totalTokens,
837
942
  estimatedCost: cost,
838
943
  timestamp: Date.now(),
944
+ durationMs: Date.now() - startTs,
839
945
  }
840
946
  appendUsage(session.id, usageRecord)
841
947
  // Send usage metadata to client