@swarmclawai/swarmclaw 1.4.7 → 1.4.8

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 (33) hide show
  1. package/README.md +13 -2
  2. package/package.json +1 -1
  3. package/src/app/api/swarmfeed/actions/route.ts +101 -0
  4. package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
  5. package/src/app/api/swarmfeed/notifications/route.ts +30 -0
  6. package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
  7. package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
  8. package/src/app/api/swarmfeed/posts/route.ts +12 -52
  9. package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
  10. package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
  11. package/src/app/api/swarmfeed/route.ts +15 -13
  12. package/src/app/api/swarmfeed/search/route.ts +30 -0
  13. package/src/app/api/swarmfeed/suggested/route.ts +25 -0
  14. package/src/cli/index.js +11 -0
  15. package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
  16. package/src/features/swarmfeed/compose-post.tsx +72 -87
  17. package/src/features/swarmfeed/feed-page.tsx +607 -76
  18. package/src/features/swarmfeed/post-card.tsx +205 -73
  19. package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
  20. package/src/features/swarmfeed/profile-sheet.tsx +179 -0
  21. package/src/features/swarmfeed/queries.ts +191 -8
  22. package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
  23. package/src/lib/server/runtime/heartbeat-service.ts +8 -1
  24. package/src/lib/server/runtime/queue/core.ts +2 -0
  25. package/src/lib/server/session-tools/swarmfeed.ts +226 -63
  26. package/src/lib/server/storage-normalization.ts +1 -0
  27. package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
  28. package/src/lib/server/swarmfeed-runtime.ts +131 -0
  29. package/src/lib/server/tasks/task-route-service.ts +2 -0
  30. package/src/lib/swarmfeed-client.ts +130 -28
  31. package/src/lib/tool-definitions.ts +1 -1
  32. package/src/types/agent.ts +1 -0
  33. package/src/types/swarmfeed.ts +105 -5
@@ -1,16 +1,43 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1
2
  import { api } from '@/lib/app/api-client'
2
- import type { SwarmFeedPost, SwarmFeedChannel, FeedType } from '@/types/swarmfeed'
3
+ import type {
4
+ CreatePostInput,
5
+ FeedType,
6
+ SwarmFeedChannel,
7
+ SwarmFeedFeedResponse,
8
+ SwarmFeedNotification,
9
+ SwarmFeedNotificationsResponse,
10
+ SwarmFeedPost,
11
+ SwarmFeedProfile,
12
+ SwarmFeedSearchResponse,
13
+ SwarmFeedSearchType,
14
+ SwarmFeedSuggestedResponse,
15
+ } from '@/types/swarmfeed'
16
+
17
+ export const swarmFeedQueryKeys = {
18
+ all: ['swarmfeed'] as const,
19
+ channels: () => ['swarmfeed', 'channels'] as const,
20
+ feed: (params: { type: FeedType; agentId?: string; channelId?: string }) => ['swarmfeed', 'feed', params] as const,
21
+ bookmarks: (agentId: string) => ['swarmfeed', 'bookmarks', agentId] as const,
22
+ notifications: (agentId: string) => ['swarmfeed', 'notifications', agentId] as const,
23
+ suggested: (agentId?: string) => ['swarmfeed', 'suggested', agentId || 'public'] as const,
24
+ search: (params: { query: string; type?: SwarmFeedSearchType }) => ['swarmfeed', 'search', params] as const,
25
+ profile: (agentId: string, viewerAgentId?: string) => ['swarmfeed', 'profile', agentId, viewerAgentId || null] as const,
26
+ profilePosts: (agentId: string) => ['swarmfeed', 'profile-posts', agentId] as const,
27
+ thread: (postId: string) => ['swarmfeed', 'thread', postId] as const,
28
+ }
3
29
 
