@swarmclawai/swarmclaw 0.3.1 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -1,14 +1,19 @@
1
1
  import { z } from 'zod'
2
2
  import { tool } from '@langchain/core/tools'
3
- import { createReactAgent } from '@langchain/langgraph/prebuilt'
3
+ import { StateGraph, MessagesAnnotation, START, END } from '@langchain/langgraph'
4
+ import { ToolNode } from '@langchain/langgraph/prebuilt'
5
+ import { AIMessage } from '@langchain/core/messages'
4
6
  import { loadSessions, saveSessions, loadAgents, loadCredentials, loadSettings, loadSecrets, loadTasks, saveTasks, decryptKey, loadSkills } from './storage'
7
+ import { WORKSPACE_DIR } from './data-dir'
5
8
  import { loadRuntimeSettings, getOrchestratorLoopRecursionLimit } from './runtime-settings'
6
9
  import { getMemoryDb } from './memory-db'
7
10
  import { buildChatModel } from './build-llm'
8
- import crypto from 'crypto'
9
- import type { Agent, TaskComment } from '@/types'
10
-
11
- const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
11
+ import { getCheckpointSaver } from './langgraph-checkpoint'
12
+ import { notify } from './ws-hub'
13
+ import { pushMainLoopEventToMainSessions } from './main-agent-loop'
14
+ import { genId } from '@/lib/id'
15
+ import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
16
+ import type { Agent, TaskComment, MessageToolEvent } from '@/types'
12
17
 
