@swarmclawai/swarmclaw 1.3.6 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +32 -1
  2. package/package.json +9 -3
  3. package/src/.env.local +4 -0
  4. package/src/app/api/.well-known/agent-card/route.ts +46 -0
  5. package/src/app/api/a2a/route.ts +56 -0
  6. package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
  7. package/src/app/api/chats/[id]/deploy/route.ts +2 -2
  8. package/src/app/api/openclaw/sync/route.ts +1 -1
  9. package/src/app/api/swarmfeed/channels/route.ts +14 -0
  10. package/src/app/api/swarmfeed/posts/route.ts +60 -0
  11. package/src/app/api/swarmfeed/route.ts +37 -0
  12. package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
  13. package/src/app/protocols/page.tsx +16 -7
  14. package/src/app/swarmfeed/page.tsx +7 -0
  15. package/src/cli/index.js +19 -0
  16. package/src/cli/spec.js +8 -0
  17. package/src/components/agents/agent-avatar.tsx +2 -5
  18. package/src/components/agents/agent-sheet.tsx +10 -0
  19. package/src/components/auth/access-key-gate.tsx +25 -0
  20. package/src/components/layout/sidebar-rail.tsx +52 -0
  21. package/src/components/protocols/builder/edge-editor.tsx +43 -0
  22. package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
  23. package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
  24. package/src/components/protocols/builder/edge-types/index.ts +3 -0
  25. package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
  26. package/src/components/protocols/builder/node-inspector.tsx +227 -0
  27. package/src/components/protocols/builder/node-palette.tsx +97 -0
  28. package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
  29. package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
  30. package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
  31. package/src/components/protocols/builder/node-types/index.ts +9 -0
  32. package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
  33. package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
  34. package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
  35. package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
  36. package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
  37. package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
  38. package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
  39. package/src/components/protocols/builder/run-overlay.tsx +29 -0
  40. package/src/components/protocols/builder/template-gallery.tsx +53 -0
  41. package/src/components/protocols/builder/validation-panel.tsx +57 -0
  42. package/src/components/skills/skills-workspace.tsx +1 -9
  43. package/src/features/protocols/builder/hooks/index.ts +2 -0
  44. package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
  45. package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
  46. package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
  47. package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
  48. package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
  49. package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
  50. package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
  51. package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
  52. package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
  53. package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
  54. package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
  55. package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
  56. package/src/features/swarmfeed/compose-post.tsx +139 -0
  57. package/src/features/swarmfeed/feed-page.tsx +136 -0
  58. package/src/features/swarmfeed/post-card.tsx +114 -0
  59. package/src/features/swarmfeed/queries.ts +28 -0
  60. package/src/lib/a2a/agent-card.ts +61 -0
  61. package/src/lib/a2a/auth.ts +54 -0
  62. package/src/lib/a2a/client.ts +133 -0
  63. package/src/lib/a2a/discovery.ts +116 -0
  64. package/src/lib/a2a/handlers.ts +176 -0
  65. package/src/lib/a2a/json-rpc-router.ts +38 -0
  66. package/src/lib/a2a/types.ts +95 -0
  67. package/src/lib/app/navigation.ts +1 -0
  68. package/src/lib/app/view-constants.ts +9 -1
  69. package/src/lib/providers/anthropic.ts +111 -107
  70. package/src/lib/providers/openai.ts +146 -142
  71. package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
  72. package/src/lib/server/agents/main-agent-loop.ts +377 -41
  73. package/src/lib/server/chat-execution/chat-execution.ts +12 -7
  74. package/src/lib/server/extensions.ts +11 -0
  75. package/src/lib/server/openclaw/sync.ts +4 -4
  76. package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
  77. package/src/lib/server/protocols/protocol-normalization.ts +1 -0
  78. package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
  79. package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
  80. package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
  81. package/src/lib/server/protocols/protocol-types.ts +1 -0
  82. package/src/lib/server/session-tools/delegate.ts +151 -77
  83. package/src/lib/server/storage-auth.ts +10 -2
  84. package/src/lib/server/storage-normalization.ts +11 -0
  85. package/src/lib/server/storage.ts +100 -0
  86. package/src/lib/server/working-state/service.test.ts +2 -3
  87. package/src/lib/server/working-state/service.ts +37 -6
  88. package/src/lib/swarmfeed-client.ts +157 -0
  89. package/src/lib/validation/schemas.ts +1 -1
  90. package/src/stores/slices/data-slice.ts +3 -0
  91. package/src/stores/use-approval-store.ts +4 -1
  92. package/src/types/agent.ts +31 -1
  93. package/src/types/index.ts +1 -0
  94. package/src/types/protocol.ts +19 -0
  95. package/src/types/session.ts +1 -1
  96. package/src/types/swarmfeed.ts +30 -0
