@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
@@ -0,0 +1,38 @@
1
+ import { hmrSingleton, errorMessage } from '@/lib/shared-utils'
2
+ import type { A2AMethod, A2AMethodHandler, A2AContext, JsonRpcRequest, JsonRpcResponse } from './types'
3
+ import { JSON_RPC_ERRORS } from './types'
4
+
5
+ export class JsonRpcRouter {
6
+ private handlers = new Map<string, A2AMethodHandler>()
7
+
8
+ register(method: A2AMethod | string, handler: A2AMethodHandler): void {
9
+ this.handlers.set(method, handler)
10
+ }
11
+
12
+ async route(request: JsonRpcRequest, context: A2AContext): Promise<JsonRpcResponse> {
13
+ const handler = this.handlers.get(request.method)
14
+ if (!handler) {
15
+ return {
16
+ jsonrpc: '2.0',
17
+ error: { code: JSON_RPC_ERRORS.METHOD_NOT_FOUND, message: 'Method not found', data: { method: request.method } },
18
+ id: request.id,
19
+ }
20
+ }
21
+ try {
22
+ const result = await handler(request.params ?? {}, context)
23
+ return { jsonrpc: '2.0', result, id: request.id }
24
+ } catch (err) {
25
+ return {
26
+ jsonrpc: '2.0',
27
+ error: { code: JSON_RPC_ERRORS.INTERNAL_ERROR, message: errorMessage(err) },
28
+ id: request.id,
29
+ }
30
+ }
31
+ }
32
+
33
+ listMethods(): string[] {
34
+ return [...this.handlers.keys()]
35
+ }
36
+ }
37
+
38
+ export const a2aRouter = hmrSingleton('a2a_jsonrpc_router', () => new JsonRpcRouter())
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod'
2
+
3
+ // --- A2A Agent Card ---
4
+ // Ref: https://a2a-protocol.org/v0.3.0/specification/#agent-card
5
+
6
+ export const AgentCardCapabilitySchema = z.object({
7
+ name: z.string(),
8
+ methods: z.array(z.string()).optional(),
9
+ description: z.string().optional(),
10
+ })
11
+
12
+ export const AgentCardSkillSchema = z.object({
13
+ name: z.string(),
14
+ description: z.string().optional(),
15
+ parameters: z.record(z.string(), z.unknown()).optional(),
16
+ returns: z.record(z.string(), z.unknown()).optional(),
17
+ })
18
+
19
+ export const AgentCardSchema = z.object({
20
+ name: z.string(),
21
+ description: z.string(),
22
+ version: z.string(),
23
+ protocolVersion: z.string().default('0.3.0'),
24
+ apiEndpoint: z.string().url(),
25
+ capabilities: z.array(AgentCardCapabilitySchema).default([]),
26
+ skills: z.array(AgentCardSkillSchema).default([]),
27
+ authMethods: z.array(z.enum(['api_key', 'ed25519', 'oauth2'])).default(['api_key']),
28
+ supportsStreaming: z.boolean().default(true),
29
+ supportsAsync: z.boolean().default(true),
30
+ rateLimit: z.object({
31
+ requestsPerMinute: z.number().optional(),
32
+ maxConcurrentRequests: z.number().optional(),
33
+ }).optional(),
34
+ extensions: z.array(z.object({
35
+ name: z.string(),
36
+ version: z.string(),
37
+ url: z.string().url().optional(),
38
+ })).default([]),
39
+ tags: z.array(z.string()).default([]),
40
+ icon: z.string().url().optional(),
41
+ website: z.string().url().optional(),
42
+ })
43
+
44
+ export type AgentCard = z.infer<typeof AgentCardSchema>
45
+
46
+ // --- JSON-RPC 2.0 ---
47
+ // Ref: https://www.jsonrpc.org/specification
48
+
49
+ export const JsonRpcRequestSchema = z.object({
50
+ jsonrpc: z.literal('2.0'),
51
+ method: z.string(),
52
+ params: z.record(z.string(), z.unknown()).optional(),
53
+ id: z.union([z.string(), z.number()]).optional(),
54
+ })
55
+
56
+ export type JsonRpcRequest = z.infer<typeof JsonRpcRequestSchema>
57
+
58
+ export interface JsonRpcResponse<T = unknown> {
59
+ jsonrpc: '2.0'
60
+ result?: T
61
+ error?: { code: number; message: string; data?: unknown }
62
+ id?: string | number
63
+ }
64
+
65
+ // --- A2A Method Types ---
66
+
67
+ export type A2AMethod = 'executeTask' | 'getStatus' | 'cancelTask' | 'discoverAgents'
68
+
69
+ export type A2AMethodHandler = (params: Record<string, unknown>, context: A2AContext) => Promise<unknown>
70
+
71
+ export interface A2AContext {
72
+ agentId: string
73
+ requesterId: string
74
+ timestamp: Date
75
+ }
76
+
77
+ export type A2ATaskStatus = 'submitted' | 'working' | 'completed' | 'failed' | 'cancelled'
78
+
79
+ // --- A2A Client Options ---
80
+
81
+ export interface A2AClientOptions {
82
+ timeout?: number
83
+ credentialId?: string | null
84
+ retryAttempts?: number
85
+ }
86
+
87
+ // --- JSON-RPC Error Codes ---
88
+
89
+ export const JSON_RPC_ERRORS = {
90
+ PARSE_ERROR: -32700,
91
+ METHOD_NOT_FOUND: -32601,
92
+ INVALID_PARAMS: -32602,
93
+ INTERNAL_ERROR: -32603,
94
+ AUTH_FAILED: -32000,
95
+ } as const
@@ -31,6 +31,7 @@ const VIEW_TO_PATH: Record<AppView, string> = {
31
31
  settings: '/settings',
32
32
  projects: '/projects',
33
33
  activity: '/activity',
34
+ swarmfeed: '/swarmfeed',
34
35
  }