13
18
  function resolveCredential(credentialId: string | null | undefined): string | null {
14
19
  if (!credentialId) return null
@@ -73,11 +78,11 @@ function getSecretsForOrchestrator(orchestratorId: string): { name: string; serv
73
78
  return result
74
79
  }
75
80
 
76
- function saveMessage(sessionId: string, role: 'user' | 'assistant', text: string) {
81
+ function saveMessage(sessionId: string, role: 'user' | 'assistant', text: string, toolEvents?: MessageToolEvent[]) {
77
82
  const sessions = loadSessions()
78
83
  const session = sessions[sessionId]
79
84
  if (!session) return
80
- session.messages.push({ role, text, time: Date.now() })
85
+ session.messages.push({ role, text, time: Date.now(), ...(toolEvents?.length ? { toolEvents } : {}) })
81
86
  session.lastActiveAt = Date.now()
82
87
  saveSessions(sessions)
83
88
  }
@@ -86,16 +91,15 @@ function saveMessage(sessionId: string, role: 'user' | 'assistant', text: string
86
91
  async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId: string): Promise<string> {
87
92
  // Dynamic import to avoid circular deps
88
93
  const { callProvider } = await import('./orchestrator')
89
- const crypto = await import('crypto')
90
94
  const { loadSessions: ls, saveSessions: ss } = await import('./storage')
91
95
 
92
96
  const sessions = ls()
93
97
  const parentSession = sessions[parentSessionId]
94
- const childId = crypto.randomBytes(4).toString('hex')
98
+ const childId = genId()
95
99
  sessions[childId] = {
96
100
  id: childId,
97
101
  name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
98
- cwd: parentSession?.cwd || process.cwd(),
102
+ cwd: parentSession?.cwd || WORKSPACE_DIR,
99
103
  user: 'system',
100
104
  provider: agent.provider,
101
105
  model: agent.model,
@@ -136,6 +140,7 @@ export async function executeLangGraphOrchestrator(
136
140
  orchestrator: Agent,
137
141
  task: string,
138
142
  sessionId: string,
143
+ taskId?: string,
139
144
  ): Promise<string> {
140
145
  const allAgents = loadAgents()
141
146
 
@@ -165,9 +170,12 @@ export async function executeLangGraphOrchestrator(
165
170
  return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
166
171
  }
167
172
  console.log(`[orchestrator-lg] Delegating to ${agent.name}: ${agentTask.slice(0, 80)}`)
168
- saveMessage(sessionId, 'assistant', `[Delegating to ${agent.name}]: ${agentTask}`)
169
173
  const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
170
- saveMessage(sessionId, 'user', `[Agent ${agent.name} result]: ${result.slice(0, 2000)}`)
174
+ saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
175
+ name: 'delegate_to_agent',
176
+ input: JSON.stringify({ agentName: agent.name, agentId: agent.id, task: agentTask }),
177
+ output: result.slice(0, 2000),
178
+ }])
171
179
  return result
172
180
  },
173
181
  {
@@ -263,7 +271,7 @@ export async function executeLangGraphOrchestrator(
263
271
  if (!t) return `Task "${taskId}" not found.`
264
272
  if (!t.comments) t.comments = []
265
273
  const c: TaskComment = {
266
- id: crypto.randomBytes(4).toString('hex'),
274
+ id: genId(),
267
275
  author: orchestrator.name,
268
276
  agentId: orchestrator.id,
269
277
  text: comment,
@@ -288,7 +296,7 @@ export async function executeLangGraphOrchestrator(
288
296
  const createTaskTool = tool(
289
297
  async ({ title, description: desc }) => {
290
298
  const tasks = loadTasks()
291
- const id = crypto.randomBytes(4).toString('hex')
299
+ const id = genId()
292
300
  tasks[id] = {
293
301
  id,
294
302
  title,
@@ -369,12 +377,102 @@ export async function executeLangGraphOrchestrator(
369
377
  taskContext,
370
378
  ].join('\n')
371
379
 
372
- const agent = createReactAgent({
373
- llm,
374
- tools: [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool],
375
- stateModifier: systemMessage,
380
+ const checkpointSaver = getCheckpointSaver()
381
+ const isStrictMode = settings.capabilityPolicyMode === 'strict'
382
+ const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
383
+ const llmWithTools = llm.bindTools(allTools)
384
+ const toolNode = new ToolNode(allTools)
385
+
386
+ // Track fallback attempts for delegate_to_agent failures
387
+ let fallbackAttempts = 0
388
+ const MAX_FALLBACK_ATTEMPTS = 2
389
+
390
+ // Agent node: calls LLM with tools
391
+ async function agentNode(state: typeof MessagesAnnotation.State) {
392
+ const response = await llmWithTools.invoke([
393
+ { role: 'system' as const, content: systemMessage },
394
+ ...state.messages,
395
+ ])
396
+ return { messages: [response] }
397
+ }
398
+
399
+ // Router node: inspects tool results, decides next step
400
+ function routerNode(state: typeof MessagesAnnotation.State) {
401
+ const messages = state.messages
402
+ const lastMsg = messages[messages.length - 1]
403
+
404
+ // Check if the last tool message contains an error from delegate_to_agent
405
+ if (lastMsg && typeof (lastMsg as any).content === 'string') {
406
+ const content = (lastMsg as any).content as string
407
+ const isError = content.startsWith('Error:') || content.startsWith('Agent "') && content.includes('not found')
408
+
409
+ if (isError && fallbackAttempts < MAX_FALLBACK_ATTEMPTS) {
410
+ fallbackAttempts++
411
+ // Look for a delegate tool call in recent messages and try to find alternative agent
412
+ const failedToolCall = [...messages].reverse().find(
413
+ (m) => (m as any).tool_calls?.some((tc: any) => tc.name === 'delegate_to_agent')
414
+ )
415
+ if (failedToolCall) {
416
+ const tc = (failedToolCall as any).tool_calls?.find((tc: any) => tc.name === 'delegate_to_agent')
417
+ const failedAgentName = tc?.args?.agentName || 'unknown'
418
+ const fallbackHint = `The agent "${failedAgentName}" failed. Try delegating to a different agent with matching capabilities, or re-plan your approach. Fallback attempt ${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS}.`
419
+ return { messages: [new AIMessage({ content: fallbackHint })] }
420
+ }
421
+ }
422
+ }
423
+
424
+ // No fallback needed — pass through
425
+ return { messages: [] }
426
+ }
427
+
428
+ // Routing function: after agent node, check if there are tool calls
429
+ function shouldContinue(state: typeof MessagesAnnotation.State) {
430
+ const lastMsg = state.messages[state.messages.length - 1]
431
+ const toolCalls = (lastMsg as any)?.tool_calls
432
+ if (toolCalls && toolCalls.length > 0) {
433
+ return 'tools'
434
+ }
435
+ return END
436
+ }
437
+
438
+ // After router, decide whether to go back to agent or end
439
+ function afterRouter(state: typeof MessagesAnnotation.State) {
440
+ const messages = state.messages
441
+ // If router added a fallback hint, route back to agent
442
+ const lastMsg = messages[messages.length - 1]
443
+ if (lastMsg && typeof (lastMsg as any).content === 'string' && (lastMsg as any).content.includes('Fallback attempt')) {
444
+ return 'agent'
445
+ }
446
+ return 'agent'
447
+ }
448
+
449
+ // Build the StateGraph
450
+ const graph = new StateGraph(MessagesAnnotation)
451
+ .addNode('agent', agentNode)
452
+ .addNode('tools', toolNode)
453
+ .addNode('router', routerNode)
454
+ .addEdge(START, 'agent')
455
+ .addConditionalEdges('agent', shouldContinue, { tools: 'tools', [END]: END })
456
+ .addEdge('tools', 'router')
457
+ .addConditionalEdges('router', afterRouter, { agent: 'agent' })
458
+
459
+ const compiledGraph = graph.compile({
460
+ checkpointer: checkpointSaver,
461
+ ...(isStrictMode ? { interruptBefore: ['tools'] } : {}),
376
462
  })
377
463
 
464
+ // Export graph structure for introspection
465
+ ;(compiledGraph as any).__graphStructure = {
466
+ nodes: ['agent', 'tools', 'router'],
467
+ edges: [
468
+ { from: START, to: 'agent' },
469
+ { from: 'agent', to: 'tools', condition: 'has_tool_calls' },
470
+ { from: 'agent', to: END, condition: 'no_tool_calls' },
471
+ { from: 'tools', to: 'router' },
472
+ { from: 'router', to: 'agent', condition: 'fallback_or_continue' },
473
+ ],
474
+ }
475
+
378
476
  // Save initial user message
379
477
  saveMessage(sessionId, 'user', task)
380
478
 
@@ -391,9 +489,15 @@ export async function executeLangGraphOrchestrator(
391
489
  : null
392
490
 
393
491
  try {
394
- const stream = await agent.stream(
492
+ const threadId = taskId || sessionId
493
+ const streamConfig = {
494
+ recursionLimit,
495
+ signal: abortController.signal,
496
+ configurable: { thread_id: threadId },
497
+ }
498
+ const stream = await compiledGraph.stream(
395
499
  { messages: [{ role: 'user' as const, content: task }] },
396
- { recursionLimit, signal: abortController.signal },
500
+ streamConfig,
397
501
  )
398
502
 
399
503
  for await (const chunk of stream) {
@@ -414,6 +518,65 @@ export async function executeLangGraphOrchestrator(
414
518
  }
415
519
  }
416
520
  }
521
+
522
+ // Check for interrupt (paused before tool execution in strict mode)
523
+ if (isStrictMode && taskId) {
524
+ const state = await compiledGraph.getState({ configurable: { thread_id: threadId } })
525
+ const nextNodes = state?.next || []
526
+ if (nextNodes.includes('tools')) {
527
+ // Graph is paused before tool execution — extract pending tool call
528
+ const messages = state.values?.messages || []
529
+ const lastMsg = messages[messages.length - 1]
530
+ const toolCalls = lastMsg?.tool_calls || lastMsg?.additional_kwargs?.tool_calls || []
531
+ const pendingCall = toolCalls[0]
532
+ if (pendingCall) {
533
+ // Try OpenClaw approval bridge first when agent uses openclaw provider
534
+ try {
535
+ if (orchestrator.provider === 'openclaw') {
536
+ const { forwardApprovalToOpenClaw } = await import('./openclaw-approvals')
537
+ const toolName = pendingCall.name || pendingCall.function?.name || 'unknown'
538
+ const toolArgs = pendingCall.args || (pendingCall.function?.arguments ? JSON.parse(pendingCall.function.arguments) : {})
539
+ const decision = await forwardApprovalToOpenClaw({ toolName, args: toolArgs })
540
+ if (decision) {
541
+ if (decision.approved) {
542
+ // OpenClaw approved — resume the graph instead of pausing
543
+ console.log(`[orchestrator-lg] OpenClaw approved tool "${toolName}" — resuming graph`)
544
+ // Don't set pendingApproval, let the loop continue
545
+ } else {
546
+ console.log(`[orchestrator-lg] OpenClaw rejected tool "${toolName}": ${decision.reason || 'no reason'}`)
547
+ // Fall through to SwarmClaw's pendingApproval UI
548
+ }
549
+ }
550
+ // If decision is null (socket unavailable), fall through to SwarmClaw UI
551
+ }
552
+ } catch {
553
+ // OpenClaw approval bridge not available — fall through
554
+ }
555
+
556
+ const tasks = loadTasks()
557
+ const t = tasks[taskId]
558
+ if (t) {
559
+ t.pendingApproval = {
560
+ toolName: pendingCall.name || pendingCall.function?.name || 'unknown',
561
+ args: pendingCall.args || (pendingCall.function?.arguments ? JSON.parse(pendingCall.function.arguments) : {}),
562
+ threadId,
563
+ }
564
+ t.updatedAt = Date.now()
565
+ saveTasks(tasks)
566
+ notify('tasks')
567
+ const approvalMsg = `[Awaiting approval] Tool: ${t.pendingApproval.toolName}\nArgs: ${JSON.stringify(t.pendingApproval.args).slice(0, 300)}\n\nApprove or reject in the task board.`
568
+ saveMessage(sessionId, 'assistant', approvalMsg)
569
+ notify(`messages:${sessionId}`)
570
+ pushMainLoopEventToMainSessions({
571
+ type: 'pending_approval',
572
+ text: `Task "${t.title}" needs approval: ${t.pendingApproval.toolName}(${JSON.stringify(t.pendingApproval.args).slice(0, 100)})`,
573
+ })
574
+ console.log(`[orchestrator-lg] Interrupt: waiting for approval of tool "${t.pendingApproval.toolName}" on task ${taskId}`)
575
+ return approvalMsg
576
+ }
577
+ }
578
+ }
579
+ }
417
580
  } catch (err: any) {
418
581
  const errMsg = timedOut
419
582
  ? 'Ongoing loop stopped after reaching the configured runtime limit.'
@@ -429,3 +592,242 @@ export async function executeLangGraphOrchestrator(
429
592
  const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
430
593
  return completeMatch ? completeMatch[1].trim() : finalResult
431
594
  }
595
+
596
+ /**
597
+ * Resume a paused orchestrator run after human approval.
598
+ * Re-creates the same agent graph and streams from the saved checkpoint.
599
+ */
600
+ export async function resumeLangGraphOrchestrator(
601
+ orchestrator: Agent,
602
+ sessionId: string,
603
+ threadId: string,
604
+ ): Promise<string> {
605
+ const allAgents = loadAgents()
606
+ const agentIds = orchestrator.subAgentIds || []
607
+ const agents = agentIds.map((id) => allAgents[id]).filter(Boolean) as Agent[]
608
+
609
+ // Recreate the same tools
610
+ const delegateTool = tool(
611
+ async ({ agentName, task: agentTask }) => {
612
+ const agent = agents.find((a) => a.name.toLowerCase() === agentName.toLowerCase())
613
+ if (!agent) return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
614
+ const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
615
+ saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
616
+ name: 'delegate_to_agent',
617
+ input: JSON.stringify({ agentName: agent.name, agentId: agent.id, task: agentTask }),
618
+ output: result.slice(0, 2000),
619
+ }])
620
+ return result
621
+ },
622
+ {
623
+ name: 'delegate_to_agent',
624
+ description: 'Delegate a task to one of the available agents.',
625
+ schema: z.object({
626
+ agentName: z.string().describe('Name of the agent to delegate to'),
627
+ task: z.string().describe('The task description for the agent'),
628
+ }),
629
+ },
630
+ )
631
+
632
+ const db = getMemoryDb()
633
+ const storeMemoryTool = tool(
634
+ async ({ category, title, content }) => {
635
+ db.add({ agentId: orchestrator.id, sessionId, category, title, content })
636
+ return 'Memory stored successfully.'
637
+ },
638
+ {
639
+ name: 'store_memory',
640
+ description: 'Store information in long-term memory.',
641
+ schema: z.object({
642
+ category: z.string(), title: z.string(), content: z.string(),
643
+ }),
644
+ },
645
+ )
646
+
647
+ const searchMemoryTool = tool(
648
+ async ({ query }) => {
649
+ const results = db.search(query, orchestrator.id)
650
+ if (!results.length) return 'No matching memories found.'
651
+ return results.map((m) => `[${m.category}] ${m.title}: ${m.content.slice(0, 300)}`).join('\n')
652
+ },
653
+ {
654
+ name: 'search_memory',
655
+ description: 'Search long-term memory.',
656
+ schema: z.object({ query: z.string() }),
657
+ },
658
+ )
659
+
660
+ const markCompleteTool = tool(
661
+ async ({ summary }) => `ORCHESTRATION_COMPLETE: ${summary}`,
662
+ {
663
+ name: 'mark_complete',
664
+ description: 'Signal orchestration is done.',
665
+ schema: z.object({ summary: z.string() }),
666
+ },
667
+ )
668
+
669
+ const availableSecrets = getSecretsForOrchestrator(orchestrator.id)
670
+ const getSecretTool = tool(
671
+ async ({ serviceName }) => {
672
+ const match = availableSecrets.find(
673
+ (s) => s.service.toLowerCase() === serviceName.toLowerCase() || s.name.toLowerCase() === serviceName.toLowerCase(),
674
+ )
675
+ if (!match) return `No secret found for "${serviceName}".`
676
+ return JSON.stringify({ name: match.name, service: match.service, value: match.value })
677
+ },
678
+ {
679
+ name: 'get_secret',
680
+ description: 'Retrieve a stored credential/secret by service name.',
681
+ schema: z.object({ serviceName: z.string() }),
682
+ },
683
+ )
684
+
685
+ const commentOnTaskTool = tool(
686
+ async ({ taskId, comment }) => {
687
+ const tasks = loadTasks()
688
+ const t = tasks[taskId]
689
+ if (!t) return `Task "${taskId}" not found.`
690
+ if (!t.comments) t.comments = []
691
+ t.comments.push({
692
+ id: genId(),
693
+ author: orchestrator.name,
694
+ agentId: orchestrator.id,
695
+ text: comment,
696
+ createdAt: Date.now(),
697
+ })
698
+ t.updatedAt = Date.now()
699
+ saveTasks(tasks)
700
+ return `Comment added to task "${t.title}".`
701
+ },
702
+ {
703
+ name: 'comment_on_task',
704
+ description: 'Add a comment to a task.',
705
+ schema: z.object({ taskId: z.string(), comment: z.string() }),
706
+ },
707
+ )
708
+
709
+ const createTaskTool = tool(
710
+ async ({ title, description: desc }) => {
711
+ const tasks = loadTasks()
712
+ const id = genId()
713
+ tasks[id] = {
714
+ id, title, description: desc, status: 'backlog',
715
+ agentId: orchestrator.id, sessionId: null, result: null, error: null,
716
+ comments: [], createdAt: Date.now(), updatedAt: Date.now(),
717
+ queuedAt: null, startedAt: null, completedAt: null,
718
+ }
719
+ saveTasks(tasks)
720
+ return `Task "${title}" created in backlog (id: ${id}).`
721
+ },
722
+ {
723
+ name: 'create_task',
724
+ description: 'Create a new task in the backlog.',
725
+ schema: z.object({ title: z.string(), description: z.string() }),
726
+ },
727
+ )
728
+
729
+ const engine = getOrchestrationEngineConfig(orchestrator)
730
+ const llm = buildChatModel({
731
+ provider: engine.provider, model: engine.model,
732
+ apiKey: engine.apiKey, apiEndpoint: engine.apiEndpoint,
733
+ })
734
+
735
+ const checkpointSaver = getCheckpointSaver()
736
+ const settings = loadSettings()
737
+ const isStrictMode = settings.capabilityPolicyMode === 'strict'
738
+
739
+ const allTools = [delegateTool, storeMemoryTool, searchMemoryTool, getSecretTool, commentOnTaskTool, createTaskTool, markCompleteTool]
740
+ const llmWithTools = llm.bindTools(allTools)
741
+ const toolNode = new ToolNode(allTools)
742
+
743
+ async function agentNode(state: typeof MessagesAnnotation.State) {
744
+ const response = await llmWithTools.invoke(state.messages)
745
+ return { messages: [response] }
746
+ }
747
+ function routerNode(state: typeof MessagesAnnotation.State) {
748
+ return { messages: [] }
749
+ }
750
+ function shouldContinue(state: typeof MessagesAnnotation.State) {
751
+ const lastMsg = state.messages[state.messages.length - 1]
752
+ const toolCalls = (lastMsg as any)?.tool_calls
753
+ if (toolCalls && toolCalls.length > 0) return 'tools'
754
+ return END
755
+ }
756
+
757
+ const graphAgent = new StateGraph(MessagesAnnotation)
758
+ .addNode('agent', agentNode)
759
+ .addNode('tools', toolNode)
760
+ .addNode('router', routerNode)
761
+ .addEdge(START, 'agent')
762
+ .addConditionalEdges('agent', shouldContinue, { tools: 'tools', [END]: END })
763
+ .addEdge('tools', 'router')
764
+ .addEdge('router', 'agent')
765
+ .compile({
766
+ checkpointer: checkpointSaver,
767
+ ...(isStrictMode ? { interruptBefore: ['tools'] } : {}),
768
+ })
769
+
770
+ let finalResult = ''
771
+ const runtime = loadRuntimeSettings()
772
+ const recursionLimit = getOrchestratorLoopRecursionLimit(runtime)
773
+
774
+ try {
775
+ // Resume from checkpoint with null input — the checkpoint has the full state
776
+ const stream = await graphAgent.stream(null, {
777
+ recursionLimit,
778
+ configurable: { thread_id: threadId },
779
+ })
780
+
781
+ for await (const chunk of stream) {
782
+ const agentChunk = (chunk as any).agent
783
+ if (agentChunk?.messages) {
784
+ const msgs = Array.isArray(agentChunk.messages) ? agentChunk.messages : [agentChunk.messages]
785
+ for (const msg of msgs) {
786
+ const text = typeof msg.content === 'string'
787
+ ? msg.content
788
+ : Array.isArray(msg.content)
789
+ ? msg.content.map((c: any) => c.text || '').join('')
790
+ : ''
791
+ if (text) {
792
+ finalResult = text
793
+ saveMessage(sessionId, 'assistant', text)
794
+ }
795
+ }
796
+ }
797
+ }
798
+
799
+ // Check for another interrupt
800
+ const state = await graphAgent.getState({ configurable: { thread_id: threadId } })
801
+ const nextNodes = state?.next || []
802
+ if (nextNodes.includes('tools')) {
803
+ const messages = state.values?.messages || []
804
+ const lastMsg = messages[messages.length - 1]
805
+ const toolCalls = lastMsg?.tool_calls || lastMsg?.additional_kwargs?.tool_calls || []
806
+ const pendingCall = toolCalls[0]
807
+ if (pendingCall) {
808
+ // Find the task by threadId
809
+ const tasks = loadTasks()
810
+ for (const t of Object.values(tasks)) {
811
+ if (t.pendingApproval?.threadId === threadId || t.id === threadId) {
812
+ t.pendingApproval = {
813
+ toolName: pendingCall.name || pendingCall.function?.name || 'unknown',
814
+ args: pendingCall.args || {},
815
+ threadId,
816
+ }
817
+ t.updatedAt = Date.now()
818
+ saveTasks(tasks)
819
+ notify('tasks')
820
+ return `Waiting for approval: ${t.pendingApproval.toolName}`
821
+ }
822
+ }
823
+ }
824
+ }
825
+ } catch (err: any) {
826
+ console.error(`[orchestrator-lg] Resume error:`, err.message || String(err))
827
+ saveMessage(sessionId, 'assistant', `[Error] ${err.message || String(err)}`)
828
+ throw err
829
+ }
830
+
831
+ const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
832
+ return completeMatch ? completeMatch[1].trim() : finalResult
833
+ }
@@ -1,8 +1,9 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import {
3
3
  loadSessions, saveSessions, loadAgents,
4
4
  loadCredentials, decryptKey, loadSettings, loadSkills,
5
5
  } from './storage'
6
+ import { WORKSPACE_DIR } from './data-dir'
6
7
  import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
7
8
  import { getMemoryDb } from './memory-db'
8
9
  import { getProvider } from '../providers'
@@ -19,11 +20,11 @@ export function createOrchestratorSession(
19
20
  cwd?: string,
20
21
  ): string {
21
22
  const sessions = loadSessions()
22
- const sessionId = crypto.randomBytes(4).toString('hex')
23
+ const sessionId = genId()
23
24
  sessions[sessionId] = {
24
25
  id: sessionId,
25
26
  name: `[Orch] ${orchestrator.name}: ${task.slice(0, 40)}`,
26
- cwd: cwd || process.cwd(),
27
+ cwd: cwd || WORKSPACE_DIR,
27
28
  user: 'system',
28
29
  provider: orchestrator.provider,
29
30
  model: orchestrator.model,
@@ -63,13 +64,14 @@ export async function executeOrchestrator(
63
64
  orchestrator: Agent,
64
65
  task: string,
65
66
  sessionId: string,
67
+ taskId?: string,
66
68
  ): Promise<string> {
67
69
  // Use LangGraph for all non-CLI providers (including OpenAI-compatible custom providers)
68
70
  const isCliProvider = orchestrator.provider === 'claude-cli' || orchestrator.provider === 'codex-cli' || orchestrator.provider === 'opencode-cli'
69
71
  if (!isCliProvider) {
70
72
  console.log(`[orchestrator] Using LangGraph engine for ${orchestrator.name} (${orchestrator.provider})`)
71
73
  const { executeLangGraphOrchestrator } = await import('./orchestrator-lg')
72
- return executeLangGraphOrchestrator(orchestrator, task, sessionId)
74
+ return executeLangGraphOrchestrator(orchestrator, task, sessionId, taskId)
73
75
  }
74
76
 
75
77
  // claude-cli fallback (no structured tool calling)
@@ -156,7 +158,10 @@ async function executeOrchestratorLegacy(
156
158
  }
157
159
  }
158
160
 
159
- const fullText = await callProvider(orchestrator, systemPrompt, conversationHistory)
161
+ const windowedHistory = conversationHistory.length > 10
162
+ ? [conversationHistory[0], ...conversationHistory.slice(-9)]
163
+ : conversationHistory
164
+ const fullText = await callProvider(orchestrator, systemPrompt, windowedHistory)
160
165
  conversationHistory.push({ role: 'assistant', text: fullText })
161
166
 
162
167
  // Save to session
@@ -189,6 +194,21 @@ async function executeOrchestratorLegacy(
189
194
  role: 'user',
190
195
  text: `[Agent ${agent.name} result]:\n${subResult}`,
191
196
  })
197
+ // Save structured delegation message for rich card rendering
198
+ session.messages.push({
199
+ role: 'assistant' as const,
200
+ text: `Delegated to ${agent.name}: ${cmd.delegate.task.slice(0, 100)}`,
201
+ time: Date.now(),
202
+ toolEvents: [{
203
+ name: 'delegate_to_agent',
204
+ input: JSON.stringify({ agentName: agent.name, agentId: agent.id, task: cmd.delegate.task }),
205
+ output: subResult.slice(0, 2000),
206
+ }],
207
+ })
208
+ session.lastActiveAt = Date.now()
209
+ const ds = loadSessions()
210
+ ds[sessionId] = session
211
+ saveSessions(ds)
192
212
  }
193
213
 
194
214
  if (cmd.memory_store) {
@@ -244,11 +264,11 @@ async function executeSubTask(
244
264
  // Look up parent session cwd to inherit
245
265
  const sessions = loadSessions()
246
266
  const parentSession = sessions[parentSessionId]
247
- const childId = crypto.randomBytes(4).toString('hex')
267
+ const childId = genId()
248
268
  const childSession = {
249
269
  id: childId,
250
270
  name: `[Agent] ${agent.name}: ${task.slice(0, 40)}`,
251
- cwd: parentSession?.cwd || process.cwd(),
271
+ cwd: parentSession?.cwd || WORKSPACE_DIR,
252
272
  user: 'system',
253
273
  provider: agent.provider,
254
274
  model: agent.model,
@@ -305,12 +325,12 @@ export async function callProvider(
305
325
 
306
326
  // Build a mock session for the provider
307
327
  const mockSession = {
308
- id: 'orch-' + crypto.randomBytes(2).toString('hex'),
328
+ id: 'orch-' + genId(2),
309
329
  provider: agent.provider,
310
330
  model: agent.model,
311
331
  credentialId: agent.credentialId,
312
332
  apiEndpoint: agent.apiEndpoint,
313
- cwd: process.cwd(),
333
+ cwd: WORKSPACE_DIR,
314
334
  tools: agent.tools || [],
315
335
  messages: history.map((h) => ({
316
336
  role: h.role as 'user' | 'assistant',
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
3
3
 
4
4
  const MAX_LOG_CHARS = 200_000
@@ -99,7 +99,7 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
99
99
  }
100
100
 
101
101
  export async function startManagedProcess(opts: StartProcessOptions): Promise<StartProcessResult> {
102
- const id = crypto.randomBytes(8).toString('hex')
102
+ const id = genId(8)
103
103
  const timeoutMs = Math.max(1000, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
104
104
  const yieldMs = Math.max(250, opts.yieldMs ?? DEFAULT_BACKGROUND_YIELD_MS)
105
105
  const startedAt = now()