@swarmclawai/swarmclaw 0.7.0 → 0.7.2

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 (119) hide show
  1. package/README.md +85 -139
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/thread/route.ts +1 -2
  4. package/src/app/api/agents/route.ts +1 -1
  5. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  6. package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
  7. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  8. package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
  9. package/src/app/api/{sessions → chats}/route.ts +5 -7
  10. package/src/app/api/plugins/route.ts +3 -0
  11. package/src/app/api/plugins/settings/route.ts +35 -0
  12. package/src/app/api/usage/route.ts +30 -0
  13. package/src/cli/index.js +35 -33
  14. package/src/cli/index.ts +40 -39
  15. package/src/cli/spec.js +29 -27
  16. package/src/components/agents/agent-card.tsx +1 -1
  17. package/src/components/agents/agent-chat-list.tsx +3 -3
  18. package/src/components/agents/agent-list.tsx +8 -13
  19. package/src/components/agents/agent-sheet.tsx +2 -2
  20. package/src/components/agents/cron-job-form.tsx +3 -3
  21. package/src/components/agents/inspector-panel.tsx +2 -2
  22. package/src/components/auth/setup-wizard.tsx +5 -38
  23. package/src/components/chat/chat-area.tsx +10 -14
  24. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
  25. package/src/components/chat/chat-header.tsx +156 -73
  26. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
  27. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  28. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  29. package/src/components/chat/message-bubble.tsx +4 -1
  30. package/src/components/chat/message-list.tsx +2 -2
  31. package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
  32. package/src/components/chat/session-debug-panel.tsx +1 -1
  33. package/src/components/chat/tool-request-banner.tsx +3 -3
  34. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  35. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  36. package/src/components/connectors/connector-sheet.tsx +1 -1
  37. package/src/components/home/home-view.tsx +1 -1
  38. package/src/components/layout/app-layout.tsx +23 -2
  39. package/src/components/plugins/plugin-list.tsx +475 -254
  40. package/src/components/plugins/plugin-sheet.tsx +124 -10
  41. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  42. package/src/components/shared/command-palette.tsx +0 -1
  43. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  44. package/src/components/shared/settings/section-providers.tsx +1 -1
  45. package/src/components/shared/settings/settings-page.tsx +1 -12
  46. package/src/components/usage/metrics-dashboard.tsx +73 -0
  47. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  48. package/src/lib/chat.ts +1 -1
  49. package/src/lib/{sessions.ts → chats.ts} +28 -18
  50. package/src/lib/providers/claude-cli.ts +1 -1
  51. package/src/lib/server/approvals.ts +4 -4
  52. package/src/lib/server/capability-router.ts +10 -8
  53. package/src/lib/server/chat-execution.ts +36 -105
  54. package/src/lib/server/chatroom-helpers.ts +3 -3
  55. package/src/lib/server/connectors/manager.ts +4 -4
  56. package/src/lib/server/cost.ts +34 -1
  57. package/src/lib/server/daemon-state.ts +2 -2
  58. package/src/lib/server/heartbeat-service.ts +1 -1
  59. package/src/lib/server/main-agent-loop.ts +25 -160
  60. package/src/lib/server/main-session.ts +6 -13
  61. package/src/lib/server/orchestrator-lg.ts +3 -3
  62. package/src/lib/server/orchestrator.ts +5 -5
  63. package/src/lib/server/plugins.ts +112 -4
  64. package/src/lib/server/provider-health.ts +5 -3
  65. package/src/lib/server/queue.ts +12 -10
  66. package/src/lib/server/session-run-manager.test.ts +9 -6
  67. package/src/lib/server/session-run-manager.ts +1 -3
  68. package/src/lib/server/session-tools/calendar.ts +376 -0
  69. package/src/lib/server/session-tools/canvas.ts +1 -1
  70. package/src/lib/server/session-tools/chatroom.ts +4 -2
  71. package/src/lib/server/session-tools/connector.ts +5 -2
  72. package/src/lib/server/session-tools/context.ts +7 -3
  73. package/src/lib/server/session-tools/crud.ts +14 -6
  74. package/src/lib/server/session-tools/delegate.ts +95 -8
  75. package/src/lib/server/session-tools/discovery.ts +2 -2
  76. package/src/lib/server/session-tools/edit_file.ts +4 -2
  77. package/src/lib/server/session-tools/email.ts +322 -0
  78. package/src/lib/server/session-tools/file.ts +5 -2
  79. package/src/lib/server/session-tools/git.ts +1 -1
  80. package/src/lib/server/session-tools/http.ts +1 -1
  81. package/src/lib/server/session-tools/image-gen.ts +382 -0
  82. package/src/lib/server/session-tools/index.ts +74 -49
  83. package/src/lib/server/session-tools/memory.ts +139 -2
  84. package/src/lib/server/session-tools/monitor.ts +1 -1
  85. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  86. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  87. package/src/lib/server/session-tools/platform.ts +6 -3
  88. package/src/lib/server/session-tools/plugin-creator.ts +3 -3
  89. package/src/lib/server/session-tools/replicate.ts +303 -0
  90. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  91. package/src/lib/server/session-tools/sandbox.ts +4 -2
  92. package/src/lib/server/session-tools/schedule.ts +4 -2
  93. package/src/lib/server/session-tools/session-info.ts +7 -4
  94. package/src/lib/server/session-tools/shell.ts +5 -2
  95. package/src/lib/server/session-tools/subagent.ts +2 -2
  96. package/src/lib/server/session-tools/wallet.ts +29 -2
  97. package/src/lib/server/session-tools/web.ts +51 -6
  98. package/src/lib/server/storage.ts +29 -9
  99. package/src/lib/server/stream-agent-chat.ts +72 -249
  100. package/src/lib/server/tool-aliases.ts +26 -15
  101. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  102. package/src/lib/server/tool-capability-policy.ts +32 -27
  103. package/src/lib/tool-definitions.ts +4 -0
  104. package/src/lib/validation/schemas.ts +3 -1
  105. package/src/stores/use-app-store.ts +5 -5
  106. package/src/stores/use-chat-store.ts +7 -7
  107. package/src/types/index.ts +65 -3
  108. /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
  109. /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
  110. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  111. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  112. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  113. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  114. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  115. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  116. /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
  117. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  118. /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
  119. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -4,30 +4,17 @@ 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