35
36
 
36
37
  /** Build a URL path for a given view, optionally with an entity ID. */
@@ -27,6 +27,7 @@ export const VIEW_LABELS: Record<AppView, string> = {
27
27
  settings: 'Settings',
28
28
  projects: 'Projects',
29
29
  activity: 'Activity',
30
+ swarmfeed: 'Feed',
30
31
  }
31
32
 
32
33
  export const CREATE_LABELS: Partial<Record<AppView, string>> = {
@@ -71,6 +72,7 @@ export const VIEW_DESCRIPTIONS: Record<AppView, string> = {
71
72
  settings: 'Manage defaults, providers, secrets, and automation settings',
72
73
  projects: 'Group agents, tasks & schedules into projects',
73
74
  activity: 'Audit trail of all entity mutations',
75
+ swarmfeed: 'Social feed for AI agents to post, follow, and engage',
74
76
  }
75
77
 
76
78
  export const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: string; title: string; description: string; features: string[] }> = {
@@ -213,10 +215,16 @@ export const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { ic
213
215
  description: 'Audit trail of all entity mutations across the system.',
214
216
  features: ['Track agent, task, and connector changes', 'Filter by entity type and action', 'Real-time updates via WebSocket', 'Relative timestamps'],
215
217
  },
218
+ swarmfeed: {
219
+ icon: 'rss',
220
+ title: 'Feed',
221
+ description: 'A social feed where your AI agents post updates, follow each other, and engage with content.',
222
+ features: ['Agents post status updates and insights', 'Follow agents and browse trending content', 'Channel-based topic organization', 'Like, repost, and reply interactions'],
223
+ },
216
224
  }
217
225
 
218
226
  export const FULL_WIDTH_VIEWS = new Set<AppView>([
219
227
  'home', 'org_chart', 'inbox', 'chatrooms', 'protocols', 'schedules', 'secrets', 'wallets', 'providers', 'skills',
220
228
  'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'extensions',
221
- 'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects',
229
+ 'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects', 'swarmfeed',
222
230
  ])
@@ -26,119 +26,123 @@ async function fileToContentBlocks(filePath: string): Promise<Array<Record<strin
26
26
  }
27
27
 
28
28
  export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
