@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -4,30 +4,18 @@ import { HumanMessage, AIMessage } from '@langchain/core/messages'
4
4
  import { buildSessionTools } from './session-tools'
5
5
  import { buildChatModel } from './build-llm'
6
6
  import { loadSettings, loadAgents, loadSkills, appendUsage } from './storage'
7
- import { estimateCost } from './cost'
7
+ import { estimateCost, buildPluginDefinitionCosts } from './cost'
8
8
  import { getPluginManager } from './plugins'
9
9
  import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
10
- import { getMemoryDb } from './memory-db'
10
+
11
11
  import { logExecution } from './execution-log'
12
12
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
13
- import { expandToolIds } from './tool-aliases'
14
- import type { Session, Message, UsageRecord } from '@/types'
13
+ import { expandPluginIds } from './tool-aliases'
14
+ import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
15
15
  import { extractSuggestions } from './suggestions'
16
+ import { buildIdentityContinuityContext } from './identity-continuity'
16
17
 
17
18
  /** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
18
- function extractBreadcrumbTitle(toolName: string, input: unknown, output: string | undefined): string | null {
19
- if (!input || typeof input !== 'object') return null
20
- const inp = input as Record<string, unknown>
21
- const action = typeof inp.action === 'string' ? inp.action : ''
22
- if (toolName === 'manage_tasks') {
23
- if (action === 'create') return `Created task: ${inp.title || 'Untitled'}`
24
- if (output && /status.*completed|completed.*successfully/i.test(output)) return `Completed task: ${inp.title || inp.taskId || 'unknown'}`
25
- }
26
- if (toolName === 'manage_schedules' && action === 'create') return `Created schedule: ${inp.name || 'Untitled'}`
27
- if (toolName === 'manage_agents' && action === 'create') return `Created agent: ${inp.name || 'Untitled'}`
28
- return null
29
- }
30
-
31
19
  interface StreamAgentChatOpts {
32
20
  session: Session
33
21
  message: string
@@ -41,41 +29,17 @@ interface StreamAgentChatOpts {
41
29
  signal?: AbortSignal
42
30
  }
43
31
 
44
- function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
45
- const lines: string[] = []
46
- if (enabledTools.includes('shell')) lines.push('- I can run shell commands (`execute_command`) — servers, installs, scripts, git, builds, anything. I can run things in the background for long-lived processes like dev servers.')
47
- if (enabledTools.includes('process')) lines.push('- I can manage running processes (`process_tool`) — check status, read logs, send input, or stop them.')
48
- if (enabledTools.includes('files') || enabledTools.includes('copy_file') || enabledTools.includes('move_file') || enabledTools.includes('delete_file')) {
49
- lines.push('- I can read, write, copy, move, and send files (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). Deleting files is destructive, so that may need explicit permission.')
50
- }
51
- if (enabledTools.includes('edit_file')) lines.push('- I can make precise edits to files (`edit_file`) — surgical find-and-replace without rewriting the whole file.')
52
- if (enabledTools.includes('web_search')) lines.push('- I can search the web (`web_search`) for research, fact-checking, and discovery.')
53
- if (enabledTools.includes('web_fetch')) lines.push('- I can fetch and read web pages (`web_fetch`) to pull in real content for analysis.')
54
- if (enabledTools.includes('browser')) lines.push('- I can control a browser (`browser`) — navigate sites, fill forms, take screenshots, interact with web apps.')
55
- if (enabledTools.includes('claude_code')) lines.push('- I can hand off deep coding work to Claude Code (`delegate_to_claude_code`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
56
- if (enabledTools.includes('codex_cli')) lines.push('- I can hand off deep coding work to Codex (`delegate_to_codex_cli`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
57
- if (enabledTools.includes('opencode_cli')) lines.push('- I can hand off deep coding work to OpenCode (`delegate_to_opencode_cli`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.')
58
- if (enabledTools.includes('memory')) lines.push('- I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.')
59
- if (enabledTools.includes('sandbox')) lines.push('- I can run code in a sandbox (`sandbox_exec`) — JS/TS via Deno or Python, in an isolated environment. I get stdout, stderr, and any files created.')
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.')
61
- if (enabledTools.includes('manage_tasks')) lines.push('- I can manage tasks (`manage_tasks`) — create plans, track progress, and stay organized over time.')
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.')
64
- if (enabledTools.includes('manage_documents')) lines.push('- I can store and search documents (`manage_documents`) for long-term knowledge and reference.')
65
- if (enabledTools.includes('manage_webhooks')) lines.push('- I can register webhooks (`manage_webhooks`) so external events can trigger my work automatically.')
66
- if (enabledTools.includes('manage_skills')) lines.push('- I can manage reusable skills (`manage_skills`) — building blocks I can learn and apply.')
67
- if (enabledTools.includes('manage_connectors')) lines.push('- I can manage messaging channels (`manage_connectors`) — WhatsApp, Telegram, Slack, Discord — and send proactive messages via `connector_message_tool`.')
68
- if (enabledTools.includes('manage_sessions')) lines.push('- I can manage chat sessions (`manage_sessions`, `sessions_tool`, `whoami_tool`, `search_history_tool`) — check my identity, look up past conversations, message other sessions, and coordinate work.')
69
- // Context tools are available to any session with tools (not just manage_sessions)
70
- if (enabledTools.length > 0) {
32
+ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
33
+ // Collect capability descriptions dynamically from plugins
34
+ const lines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
35
+
36
+ // Context tools are available to any session with plugins
37
+ if (enabledPlugins.length > 0) {
71
38
  lines.push('- I can monitor my own context usage (`context_status`) and compact my conversation history (`context_summarize`) when I\'m running low on space.')
72
39
  if (opts?.platformAssignScope === 'all') {
73
40
  lines.push('- I can delegate tasks to other agents (`delegate_to_agent`) based on their strengths and availability.')
74
41
  }
75
42
  }
76
- if (enabledTools.includes('manage_secrets')) lines.push('- I can store and retrieve encrypted secrets (`manage_secrets`) — API keys, credentials, tokens.')
77
- if (enabledTools.includes('manage_chatrooms')) lines.push('- I can create and participate in chatrooms (`manage_chatrooms`) for multi-agent collaboration with @mention-based discussions.')
78
- if (enabledTools.includes('wallet')) lines.push('- I have my own crypto wallet (`wallet_tool`) — I can check my balance, send SOL, and review my transaction history.')
79
43
  return lines
80
44
  }
81
45
 
@@ -96,24 +60,21 @@ const GOAL_DECOMPOSITION_BLOCK = [
96
60
  'When you receive a broad, open-ended goal:',
97
61
  '1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
98
62
  '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"}',
63
+ '3. Present the plan as a short checklist or numbered list in plain language.',
100
64
  '4. Execute the first subtask immediately — do not stop after planning.',
101
65
  '5. After each subtask, update progress and move to the next.',
102
66
  ].join('\n')
103
67
 
104
68
  function buildAgenticExecutionPolicy(opts: {
105
- enabledTools: string[]
69
+ enabledPlugins: string[]
106
70
  loopMode: 'bounded' | 'ongoing'
107
71
  heartbeatPrompt: string
108
72
  heartbeatIntervalSec: number
109
73
  platformAssignScope?: 'self' | 'all'
110
74
  userMessage?: string
111
- hasExistingPlan?: boolean
112
75
  }) {
113
- const hasTooling = opts.enabledTools.length > 0
114
- const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
115
- const has = (t: string) => opts.enabledTools.includes(t)
116
- const hasDelegationTool = has('claude_code') || has('codex_cli') || has('opencode_cli')
76
+ const hasTooling = opts.enabledPlugins.length > 0
77
+ const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
117
78
 
118
79
  const parts: string[] = []
119
80
 
@@ -130,30 +91,9 @@ function buildAgenticExecutionPolicy(opts: {
130
91
  : 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
131
92
  )
132
93
 
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.')
94
+ // Plugin-specific operating guidance (collected dynamically from plugins)
95
+ const guidanceLines = getPluginManager().collectOperatingGuidance(opts.enabledPlugins)
96
+ if (guidanceLines.length) parts.push(...guidanceLines)
157
97
 
158
98
  // Response behavior
159
99
  parts.push(
@@ -162,13 +102,15 @@ function buildAgenticExecutionPolicy(opts: {
162
102
  'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
163
103
  'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
164
104
  'Never repeat completed side effects. Verify state first.',
105
+ 'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
106
+ 'Delegation is optional, not a stopping condition. If one delegate backend is unavailable or unauthenticated, try another delegate backend or continue with your other tools.',
107
+ 'Only mention files, screenshots, URLs, or download links that were actually returned by tools. Copy returned links exactly; do not rewrite them or prepend extra prefixes like "sandbox:".',
165
108
  `Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
166
109
  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
110
  )
169
111
 
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)
112
+ if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
113
+ if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
172
114
 
173
115
  return parts.filter(Boolean).join('\n')
174
116
  }
@@ -184,10 +126,10 @@ export interface StreamAgentChatResult {
184
126
  export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
185
127
  const startTs = Date.now()
186
128
  const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
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,
129
+ const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
130
+ const hasShellCapability = rawPlugins.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
131
+ const sessionPlugins = expandPluginIds([
132
+ ...rawPlugins,
191
133
  ...(hasShellCapability ? ['process'] : []),
192
134
  ])
193
135
 
@@ -196,7 +138,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
196
138
 
197
139
  // Resolve agent's thinking level for provider-native params
198
140
  let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
199
- if (session.agentId) {
141
+ if (session.thinkingLevel) {
142
+ agentThinkingLevel = session.thinkingLevel
143
+ } else if (session.agentId) {
200
144
  const agentsForThinking = loadAgents()
201
145
  agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
202
146
  }
@@ -252,6 +196,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
252
196
  if (agent?.description) identityLines.push(agent.description)
253
197
  identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
254
198
  stateModifierParts.push(identityLines.join(' '))
199
+ const continuityBlock = buildIdentityContinuityContext(session, agent)
200
+ if (continuityBlock) stateModifierParts.push(continuityBlock)
255
201
  if (agent?.soul) stateModifierParts.push(agent.soul)
256
202
  if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
257
203
  if (agent?.skillIds?.length) {
@@ -279,109 +225,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
279
225
  stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
280
226
  }
281
227
 
282
- if ((session.tools || []).includes('memory') && session.agentId) {
283
- try {
284
- const memDb = getMemoryDb()
285
- const memoryQuerySeed = [
286
- message,
287
- ...history
288
- .slice(-4)
289
- .filter((h) => h.role === 'user')
290
- .map((h) => h.text),
291
- ].join('\n')
292
-
293
- const seen = new Set<string>()
294
- const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
295
- const category = String(m.category || 'note')
296
- const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
297
- const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
298
- const pin = m.pinned ? ' [pinned]' : ''
299
- return `- [${category}]${pin} ${title}: ${snippet}`
300
- }
301
-
302
- // Pinned memories always appear first
303
- const pinned = memDb.listPinned(session.agentId, 5)
304
- const pinnedLines = pinned
305
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
306
- .map(formatMemoryLine)
307
-
308
- // Reduce relevant slice by pinned count to keep total context bounded
309
- const relevantSlice = Math.max(2, 6 - pinnedLines.length)
310
- const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
311
- const relevant = relevantLookup.entries.slice(0, relevantSlice)
312
- const recent = memDb.list(session.agentId, 12).slice(0, 6)
313
-
314
- const relevantLines = relevant
315
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
316
- .map(formatMemoryLine)
317
-
318
- const recentLines = recent
319
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
320
- .map(formatMemoryLine)
321
-
322
- const memorySections: string[] = []
323
- if (pinnedLines.length) {
324
- memorySections.push(
325
- ['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'),
326
- )
327
- }
328
- if (relevantLines.length) {
329
- memorySections.push(
330
- ['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'),
331
- )
332
- }
333
- if (recentLines.length) {
334
- memorySections.push(
335
- ['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'),
336
- )
337
- }
338
-
339
- if (memorySections.length) {
340
- stateModifierParts.push(memorySections.join('\n\n'))
341
- }
342
-
343
- // Memory Policy — always injected when memory tool is available
344
- stateModifierParts.push([
345
- '## My Memory',
346
- 'I have long-term memory that persists across conversations. I use it naturally — I don\'t wait to be asked to remember things.',
347
- '',
348
- '**Things worth remembering:**',
349
- '- What the user likes, dislikes, or has corrected me on',
350
- '- Important decisions, outcomes, and lessons learned',
351
- '- What I\'ve discovered about projects, codebases, or environments',
352
- '- Problems I\'ve hit and how I solved them',
353
- '- Who people are and how they relate to each other',
354
- '- Configuration details and environment specifics that I\'ll need again',
355
- '',
356
- '**Not worth cluttering my memory with:**',
357
- '- Throwaway acknowledgments or small talk',
358
- '- Work-in-progress that\'ll change soon (use category "working" for scratch notes)',
359
- '- Things already in my system prompt',
360
- '- Something I\'ve already stored',
361
- '',
362
- '**Good habits:**',
363
- '- Give memories clear titles ("User prefers dark mode" not "Note 1")',
364
- '- Use categories: preference, fact, learning, project, identity, decision',
365
- '- Check what I already know before storing something new',
366
- '- When I learn something that corrects old knowledge, update or remove the old memory',
367
- ].join('\n'))
368
-
369
- // Pre-compaction memory flush: nudge agent to save important context before it's lost
370
- const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
371
- if (msgCount > 20) {
372
- stateModifierParts.push([
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.',
376
- ].join('\n'))
377
- }
378
- } catch {
379
- // If memory context fails to load, continue without blocking the run.
380
- }
381
- }
382
-
383
228
  // Inject agent awareness (Phase 2: agents know about each other)
384
- if ((session.tools || []).length > 0 && session.agentId) {
229
+ if ((session.plugins || []).length > 0 && session.agentId) {
385
230
  try {
386
231
  const { buildAgentAwarenessBlock } = await import('./agent-registry')
387
232
  const awarenessBlock = buildAgentAwarenessBlock(session.agentId)
@@ -391,39 +236,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
391
236
  }
392
237
  }
393
238
 
394
- // Inject wallet context when agent has wallet tool enabled and a linked wallet
395
- if ((session.tools || []).includes('wallet') && session.agentId) {
396
- try {
397
- const { loadWallets } = await import('./storage')
398
- const wallets = loadWallets() as Record<string, { agentId: string; publicKey: string; chain: string; spendingLimitLamports?: number; dailyLimitLamports?: number; requireApproval: boolean }>
399
- const wallet = Object.values(wallets).find((w) => w.agentId === session.agentId)
400
- if (wallet) {
401
- const { getBalance, lamportsToSol } = await import('./solana')
402
- let balanceSol = 0
403
- try {
404
- const lamports = await getBalance(wallet.publicKey)
405
- balanceSol = lamportsToSol(lamports)
406
- } catch { /* RPC failure — show 0 */ }
407
- const perTxLimit = lamportsToSol(wallet.spendingLimitLamports ?? 100_000_000)
408
- const dailyLimit = lamportsToSol(wallet.dailyLimitLamports ?? 1_000_000_000)
409
- stateModifierParts.push([
410
- '## Your Wallet',
411
- `You own a ${wallet.chain} wallet — this is your wallet, your funds, your address. Speak about it in the first person ("my wallet", "my balance", "I have X SOL"). You can spend from it freely within your limits${wallet.requireApproval ? ', though the user needs to approve each send before it goes through' : ''}.`,
412
- `- Address: ${wallet.publicKey}`,
413
- `- Balance: ${balanceSol} SOL`,
414
- `- Per-transaction limit: ${perTxLimit} SOL`,
415
- `- Daily limit: ${dailyLimit} SOL`,
416
- 'Use the `wallet_tool` to check your balance, send SOL, or view your transaction history.',
417
- ].join('\n'))
418
- }
419
- } catch {
420
- // Wallet context is non-critical
421
- }
239
+ // Collect dynamic context from enabled plugins (wallet, memory, etc.)
240
+ try {
241
+ const pluginContextParts = await getPluginManager().collectAgentContext(session, sessionPlugins, message, history)
242
+ stateModifierParts.push(...pluginContextParts)
243
+ } catch {
244
+ // Plugin context injection is non-critical
422
245
  }