16
 
17
17
  /** 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
18
  interface StreamAgentChatOpts {
32
19
  session: Session
33
20
  message: string
@@ -41,41 +28,17 @@ interface StreamAgentChatOpts {
41
28
  signal?: AbortSignal
42
29
  }
43
30
 
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) {
31
+ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
32
+ // Collect capability descriptions dynamically from plugins
33
+ const lines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
34
+
35
+ // Context tools are available to any session with plugins
36
+ if (enabledPlugins.length > 0) {
71
37
  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
38
  if (opts?.platformAssignScope === 'all') {
73
39
  lines.push('- I can delegate tasks to other agents (`delegate_to_agent`) based on their strengths and availability.')
74
40
  }
75
41
  }
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
42
  return lines
80
43
  }
81
44
 
@@ -102,7 +65,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
102
65
  ].join('\n')
103
66
 
104
67
  function buildAgenticExecutionPolicy(opts: {
105
- enabledTools: string[]
68
+ enabledPlugins: string[]
106
69
  loopMode: 'bounded' | 'ongoing'
107
70
  heartbeatPrompt: string
108
71
  heartbeatIntervalSec: number
@@ -110,10 +73,8 @@ function buildAgenticExecutionPolicy(opts: {
110
73
  userMessage?: string
111
74
  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(
@@ -167,7 +107,7 @@ function buildAgenticExecutionPolicy(opts: {
167
107
  'For SWARM_MAIN_MISSION_TICK / SWARM_MAIN_AUTO_FOLLOWUP messages, follow the response contract and include [MAIN_LOOP_META] JSON.',
168
108
  )
169
109
 
170
- if (toolLines.length) parts.push('What I can do:\n' + toolLines.join('\n'))
110
+ if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
171
111
  if (opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
172
112
 
173
113
  return parts.filter(Boolean).join('\n')
@@ -184,10 +124,10 @@ export interface StreamAgentChatResult {
184
124
  export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
185
125
  const startTs = Date.now()
186
126
  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,
127
+ const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
128
+ const hasShellCapability = rawPlugins.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
129
+ const sessionPlugins = expandPluginIds([
130
+ ...rawPlugins,
191
131
  ...(hasShellCapability ? ['process'] : []),
192
132
  ])
193
133
 
@@ -279,109 +219,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
279
219
  stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
280
220
  }
281
221
 
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
222
  // Inject agent awareness (Phase 2: agents know about each other)
384
- if ((session.tools || []).length > 0 && session.agentId) {
223
+ if ((session.plugins || []).length > 0 && session.agentId) {
385
224
  try {
386
225
  const { buildAgentAwarenessBlock } = await import('./agent-registry')
387
226
  const awarenessBlock = buildAgentAwarenessBlock(session.agentId)
@@ -391,39 +230,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
391
230
  }
392
231
  }
393
232
 
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
- }
233
+ // Collect dynamic context from enabled plugins (wallet, memory, etc.)
234
+ try {
235
+ const pluginContextParts = await getPluginManager().collectAgentContext(session, sessionPlugins, message, history)
236
+ stateModifierParts.push(...pluginContextParts)
237
+ } catch {
238
+ // Plugin context injection is non-critical
422
239
  }
423
240
 
424
241
  // Tell the LLM about available plugins and their access status
425
242
  {
426
- const agentEnabledSet = new Set(sessionToolsWithImplicitProcess)
243
+ const agentEnabledSet = new Set(sessionPlugins)
427
244
  const { getPluginManager } = await import('./plugins')
428
245
  const allPlugins = getPluginManager().listPlugins()
429
246
  const mcpDisabled = agentMcpDisabledTools ?? []
@@ -478,7 +295,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
478
295
 
479
296
  stateModifierParts.push(
480
297
  buildAgenticExecutionPolicy({
481
- enabledTools: sessionToolsWithImplicitProcess,
298
+ enabledPlugins: sessionPlugins,
482
299
  loopMode: runtime.loopMode,
483
300
  heartbeatPrompt,
484
301
  heartbeatIntervalSec,
@@ -490,7 +307,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
490
307
 
491
308
  let stateModifier = stateModifierParts.join('\n\n')
492
309
 
493
- const { tools, cleanup } = await buildSessionTools(session.cwd, sessionToolsWithImplicitProcess, {
310
+ const { tools, cleanup, toolToPluginMap } = await buildSessionTools(session.cwd, sessionPlugins, {
494
311
  agentId: session.agentId,
495
312
  sessionId: session.id,
496
313
  platformAssignScope: agentPlatformAssignScope,
@@ -582,12 +399,27 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
582
399
  return parts
583
400
  }
584
401
 
585
- // Auto-compaction: prune old history if approaching context window limit
586
- let effectiveHistory = history
402
+ // Apply context-clear boundary: slice from most recent context-clear marker
403
+ let contextStart = 0
404
+ for (let i = history.length - 1; i >= 0; i--) {
405
+ if (history[i].kind === 'context-clear') {
406
+ contextStart = i + 1
407
+ break
408
+ }
409
+ }
410
+ const postClearHistory = history.slice(contextStart)
411
+
412
+ // Hard cap: only send the most recent 30 messages to the LLM
413
+ const recentHistory = postClearHistory.slice(-30)
414
+
415
+ // Auto-compaction: only trigger if the messages we'll actually send exceed context limits.
416
+ // The .slice(-30) hard cap already prevents context overflow for long conversations,
417
+ // so this only fires for sessions with very large individual messages.
418
+ let effectiveHistory = recentHistory
587
419
  try {
588
420
  const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
589
421
  const systemPromptTokens = estimateTokens(stateModifier)
590
- if (shouldAutoCompact(history, systemPromptTokens, session.provider, session.model)) {
422
+ if (shouldAutoCompact(recentHistory, systemPromptTokens, session.provider, session.model)) {
591
423
  const summarize = async (prompt: string): Promise<string> => {
592
424
  const response = await llm.invoke([new HumanMessage(prompt)])
593
425
  if (typeof response.content === 'string') return response.content
@@ -599,7 +431,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
599
431
  return ''
600
432
  }
601
433
  const result = await llmCompact({
602
- messages: history,
434
+ messages: recentHistory,
603
435
  provider: session.provider,
604
436
  model: session.model,
605
437
  agentId: session.agentId || null,
@@ -608,12 +440,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
608
440
  })
609
441
  effectiveHistory = result.messages
610
442
  console.log(
611
- `[stream-agent-chat] Auto-compacted ${session.id}: ${history.length} → ${effectiveHistory.length} msgs` +
443
+ `[stream-agent-chat] Auto-compacted ${session.id}: ${recentHistory.length} → ${effectiveHistory.length} msgs` +
612
444
  (result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
613
445
  )
614
446
  }
615
447
  } catch {
616
- // Context manager failure — continue with full history
448
+ // Context manager failure — continue with recent history
617
449
  }
618
450
 
619
451
  // Context degradation warning: prepend warning to system prompt when nearing limits
@@ -629,18 +461,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
629
461
  // Warning failure is non-critical
630
462
  }
631
463
 
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
464
  const langchainMessages: Array<HumanMessage | AIMessage> = []
643
- for (const m of postClearHistory.slice(-30)) {
465
+ for (const m of effectiveHistory) {
644
466
  if (m.role === 'user') {
645
467
  langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
646
468
  } else {
@@ -660,6 +482,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
660
482
  let totalOutputTokens = 0
661
483
  let lastToolInput: unknown = null
662
484
  let accumulatedThinking = ''
485
+ const pluginInvocations: PluginInvocationRecord[] = []
486
+ let currentToolInputTokens = 0
663
487
 
664
488
  // Plugin hooks: beforeAgentStart
665
489
  const pluginMgr = getPluginManager()
@@ -763,9 +587,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
763
587
  const toolName = event.name || 'unknown'
764
588
  const input = event.data?.input
765
589
  lastToolInput = input
590
+ // Estimate input tokens for plugin invocation tracking
591
+ const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
592
+ currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
766
593
  // Plugin hooks: beforeToolExec
767
594
  await pluginMgr.runHook('beforeToolExec', { toolName, input })
768
- const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
769
595
  logExecution(session.id, 'tool_call', `${toolName} invoked`, {
770
596
  agentId: session.agentId,
771
597
  detail: { toolName, input: inputStr?.slice(0, 4000) },
@@ -784,23 +610,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
784
610
  ? String(output.content)
785
611
  : JSON.stringify(output)
786
612
  // 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
- }
613
+ await pluginMgr.runHook('afterToolExec', { session, toolName, input: lastToolInput as Record<string, unknown> | null, output: outputStr })
804
614
  lastToolInput = null
805
615
  logExecution(session.id, 'tool_result', `${toolName} returned`, {
806
616
  agentId: session.agentId,
@@ -825,6 +635,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
825
635
  })
826
636
  }
827
637
  }
638
+ // Track plugin invocation token estimates
639
+ const pluginId = toolToPluginMap[toolName] || '_unknown'
640
+ pluginInvocations.push({
641
+ pluginId,
642
+ toolName,
643
+ inputTokens: currentToolInputTokens,
644
+ outputTokens: Math.ceil((outputStr?.length || 0) / 4),
645
+ })
646
+ currentToolInputTokens = 0
647
+
828
648
  write(`data: ${JSON.stringify({
829
649
  t: 'tool_result',
830
650
  toolName,
@@ -931,6 +751,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
931
751
  const totalTokens = totalInputTokens + totalOutputTokens
932
752
  if (totalTokens > 0) {
933
753
  const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
754
+ const pluginDefinitionCosts = buildPluginDefinitionCosts(tools, toolToPluginMap)
934
755
  const usageRecord: UsageRecord = {
935
756
  sessionId: session.id,
936
757
  messageIndex: history.length,
@@ -942,6 +763,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
942
763
  estimatedCost: cost,
943
764
  timestamp: Date.now(),
944
765
  durationMs: Date.now() - startTs,
766
+ pluginDefinitionCosts,
767
+ pluginInvocations: pluginInvocations.length > 0 ? pluginInvocations : undefined,
945
768
  }
946
769
  appendUsage(session.id, usageRecord)
947
770
  // Send usage metadata to client
@@ -1,10 +1,10 @@
1
- const TOOL_ALIAS_GROUPS: string[][] = [
1
+ const PLUGIN_ALIAS_GROUPS: string[][] = [
2
2
  ['shell', 'execute_command', 'process_tool', 'process'],
3
3
  ['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
4
4
  ['edit_file'],
5
5
  ['web', 'web_search', 'web_fetch'],
6
6
  ['browser', 'openclaw_browser'],
7
- ['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'],
7
+ ['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'],
8
8
  ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_secrets', 'manage_sessions'],
9
9
  ['manage_connectors', 'connectors', 'connector_message_tool'],
10
10
  ['manage_chatrooms', 'chatroom'],
@@ -20,37 +20,41 @@ const TOOL_ALIAS_GROUPS: string[][] = [
20
20
  ['context_mgmt', 'context_status', 'context_summarize'],
21
21
  ['openclaw_workspace'],
22
22
  ['openclaw_nodes'],
23
+ ['image_gen', 'generate_image'],
24
+ ['email', 'send_email'],
25
+ ['calendar', 'calendar_events'],
26
+ ['replicate', 'replicate_run', 'replicate_models'],
23
27
  ]
24
28
 
25
- const TOOL_ALIAS_MAP = (() => {
29
+ const PLUGIN_ALIAS_MAP = (() => {
26
30
  const map = new Map<string, Set<string>>()
27
- for (const group of TOOL_ALIAS_GROUPS) {
28
- const normalized = group.map((tool) => tool.trim().toLowerCase()).filter(Boolean)
29
- for (const tool of normalized) {
30
- const current = map.get(tool) || new Set<string>()
31
+ for (const group of PLUGIN_ALIAS_GROUPS) {
32
+ const normalized = group.map((id) => id.trim().toLowerCase()).filter(Boolean)
33
+ for (const id of normalized) {
34
+ const current = map.get(id) || new Set<string>()
31
35
  for (const alias of normalized) current.add(alias)
32
- map.set(tool, current)
36
+ map.set(id, current)
33
37
  }
34
38
  }
35
39
  return map
36
40
  })()
37
41
 
38
- export function normalizeToolId(value: unknown): string {
42
+ export function normalizePluginId(value: unknown): string {
39
43
  return typeof value === 'string' ? value.trim().toLowerCase() : ''
40
44
  }
41
45
 
42
- export function expandToolIds(values: string[] | null | undefined): string[] {
46
+ export function expandPluginIds(values: string[] | null | undefined): string[] {
43
47
  if (!Array.isArray(values) || values.length === 0) return []
44
48
  const expanded = new Set<string>()
45
49
  const queue: string[] = values
46
- .map((tool) => normalizeToolId(tool))
50
+ .map((id) => normalizePluginId(id))
47
51
  .filter(Boolean)
48
52
 
49
53
  while (queue.length > 0) {
50
54
  const next = queue.shift()!
51
55
  if (expanded.has(next)) continue
52
56
  expanded.add(next)
53
- const aliases = TOOL_ALIAS_MAP.get(next)
57
+ const aliases = PLUGIN_ALIAS_MAP.get(next)
54
58
  if (!aliases) continue
55
59
  for (const alias of aliases) {
56
60
  if (!expanded.has(alias)) queue.push(alias)
@@ -60,9 +64,16 @@ export function expandToolIds(values: string[] | null | undefined): string[] {
60
64
  return Array.from(expanded)
61
65
  }
62
66
 
63
- export function toolIdMatches(enabledTools: string[] | null | undefined, toolId: string): boolean {
64
- const normalized = normalizeToolId(toolId)
67
+ export function pluginIdMatches(enabledPlugins: string[] | null | undefined, pluginId: string): boolean {
68
+ const normalized = normalizePluginId(pluginId)
65
69
  if (!normalized) return false
66
- return expandToolIds(enabledTools).includes(normalized)
70
+ return expandPluginIds(enabledPlugins).includes(normalized)
67
71
  }
68
72
 
73
+ /** @deprecated Use normalizePluginId */
74
+ export const normalizeToolId = normalizePluginId
75
+ /** @deprecated Use expandPluginIds */
76
+ export const expandToolIds = expandPluginIds
77
+ /** @deprecated Use pluginIdMatches */
78
+ export const toolIdMatches = pluginIdMatches
79
+
@@ -7,15 +7,15 @@ import {
7
7
 
8
8
  test('capability policy permissive mode allows non-blocked tools', () => {
9
9
  const decision = resolveSessionToolPolicy(['shell', 'web_search'], { capabilityPolicyMode: 'permissive' })
10
- assert.deepEqual(decision.enabledTools, ['shell', 'web_search'])
11
- assert.equal(decision.blockedTools.length, 0)
10
+ assert.deepEqual(decision.enabledPlugins, ['shell', 'web_search'])
11
+ assert.equal(decision.blockedPlugins.length, 0)
12
12
  })
13
13
 
14
14
  test('capability policy balanced mode blocks destructive delete_file', () => {
15
15
  const decision = resolveSessionToolPolicy(['files', 'delete_file'], { capabilityPolicyMode: 'balanced' })
16
- assert.deepEqual(decision.enabledTools, ['files'])
17
- assert.equal(decision.blockedTools.length, 1)
18
- assert.equal(decision.blockedTools[0].tool, 'delete_file')
16
+ assert.deepEqual(decision.enabledPlugins, ['files'])
17
+ assert.equal(decision.blockedPlugins.length, 1)
18
+ assert.equal(decision.blockedPlugins[0].tool, 'delete_file')
19
19
  })
20
20
 
21
21
  test('capability policy strict mode blocks execution/platform families', () => {
@@ -23,9 +23,9 @@ test('capability policy strict mode blocks execution/platform families', () => {
23
23
  ['shell', 'manage_tasks', 'web_search', 'memory'],
24
24
  { capabilityPolicyMode: 'strict' },
25
25
  )
26
- assert.deepEqual(decision.enabledTools, ['web_search', 'memory'])
27
- assert.equal(decision.blockedTools.some((entry) => entry.tool === 'shell'), true)
28
- assert.equal(decision.blockedTools.some((entry) => entry.tool === 'manage_tasks'), true)
26
+ assert.deepEqual(decision.enabledPlugins, ['web_search', 'memory'])
27
+ assert.equal(decision.blockedPlugins.some((entry) => entry.tool === 'shell'), true)
28
+ assert.equal(decision.blockedPlugins.some((entry) => entry.tool === 'manage_tasks'), true)
29
29
  })
30
30
 
31
31
  test('capability policy respects explicit allow overrides', () => {
@@ -36,7 +36,7 @@ test('capability policy respects explicit allow overrides', () => {
36
36
  capabilityAllowedTools: ['shell'],
37
37
  },
38
38
  )
39
- assert.deepEqual(decision.enabledTools, ['shell', 'web_search'])
39
+ assert.deepEqual(decision.enabledPlugins, ['shell', 'web_search'])
40
40
  })
41
41
 
42
42
  test('concrete tool checks inherit blocked family rules', () => {