29
- return new Promise(async (resolve, reject) => {
30
- const messages = await buildMessages(session, message, imagePath, loadHistory)
31
- const model = session.model || 'claude-sonnet-4-6'
32
- let usageInput = 0
33
- let usageOutput = 0
34
-
35
- const body: Record<string, unknown> = {
36
- model,
37
- max_tokens: ANTHROPIC_MAX_TOKENS,
38
- messages,
39
- stream: true,
40
- }
41
- if (systemPrompt) {
42
- body.system = systemPrompt
43
- }
29
+ return new Promise((resolve, reject) => {
30
+ ;(async () => {
31
+ try {
32
+ const messages = await buildMessages(session, message, imagePath, loadHistory)
33
+ const model = session.model || 'claude-sonnet-4-6'
34
+ let usageInput = 0
35
+ let usageOutput = 0
36
+
37
+ const body: Record<string, unknown> = {
38
+ model,
39
+ max_tokens: ANTHROPIC_MAX_TOKENS,
40
+ messages,
41
+ stream: true,
42
+ }
43
+ if (systemPrompt) {
44
+ body.system = systemPrompt
45
+ }
44
46
 
45
- const payload = JSON.stringify(body)
46
- const abortController = { aborted: false }
47
- let fullResponse = ''
48
- let apiReqRef: ReturnType<typeof https.request> | null = null
47
+ const payload = JSON.stringify(body)
48
+ const abortController = { aborted: false }
49
+ let fullResponse = ''
50
+ let apiReqRef: ReturnType<typeof https.request> | null = null
51
+
52
+ if (signal) {
53
+ if (signal.aborted) {
54
+ abortController.aborted = true
55
+ } else {
56
+ signal.addEventListener('abort', () => {
57
+ abortController.aborted = true
58
+ apiReqRef?.destroy()
59
+ }, { once: true })
60
+ }
61
+ }
49
62
 
50
- if (signal) {
51
- if (signal.aborted) {
52
- abortController.aborted = true
53
- } else {
54
- signal.addEventListener('abort', () => {
55
- abortController.aborted = true
56
- apiReqRef?.destroy()
57
- }, { once: true })
58
- }
59
- }
63
+ const apiReq = https.request({
64
+ hostname: PROVIDER_DEFAULTS.anthropic,
65
+ path: '/v1/messages',
66
+ method: 'POST',
67
+ timeout: 60_000,
68
+ headers: {
69
+ 'x-api-key': apiKey || '',
70
+ 'anthropic-version': '2023-06-01',
71
+ 'Content-Type': 'application/json',
72
+ },
73
+ }, (apiRes) => {
74
+ if (apiRes.statusCode !== 200) {
75
+ let errBody = ''
76
+ apiRes.on('data', (c: Buffer) => errBody += c)
77
+ apiRes.on('end', () => {
78
+ const msg = `Anthropic error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
79
+ log.error(TAG, `[${session.id}] ${msg}`)
80
+ let errMsg = `Anthropic API error (${apiRes.statusCode})`
81
+ try {
82
+ const parsed = JSON.parse(errBody)
83
+ if (parsed.error?.message) errMsg = parsed.error.message
84
+ } catch {}
85
+ writeSSE(write, 'err', errMsg)
86
+ active.delete(session.id)
87
+ reject(new Error(msg))
88
+ })
89
+ return
90
+ }
91
+
92
+ let buf = ''
93
+ apiRes.on('data', (chunk: Buffer) => {
94
+ if (abortController.aborted) return
95
+ buf += chunk.toString()
96
+ const lines = buf.split('\n')
97
+ buf = lines.pop()!
98
+
99
+ for (const line of lines) {
100
+ if (!line.startsWith('data: ')) continue
101
+ const data = line.slice(6).trim()
102
+ if (!data) continue
103
+ try {
104
+ const parsed = JSON.parse(data)
105
+ if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
106
+ fullResponse += parsed.delta.text
107
+ writeSSE(write, 'd', parsed.delta.text)
108
+ }
109
+ if (parsed.type === 'message_start' && parsed.message?.usage) {
110
+ usageInput = parsed.message.usage.input_tokens || 0
111
+ }
112
+ if (parsed.type === 'message_delta' && parsed.usage) {
113
+ usageOutput = parsed.usage.output_tokens || 0
114
+ }
115
+ } catch {}
116
+ }
117
+ })
60
118
 
61
- const apiReq = https.request({
62
- hostname: PROVIDER_DEFAULTS.anthropic,
63
- path: '/v1/messages',
64
- method: 'POST',
65
- timeout: 60_000,
66
- headers: {
67
- 'x-api-key': apiKey || '',
68
- 'anthropic-version': '2023-06-01',
69
- 'Content-Type': 'application/json',
70
- },
71
- }, (apiRes) => {
72
- if (apiRes.statusCode !== 200) {
73
- let errBody = ''
74
- apiRes.on('data', (c: Buffer) => errBody += c)
75
- apiRes.on('end', () => {
76
- const msg = `Anthropic error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
77
- log.error(TAG, `[${session.id}] ${msg}`)
78
- let errMsg = `Anthropic API error (${apiRes.statusCode})`
79
- try {
80
- const parsed = JSON.parse(errBody)
81
- if (parsed.error?.message) errMsg = parsed.error.message
82
- } catch {}
83
- writeSSE(write, 'err', errMsg)
84
- active.delete(session.id)
85
- reject(new Error(msg))
119
+ apiRes.on('end', () => {
120
+ if (onUsage && (usageInput > 0 || usageOutput > 0)) {
121
+ onUsage({ inputTokens: usageInput, outputTokens: usageOutput })
122
+ }
123
+ active.delete(session.id)
124
+ resolve(fullResponse)
125
+ })
86
126
  })
87
- return
88
- }
89
127
 
90
- let buf = ''
91
- apiRes.on('data', (chunk: Buffer) => {
92
- if (abortController.aborted) return
93
- buf += chunk.toString()
94
- const lines = buf.split('\n')
95
- buf = lines.pop()!
96
-
97
- for (const line of lines) {
98
- if (!line.startsWith('data: ')) continue
99
- const data = line.slice(6).trim()
100
- if (!data) continue
101
- try {
102
- const parsed = JSON.parse(data)
103
- if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
104
- fullResponse += parsed.delta.text
105
- writeSSE(write, 'd', parsed.delta.text)
106
- }
107
- if (parsed.type === 'message_start' && parsed.message?.usage) {
108
- usageInput = parsed.message.usage.input_tokens || 0
109
- }
110
- if (parsed.type === 'message_delta' && parsed.usage) {
111
- usageOutput = parsed.usage.output_tokens || 0
112
- }
113
- } catch {}
114
- }
115
- })
128
+ apiReqRef = apiReq
129
+ active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
116
130
 
117
- apiRes.on('end', () => {
118
- if (onUsage && (usageInput > 0 || usageOutput > 0)) {
119
- onUsage({ inputTokens: usageInput, outputTokens: usageOutput })
120
- }
121
- active.delete(session.id)
122
- resolve(fullResponse)
123
- })
124
- })
125
-
126
- apiReqRef = apiReq
127
- active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
128
-
129
- apiReq.on('timeout', () => {
130
- log.error(TAG, `[${session.id}] anthropic request timed out after 60s`)
131
- apiReq.destroy(new Error('Request timed out after 60s'))
132
- })
133
-
134
- apiReq.on('error', (e) => {
135
- log.error(TAG, `[${session.id}] anthropic request error:`, e.message)
136
- writeSSE(write, 'err', e.message)
137
- active.delete(session.id)
138
- reject(e)
139
- })
140
-
141
- apiReq.end(payload)
131
+ apiReq.on('timeout', () => {
132
+ log.error(TAG, `[${session.id}] anthropic request timed out after 60s`)
133
+ apiReq.destroy(new Error('Request timed out after 60s'))
134
+ })
135
+
136
+ apiReq.on('error', (e) => {
137
+ log.error(TAG, `[${session.id}] anthropic request error:`, e.message)
138
+ writeSSE(write, 'err', e.message)
139
+ active.delete(session.id)
140
+ reject(e)
141
+ })
142
+
143
+ apiReq.end(payload)
144
+ } catch (err) { reject(err) }
145
+ })()
142
146
  })
143
147
  }
144
148