@swarmclawai/swarmclaw 0.6.3 → 0.6.6
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 +5 -3
- package/package.json +5 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +41 -2
- package/src/app/api/chatrooms/[id]/route.ts +15 -1
- package/src/app/api/chatrooms/route.ts +15 -2
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/tasks/route.ts +24 -0
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/app/page.tsx +7 -3
- package/src/cli/index.js +15 -0
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +1 -0
- package/src/components/agents/agent-chat-list.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +112 -26
- package/src/components/auth/access-key-gate.tsx +22 -11
- package/src/components/chat/chat-area.tsx +2 -2
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +14 -3
- package/src/components/chat/message-list.tsx +5 -4
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/tool-call-bubble.test.ts +28 -0
- package/src/components/chat/tool-call-bubble.tsx +13 -1
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +7 -6
- package/src/components/chatrooms/chatroom-message.tsx +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/home/home-view.tsx +2 -1
- package/src/components/input/chat-input.tsx +5 -4
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +23 -9
- package/src/components/logs/log-list.tsx +7 -7
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/new-session-sheet.tsx +4 -3
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-sheet.tsx +21 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-media-query.ts +30 -4
- package/src/lib/api-client.ts +6 -18
- package/src/lib/fetch-timeout.ts +17 -0
- package/src/lib/notification-sounds.ts +4 -4
- package/src/lib/safe-storage.ts +42 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/chat-execution.ts +35 -3
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +64 -11
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +80 -2
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +8 -5
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +226 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +6 -2
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web-output.test.ts +29 -0
- package/src/lib/server/session-tools/web-output.ts +16 -0
- package/src/lib/server/session-tools/web.ts +7 -3
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +38 -0
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/view-routes.ts +6 -1
- package/src/stores/use-app-store.ts +17 -11
- package/src/types/index.ts +60 -1
package/src/lib/server/queue.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { executeSessionChatTurn } from './chat-execution'
|
|
|
12
12
|
import { extractTaskResult, formatResultBody } from './task-result'
|
|
13
13
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
14
14
|
import { isProtectedMainSession } from './main-session'
|
|
15
|
-
import type { Agent, BoardTask, Message } from '@/types'
|
|
15
|
+
import type { Agent, BoardTask, Connector, Message } from '@/types'
|
|
16
16
|
|
|
17
17
|
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
18
18
|
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
|
|
@@ -22,6 +22,10 @@ interface SessionMessageLike {
|
|
|
22
22
|
text?: string
|
|
23
23
|
time?: number
|
|
24
24
|
kind?: 'chat' | 'heartbeat' | 'system' | 'context-clear'
|
|
25
|
+
source?: {
|
|
26
|
+
connectorId?: string
|
|
27
|
+
channelId?: string
|
|
28
|
+
}
|
|
25
29
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -39,6 +43,20 @@ interface ScheduleTaskMeta extends BoardTask {
|
|
|
39
43
|
createdByAgentId?: string | null
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
interface RunningConnectorLike {
|
|
47
|
+
id: string
|
|
48
|
+
platform: string
|
|
49
|
+
agentId: string | null
|
|
50
|
+
supportsSend: boolean
|
|
51
|
+
configuredTargets: string[]
|
|
52
|
+
recentChannelId: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ConnectorTaskFollowupTarget {
|
|
56
|
+
connectorId: string
|
|
57
|
+
channelId: string
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
function sameReasons(a?: string[] | null, b?: string[] | null): boolean {
|
|
43
61
|
const av = Array.isArray(a) ? a : []
|
|
44
62
|
const bv = Array.isArray(b) ? b : []
|
|
@@ -164,6 +182,135 @@ function maybeResolveUploadMediaPathFromUrl(url: string | undefined): string | u
|
|
|
164
182
|
return fs.existsSync(fullPath) ? fullPath : undefined
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
const OUTPUT_FILE_BACKTICK_RE = /`([^`\n]+\.(?:txt|md|json|csv|pdf|png|jpe?g|webp|gif|svg|mp4|webm|mov|zip|tar|gz|log|yml|yaml|xml|html|css|js|ts|tsx|jsx|py|go|rs|java|swift|kt|sql))`/gi
|
|
186
|
+
const OUTPUT_FILE_PATH_RE = /\b((?:\.{1,2}\/|~\/|\/)?[\w./-]+\.(?:txt|md|json|csv|pdf|png|jpe?g|webp|gif|svg|mp4|webm|mov|zip|tar|gz|log|yml|yaml|xml|html|css|js|ts|tsx|jsx|py|go|rs|java|swift|kt|sql))\b/gi
|
|
187
|
+
const MAX_CONNECTOR_ATTACHMENT_BYTES = 25 * 1024 * 1024
|
|
188
|
+
|
|
189
|
+
function extractLikelyOutputFiles(text: string): string[] {
|
|
190
|
+
const out: string[] = []
|
|
191
|
+
const seen = new Set<string>()
|
|
192
|
+
const push = (raw: string) => {
|
|
193
|
+
const value = raw.trim().replace(/^['"]|['"]$/g, '')
|
|
194
|
+
if (!value || /^https?:\/\//i.test(value)) return
|
|
195
|
+
if (value.startsWith('/api/uploads/')) return
|
|
196
|
+
const key = value.toLowerCase()
|
|
197
|
+
if (seen.has(key)) return
|
|
198
|
+
seen.add(key)
|
|
199
|
+
out.push(value)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const match of text.matchAll(OUTPUT_FILE_BACKTICK_RE)) {
|
|
203
|
+
push(match[1] || '')
|
|
204
|
+
if (out.length >= 8) return out
|
|
205
|
+
}
|
|
206
|
+
for (const match of text.matchAll(OUTPUT_FILE_PATH_RE)) {
|
|
207
|
+
push(match[1] || '')
|
|
208
|
+
if (out.length >= 8) return out
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return out
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveExistingOutputFilePath(fileRef: string, cwd: string): string | null {
|
|
215
|
+
const ref = (fileRef || '').trim()
|
|
216
|
+
if (!ref) return null
|
|
217
|
+
if (ref.startsWith('/api/uploads/')) {
|
|
218
|
+
return maybeResolveUploadMediaPathFromUrl(ref) || null
|
|
219
|
+
}
|
|
220
|
+
const withoutFileScheme = ref.replace(/^file:\/\//i, '')
|
|
221
|
+
const candidates = path.isAbsolute(withoutFileScheme)
|
|
222
|
+
? [withoutFileScheme]
|
|
223
|
+
: [
|
|
224
|
+
cwd ? path.resolve(cwd, withoutFileScheme) : '',
|
|
225
|
+
path.resolve(WORKSPACE_DIR, withoutFileScheme),
|
|
226
|
+
].filter(Boolean)
|
|
227
|
+
|
|
228
|
+
for (const candidate of candidates) {
|
|
229
|
+
try {
|
|
230
|
+
const stat = fs.statSync(candidate)
|
|
231
|
+
if (stat.isFile()) return candidate
|
|
232
|
+
} catch {
|
|
233
|
+
// ignore missing candidate
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isSendableAttachment(filePath: string): boolean {
|
|
240
|
+
try {
|
|
241
|
+
const stat = fs.statSync(filePath)
|
|
242
|
+
return stat.isFile() && stat.size <= MAX_CONNECTOR_ATTACHMENT_BYTES
|
|
243
|
+
} catch {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
249
|
+
task: BoardTask
|
|
250
|
+
sessions: Record<string, SessionLike>
|
|
251
|
+
connectors: Record<string, Connector>
|
|
252
|
+
running: RunningConnectorLike[]
|
|
253
|
+
}): ConnectorTaskFollowupTarget | null {
|
|
254
|
+
const { task, sessions, connectors, running } = params
|
|
255
|
+
const metaTask = task as ScheduleTaskMeta
|
|
256
|
+
const delegatedByAgentId = typeof metaTask.delegatedByAgentId === 'string'
|
|
257
|
+
? metaTask.delegatedByAgentId.trim()
|
|
258
|
+
: ''
|
|
259
|
+
const sourceSessionId = typeof metaTask.createdInSessionId === 'string'
|
|
260
|
+
? metaTask.createdInSessionId.trim()
|
|
261
|
+
: ''
|
|
262
|
+
if (!sourceSessionId) return null
|
|
263
|
+
const sourceSession = sessions[sourceSessionId]
|
|
264
|
+
if (!sourceSession || !Array.isArray(sourceSession.messages)) return null
|
|
265
|
+
|
|
266
|
+
const runningById = new Map<string, RunningConnectorLike>()
|
|
267
|
+
for (const entry of running) {
|
|
268
|
+
if (!entry?.id) continue
|
|
269
|
+
runningById.set(entry.id, entry)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (let i = sourceSession.messages.length - 1; i >= 0; i--) {
|
|
273
|
+
const message = sourceSession.messages[i]
|
|
274
|
+
if (!message || message.role !== 'user') continue
|
|
275
|
+
|
|
276
|
+
const connectorId = typeof message.source?.connectorId === 'string'
|
|
277
|
+
? message.source.connectorId.trim()
|
|
278
|
+
: ''
|
|
279
|
+
if (!connectorId) continue
|
|
280
|
+
|
|
281
|
+
const connector = connectors[connectorId]
|
|
282
|
+
if (!connector) continue
|
|
283
|
+
const ownerId = typeof connector.agentId === 'string' ? connector.agentId.trim() : ''
|
|
284
|
+
if (ownerId) {
|
|
285
|
+
const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
|
|
286
|
+
if (!allowedOwners.has(ownerId)) continue
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const runtime = runningById.get(connectorId)
|
|
290
|
+
if (runtime && !runtime.supportsSend) continue
|
|
291
|
+
|
|
292
|
+
const sourceChannel = typeof message.source?.channelId === 'string'
|
|
293
|
+
? message.source.channelId.trim()
|
|
294
|
+
: ''
|
|
295
|
+
const fallbackChannel = runtime?.recentChannelId
|
|
296
|
+
|| runtime?.configuredTargets?.[0]
|
|
297
|
+
|| connector.config?.outboundJid
|
|
298
|
+
|| connector.config?.outboundTarget
|
|
299
|
+
|| ''
|
|
300
|
+
const rawChannel = sourceChannel || fallbackChannel
|
|
301
|
+
if (!rawChannel) continue
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
connectorId,
|
|
305
|
+
channelId: connector.platform === 'whatsapp'
|
|
306
|
+
? normalizeWhatsappTarget(rawChannel)
|
|
307
|
+
: rawChannel,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
|
|
167
314
|
// Task result extraction now uses Zod-validated structured data
|
|
168
315
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
169
316
|
|
|
@@ -270,37 +417,67 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
270
417
|
statusLabel: string
|
|
271
418
|
summaryText: string
|
|
272
419
|
imageUrl?: string
|
|
420
|
+
mediaPath?: string
|
|
421
|
+
mediaFileName?: string
|
|
273
422
|
}) {
|
|
274
|
-
const { task, statusLabel, summaryText, imageUrl } = params
|
|
423
|
+
const { task, statusLabel, summaryText, imageUrl, mediaPath, mediaFileName } = params
|
|
275
424
|
|
|
276
425
|
const connectors = loadConnectors()
|
|
277
426
|
const running = (await import('./connectors/manager')).listRunningConnectors()
|
|
278
427
|
const manager = await import('./connectors/manager')
|
|
428
|
+
const sessions = loadSessions()
|
|
279
429
|
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
|
|
430
|
+
const candidateByKey = new Map<string, ConnectorTaskFollowupTarget>()
|
|
431
|
+
const addCandidate = (candidate: ConnectorTaskFollowupTarget | null | undefined) => {
|
|
432
|
+
if (!candidate?.connectorId || !candidate?.channelId) return
|
|
433
|
+
const key = `${candidate.connectorId}|${candidate.channelId}`
|
|
434
|
+
if (!candidateByKey.has(key)) candidateByKey.set(key, candidate)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const originTarget = resolveTaskOriginConnectorFollowupTarget({
|
|
438
|
+
task,
|
|
439
|
+
sessions: sessions as Record<string, SessionLike>,
|
|
440
|
+
connectors,
|
|
441
|
+
running: running as RunningConnectorLike[],
|
|
286
442
|
})
|
|
287
|
-
|
|
443
|
+
addCandidate(originTarget)
|
|
444
|
+
const preferredTargetKey = originTarget
|
|
445
|
+
? `${originTarget.connectorId}|${originTarget.channelId}`
|
|
446
|
+
: ''
|
|
288
447
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
const connector = connectors[
|
|
448
|
+
for (const entry of running) {
|
|
449
|
+
if (!entry.supportsSend || !entry.id) continue
|
|
450
|
+
const connector = connectors[entry.id]
|
|
292
451
|
if (!connector) continue
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
452
|
+
if (connector.agentId !== task.agentId) continue
|
|
453
|
+
if (!isEnabledFlag(connector.config?.taskFollowups)) continue
|
|
454
|
+
const channelTargetRaw = entry.recentChannelId
|
|
455
|
+
|| entry.configuredTargets[0]
|
|
296
456
|
|| connector.config?.outboundJid
|
|
297
457
|
|| connector.config?.outboundTarget
|
|
298
458
|
|| ''
|
|
299
459
|
if (!channelTargetRaw) continue
|
|
460
|
+
addCandidate({
|
|
461
|
+
connectorId: entry.id,
|
|
462
|
+
channelId: connector.platform === 'whatsapp'
|
|
463
|
+
? normalizeWhatsappTarget(channelTargetRaw)
|
|
464
|
+
: channelTargetRaw,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
const targets = [...candidateByKey.values()].sort((a, b) => {
|
|
468
|
+
if (!preferredTargetKey) return 0
|
|
469
|
+
const aKey = `${a.connectorId}|${a.channelId}`
|
|
470
|
+
const bKey = `${b.connectorId}|${b.channelId}`
|
|
471
|
+
if (aKey === preferredTargetKey && bKey !== preferredTargetKey) return -1
|
|
472
|
+
if (bKey === preferredTargetKey && aKey !== preferredTargetKey) return 1
|
|
473
|
+
return 0
|
|
474
|
+
})
|
|
475
|
+
if (!targets.length) return
|
|
300
476
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
477
|
+
const summary = summaryText.trim().slice(0, 1400)
|
|
478
|
+
for (const target of targets) {
|
|
479
|
+
const connector = connectors[target.connectorId]
|
|
480
|
+
if (!connector) continue
|
|
304
481
|
|
|
305
482
|
const template = typeof connector.config?.taskFollowupTemplate === 'string'
|
|
306
483
|
? connector.config.taskFollowupTemplate.trim()
|
|
@@ -316,23 +493,29 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
316
493
|
`Task ${statusLabel}: ${task.title}`,
|
|
317
494
|
summary || 'No summary provided.',
|
|
318
495
|
].join('\n\n')
|
|
496
|
+
const targetKey = `${target.connectorId}|${target.channelId}`
|
|
497
|
+
const preferredChannelNote = !template && preferredTargetKey && targetKey === preferredTargetKey
|
|
498
|
+
? '\n\n(Update sent in the same channel that requested this task.)'
|
|
499
|
+
: ''
|
|
500
|
+
const outboundMessage = `${message}${preferredChannelNote}`
|
|
319
501
|
|
|
320
|
-
const resolvedMediaPath = maybeResolveUploadMediaPathFromUrl(imageUrl)
|
|
502
|
+
const resolvedMediaPath = mediaPath || maybeResolveUploadMediaPathFromUrl(imageUrl)
|
|
321
503
|
try {
|
|
322
504
|
await manager.sendConnectorMessage({
|
|
323
|
-
connectorId:
|
|
324
|
-
channelId,
|
|
325
|
-
text:
|
|
505
|
+
connectorId: target.connectorId,
|
|
506
|
+
channelId: target.channelId,
|
|
507
|
+
text: outboundMessage,
|
|
326
508
|
...(resolvedMediaPath
|
|
327
509
|
? {
|
|
328
510
|
mediaPath: resolvedMediaPath,
|
|
329
|
-
|
|
511
|
+
fileName: mediaFileName || path.basename(resolvedMediaPath),
|
|
512
|
+
caption: outboundMessage,
|
|
330
513
|
}
|
|
331
514
|
: {}),
|
|
332
515
|
})
|
|
333
516
|
} catch (err: unknown) {
|
|
334
517
|
const errMsg = err instanceof Error ? err.message : String(err)
|
|
335
|
-
console.warn(`[queue] Failed task follow-up send on connector ${
|
|
518
|
+
console.warn(`[queue] Failed task follow-up send on connector ${target.connectorId}: ${errMsg}`)
|
|
336
519
|
}
|
|
337
520
|
}
|
|
338
521
|
}
|
|
@@ -358,10 +541,16 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
358
541
|
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
359
542
|
)
|
|
360
543
|
const resultBody = formatResultBody(taskResult)
|
|
544
|
+
const outputFileRefs = Array.isArray(task.outputFiles) && task.outputFiles.length > 0
|
|
545
|
+
? task.outputFiles
|
|
546
|
+
: extractLikelyOutputFiles(resultBody)
|
|
361
547
|
|
|
362
548
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
363
549
|
const taskLink = `[${task.title}](#task:${task.id})`
|
|
364
550
|
const firstImage = taskResult.artifacts.find((a) => a.type === 'image')
|
|
551
|
+
const firstArtifactMediaPath = taskResult.artifacts
|
|
552
|
+
.map((artifact) => maybeResolveUploadMediaPathFromUrl(artifact.url))
|
|
553
|
+
.find((candidate): candidate is string => Boolean(candidate))
|
|
365
554
|
const now = Date.now()
|
|
366
555
|
let changed = false
|
|
367
556
|
|
|
@@ -377,6 +566,11 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
377
566
|
|
|
378
567
|
// Get working directory from execution session
|
|
379
568
|
const execCwd = runSession?.cwd || ''
|
|
569
|
+
const existingOutputPaths = outputFileRefs
|
|
570
|
+
.map((fileRef) => resolveExistingOutputFilePath(fileRef, execCwd))
|
|
571
|
+
.filter((candidate): candidate is string => Boolean(candidate))
|
|
572
|
+
const firstLocalOutputPath = existingOutputPaths.find((candidate) => isSendableAttachment(candidate))
|
|
573
|
+
const followupMediaPath = firstArtifactMediaPath || firstLocalOutputPath || undefined
|
|
380
574
|
|
|
381
575
|
const buildMsg = (text: string): Message => {
|
|
382
576
|
const msg: Message = { role: 'assistant', text, time: now, kind: 'system' }
|
|
@@ -387,6 +581,10 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
387
581
|
const buildResultBlock = (prefix: string): string => {
|
|
388
582
|
const parts = [prefix]
|
|
389
583
|
if (execCwd) parts.push(`Working directory: \`${execCwd}\``)
|
|
584
|
+
if (outputFileRefs.length > 0) {
|
|
585
|
+
parts.push(`Output files:\n${outputFileRefs.slice(0, 8).map((fileRef) => `- \`${fileRef}\``).join('\n')}`)
|
|
586
|
+
}
|
|
587
|
+
if (task.completionReportPath) parts.push(`Task report: \`${task.completionReportPath}\``)
|
|
390
588
|
if (resumeLines.length > 0) parts.push(resumeLines.join(' | '))
|
|
391
589
|
parts.push(resultBody || 'No summary.')
|
|
392
590
|
return parts.join('\n\n')
|
|
@@ -449,6 +647,8 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
449
647
|
statusLabel,
|
|
450
648
|
summaryText: resultBody || '',
|
|
451
649
|
imageUrl: firstImage?.url,
|
|
650
|
+
mediaPath: followupMediaPath,
|
|
651
|
+
mediaFileName: followupMediaPath ? path.basename(followupMediaPath) : undefined,
|
|
452
652
|
})
|
|
453
653
|
}
|
|
454
654
|
|
|
@@ -812,6 +1012,8 @@ export async function processNext() {
|
|
|
812
1012
|
)
|
|
813
1013
|
const enrichedResult = formatResultBody(taskResult)
|
|
814
1014
|
t2[taskId].result = enrichedResult.slice(0, 4000) || null
|
|
1015
|
+
t2[taskId].artifacts = taskResult.artifacts.slice(0, 24)
|
|
1016
|
+
t2[taskId].outputFiles = extractLikelyOutputFiles(enrichedResult).slice(0, 24)
|
|
815
1017
|
t2[taskId].updatedAt = Date.now()
|
|
816
1018
|
const report = ensureTaskCompletionReport(t2[taskId])
|
|
817
1019
|
if (report?.relativePath) t2[taskId].completionReportPath = report.relativePath
|
|
@@ -146,7 +146,10 @@ async function tick() {
|
|
|
146
146
|
existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
|
|
147
147
|
existingTask.result = null
|
|
148
148
|
existingTask.error = null
|
|
149
|
+
existingTask.outputFiles = []
|
|
150
|
+
existingTask.artifacts = []
|
|
149
151
|
existingTask.sessionId = null
|
|
152
|
+
existingTask.completionReportPath = null
|
|
150
153
|
existingTask.updatedAt = now
|
|
151
154
|
existingTask.queuedAt = null
|
|
152
155
|
existingTask.startedAt = null
|
|
@@ -13,7 +13,7 @@ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
13
13
|
if (hasTool('manage_chatrooms')) {
|
|
14
14
|
tools.push(
|
|
15
15
|
tool(
|
|
16
|
-
async ({ action, chatroomId, name, description, agentIds, agentId, message }) => {
|
|
16
|
+
async ({ action, chatroomId, name, description, agentIds, agentId, message, chatMode, autoAddress }) => {
|
|
17
17
|
try {
|
|
18
18
|
const chatrooms = loadChatrooms() as Record<string, Chatroom>
|
|
19
19
|
|
|
@@ -31,13 +31,20 @@ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
31
31
|
if (action === 'create_chatroom') {
|
|
32
32
|
const id = genId()
|
|
33
33
|
const agents = loadAgents()
|
|
34
|
-
const
|
|
34
|
+
const requestedAgentIds = agentIds || []
|
|
35
|
+
const invalidAgentIds = requestedAgentIds.filter((aid: string) => !agents[aid])
|
|
36
|
+
if (invalidAgentIds.length > 0) {
|
|
37
|
+
return `Error: unknown agent IDs: ${invalidAgentIds.join(', ')}`
|
|
38
|
+
}
|
|
39
|
+
const validAgentIds = requestedAgentIds
|
|
35
40
|
const chatroom: Chatroom = {
|
|
36
41
|
id,
|
|
37
42
|
name: name || 'New Chatroom',
|
|
38
43
|
description: description || '',
|
|
39
44
|
agentIds: validAgentIds,
|
|
40
45
|
messages: [],
|
|
46
|
+
chatMode: chatMode === 'parallel' ? 'parallel' : 'sequential',
|
|
47
|
+
autoAddress: Boolean(autoAddress),
|
|
41
48
|
createdAt: Date.now(),
|
|
42
49
|
updatedAt: Date.now(),
|
|
43
50
|
}
|
|
@@ -124,6 +131,8 @@ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
124
131
|
name: z.string().optional().describe('Chatroom name (for create_chatroom)'),
|
|
125
132
|
description: z.string().optional().describe('Chatroom description (for create_chatroom)'),
|
|
126
133
|
agentIds: z.array(z.string()).optional().describe('Initial agent IDs (for create_chatroom)'),
|
|
134
|
+
chatMode: z.enum(['sequential', 'parallel']).optional().describe('Optional orchestration mode for create_chatroom'),
|
|
135
|
+
autoAddress: z.boolean().optional().describe('Whether to auto-address all members when no @mention is present'),
|
|
127
136
|
agentId: z.string().optional().describe('Agent ID (for add_agent/remove_agent)'),
|
|
128
137
|
message: z.string().optional().describe('Message text (for send_message)'),
|
|
129
138
|
}),
|
|
@@ -28,7 +28,7 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: 'context_status',
|
|
31
|
-
description: 'Check
|
|
31
|
+
description: 'Check how much of my context window I\'ve used. Returns my token usage, the model\'s limit, percentage used, and whether I should compact.',
|
|
32
32
|
schema: z.object({}),
|
|
33
33
|
},
|
|
34
34
|
),
|
|
@@ -108,7 +108,7 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
name: 'context_summarize',
|
|
111
|
-
description: '
|
|
111
|
+
description: 'Compact my conversation history to free up context space. I\'ll save important decisions, facts, and results to memory, then replace older messages with a summary. I should check context_status first to see if this is needed.',
|
|
112
112
|
schema: z.object({
|
|
113
113
|
keepLastN: z.number().optional().describe('Number of recent messages to keep (default 10, min 2).'),
|
|
114
114
|
}),
|
|
@@ -21,6 +21,7 @@ import { buildSubagentTools } from './subagent'
|
|
|
21
21
|
import { buildCanvasTools } from './canvas'
|
|
22
22
|
import { buildHttpTools } from './http'
|
|
23
23
|
import { buildGitTools } from './git'
|
|
24
|
+
import { buildWalletTools } from './wallet'
|
|
24
25
|
|
|
25
26
|
export type { ToolContext, SessionToolsResult }
|
|
26
27
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
@@ -34,7 +35,9 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
34
35
|
const cliProcessTimeoutMs = runtime.cliProcessTimeoutMs
|
|
35
36
|
const appSettings = loadSettings()
|
|
36
37
|
const toolPolicy = resolveSessionToolPolicy(enabledTools, appSettings)
|
|
37
|
-
const activeTools = toolPolicy.enabledTools
|
|
38
|
+
const activeTools = toolPolicy.enabledTools.includes('shell') && !toolPolicy.enabledTools.includes('process')
|
|
39
|
+
? [...toolPolicy.enabledTools, 'process']
|
|
40
|
+
: toolPolicy.enabledTools
|
|
38
41
|
const hasTool = (toolName: string) => activeTools.includes(toolName)
|
|
39
42
|
|
|
40
43
|
if (toolPolicy.blockedTools.length > 0) {
|
|
@@ -107,6 +110,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
107
110
|
...buildCanvasTools(bctx),
|
|
108
111
|
...buildHttpTools(bctx),
|
|
109
112
|
...buildGitTools(bctx),
|
|
113
|
+
...buildWalletTools(bctx),
|
|
110
114
|
)
|
|
111
115
|
|
|
112
116
|
// ---------------------------------------------------------------------------
|
|
@@ -158,7 +162,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
158
162
|
},
|
|
159
163
|
{
|
|
160
164
|
name: 'request_tool_access',
|
|
161
|
-
description: '
|
|
165
|
+
description: 'Ask the user for access to a tool I don\'t currently have. They\'ll get a prompt to grant it, and once they do, I\'ll automatically continue where I left off. I should end my current response after calling this — no need to ask the user to confirm, it happens on its own.',
|
|
162
166
|
schema: z.object({
|
|
163
167
|
toolId: z.string().describe('The tool ID to request access for (e.g. manage_tasks, shell, claude_code)'),
|
|
164
168
|
reason: z.string().describe('Brief explanation of why you need this tool'),
|
|
@@ -187,7 +187,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
name: 'memory_tool',
|
|
190
|
-
description: '
|
|
190
|
+
description: 'My long-term memory — things I remember across conversations. I can store personal notes, recall past context, and build up knowledge over time. Memories can be private to me or shared with other agents. I can also attach files, link related memories, and contribute to a shared knowledge base. Actions: store, get, search, list, delete, link, unlink, knowledge_store, knowledge_search.',
|
|
191
191
|
schema: z.object({
|
|
192
192
|
action: z.enum(['store', 'get', 'search', 'list', 'delete', 'link', 'unlink', 'knowledge_store', 'knowledge_search']).describe('The action to perform'),
|
|
193
193
|
key: z.string().describe('For store: memory title. For get/delete/link/unlink: memory ID. For search: optional query fallback.'),
|
|
@@ -49,7 +49,7 @@ export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
name: 'execute_command',
|
|
52
|
-
description: '
|
|
52
|
+
description: 'Run a shell command in my working directory. This is how I run servers, install packages, execute scripts, use git, and do anything hands-on. Use background=true for long-running processes like dev servers. Supports timeout/yield controls.',
|
|
53
53
|
schema: z.object({
|
|
54
54
|
command: z.string().describe('The shell command to execute'),
|
|
55
55
|
background: z.boolean().optional().describe('If true, start command in background immediately'),
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { ToolBuildContext } from './context'
|
|
4
|
+
import { loadWallets, loadWalletTransactions } from '../storage'
|
|
5
|
+
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
6
|
+
|
|
7
|
+
export function buildWalletTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
8
|
+
if (!bctx.hasTool('wallet')) return []
|
|
9
|
+
|
|
10
|
+
const agentId = bctx.ctx?.agentId
|
|
11
|
+
|
|
12
|
+
function getAgentWallet(): AgentWallet | null {
|
|
13
|
+
if (!agentId) return null
|
|
14
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
15
|
+
return Object.values(wallets).find((w) => w.agentId === agentId) ?? null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
tool(
|
|
20
|
+
async ({ action, toAddress, amountSol, memo, limit }) => {
|
|
21
|
+
const wallet = getAgentWallet()
|
|
22
|
+
if (!wallet) {
|
|
23
|
+
return JSON.stringify({ error: 'No wallet linked to this agent. Ask the user to create one in the Wallets section.' })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
switch (action) {
|
|
27
|
+
case 'balance': {
|
|
28
|
+
try {
|
|
29
|
+
const { getBalance, lamportsToSol } = await import('../solana')
|
|
30
|
+
const balanceLamports = await getBalance(wallet.publicKey)
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
address: wallet.publicKey,
|
|
33
|
+
chain: wallet.chain,
|
|
34
|
+
balanceLamports,
|
|
35
|
+
balanceSol: lamportsToSol(balanceLamports),
|
|
36
|
+
})
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
return JSON.stringify({ error: `Failed to fetch balance: ${err instanceof Error ? err.message : String(err)}` })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case 'address': {
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
address: wallet.publicKey,
|
|
45
|
+
chain: wallet.chain,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'send': {
|
|
50
|
+
if (!toAddress) return JSON.stringify({ error: 'toAddress is required for send action' })
|
|
51
|
+
if (!amountSol || amountSol <= 0) return JSON.stringify({ error: 'amountSol must be positive' })
|
|
52
|
+
|
|
53
|
+
const { isValidSolanaAddress, solToLamports, lamportsToSol } = await import('../solana')
|
|
54
|
+
if (!isValidSolanaAddress(toAddress)) {
|
|
55
|
+
return JSON.stringify({ error: 'Invalid Solana address' })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const amountLamports = solToLamports(amountSol)
|
|
59
|
+
|
|
60
|
+
// Check per-tx limit
|
|
61
|
+
const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
|
|
62
|
+
if (amountLamports > perTxLimit) {
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
error: `Amount ${amountSol} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Send via API to enforce all limits and approval flow
|
|
69
|
+
try {
|
|
70
|
+
const baseUrl = process.env.NEXTAUTH_URL || `http://localhost:${process.env.PORT || 3456}`
|
|
71
|
+
const res = await fetch(`${baseUrl}/api/wallets/${wallet.id}/send`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'X-Access-Key': process.env.ACCESS_KEY || '',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ toAddress, amountLamports, memo }),
|
|
78
|
+
})
|
|
79
|
+
const result = await res.json()
|
|
80
|
+
return JSON.stringify(result)
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
return JSON.stringify({ error: `Send failed: ${err instanceof Error ? err.message : String(err)}` })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'transactions': {
|
|
87
|
+
const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
|
|
88
|
+
const walletTxs = Object.values(allTxs)
|
|
89
|
+
.filter((tx) => tx.walletId === wallet.id)
|
|
90
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
91
|
+
.slice(0, limit ?? 10)
|
|
92
|
+
.map((tx) => ({
|
|
93
|
+
id: tx.id,
|
|
94
|
+
type: tx.type,
|
|
95
|
+
status: tx.status,
|
|
96
|
+
amountLamports: tx.amountLamports,
|
|
97
|
+
toAddress: tx.toAddress,
|
|
98
|
+
fromAddress: tx.fromAddress,
|
|
99
|
+
signature: tx.signature || undefined,
|
|
100
|
+
memo: tx.memo,
|
|
101
|
+
timestamp: tx.timestamp,
|
|
102
|
+
}))
|
|
103
|
+
|
|
104
|
+
return JSON.stringify({ transactions: walletTxs, count: walletTxs.length })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
return JSON.stringify({ error: `Unknown action: ${action}. Use balance, address, send, or transactions.` })
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'wallet_tool',
|
|
113
|
+
description: 'Manage your own crypto wallet. Actions: balance (check your SOL balance), address (get your wallet address), send (send SOL from your wallet — subject to your spending limits and user approval), transactions (view your recent transaction history).',
|
|
114
|
+
schema: z.object({
|
|
115
|
+
action: z.enum(['balance', 'address', 'send', 'transactions']).describe('Wallet action to perform'),
|
|
116
|
+
toAddress: z.string().optional().describe('Recipient Solana address (required for send)'),
|
|
117
|
+
amountSol: z.number().optional().describe('Amount in SOL to send (required for send)'),
|
|
118
|
+
memo: z.string().optional().describe('Reason or memo for the transaction'),
|
|
119
|
+
limit: z.number().optional().describe('Number of transactions to return (default 10, for transactions action)'),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { dedupeScreenshotMarkdownLines } from './web-output'
|
|
4
|
+
|
|
5
|
+
describe('dedupeScreenshotMarkdownLines', () => {
|
|
6
|
+
it('prefers screenshot-* image when both browser-* and screenshot-* variants are present', () => {
|
|
7
|
+
const parts = [
|
|
8
|
+
'',
|
|
9
|
+
'',
|
|
10
|
+
'Saved to: example_screenshot.png',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const next = dedupeScreenshotMarkdownLines(parts)
|
|
14
|
+
assert.deepEqual(next, [
|
|
15
|
+
'',
|
|
16
|
+
'Saved to: example_screenshot.png',
|
|
17
|
+
])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('keeps single image output untouched', () => {
|
|
21
|
+
const parts = [
|
|
22
|
+
'',
|
|
23
|
+
'Saved to: example_screenshot.png',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const next = dedupeScreenshotMarkdownLines(parts)
|
|
27
|
+
assert.deepEqual(next, parts)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function dedupeScreenshotMarkdownLines(parts: string[]): string[] {
|
|
2
|
+
const imageLineRe = /^!\[[^\]]*]\(\/api\/uploads\/([^)]+)\)$/
|
|
3
|
+
const imageLines = parts
|
|
4
|
+
.map((line, index) => ({ line: line.trim(), index }))
|
|
5
|
+
.map((entry) => {
|
|
6
|
+
const match = entry.line.match(imageLineRe)
|
|
7
|
+
return match ? { ...entry, filename: match[1] } : null
|
|
8
|
+
})
|
|
9
|
+
.filter((entry): entry is { line: string; index: number; filename: string } => !!entry)
|
|
10
|
+
|
|
11
|
+
if (imageLines.length <= 1) return parts
|
|
12
|
+
|
|
13
|
+
const preferred = imageLines.find((entry) => !entry.filename.startsWith('browser-')) || imageLines[0]
|
|
14
|
+
const keepIndex = preferred.index
|
|
15
|
+
return parts.filter((_, index) => !imageLines.some((entry) => entry.index === index) || index === keepIndex)
|
|
16
|
+
}
|