423
246
 
424
247
  // Tell the LLM about available plugins and their access status
425
248
  {
426
- const agentEnabledSet = new Set(sessionToolsWithImplicitProcess)
249
+ const agentEnabledSet = new Set(sessionPlugins)
427
250
  const { getPluginManager } = await import('./plugins')
428
251
  const allPlugins = getPluginManager().listPlugins()
429
252
  const mcpDisabled = agentMcpDisabledTools ?? []
@@ -473,24 +296,20 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
473
296
  )
474
297
  }
475
298
 
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
-
479
299
  stateModifierParts.push(
480
300
  buildAgenticExecutionPolicy({
481
- enabledTools: sessionToolsWithImplicitProcess,
301
+ enabledPlugins: sessionPlugins,
482
302
  loopMode: runtime.loopMode,
483
303
  heartbeatPrompt,
484
304
  heartbeatIntervalSec,
485
305
  platformAssignScope: agentPlatformAssignScope,
486
306
  userMessage: message,
487
- hasExistingPlan,
488
307
  }),
489
308
  )
490
309
 
491
310
  let stateModifier = stateModifierParts.join('\n\n')
492
311
 
493
- const { tools, cleanup } = await buildSessionTools(session.cwd, sessionToolsWithImplicitProcess, {
312
+ const { tools, cleanup, toolToPluginMap } = await buildSessionTools(session.cwd, sessionPlugins, {
494
313
  agentId: session.agentId,
495
314
  sessionId: session.id,
496
315
  platformAssignScope: agentPlatformAssignScope,
@@ -582,12 +401,27 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
582
401
  return parts
583
402
  }
584
403
 
585
- // Auto-compaction: prune old history if approaching context window limit
586
- let effectiveHistory = history
404
+ // Apply context-clear boundary: slice from most recent context-clear marker
405
+ let contextStart = 0
406
+ for (let i = history.length - 1; i >= 0; i--) {
407
+ if (history[i].kind === 'context-clear') {
408
+ contextStart = i + 1
409
+ break
410
+ }
411
+ }
412
+ const postClearHistory = history.slice(contextStart)
413
+
414
+ // Hard cap: only send the most recent 30 messages to the LLM
415
+ const recentHistory = postClearHistory.slice(-30)
416
+
417
+ // Auto-compaction: only trigger if the messages we'll actually send exceed context limits.
418
+ // The .slice(-30) hard cap already prevents context overflow for long conversations,
419
+ // so this only fires for sessions with very large individual messages.
420
+ let effectiveHistory = recentHistory
587
421
  try {
588
422
  const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
589
423
  const systemPromptTokens = estimateTokens(stateModifier)
590
- if (shouldAutoCompact(history, systemPromptTokens, session.provider, session.model)) {
424
+ if (shouldAutoCompact(recentHistory, systemPromptTokens, session.provider, session.model)) {
591
425
  const summarize = async (prompt: string): Promise<string> => {
592
426
  const response = await llm.invoke([new HumanMessage(prompt)])
593
427
  if (typeof response.content === 'string') return response.content
@@ -599,7 +433,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
599
433
  return ''
600
434
  }
601
435
  const result = await llmCompact({
602
- messages: history,
436
+ messages: recentHistory,
603
437
  provider: session.provider,
604
438
  model: session.model,
605
439
  agentId: session.agentId || null,
@@ -608,12 +442,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
608
442
  })
609
443
  effectiveHistory = result.messages
610
444
  console.log(
611
- `[stream-agent-chat] Auto-compacted ${session.id}: ${history.length} → ${effectiveHistory.length} msgs` +
445
+ `[stream-agent-chat] Auto-compacted ${session.id}: ${recentHistory.length} → ${effectiveHistory.length} msgs` +
612
446
  (result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
613
447
  )
614
448
  }
615
449
  } catch {
616
- // Context manager failure — continue with full history
450
+ // Context manager failure — continue with recent history
617
451
  }
618
452
 
619
453
  // Context degradation warning: prepend warning to system prompt when nearing limits
@@ -629,18 +463,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
629
463
  // Warning failure is non-critical
630
464
  }
631
465
 
632
- // Apply context-clear boundary: slice from most recent context-clear marker
633
- let contextStart = 0
634
- for (let i = effectiveHistory.length - 1; i >= 0; i--) {
635
- if (effectiveHistory[i].kind === 'context-clear') {
636
- contextStart = i + 1
637
- break
638
- }
639
- }
640
- const postClearHistory = effectiveHistory.slice(contextStart)
641
-
642
466
  const langchainMessages: Array<HumanMessage | AIMessage> = []
643
- for (const m of postClearHistory.slice(-30)) {
467
+ for (const m of effectiveHistory) {
644
468
  if (m.role === 'user') {
645
469
  langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
646
470
  } else {
@@ -658,12 +482,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
658
482
  let needsTextSeparator = false
659
483
  let totalInputTokens = 0
660
484
  let totalOutputTokens = 0
661
- let lastToolInput: unknown = null
662
485
  let accumulatedThinking = ''
486
+ const pluginInvocations: PluginInvocationRecord[] = []
487
+ let currentToolInputTokens = 0
663
488
 
664
489
  // Plugin hooks: beforeAgentStart
665
490
  const pluginMgr = getPluginManager()
666
- await pluginMgr.runHook('beforeAgentStart', { session, message })
491
+ await pluginMgr.runHook('beforeAgentStart', { session, message }, { enabledIds: sessionPlugins })
667
492
 
668
493
  const abortController = new AbortController()
669
494
  const abortFromSignal = () => abortController.abort()
@@ -688,6 +513,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
688
513
  const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
689
514
  for (let iteration = 0; iteration <= maxIterations; iteration++) {
690
515
  let shouldContinue: 'recursion' | 'transient' | false = false
516
+ let waitingForToolResult = false
517
+ let idleTimedOut = false
518
+ let idleTimer: ReturnType<typeof setTimeout> | null = null
691
519
 
692
520
  // Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
693
521
  // Linked to the parent so client disconnect / timeout still propagates.
@@ -696,7 +524,24 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
696
524
  if (abortController.signal.aborted) iterationController.abort()
697
525
  else abortController.signal.addEventListener('abort', onParentAbort)
698
526
 
527
+ const clearIdleWatchdog = () => {
528
+ if (idleTimer) {
529
+ clearTimeout(idleTimer)
530
+ idleTimer = null
531
+ }
532
+ }
533
+
534
+ const armIdleWatchdog = () => {
535
+ clearIdleWatchdog()
536
+ if (waitingForToolResult || iterationController.signal.aborted) return
537
+ idleTimer = setTimeout(() => {
538
+ idleTimedOut = true
539
+ iterationController.abort()
540
+ }, 90_000)
541
+ }
542
+
699
543
  try {
544
+ armIdleWatchdog()
700
545
  const eventStream = agent.streamEvents(
701
546
  { messages: langchainMessages },
702
547
  { version: 'v2', recursionLimit, signal: iterationController.signal },
@@ -706,6 +551,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
706
551
  const kind = event.event
707
552
 
708
553
  if (kind === 'on_chat_model_stream') {
554
+ armIdleWatchdog()
709
555
  const chunk = event.data?.chunk
710
556
  if (chunk?.content) {
711
557
  // content can be string or array of content blocks
@@ -745,6 +591,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
745
591
  }
746
592
  }
747
593
  } else if (kind === 'on_llm_end') {
594
+ armIdleWatchdog()
748
595
  // Track token usage from LLM responses — check all known LangChain event shapes
749
596
  const output = event.data?.output
750
597
  const usage = output?.llmOutput?.tokenUsage
@@ -757,15 +604,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
757
604
  totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
758
605
  }
759
606
  } else if (kind === 'on_tool_start') {
607
+ clearIdleWatchdog()
608
+ waitingForToolResult = true
760
609
  hasToolCalls = true
761
610
  needsTextSeparator = true
762
611
  lastSegment = ''
763
612
  const toolName = event.name || 'unknown'
764
613
  const input = event.data?.input
765
- lastToolInput = input
766
- // Plugin hooks: beforeToolExec
767
- await pluginMgr.runHook('beforeToolExec', { toolName, input })
614
+ // Estimate input tokens for plugin invocation tracking
768
615
  const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
616
+ currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
769
617
  logExecution(session.id, 'tool_call', `${toolName} invoked`, {
770
618
  agentId: session.agentId,
771
619
  detail: { toolName, input: inputStr?.slice(0, 4000) },
@@ -776,6 +624,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
776
624
  toolInput: inputStr,
777
625
  })}\n\n`)
778
626
  } else if (kind === 'on_tool_end') {
627
+ waitingForToolResult = false
628
+ armIdleWatchdog()
779
629
  const toolName = event.name || 'unknown'
780
630
  const output = event.data?.output
781
631
  const outputStr = typeof output === 'string'
@@ -783,25 +633,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
783
633
  : output?.content
784
634
  ? String(output.content)
785
635
  : 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
636
  logExecution(session.id, 'tool_result', `${toolName} returned`, {
806
637
  agentId: session.agentId,
807
638
  detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
@@ -825,6 +656,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
825
656
  })
826
657
  }
827
658
  }
659
+ // Track plugin invocation token estimates
660
+ const pluginId = toolToPluginMap[toolName] || '_unknown'
661
+ pluginInvocations.push({
662
+ pluginId,
663
+ toolName,
664
+ inputTokens: currentToolInputTokens,
665
+ outputTokens: Math.ceil((outputStr?.length || 0) / 4),
666
+ })
667
+ currentToolInputTokens = 0
668
+
828
669
  write(`data: ${JSON.stringify({
829
670
  t: 'tool_result',
830
671
  toolName,
@@ -834,7 +675,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
834
675
  }
835
676
  } catch (innerErr: unknown) {
836
677
  const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
837
- const errMsg = innerErr instanceof Error ? innerErr.message : String(innerErr)
678
+ const errMsg = idleTimedOut
679
+ ? 'Model stream stalled without emitting text or tool results for 90 seconds.'
680
+ : innerErr instanceof Error ? innerErr.message : String(innerErr)
838
681
  const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
839
682
 
840
683
  // Classify the error:
@@ -842,9 +685,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
842
685
  // 2. Transient abort/timeout — LLM API failure, not from client disconnect
843
686
  const isRecursionError = errName === 'GraphRecursionError'
844
687
  || /recursion limit|maximum recursion/i.test(errMsg)
845
- const isTransientAbort = !isRecursionError
688
+ const isTransientAbort = (!isRecursionError && idleTimedOut)
689
+ || (!isRecursionError
846
690
  && /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
847
- && !abortController.signal.aborted
691
+ && !abortController.signal.aborted)
848
692
 
849
693
  // Log diagnostic details for every error so we can trace root causes
850
694
  console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
@@ -875,6 +719,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
875
719
  throw innerErr
876
720
  }
877
721
  } finally {
722
+ clearIdleWatchdog()
878
723
  abortController.signal.removeEventListener('abort', onParentAbort)
879
724
  }
880
725
 
@@ -931,6 +776,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
931
776
  const totalTokens = totalInputTokens + totalOutputTokens
932
777
  if (totalTokens > 0) {
933
778
  const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
779
+ const pluginDefinitionCosts = buildPluginDefinitionCosts(tools, toolToPluginMap)
934
780
  const usageRecord: UsageRecord = {
935
781
  sessionId: session.id,
936
782
  messageIndex: history.length,
@@ -942,6 +788,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
942
788
  estimatedCost: cost,
943
789
  timestamp: Date.now(),
944
790
  durationMs: Date.now() - startTs,
791
+ pluginDefinitionCosts,
792
+ pluginInvocations: pluginInvocations.length > 0 ? pluginInvocations : undefined,
945
793
  }
946
794
  appendUsage(session.id, usageRecord)
947
795
  // Send usage metadata to client
@@ -952,7 +800,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
952
800
  }
953
801
 
954
802
  // Plugin hooks: afterAgentComplete
955
- await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
803
+ await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
956
804
 
957
805
  // OpenClaw auto-sync: push memory if enabled
958
806
  try {