4
30
  export async function fetchFeed(
5
31
  type: FeedType,
6
- params?: { channelId?: string; cursor?: string; limit?: number },
7
- ): Promise<{ posts: SwarmFeedPost[]; nextCursor?: string }> {
32
+ params?: { agentId?: string; channelId?: string; cursor?: string; limit?: number },
33
+ ): Promise<SwarmFeedFeedResponse> {
8
34
  const searchParams = new URLSearchParams()
9
35
  searchParams.set('type', type)
36
+ if (params?.agentId) searchParams.set('agentId', params.agentId)
10
37
  if (params?.channelId) searchParams.set('channelId', params.channelId)
11
38
  if (params?.cursor) searchParams.set('cursor', params.cursor)
12
39
  if (params?.limit) searchParams.set('limit', String(params.limit))
13
- return api<{ posts: SwarmFeedPost[]; nextCursor?: string }>('GET', `/swarmfeed?${searchParams.toString()}`)
40
+ return api<SwarmFeedFeedResponse>('GET', `/swarmfeed?${searchParams.toString()}`)
14
41
  }
15
42
 
16
43
  export async function fetchChannels(): Promise<SwarmFeedChannel[]> {
@@ -18,11 +45,167 @@ export async function fetchChannels(): Promise<SwarmFeedChannel[]> {
18
45
  return result.channels
19
46
  }
20
47
 
21
- export async function submitPost(agentId: string, content: string, channelId?: string, parentId?: string): Promise<SwarmFeedPost> {
48
+ export async function fetchBookmarks(agentId: string): Promise<SwarmFeedPost[]> {
49
+ const result = await api<SwarmFeedFeedResponse>('GET', `/swarmfeed/bookmarks?agentId=${encodeURIComponent(agentId)}`)
50
+ return result.posts
51
+ }
52
+
53
+ export async function fetchNotifications(agentId: string): Promise<SwarmFeedNotification[]> {
54
+ const result = await api<SwarmFeedNotificationsResponse>('GET', `/swarmfeed/notifications?agentId=${encodeURIComponent(agentId)}`)
55
+ return result.notifications
56
+ }
57
+
58
+ export async function fetchSuggestedFollows(agentId?: string): Promise<SwarmFeedSuggestedResponse> {
59
+ const query = agentId ? `?agentId=${encodeURIComponent(agentId)}` : ''
60
+ return api<SwarmFeedSuggestedResponse>('GET', `/swarmfeed/suggested${query}`)
61
+ }
62
+
63
+ export async function searchFeed(query: string, type?: SwarmFeedSearchType): Promise<SwarmFeedSearchResponse> {
64
+ const params = new URLSearchParams({ q: query })
65
+ if (type) params.set('type', type)
66
+ return api<SwarmFeedSearchResponse>('GET', `/swarmfeed/search?${params.toString()}`)
67
+ }
68
+
69
+ export async function submitPost(
70
+ agentId: string,
71
+ input: CreatePostInput,
72
+ ): Promise<SwarmFeedPost> {
22
73
  return api<SwarmFeedPost>('POST', '/swarmfeed/posts', {
23
74
  agentId,
24
- content,
25
- channelId,
26
- parentId,
75
+ content: input.content,
76
+ channelId: input.channelId,
77
+ parentId: input.parentId,
78
+ quotedPostId: input.quotedPostId,
79
+ })
80
+ }
81
+
82
+ export async function fetchProfile(agentId: string, viewerAgentId?: string): Promise<SwarmFeedProfile> {
83
+ const params = new URLSearchParams()
84
+ if (viewerAgentId) params.set('viewerAgentId', viewerAgentId)
85
+ const query = params.toString()
86
+ return api<SwarmFeedProfile>('GET', `/swarmfeed/profiles/${encodeURIComponent(agentId)}${query ? `?${query}` : ''}`)
87
+ }
88
+
89
+ export async function fetchProfilePosts(agentId: string): Promise<SwarmFeedPost[]> {
90
+ const result = await api<SwarmFeedFeedResponse>('GET', `/swarmfeed/profiles/${encodeURIComponent(agentId)}/posts?limit=20&filter=posts`)
91
+ return result.posts
92
+ }
93
+
94
+ export async function fetchPostThread(postId: string): Promise<{ post: SwarmFeedPost; replies: SwarmFeedPost[] }> {
95
+ const [post, replies] = await Promise.all([
96
+ api<SwarmFeedPost>('GET', `/swarmfeed/posts/${encodeURIComponent(postId)}`),
97
+ api<SwarmFeedFeedResponse>('GET', `/swarmfeed/posts/${encodeURIComponent(postId)}/replies?limit=30`),
98
+ ])
99
+ return { post, replies: replies.posts }
100
+ }
101
+
102
+ export async function runSwarmFeedAction(input: {
103
+ action: 'like' | 'unlike' | 'repost' | 'unrepost' | 'bookmark' | 'unbookmark' | 'follow' | 'unfollow' | 'quote_repost'
104
+ agentId: string
105
+ postId?: string
106
+ targetAgentId?: string
107
+ content?: string
108
+ channelId?: string
109
+ }): Promise<unknown> {
110
+ return api('POST', '/swarmfeed/actions', input)
111
+ }
112
+
113
+ export function useSwarmFeedFeedQuery(params: { type: FeedType; agentId?: string; channelId?: string; enabled?: boolean }) {
114
+ return useQuery<SwarmFeedFeedResponse>({
115
+ queryKey: swarmFeedQueryKeys.feed({ type: params.type, agentId: params.agentId, channelId: params.channelId }),
116
+ queryFn: () => fetchFeed(params.type, { agentId: params.agentId, channelId: params.channelId, limit: 50 }),
117
+ enabled: params.enabled,
118
+ staleTime: 15_000,
119
+ })
120
+ }
121
+
122
+ export function useSwarmFeedChannelsQuery() {
123
+ return useQuery<SwarmFeedChannel[]>({
124
+ queryKey: swarmFeedQueryKeys.channels(),
125
+ queryFn: fetchChannels,
126
+ staleTime: 60_000,
127
+ })
128
+ }
129
+
130
+ export function useSwarmFeedBookmarksQuery(agentId: string, enabled = true) {
131
+ return useQuery<SwarmFeedPost[]>({
132
+ queryKey: swarmFeedQueryKeys.bookmarks(agentId),
133
+ queryFn: () => fetchBookmarks(agentId),
134
+ enabled: enabled && !!agentId,
135
+ staleTime: 15_000,
136
+ })
137
+ }
138
+
139
+ export function useSwarmFeedNotificationsQuery(agentId: string, enabled = true) {
140
+ return useQuery<SwarmFeedNotification[]>({
141
+ queryKey: swarmFeedQueryKeys.notifications(agentId),
142
+ queryFn: () => fetchNotifications(agentId),
143
+ enabled: enabled && !!agentId,
144
+ staleTime: 15_000,
145
+ })
146
+ }
147
+
148
+ export function useSwarmFeedSuggestedQuery(agentId?: string, enabled = true) {
149
+ return useQuery<SwarmFeedSuggestedResponse>({
150
+ queryKey: swarmFeedQueryKeys.suggested(agentId),
151
+ queryFn: () => fetchSuggestedFollows(agentId),
152
+ enabled,
153
+ staleTime: 30_000,
154
+ })
155
+ }
156
+
157
+ export function useSwarmFeedSearchQuery(params: { query: string; type?: SwarmFeedSearchType; enabled?: boolean }) {
158
+ return useQuery<SwarmFeedSearchResponse>({
159
+ queryKey: swarmFeedQueryKeys.search({ query: params.query, type: params.type }),
160
+ queryFn: () => searchFeed(params.query, params.type),
161
+ enabled: params.enabled && params.query.trim().length > 0,
162
+ staleTime: 15_000,
163
+ })
164
+ }
165
+
166
+ export function useSwarmFeedProfileQuery(agentId: string, viewerAgentId?: string, enabled = true) {
167
+ return useQuery<SwarmFeedProfile>({
168
+ queryKey: swarmFeedQueryKeys.profile(agentId, viewerAgentId),
169
+ queryFn: () => fetchProfile(agentId, viewerAgentId),
170
+ enabled: enabled && !!agentId,
171
+ staleTime: 20_000,
172
+ })
173
+ }
174
+
175
+ export function useSwarmFeedProfilePostsQuery(agentId: string, enabled = true) {
176
+ return useQuery<SwarmFeedPost[]>({
177
+ queryKey: swarmFeedQueryKeys.profilePosts(agentId),
178
+ queryFn: () => fetchProfilePosts(agentId),
179
+ enabled: enabled && !!agentId,
180
+ staleTime: 20_000,
181
+ })
182
+ }
183
+
184
+ export function useSwarmFeedThreadQuery(postId: string, enabled = true) {
185
+ return useQuery<{ post: SwarmFeedPost; replies: SwarmFeedPost[] }>({
186
+ queryKey: swarmFeedQueryKeys.thread(postId),
187
+ queryFn: () => fetchPostThread(postId),
188
+ enabled: enabled && !!postId,
189
+ staleTime: 10_000,
190
+ })
191
+ }
192
+
193
+ export function useSwarmFeedPostMutation() {
194
+ const queryClient = useQueryClient()
195
+ return useMutation({
196
+ mutationFn: ({ agentId, input }: { agentId: string; input: CreatePostInput }) => submitPost(agentId, input),
197
+ onSuccess: async () => {
198
+ await queryClient.invalidateQueries({ queryKey: swarmFeedQueryKeys.all })
199
+ },
200
+ })
201
+ }
202
+
203
+ export function useSwarmFeedActionMutation() {
204
+ const queryClient = useQueryClient()
205
+ return useMutation({
206
+ mutationFn: runSwarmFeedAction,
207
+ onSuccess: async () => {
208
+ await queryClient.invalidateQueries({ queryKey: swarmFeedQueryKeys.all })
209
+ },
27
210
  })
28
211
  }
@@ -1,14 +1,22 @@
1
1
  import { registerAgent } from '@/lib/swarmfeed-client'
2
- import { patchAgent } from '@/lib/server/agents/agent-repository'
2
+ import { getAgent, patchAgent } from '@/lib/server/agents/agent-repository'
3
3
  import { log } from '@/lib/server/logger'
4
4
  import type { Agent } from '@/types'
5
5
 
6
- /**
7
- * Auto-register an agent on SwarmFeed when enabled but missing API key.
8
- * Fire-and-forget — called after agent save, patches agent with the returned credentials.
9
- */
10
- export async function tryAutoRegisterSwarmFeed(agent: Agent): Promise<void> {
11
- if (!agent.swarmfeedEnabled || agent.swarmfeedApiKey) return
6
+ type EnsureSwarmFeedAgentOptions = {
7
+ requireEnabled?: boolean
8
+ }
9
+
10
+ export async function ensureSwarmFeedAgent(
11
+ agentId: string,
12
+ options: EnsureSwarmFeedAgentOptions = {},
13
+ ): Promise<Agent> {
14
+ const agent = getAgent(agentId) as Agent | undefined
15
+ if (!agent) throw new Error('Agent not found')
16
+ if (options.requireEnabled !== false && !agent.swarmfeedEnabled) {
17
+ throw new Error('SwarmFeed is not enabled for this agent')
18
+ }
19
+ if (agent.swarmfeedApiKey && agent.swarmfeedAgentId) return agent
12
20
 
13
21
  log.info('swarm-registration', `Auto-registering agent "${agent.name}" on SwarmFeed`)
14
22
  const reg = await registerAgent({
@@ -31,5 +39,20 @@ export async function tryAutoRegisterSwarmFeed(agent: Agent): Promise<void> {
31
39
  }
32
40
  })
33
41
 
34
- log.info('swarm-registration', `Agent "${agent.name}" registered on SwarmFeed as ${reg.agentId}`)
42
+ const updated = getAgent(agentId) as Agent | undefined
43
+ if (!updated?.swarmfeedApiKey || !updated.swarmfeedAgentId) {
44
+ throw new Error('Registration succeeded but credentials were not saved')
45
+ }
46
+
47
+ log.info('swarm-registration', `Agent "${updated.name}" registered on SwarmFeed as ${updated.swarmfeedAgentId}`)
48
+ return updated
49
+ }
50
+
51
+ /**
52
+ * Auto-register an agent on SwarmFeed when enabled but missing API key.
53
+ * Fire-and-forget — called after agent save, patches agent with the returned credentials.
54
+ */
55
+ export async function tryAutoRegisterSwarmFeed(agent: Agent): Promise<void> {
56
+ if (!agent.swarmfeedEnabled || (agent.swarmfeedApiKey && agent.swarmfeedAgentId)) return
57
+ await ensureSwarmFeedAgent(agent.id)
35
58
  }
@@ -30,6 +30,7 @@ import { errorMessage, hmrSingleton, jitteredBackoff } from '@/lib/shared-utils'
30
30
  import { logExecution } from '@/lib/server/execution-log'
31
31
  import { createNotification } from '@/lib/server/create-notification'
32
32
  import { WORKER_ONLY_PROVIDER_IDS } from '@/lib/provider-sets'
33
+ import { buildSwarmFeedHeartbeatGuidance } from '@/lib/server/swarmfeed-runtime'
33
34
 
34
35
  const HEARTBEAT_TICK_MS = 60_000
35
36
  const MAX_CONCURRENT_HEARTBEATS = 1
@@ -378,7 +379,11 @@ export function buildAgentHeartbeatPrompt(
378
379
  const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(strippedContent) ? '' : strippedContent
379
380
  if (effectiveFileContent) sections.push(`\nHEARTBEAT.md contents:\n${effectiveFileContent.slice(0, 2000)}`)
380
381
 
381
- const recentMessages = (session.id ? getRecentMessages(session.id, 5) : []) as HeartbeatPromptMessage[]
382
+ const recentMessages = (
383
+ Array.isArray(session.messages)
384
+ ? session.messages.slice(-5)
385
+ : (session.id ? getRecentMessages(session.id, 5) : [])
386
+ ) as HeartbeatPromptMessage[]
382
387
  const recentContext = recentMessages
383
388
  .map((m) => {
384
389
  const text = (m.text || '').slice(0, 200)
@@ -416,6 +421,8 @@ export function buildAgentHeartbeatPrompt(
416
421
 
417
422
  // ── Phase 5: Execution instructions ──
418
423
  if (fallbackPrompt !== DEFAULT_HEARTBEAT_PROMPT) sections.push(`\nAgent instructions:\n${fallbackPrompt}`)
424
+ const swarmFeedGuidance = buildSwarmFeedHeartbeatGuidance(agent as Agent)
425
+ if (swarmFeedGuidance) sections.push(`\n${swarmFeedGuidance}`)
419
426
 
420
427
  sections.push('')
421
428
  sections.push('You are running an autonomous heartbeat tick. Review your goal and recent context.')
@@ -24,6 +24,7 @@ import { checkAgentBudgetLimits } from '@/lib/server/cost'
24
24
  import { enqueueExecution } from '@/lib/server/execution-engine'
25
25
  import { extractTaskResult, formatResultBody } from '@/lib/server/tasks/task-result'
26
26
  import { checkoutTask } from '@/lib/server/tasks/task-checkout'
27
+ import { queueSwarmFeedTaskCompletionWake } from '@/lib/server/swarmfeed-runtime'
27
28
  import {
28
29
  classifyRuntimeFailure,
29
30
  observeAutonomyRunOutcome,
@@ -1496,6 +1497,7 @@ export async function processNext() {
1496
1497
  text: `Task completed: "${task.title}" (${taskId})`,
1497
1498
  })
1498
1499
  notifyOrchestrators(`Task completed: "${task.title}"`, `task-complete:${taskId}`)
1500
+ queueSwarmFeedTaskCompletionWake(doneTask)
1499
1501
  handleTerminalTaskResultDeliveries(doneTask)
1500
1502
  cleanupTerminalOneOffSchedule(doneTask)
1501
1503
  // Clean up LangGraph checkpoints for completed tasks