@swarmclawai/swarmclaw 0.7.1 → 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 +44 -5
  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
@@ -127,7 +127,144 @@ async function executeMemoryAction(input: any, ctx: any) {
127
127
  const MemoryPlugin: Plugin = {
128
128
  name: 'Core Memory',
129
129
  description: 'Advanced database-backed long-term memory with semantic search and graph linking.',
130
- hooks: {} as PluginHooks,
130
+ hooks: {
131
+ getAgentContext: async (ctx) => {
132
+ const agentId = ctx.session.agentId
133
+ if (!agentId) return null
134
+
135
+ const memDb = getMemoryDb()
136
+ const memoryQuerySeed = [
137
+ ctx.message,
138
+ ...ctx.history
139
+ .slice(-4)
140
+ .filter((h) => h.role === 'user')
141
+ .map((h) => h.text),
142
+ ].join('\n')
143
+
144
+ const seen = new Set<string>()
145
+ const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
146
+ const category = String(m.category || 'note')
147
+ const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
148
+ const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
149
+ const pin = m.pinned ? ' [pinned]' : ''
150
+ return `- [${category}]${pin} ${title}: ${snippet}`
151
+ }
152
+
153
+ const pinned = memDb.listPinned(agentId, 5)
154
+ const pinnedLines = pinned
155
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
156
+ .map(formatMemoryLine)
157
+
158
+ const relevantSlice = Math.max(2, 6 - pinnedLines.length)
159
+ const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
160
+ const relevant = relevantLookup.entries.slice(0, relevantSlice)
161
+ const recent = memDb.list(agentId, 12).slice(0, 6)
162
+
163
+ const relevantLines = relevant
164
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
165
+ .map(formatMemoryLine)
166
+
167
+ const recentLines = recent
168
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
169
+ .map(formatMemoryLine)
170
+
171
+ const parts: string[] = []
172
+ if (pinnedLines.length) {
173
+ parts.push(['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'))
174
+ }
175
+ if (relevantLines.length) {
176
+ parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
177
+ }
178
+ if (recentLines.length) {
179
+ parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
180
+ }
181
+
182
+ // Memory Policy
183
+ parts.push([
184
+ '## My Memory',
185
+ 'I have long-term memory that persists across conversations. I use it naturally — I don\'t wait to be asked to remember things.',
186
+ '',
187
+ '**Things worth remembering:**',
188
+ '- What the user likes, dislikes, or has corrected me on',
189
+ '- Important decisions, outcomes, and lessons learned',
190
+ '- What I\'ve discovered about projects, codebases, or environments',
191
+ '- Problems I\'ve hit and how I solved them',
192
+ '- Who people are and how they relate to each other',
193
+ '- Configuration details and environment specifics that I\'ll need again',
194
+ '',
195
+ '**Not worth cluttering my memory with:**',
196
+ '- Throwaway acknowledgments or small talk',
197
+ '- Work-in-progress that\'ll change soon (use category "working" for scratch notes)',
198
+ '- Things already in my system prompt',
199
+ '- Something I\'ve already stored',
200
+ '',
201
+ '**Good habits:**',
202
+ '- Give memories clear titles ("User prefers dark mode" not "Note 1")',
203
+ '- Use categories: preference, fact, learning, project, identity, decision',
204
+ '- Check what I already know before storing something new',
205
+ '- When I learn something that corrects old knowledge, update or remove the old memory',
206
+ ].join('\n'))
207
+
208
+ // Pre-compaction consolidation nudge
209
+ const msgCount = ctx.history.filter(m => m.role === 'user' || m.role === 'assistant').length
210
+ if (msgCount > 20) {
211
+ parts.push([
212
+ '## Reflection & Consolidation Reminder',
213
+ 'This conversation is getting long and I might lose older context soon.',
214
+ 'Save anything important I\'ve learned, decided, or discovered to memory now. Only what matters, not every detail.',
215
+ ].join('\n'))
216
+ }
217
+
218
+ return parts.join('\n\n') || null
219
+ },
220
+ afterToolExec: (ctx) => {
221
+ const agentId = ctx.session.agentId
222
+ if (!agentId) return
223
+ const inp = ctx.input
224
+ if (!inp || typeof inp !== 'object') return
225
+ const action = typeof inp.action === 'string' ? inp.action : ''
226
+ let title: string | null = null
227
+ if (ctx.toolName === 'manage_tasks') {
228
+ if (action === 'create') title = `Created task: ${inp.title || 'Untitled'}`
229
+ else if (ctx.output && /status.*completed|completed.*successfully/i.test(ctx.output)) title = `Completed task: ${inp.title || inp.taskId || 'unknown'}`
230
+ }
231
+ if (ctx.toolName === 'manage_schedules' && action === 'create') title = `Created schedule: ${inp.name || 'Untitled'}`
232
+ if (ctx.toolName === 'manage_agents' && action === 'create') title = `Created agent: ${inp.name || 'Untitled'}`
233
+ if (!title) return
234
+ try {
235
+ const memDb = getMemoryDb()
236
+ memDb.add({ agentId, sessionId: ctx.session.id, category: 'breadcrumb', title, content: '' })
237
+ } catch { /* breadcrumbs are best-effort */ }
238
+ },
239
+ afterChatTurn: (ctx) => {
240
+ if (ctx.internal) return
241
+ if (ctx.source !== 'chat' && ctx.source !== 'connector') return
242
+ const agentId = ctx.session.agentId
243
+ if (!agentId) return
244
+ const msg = (ctx.message || '').trim()
245
+ const resp = (ctx.response || '').trim()
246
+ if (msg.length < 20 || resp.length < 40) return
247
+ if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return
248
+ if (resp === 'HEARTBEAT_OK') return
249
+ const now = Date.now()
250
+ const last = typeof ctx.session.lastAutoMemoryAt === 'number' ? ctx.session.lastAutoMemoryAt : 0
251
+ if (last > 0 && now - last < 5 * 60 * 1000) return
252
+ try {
253
+ const memDb = getMemoryDb()
254
+ const compactMessage = msg.replace(/\s+/g, ' ').slice(0, 220)
255
+ const compactResponse = resp.replace(/\s+/g, ' ').slice(0, 700)
256
+ const autoTitle = `[auto] ${compactMessage.slice(0, 90)}`
257
+ const content = `source: ${ctx.source}\nuser_request: ${compactMessage}\nassistant_outcome: ${compactResponse}`
258
+ memDb.add({ agentId, sessionId: ctx.session.id, category: 'execution', title: autoTitle, content })
259
+ ctx.session.lastAutoMemoryAt = now
260
+ } catch { /* auto-memory is best-effort */ }
261
+ },
262
+ getCapabilityDescription: () => 'I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.',
263
+ getOperatingGuidance: () => [
264
+ 'Memory: search before major tasks, store concise notes after meaningful steps. Platform preloads context each turn.',
265
+ 'For open goals, form a hypothesis and execute — do not keep re-asking broad questions.',
266
+ ],
267
+ } as PluginHooks,
131
268
  tools: [
132
269
  {
133
270
  name: 'memory_tool',
@@ -155,7 +292,7 @@ const MemoryPlugin: Plugin = {
155
292
  getPluginManager().registerBuiltin('memory', MemoryPlugin)
156
293
 
157
294
  export function buildMemoryTools(bctx: ToolBuildContext) {
158
- if (!bctx.hasTool('memory')) return []
295
+ if (!bctx.hasPlugin('memory')) return []
159
296
 
160
297
  return [
161
298
  tool(
@@ -112,7 +112,7 @@ const MonitorPlugin: Plugin = {
112
112
  getPluginManager().registerBuiltin('monitor', MonitorPlugin)
113
113
 
114
114
  export function buildMonitorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
115
- if (!bctx.hasTool('monitor')) return []
115
+ if (!bctx.hasPlugin('monitor')) return []
116
116
  return [
117
117
  tool(
118
118
  async (args) => executeMonitorAction(args, { cwd: bctx.cwd }),
@@ -81,7 +81,7 @@ getPluginManager().registerBuiltin('openclaw_nodes', NodesPlugin)
81
81
  * Legacy Bridge
82
82
  */
83
83
  export function buildOpenClawNodeTools(bctx: ToolBuildContext): StructuredToolInterface[] {
84
- if (!bctx.hasTool('openclaw_nodes')) return []
84
+ if (!bctx.hasPlugin('openclaw_nodes')) return []
85
85
  return [
86
86
  tool(
87
87
  async (args) => executeNodesAction(args),
@@ -128,7 +128,7 @@ getPluginManager().registerBuiltin('openclaw_workspace', WorkspacePlugin)
128
128
  * Legacy Bridge
129
129
  */
130
130
  export function buildOpenClawWorkspaceTools(bctx: ToolBuildContext): StructuredToolInterface[] {
131
- if (!bctx.hasTool('openclaw_workspace')) return []
131
+ if (!bctx.hasPlugin('openclaw_workspace')) return []
132
132
  return [
133
133
  tool(
134
134
  async (args) => executeWorkspaceAction(args),
@@ -16,7 +16,7 @@ async function executePlatformAction(args: any, bctx: any) {
16
16
  // We reuse the existing CRUD tool logic but expose it via a single tool
17
17
  const crudTools = buildCrudTools({
18
18
  ...bctx,
19
- hasTool: (id: string) => [
19
+ hasPlugin: (id: string) => [
20
20
  'manage_agents',
21
21
  'manage_tasks',
22
22
  'manage_schedules',
@@ -45,7 +45,10 @@ async function executePlatformAction(args: any, bctx: any) {
45
45
  const PlatformPlugin: Plugin = {
46
46
  name: 'Core Platform',
47
47
  description: 'Unified management of agents, tasks, schedules, skills, documents, and secrets.',
48
- hooks: {} as PluginHooks,
48
+ hooks: {
49
+ getCapabilityDescription: () => 'I can create and configure other agents (`manage_agents`), manage tasks (`manage_tasks`), set up schedules (`manage_schedules`), store and search documents (`manage_documents`), register webhooks (`manage_webhooks`), manage reusable skills (`manage_skills`), and store encrypted secrets (`manage_secrets`).',
50
+ getOperatingGuidance: () => ['Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
51
+ } as PluginHooks,
49
52
  tools: [
50
53
  {
51
54
  name: 'manage_platform',
@@ -71,7 +74,7 @@ getPluginManager().registerBuiltin('manage_platform', PlatformPlugin)
71
74
  * Legacy Bridge
72
75
  */
73
76
  export function buildPlatformTools(bctx: ToolBuildContext): StructuredToolInterface[] {
74
- if (!bctx.hasTool('manage_platform')) return []
77
+ if (!bctx.hasPlugin('manage_platform')) return []
75
78
 
76
79
  return [
77
80
  tool(
@@ -70,9 +70,9 @@ async function executePluginCreatorAction(args: Record<string, unknown>, ctxOrBc
70
70
  const sessions = loadSessions()
71
71
  const session = sessions[pctx.sessionId!]
72
72
  if (session) {
73
- const currentTools = session.tools || []
73
+ const currentTools = session.plugins || []
74
74
  if (!currentTools.includes(filename)) {
75
- session.tools = [...currentTools, filename]
75
+ session.plugins = [...currentTools, filename]
76
76
  saveSessions(sessions)
77
77
  }
78
78
  }
@@ -220,7 +220,7 @@ getPluginManager().registerBuiltin('plugin_creator', PluginCreatorPlugin)
220
220
  * Legacy Bridge
221
221
  */
222
222
  export function buildPluginCreatorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
223
- if (!bctx.hasTool('plugin_creator')) return []
223
+ if (!bctx.hasPlugin('plugin_creator')) return []
224
224
  return [
225
225
  tool(
226
226
  async (args) => executePluginCreatorAction(args, bctx),
@@ -0,0 +1,303 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { Plugin, PluginHooks } from '@/types'
4
+ import { getPluginManager } from '../plugins'
5
+ import { normalizeToolInputArgs } from './normalize-tool-args'
6
+ import { loadSettings } from '../storage'
7
+ import type { ToolBuildContext } from './context'
8
+
9
+ interface ReplicateConfig {
10
+ apiToken: string
11
+ defaultModel: string
12
+ pollingIntervalMs: number
13
+ timeoutMs: number
14
+ }
15
+
16
+ function getConfig(): ReplicateConfig {
17
+ const settings = loadSettings()
18
+ const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.replicate ?? {}
19
+ return {
20
+ apiToken: (ps.apiToken as string) || '',
21
+ defaultModel: (ps.defaultModel as string) || '',
22
+ pollingIntervalMs: Number(ps.pollingIntervalMs) || 2000,
23
+ timeoutMs: Number(ps.timeoutMs) || 120000,
24
+ }
25
+ }
26
+
27
+ const API_BASE = 'https://api.replicate.com/v1'
28
+
29
+ async function replicateRequest(
30
+ method: string,
31
+ path: string,
32
+ token: string,
33
+ body?: unknown,
34
+ extraHeaders?: Record<string, string>,
35
+ ): Promise<{ ok: boolean; data?: unknown; error?: string }> {
36
+ const headers: Record<string, string> = {
37
+ Authorization: `Bearer ${token}`,
38
+ 'Content-Type': 'application/json',
39
+ ...extraHeaders,
40
+ }
41
+ const init: RequestInit = {
42
+ method,
43
+ headers,
44
+ signal: AbortSignal.timeout(15_000),
45
+ }
46
+ if (body && method !== 'GET' && method !== 'DELETE') {
47
+ init.body = JSON.stringify(body)
48
+ }
49
+ const res = await fetch(`${API_BASE}${path}`, init)
50
+ if (!res.ok) {
51
+ const errText = await res.text().catch(() => '')
52
+ return { ok: false, error: `Replicate ${res.status}: ${errText.slice(0, 400)}` }
53
+ }
54
+ const data = await res.json()
55
+ return { ok: true, data }
56
+ }
57
+
58
+ function formatPrediction(p: Record<string, unknown>): Record<string, unknown> {
59
+ return {
60
+ id: p.id,
61
+ model: p.model,
62
+ status: p.status,
63
+ output: p.output,
64
+ error: p.error,
65
+ logs: typeof p.logs === 'string' ? p.logs.slice(-500) : undefined,
66
+ metrics: p.metrics,
67
+ created_at: p.created_at,
68
+ started_at: p.started_at,
69
+ completed_at: p.completed_at,
70
+ }
71
+ }
72
+
73
+ async function pollPrediction(token: string, predictionId: string, cfg: ReplicateConfig): Promise<Record<string, unknown>> {
74
+ const deadline = Date.now() + cfg.timeoutMs
75
+ while (Date.now() < deadline) {
76
+ await new Promise((r) => setTimeout(r, cfg.pollingIntervalMs))
77
+ const r = await replicateRequest('GET', `/predictions/${predictionId}`, token)
78
+ if (!r.ok) return { status: 'failed', error: r.error }
79
+ const prediction = r.data as Record<string, unknown>
80
+ if (prediction.status === 'succeeded' || prediction.status === 'failed' || prediction.status === 'canceled') {
81
+ return prediction
82
+ }
83
+ }
84
+ return { status: 'failed', error: 'Prediction timed out.' }
85
+ }
86
+
87
+ async function executeReplicate(args: Record<string, unknown>): Promise<string> {
88
+ const normalized = normalizeToolInputArgs(args)
89
+ const action = String(normalized.action || 'run')
90
+ const cfg = getConfig()
91
+
92
+ if (!cfg.apiToken) {
93
+ return 'Error: Replicate API token not configured. Ask the user to add it in Plugin Settings > Replicate.'
94
+ }
95
+
96
+ try {
97
+ switch (action) {
98
+ case 'run': {
99
+ const model = String(normalized.model || cfg.defaultModel || '').trim()
100
+ if (!model) return 'Error: "model" is required (e.g. "stability-ai/sdxl", "meta/llama-2-70b-chat").'
101
+
102
+ const input = (normalized.input as Record<string, unknown>) || {}
103
+ if (typeof normalized.prompt === 'string') {
104
+ input.prompt = normalized.prompt
105
+ }
106
+
107
+ const version = typeof normalized.version === 'string' ? normalized.version.trim() : undefined
108
+
109
+ // Build request body
110
+ const body: Record<string, unknown> = { input }
111
+ if (version) {
112
+ body.version = version
113
+ } else {
114
+ body.model = model
115
+ }
116
+
117
+ // Try sync mode first (Prefer: wait blocks up to 60s)
118
+ const r = await replicateRequest('POST', '/predictions', cfg.apiToken, body, { Prefer: 'wait' })
119
+ if (!r.ok) return `Error: ${r.error}`
120
+
121
+ let prediction = r.data as Record<string, unknown>
122
+
123
+ // If sync didn't complete, poll
124
+ if (prediction.status !== 'succeeded' && prediction.status !== 'failed' && prediction.status !== 'canceled') {
125
+ prediction = await pollPrediction(cfg.apiToken, String(prediction.id), cfg)
126
+ }
127
+
128
+ if (prediction.status === 'failed') {
129
+ return `Prediction failed: ${prediction.error || 'unknown error'}`
130
+ }
131
+
132
+ return JSON.stringify(formatPrediction(prediction))
133
+ }
134
+
135
+ case 'get': {
136
+ const predictionId = String(normalized.predictionId || normalized.id || '').trim()
137
+ if (!predictionId) return 'Error: "predictionId" is required.'
138
+
139
+ const r = await replicateRequest('GET', `/predictions/${predictionId}`, cfg.apiToken)
140
+ if (!r.ok) return `Error: ${r.error}`
141
+ return JSON.stringify(formatPrediction(r.data as Record<string, unknown>))
142
+ }
143
+
144
+ case 'cancel': {
145
+ const predictionId = String(normalized.predictionId || normalized.id || '').trim()
146
+ if (!predictionId) return 'Error: "predictionId" is required.'
147
+
148
+ const r = await replicateRequest('POST', `/predictions/${predictionId}/cancel`, cfg.apiToken)
149
+ if (!r.ok) return `Error: ${r.error}`
150
+ return `Prediction ${predictionId} canceled.`
151
+ }
152
+
153
+ case 'get_model': {
154
+ const model = String(normalized.model || '').trim()
155
+ if (!model) return 'Error: "model" is required (e.g. "stability-ai/sdxl").'
156
+
157
+ const r = await replicateRequest('GET', `/models/${model}`, cfg.apiToken)
158
+ if (!r.ok) return `Error: ${r.error}`
159
+ const data = r.data as Record<string, unknown>
160
+ return JSON.stringify({
161
+ owner: data.owner,
162
+ name: data.name,
163
+ description: data.description,
164
+ visibility: data.visibility,
165
+ url: data.url,
166
+ latest_version: data.latest_version ? {
167
+ id: (data.latest_version as Record<string, unknown>).id,
168
+ created_at: (data.latest_version as Record<string, unknown>).created_at,
169
+ } : null,
170
+ run_count: data.run_count,
171
+ })
172
+ }
173
+
174
+ case 'search': {
175
+ const query = String(normalized.query || normalized.search || '').trim()
176
+ const cursor = typeof normalized.cursor === 'string' ? normalized.cursor : undefined
177
+ const params = new URLSearchParams()
178
+ if (query) params.set('query', query)
179
+ if (cursor) params.set('cursor', cursor)
180
+ const suffix = params.toString() ? `?${params}` : ''
181
+ const r = await replicateRequest('GET', `/models${suffix}`, cfg.apiToken)
182
+ if (!r.ok) return `Error: ${r.error}`
183
+ const data = r.data as Record<string, unknown>
184
+ const results = (data.results as Record<string, unknown>[]) ?? []
185
+ const items = results.slice(0, 20).map((m) => ({
186
+ owner: m.owner,
187
+ name: m.name,
188
+ description: typeof m.description === 'string' ? m.description.slice(0, 120) : '',
189
+ run_count: m.run_count,
190
+ url: m.url,
191
+ }))
192
+ return JSON.stringify({
193
+ models: items,
194
+ next_cursor: data.next ? (data.next as string).split('cursor=')[1]?.split('&')[0] : null,
195
+ })
196
+ }
197
+
198
+ case 'status': {
199
+ return JSON.stringify({
200
+ configured: true,
201
+ hasToken: !!cfg.apiToken,
202
+ defaultModel: cfg.defaultModel || '(none)',
203
+ timeoutMs: cfg.timeoutMs,
204
+ })
205
+ }
206
+
207
+ default:
208
+ return `Error: Unknown action "${action}". Use: run, get, cancel, get_model, search, status.`
209
+ }
210
+ } catch (err: unknown) {
211
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
212
+ }
213
+ }
214
+
215
+ const ReplicatePlugin: Plugin = {
216
+ name: 'Replicate',
217
+ enabledByDefault: false,
218
+ description: 'Run any AI model on Replicate — image generation, LLMs, audio, video, and more. Search models, create predictions, check status.',
219
+ hooks: {
220
+ getCapabilityDescription: () =>
221
+ 'I can run any AI model on Replicate using `replicate`. This includes image generation (SDXL, Flux), language models, audio/video processing, and thousands more. I can search for models, run predictions, and check their status.',
222
+ } as PluginHooks,
223
+ tools: [
224
+ {
225
+ name: 'replicate',
226
+ description: 'Run AI models on Replicate. Actions: run (create and wait for prediction), get (check prediction status), cancel (stop a prediction), get_model (model details), search (find models), status (check config).',
227
+ parameters: {
228
+ type: 'object',
229
+ properties: {
230
+ action: { type: 'string', enum: ['run', 'get', 'cancel', 'get_model', 'search', 'status'], description: 'Action to perform' },
231
+ model: { type: 'string', description: 'Model identifier (e.g. "stability-ai/sdxl", "meta/llama-2-70b-chat"). Required for run/get_model.' },
232
+ version: { type: 'string', description: 'Optional specific model version hash for run.' },
233
+ input: { type: 'object', description: 'Model input parameters as key-value pairs (for run). Varies by model.' },
234
+ prompt: { type: 'string', description: 'Shorthand: sets input.prompt for models that accept a prompt (for run).' },
235
+ predictionId: { type: 'string', description: 'Prediction ID (for get/cancel).' },
236
+ query: { type: 'string', description: 'Search query (for search).' },
237
+ cursor: { type: 'string', description: 'Pagination cursor (for search).' },
238
+ },
239
+ required: ['action'],
240
+ },
241
+ execute: async (args) => executeReplicate(args),
242
+ },
243
+ ],
244
+ ui: {
245
+ settingsFields: [
246
+ {
247
+ key: 'apiToken',
248
+ label: 'API Token',
249
+ type: 'secret',
250
+ required: true,
251
+ placeholder: 'r8_...',
252
+ help: 'Your Replicate API token. Find it at replicate.com/account/api-tokens.',
253
+ },
254
+ {
255
+ key: 'defaultModel',
256
+ label: 'Default Model',
257
+ type: 'text',
258
+ placeholder: 'stability-ai/sdxl',
259
+ help: 'Default model to use when none is specified. Format: owner/model-name.',
260
+ },
261
+ {
262
+ key: 'timeoutMs',
263
+ label: 'Timeout (ms)',
264
+ type: 'number',
265
+ defaultValue: 120000,
266
+ help: 'Maximum time to wait for a prediction (default: 120000ms / 2 minutes).',
267
+ },
268
+ {
269
+ key: 'pollingIntervalMs',
270
+ label: 'Polling Interval (ms)',
271
+ type: 'number',
272
+ defaultValue: 2000,
273
+ help: 'How often to poll for prediction results when sync mode times out (default: 2000ms).',
274
+ },
275
+ ],
276
+ },
277
+ }
278
+
279
+ getPluginManager().registerBuiltin('replicate', ReplicatePlugin)
280
+
281
+ export function buildReplicateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
282
+ if (!bctx.hasPlugin('replicate')) return []
283
+
284
+ return [
285
+ tool(
286
+ async (args) => executeReplicate(args),
287
+ {
288
+ name: 'replicate',
289
+ description: ReplicatePlugin.tools![0].description,
290
+ schema: z.object({
291
+ action: z.enum(['run', 'get', 'cancel', 'get_model', 'search', 'status']).describe('Action to perform'),
292
+ model: z.string().optional().describe('Model identifier (e.g. "stability-ai/sdxl")'),
293
+ version: z.string().optional().describe('Specific model version hash'),
294
+ input: z.record(z.string(), z.unknown()).optional().describe('Model input parameters'),
295
+ prompt: z.string().optional().describe('Shorthand for input.prompt'),
296
+ predictionId: z.string().optional().describe('Prediction ID (for get/cancel)'),
297
+ query: z.string().optional().describe('Search query (for search)'),
298
+ cursor: z.string().optional().describe('Pagination cursor (for search)'),
299
+ }),
300
+ },
301
+ ),
302
+ ]
303
+ }
@@ -80,7 +80,7 @@ const SampleUIPlugin: Plugin = {
80
80
  getPluginManager().registerBuiltin('sample_ui', SampleUIPlugin)
81
81
 
82
82
  export function buildSampleUITools(bctx: any) {
83
- if (!bctx.hasTool('sample_ui')) return []
83
+ if (!bctx.hasPlugin('sample_ui')) return []
84
84
  return [
85
85
  tool(
86
86
  async (args) => SampleUIPlugin.tools![0].execute(args as any, bctx),
@@ -143,7 +143,9 @@ async function executeListRuntimes() {
143
143
  const SandboxPlugin: Plugin = {
144
144
  name: 'Core Sandbox',
145
145
  description: 'Secure isolated code execution for JS, TS, and Python.',
146
- hooks: {} as PluginHooks,
146
+ hooks: {
147
+ getCapabilityDescription: () => '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.',
148
+ } as PluginHooks,
147
149
  tools: [
148
150
  {
149
151
  name: 'sandbox_exec',
@@ -174,7 +176,7 @@ getPluginManager().registerBuiltin('sandbox', SandboxPlugin)
174
176
  * Legacy Bridge
175
177
  */
176
178
  export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterface[] {
177
- if (!bctx.hasTool('sandbox')) return []
179
+ if (!bctx.hasPlugin('sandbox')) return []
178
180
  const tools: StructuredToolInterface[] = []
179
181
 
180
182
  tools.push(
@@ -40,7 +40,9 @@ async function executeScheduleWake(args: { delayMinutes: number; message: string
40
40
  const SchedulePlugin: Plugin = {
41
41
  name: 'Core Scheduler',
42
42
  description: 'Schedule wake events and reminders for agents.',
43
- hooks: {} as PluginHooks,
43
+ hooks: {
44
+ getCapabilityDescription: () => 'I can set a conversational timer (`schedule_wake`) to remind myself to check back on something later in this chat.',
45
+ } as PluginHooks,
44
46
  tools: [
45
47
  {
46
48
  name: 'schedule_wake',
@@ -64,7 +66,7 @@ getPluginManager().registerBuiltin('schedule', SchedulePlugin)
64
66
  * Legacy Bridge
65
67
  */
66
68
  export function buildScheduleTools(bctx: ToolBuildContext): StructuredToolInterface[] {
67
- if (!bctx.hasTool('schedule_wake')) return []
69
+ if (!bctx.hasPlugin('schedule_wake')) return []
68
70
  return [
69
71
  tool(
70
72
  async (args) => executeScheduleWake(args as any, { sessionId: bctx.ctx?.sessionId || undefined }),
@@ -51,10 +51,10 @@ async function executeSessionsAction(args: any, context: { sessionId?: string; a
51
51
  const id = genId()
52
52
  const now = Date.now()
53
53
  sessions[id] = {
54
- id, name: (name || `${agent.name} Session`).trim(), cwd: context.cwd, user: 'system',
54
+ id, name: (name || `${agent.name} Chat`).trim(), cwd: context.cwd, user: 'system',
55
55
  provider: agent.provider, model: agent.model, credentialId: agent.credentialId || null,
56
56
  messages: [], createdAt: now, lastActiveAt: now, sessionType: 'orchestrated',
57
- agentId: agent.id, parentSessionId: context.sessionId || undefined, tools: agent.tools || [],
57
+ agentId: agent.id, parentSessionId: context.sessionId || undefined, plugins: agent.plugins || agent.tools || [],
58
58
  }
59
59
  saveSessions(sessions)
60
60
  return JSON.stringify({ sessionId: id, name: agent.name })
@@ -69,7 +69,10 @@ async function executeSessionsAction(args: any, context: { sessionId?: string; a
69
69
  const SessionInfoPlugin: Plugin = {
70
70
  name: 'Core Session Info',
71
71
  description: 'Identify current session context and manage other agent sessions.',
72
- hooks: {} as PluginHooks,
72
+ hooks: {
73
+ getCapabilityDescription: () => '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.',
74
+ getOperatingGuidance: () => 'Inspect existing chats before creating duplicates.',
75
+ } as PluginHooks,
73
76
  tools: [
74
77
  {
75
78
  name: 'whoami_tool',
@@ -102,7 +105,7 @@ getPluginManager().registerBuiltin('session_info', SessionInfoPlugin)
102
105
  * Legacy Bridge
103
106
  */
104
107
  export function buildSessionInfoTools(bctx: ToolBuildContext): StructuredToolInterface[] {
105
- if (!bctx.hasTool('manage_sessions')) return []
108
+ if (!bctx.hasPlugin('manage_sessions')) return []
106
109
  return [
107
110
  tool(
108
111
  async () => executeWhoAmI({ sessionId: bctx.ctx?.sessionId || undefined, agentId: bctx.ctx?.agentId || undefined }),
@@ -162,7 +162,10 @@ async function executeShellAction(args: Record<string, unknown>, bctx: { cwd: st
162
162
  const ShellPlugin: Plugin = {
163
163
  name: 'Core Shell',
164
164
  description: 'Execute shell commands and manage background processes.',
165
- hooks: {} as PluginHooks,
165
+ hooks: {
166
+ getCapabilityDescription: () => '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.',
167
+ getOperatingGuidance: () => ['Shell: use `execute_command` for servers, installs, scripts, git. Use `background=true` for long-lived processes.', 'Verify servers with `process_tool` status/log and liveness probes before claiming success.', 'Resolve IPs/URLs via shell — never use placeholders. Retry path errors without workdir override.'],
168
+ } as PluginHooks,
166
169
  tools: [
167
170
  {
168
171
  name: 'shell',
@@ -185,7 +188,7 @@ const ShellPlugin: Plugin = {
185
188
  getPluginManager().registerBuiltin('shell', ShellPlugin)
186
189
 
187
190
  export function buildShellTools(bctx: ToolBuildContext) {
188
- if (!bctx.hasTool('shell')) return []
191
+ if (!bctx.hasPlugin('shell')) return []
189
192
  return [
190
193
  tool(
191
194
  async (args) => executeShellAction(args, { ...bctx.ctx, cwd: bctx.cwd }),
@@ -52,7 +52,7 @@ async function executeSubagentAction(args: any, context: { sessionId?: string; c
52
52
  id: sid, name: `subagent-${agent.name}`, cwd: cwd || context.cwd, user: 'agent',
53
53
  provider: agent.provider, model: agent.model, credentialId: agent.credentialId || null,
54
54
  messages: [], createdAt: now, lastActiveAt: now, sessionType: 'orchestrated',
55
- agentId: agent.id, parentSessionId: context.sessionId || null, tools: agent.tools || [],
55
+ agentId: agent.id, parentSessionId: context.sessionId || null, plugins: agent.plugins || agent.tools || [],
56
56
  }
57
57
  saveSessions(sessions)
58
58
 
@@ -92,7 +92,7 @@ getPluginManager().registerBuiltin('subagent', SubagentPlugin)
92
92
  * Legacy Bridge
93
93
  */
94
94
  export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
95
- if (!bctx.hasTool('spawn_subagent')) return []
95
+ if (!bctx.hasPlugin('spawn_subagent')) return []
96
96
  return [
97
97
  tool(
98
98
  async (args) => executeSubagentAction(args, { sessionId: bctx.ctx?.sessionId || undefined, cwd: bctx.cwd }),