@@ -23,7 +23,7 @@ async function fileToContentParts(filePath: string): Promise<Array<Record<string
23
23
  }
24
24
  if (filePath.endsWith('.pdf')) {
25
25
  try {
26
- // @ts-ignore — pdf-parse types
26
+ // @ts-expect-error — pdf-parse types
27
27
  const pdfParse = (await import(/* webpackIgnore: true */ 'pdf-parse')).default
28
28
  const buf = await fs.promises.readFile(filePath)
29
29
  const result = await pdfParse(buf)
@@ -45,153 +45,157 @@ async function fileToContentParts(filePath: string): Promise<Array<Record<string
45
45
  }
46
46
 
47
47
  export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
48
- return new Promise(async (resolve, reject) => {
49
- const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
50
- const model = session.model || 'gpt-4o'
51
-
52
- let fullResponse = ''
53
-
54
- // Support custom base URLs for custom providers
55
- const baseUrl = session.apiEndpoint || PROVIDER_DEFAULTS.openai
56
- const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
57
-
58
- // OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
59
- // which consumes the request body before http-proxy-middleware can forward it.
60
- // Sending as text/plain bypasses the body parser while the gateway still parses JSON.
61
- const contentType = session.contentType || 'application/json'
62
-
63
- const abortController = new AbortController()
64
- if (signal) {
65
- if (signal.aborted) abortController.abort()
66
- else signal.addEventListener('abort', () => abortController.abort(), { once: true })
67
- }
68
- active.set(session.id, { kill: () => abortController.abort() })
69
-
70
- try {
71
- // Try with stream_options first; if the provider rejects with 400, retry without it
72
- let res: Response | undefined
73
- let usageEnabled = true
74
- for (const includeStreamOptions of [true, false]) {
75
- const payloadObj: Record<string, unknown> = {
76
- model,
77
- messages,
78
- stream: true,
79
- }
80
- if (includeStreamOptions) {
81
- payloadObj.stream_options = { include_usage: true }
48
+ return new Promise((resolve, reject) => {
49
+ ;(async () => {
50
+ try {
51
+ const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
52
+ const model = session.model || 'gpt-4o'
53
+
54
+ let fullResponse = ''
55
+
56
+ // Support custom base URLs for custom providers
57
+ const baseUrl = session.apiEndpoint || PROVIDER_DEFAULTS.openai
58
+ const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
59
+
60
+ // OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
61
+ // which consumes the request body before http-proxy-middleware can forward it.
62
+ // Sending as text/plain bypasses the body parser while the gateway still parses JSON.
63
+ const contentType = session.contentType || 'application/json'
64
+
65
+ const abortController = new AbortController()
66
+ if (signal) {
67
+ if (signal.aborted) abortController.abort()
68
+ else signal.addEventListener('abort', () => abortController.abort(), { once: true })
82
69
  }
83
- const payload = JSON.stringify(payloadObj)
84
-
85
- res = await fetch(url, {
86
- method: 'POST',
87
- headers: {
88
- 'Authorization': `Bearer ${apiKey}`,
89
- 'Content-Type': contentType,
90
- },
91
- body: payload,
92
- signal: abortController.signal,
93
- })
94
-
95
- if (res.status === 400 && includeStreamOptions) {
96
- // Provider likely rejected stream_options — retry without it
97
- usageEnabled = false
98
- continue
99
- }
100
- usageEnabled = includeStreamOptions
101
- break
102
- }
103
-
104
- if (!res) {
105
- active.delete(session.id)
106
- reject(new Error('No response from provider'))
107
- return
108
- }
109
-
110
- // Detect HTML responses (e.g. landing page returned instead of API)
111
- const resContentType = res.headers.get('content-type') || ''
112
- if (resContentType.includes('text/html')) {
113
- const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
114
- log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
115
- writeSSE(write, 'err', msg)
116
- active.delete(session.id)
117
- reject(new Error(msg))
118
- return
119
- }
70
+ active.set(session.id, { kill: () => abortController.abort() })
120
71
 
121
- if (!res.ok) {
122
- const errBody = await res.text().catch(() => '')
123
- log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
124
- let errMsg = `API error (${res.status})`
125
72
  try {
126
- const parsed = JSON.parse(errBody)
127
- if (parsed.error?.message) errMsg = parsed.error.message
128
- else if (parsed.message) errMsg = parsed.message
129
- else if (parsed.detail) errMsg = parsed.detail
130
- } catch {}
131
- writeSSE(write, 'err', errMsg)
132
- active.delete(session.id)
133
- reject(new Error(`OpenAI error ${res.status}: ${errMsg}`))
134
- return
135
- }
136
-
137
- if (!res.body) {
138
- const msg = `No response body from ${baseUrl}`
139
- log.error(TAG, `[${session.id}] ${msg}`)
140
- active.delete(session.id)
141
- reject(new Error(msg))
142
- return
143
- }
144
-
145
- const reader = res.body.getReader()
146
- const decoder = new TextDecoder()
147
- let buf = ''
148
-
149
- while (true) {
150
- const { done, value } = await reader.read()
151
- if (done) break
152
- if (abortController.signal.aborted) break
153
-
154
- buf += decoder.decode(value, { stream: true })
155
- const lines = buf.split('\n')
156
- buf = lines.pop()!
157
-
158
- for (const line of lines) {
159
- if (!line.startsWith('data: ')) continue
160
- const data = line.slice(6).trim()
161
- if (data === '[DONE]') continue
162
- try {
163
- const parsed = JSON.parse(data)
164
- const delta = parsed.choices?.[0]?.delta?.content
165
- if (delta) {
166
- fullResponse += delta
167
- writeSSE(write, 'd', delta)
73
+ // Try with stream_options first; if the provider rejects with 400, retry without it
74
+ let res: Response | undefined
75
+ let usageEnabled = true
76
+ for (const includeStreamOptions of [true, false]) {
77
+ const payloadObj: Record<string, unknown> = {
78
+ model,
79
+ messages,
80
+ stream: true,
81
+ }
82
+ if (includeStreamOptions) {
83
+ payloadObj.stream_options = { include_usage: true }
168
84
  }
169
- // Extract usage from the final chunk (stream_options: include_usage)
170
- if (usageEnabled && parsed.usage && onUsage) {
171
- onUsage({
172
- inputTokens: parsed.usage.prompt_tokens || 0,
173
- outputTokens: parsed.usage.completion_tokens || 0,
174
- })
85
+ const payload = JSON.stringify(payloadObj)
86
+
87
+ res = await fetch(url, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Authorization': `Bearer ${apiKey}`,
91
+ 'Content-Type': contentType,
92
+ },
93
+ body: payload,
94
+ signal: abortController.signal,
95
+ })
96
+
97
+ if (res.status === 400 && includeStreamOptions) {
98
+ // Provider likely rejected stream_options — retry without it
99
+ usageEnabled = false
100
+ continue
175
101
  }
176
- } catch {}
102
+ usageEnabled = includeStreamOptions
103
+ break
104
+ }
105
+
106
+ if (!res) {
107
+ active.delete(session.id)
108
+ reject(new Error('No response from provider'))
109
+ return
110
+ }
111
+
112
+ // Detect HTML responses (e.g. landing page returned instead of API)
113
+ const resContentType = res.headers.get('content-type') || ''
114
+ if (resContentType.includes('text/html')) {
115
+ const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
116
+ log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
117
+ writeSSE(write, 'err', msg)
118
+ active.delete(session.id)
119
+ reject(new Error(msg))
120
+ return
121
+ }
122
+
123
+ if (!res.ok) {
124
+ const errBody = await res.text().catch(() => '')
125
+ log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
126
+ let errMsg = `API error (${res.status})`
127
+ try {
128
+ const parsed = JSON.parse(errBody)
129
+ if (parsed.error?.message) errMsg = parsed.error.message
130
+ else if (parsed.message) errMsg = parsed.message
131
+ else if (parsed.detail) errMsg = parsed.detail
132
+ } catch {}
133
+ writeSSE(write, 'err', errMsg)
134
+ active.delete(session.id)
135
+ reject(new Error(`OpenAI error ${res.status}: ${errMsg}`))
136
+ return
137
+ }
138
+
139
+ if (!res.body) {
140
+ const msg = `No response body from ${baseUrl}`
141
+ log.error(TAG, `[${session.id}] ${msg}`)
142
+ active.delete(session.id)
143
+ reject(new Error(msg))
144
+ return
145
+ }
146
+
147
+ const reader = res.body.getReader()
148
+ const decoder = new TextDecoder()
149
+ let buf = ''
150
+
151
+ while (true) {
152
+ const { done, value } = await reader.read()
153
+ if (done) break
154
+ if (abortController.signal.aborted) break
155
+
156
+ buf += decoder.decode(value, { stream: true })
157
+ const lines = buf.split('\n')
158
+ buf = lines.pop()!
159
+
160
+ for (const line of lines) {
161
+ if (!line.startsWith('data: ')) continue
162
+ const data = line.slice(6).trim()
163
+ if (data === '[DONE]') continue
164
+ try {
165
+ const parsed = JSON.parse(data)
166
+ const delta = parsed.choices?.[0]?.delta?.content
167
+ if (delta) {
168
+ fullResponse += delta
169
+ writeSSE(write, 'd', delta)
170
+ }
171
+ // Extract usage from the final chunk (stream_options: include_usage)
172
+ if (usageEnabled && parsed.usage && onUsage) {
173
+ onUsage({
174
+ inputTokens: parsed.usage.prompt_tokens || 0,
175
+ outputTokens: parsed.usage.completion_tokens || 0,
176
+ })
177
+ }
178
+ } catch {}
179
+ }
180
+ }
181
+
182
+ if (!fullResponse) {
183
+ log.error(TAG, `[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
184
+ }
185
+ } catch (err: unknown) {
186
+ const errObj = err as { name?: string; message?: string }
187
+ if (errObj.name !== 'AbortError') {
188
+ log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
189
+ writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
190
+ }
191
+ active.delete(session.id)
192
+ reject(err)
193
+ return
177
194
  }
178
- }
179
-
180
- if (!fullResponse) {
181
- log.error(TAG, `[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
182
- }
183
- } catch (err: unknown) {
184
- const errObj = err as { name?: string; message?: string }
185
- if (errObj.name !== 'AbortError') {
186
- log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
187
- writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
188
- }
189
- active.delete(session.id)
190
- reject(err)
191
- return
192
- }
193
- active.delete(session.id)
194
- resolve(fullResponse)
195
+ active.delete(session.id)
196
+ resolve(fullResponse)
197
+ } catch (err) { reject(err) }
198
+ })()
195
199
  })
196
200
  }
197
201
 
@@ -346,6 +346,100 @@ describe('main-agent-loop', () => {
346
346
  assert.equal(output.followupMessage, null)
347
347
  })
348
348
 
349
+ it('accepts structured autonomy ticks for durable objectives without falling back to legacy tags', () => {
350
+ const output = runWithTempDataDir(`
351
+ const storageMod = await import('@/lib/server/storage')
352
+ const mainLoopMod = await import('@/lib/server/agents/main-agent-loop')
353
+ const storage = storageMod.default || storageMod
354
+ const mainLoop = mainLoopMod.default || mainLoopMod
355
+
356
+ storage.saveAgents({
357
+ 'agent-a': {
358
+ id: 'agent-a',
359
+ name: 'Agent A',
360
+ provider: 'openai',
361
+ model: 'gpt-test',
362
+ },
363
+ })
364
+
365
+ storage.saveSessions({
366
+ main: {
367
+ id: 'main',
368
+ name: 'Main Agent Thread',
369
+ shortcutForAgentId: 'agent-a',
370
+ cwd: process.cwd(),
371
+ user: 'tester',
372
+ provider: 'openai',
373
+ model: 'gpt-test',
374
+ claudeSessionId: null,
375
+ messages: [],
376
+ createdAt: 1,
377
+ lastActiveAt: 1,
378
+ sessionType: 'human',
379
+ agentId: 'agent-a',
380
+ heartbeatEnabled: true,
381
+ missionId: 'mission-1',
382
+ },
383
+ })
384
+
385
+ storage.saveMissions({
386
+ 'mission-1': {
387
+ id: 'mission-1',
388
+ source: 'heartbeat',
389
+ objective: 'Ship the autonomy hardening release',
390
+ status: 'active',
391
+ phase: 'executing',
392
+ sessionId: 'main',
393
+ agentId: 'agent-a',
394
+ taskIds: [],
395
+ currentStep: 'Verify the release checklist',
396
+ plannerSummary: 'Use the durable controller state.',
397
+ verifierSummary: null,
398
+ blockerSummary: null,
399
+ waitState: null,
400
+ createdAt: 1,
401
+ updatedAt: Date.now(),
402
+ },
403
+ })
404
+
405
+ const prompt = mainLoop.buildMainLoopHeartbeatPrompt(storage.loadSessions().main, 'fallback heartbeat')
406
+ mainLoop.handleMainLoopRunResult({
407
+ sessionId: 'main',
408
+ message: 'Heartbeat tick',
409
+ internal: true,
410
+ source: 'heartbeat',
411
+ resultText: [
412
+ 'Validated the release checklist.',
413
+ '[AUTONOMY_TICK]{"status":"progress","summary":"Release checklist validated.","next_action":"publish release artifacts","plan_steps":["verify artifacts","publish release artifacts"],"current_step":"verify artifacts","completed_steps":["Verify the release checklist"],"review":{"note":"Ready for artifact publication.","confidence":0.88,"needs_replan":false}}',
414
+ ].join('\\n'),
415
+ })
416
+ const state = mainLoop.getMainLoopStateForSession('main')
417
+
418
+ console.log(JSON.stringify({
419
+ promptIncludesAutonomyTick: prompt.includes('[AUTONOMY_TICK]'),
420
+ promptIncludesLegacyPlanTags: prompt.includes('[MAIN_LOOP_PLAN]'),
421
+ stateStatus: state?.status || null,
422
+ stateSummary: state?.summary || null,
423
+ stateNextAction: state?.nextAction || null,
424
+ statePlanSteps: state?.planSteps || [],
425
+ stateCurrentPlanStep: state?.currentPlanStep || null,
426
+ stateCompletedPlanSteps: state?.completedPlanSteps || [],
427
+ stateReviewNote: state?.reviewNote || null,
428
+ }))
429
+ `)
430
+
431
+ assert.equal(output.promptIncludesAutonomyTick, true)
432
+ assert.equal(output.promptIncludesLegacyPlanTags, false)
433
+ assert.equal(output.stateStatus, 'progress')
434
+ assert.match(String(output.stateSummary), /validated/i)
435
+ assert.equal(output.stateNextAction, 'publish release artifacts')
436
+ assert.ok((output.statePlanSteps as string[]).includes('verify artifacts'))
437
+ assert.ok((output.statePlanSteps as string[]).includes('publish release artifacts'))
438
+ assert.equal(output.stateCurrentPlanStep, 'verify artifacts')
439
+ assert.ok((output.stateCompletedPlanSteps as string[]).includes('Verify the release checklist'))
440
+ assert.match(String(output.stateReviewNote), /artifact publication/i)
441
+ })
442
+
349
443
  it('does not let internal heartbeat prompts rewrite the stored goal contract', () => {
350
444
  const output = runWithTempDataDir(`
351
445
  const storageMod = await import('@/lib/server/storage')