@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.
- package/README.md +13 -2
- package/package.json +1 -1
- package/src/app/api/swarmfeed/actions/route.ts +101 -0
- package/src/app/api/swarmfeed/bookmarks/route.ts +30 -0
- package/src/app/api/swarmfeed/notifications/route.ts +30 -0
- package/src/app/api/swarmfeed/posts/[postId]/replies/route.ts +23 -0
- package/src/app/api/swarmfeed/posts/[postId]/route.ts +20 -0
- package/src/app/api/swarmfeed/posts/route.ts +12 -52
- package/src/app/api/swarmfeed/profiles/[agentId]/posts/route.ts +25 -0
- package/src/app/api/swarmfeed/profiles/[agentId]/route.ts +32 -0
- package/src/app/api/swarmfeed/route.ts +15 -13
- package/src/app/api/swarmfeed/search/route.ts +30 -0
- package/src/app/api/swarmfeed/suggested/route.ts +25 -0
- package/src/cli/index.js +11 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +7 -1
- package/src/features/swarmfeed/compose-post.tsx +72 -87
- package/src/features/swarmfeed/feed-page.tsx +607 -76
- package/src/features/swarmfeed/post-card.tsx +205 -73
- package/src/features/swarmfeed/post-thread-sheet.tsx +155 -0
- package/src/features/swarmfeed/profile-sheet.tsx +179 -0
- package/src/features/swarmfeed/queries.ts +191 -8
- package/src/lib/server/agents/agent-swarm-registration.ts +31 -8
- package/src/lib/server/runtime/heartbeat-service.ts +8 -1
- package/src/lib/server/runtime/queue/core.ts +2 -0
- package/src/lib/server/session-tools/swarmfeed.ts +226 -63
- package/src/lib/server/storage-normalization.ts +1 -0
- package/src/lib/server/swarmfeed-runtime.test.ts +188 -0
- package/src/lib/server/swarmfeed-runtime.ts +131 -0
- package/src/lib/server/tasks/task-route-service.ts +2 -0
- package/src/lib/swarmfeed-client.ts +130 -28
- package/src/lib/tool-definitions.ts +1 -1
- package/src/types/agent.ts +1 -0
- 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 {
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function
|
|
11
|
-
|
|
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
|
-
|
|
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 = (
|
|
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
|