@swarmclawai/swarmclaw 0.6.4 → 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/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/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 +3 -2
- 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/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- 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/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +18 -3
- 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/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/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.ts +2 -2
- 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 +1 -0
- package/src/stores/use-app-store.ts +8 -0
- package/src/types/index.ts +60 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { BoardTask } from '@/types'
|
|
4
|
+
import { resolveTaskOriginConnectorFollowupTarget } from './queue'
|
|
5
|
+
|
|
6
|
+
function makeTask(partial?: Partial<BoardTask> & { createdInSessionId?: string | null }): BoardTask {
|
|
7
|
+
const now = Date.now()
|
|
8
|
+
return {
|
|
9
|
+
id: 'task-1',
|
|
10
|
+
title: 'Test task',
|
|
11
|
+
description: 'desc',
|
|
12
|
+
status: 'queued',
|
|
13
|
+
agentId: 'agent-a',
|
|
14
|
+
createdAt: now,
|
|
15
|
+
updatedAt: now,
|
|
16
|
+
...(partial || {}),
|
|
17
|
+
} as BoardTask
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SessionFixtureMap = Record<string, {
|
|
21
|
+
messages: Array<{
|
|
22
|
+
role: string
|
|
23
|
+
text?: string
|
|
24
|
+
source?: {
|
|
25
|
+
connectorId?: string
|
|
26
|
+
channelId?: string
|
|
27
|
+
}
|
|
28
|
+
}>
|
|
29
|
+
}>
|
|
30
|
+
|
|
31
|
+
describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
32
|
+
it('uses connector source channel from origin session and normalizes WhatsApp numbers', () => {
|
|
33
|
+
const task = makeTask({ createdInSessionId: 'session-1' })
|
|
34
|
+
const sessions = {
|
|
35
|
+
'session-1': {
|
|
36
|
+
messages: [
|
|
37
|
+
{ role: 'assistant', text: 'ok' },
|
|
38
|
+
{
|
|
39
|
+
role: 'user',
|
|
40
|
+
text: 'please update me',
|
|
41
|
+
source: {
|
|
42
|
+
connectorId: 'conn-wa',
|
|
43
|
+
channelId: '+44 7700 900123',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
const connectors = {
|
|
50
|
+
'conn-wa': {
|
|
51
|
+
id: 'conn-wa',
|
|
52
|
+
platform: 'whatsapp',
|
|
53
|
+
agentId: 'agent-a',
|
|
54
|
+
config: {},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
const running = [
|
|
58
|
+
{
|
|
59
|
+
id: 'conn-wa',
|
|
60
|
+
platform: 'whatsapp',
|
|
61
|
+
agentId: 'agent-a',
|
|
62
|
+
supportsSend: true,
|
|
63
|
+
configuredTargets: [],
|
|
64
|
+
recentChannelId: '185200000000000@lid',
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
69
|
+
task,
|
|
70
|
+
sessions: sessions as SessionFixtureMap,
|
|
71
|
+
connectors,
|
|
72
|
+
running,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(target, {
|
|
76
|
+
connectorId: 'conn-wa',
|
|
77
|
+
channelId: '447700900123@s.whatsapp.net',
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls back to runtime recent channel when source channel is unavailable', () => {
|
|
82
|
+
const task = makeTask({ createdInSessionId: 'session-1' })
|
|
83
|
+
const sessions = {
|
|
84
|
+
'session-1': {
|
|
85
|
+
messages: [
|
|
86
|
+
{
|
|
87
|
+
role: 'user',
|
|
88
|
+
text: 'run this later',
|
|
89
|
+
source: {
|
|
90
|
+
connectorId: 'conn-telegram',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
const connectors = {
|
|
97
|
+
'conn-telegram': {
|
|
98
|
+
id: 'conn-telegram',
|
|
99
|
+
platform: 'telegram',
|
|
100
|
+
agentId: 'agent-a',
|
|
101
|
+
config: {},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
const running = [
|
|
105
|
+
{
|
|
106
|
+
id: 'conn-telegram',
|
|
107
|
+
platform: 'telegram',
|
|
108
|
+
agentId: 'agent-a',
|
|
109
|
+
supportsSend: true,
|
|
110
|
+
configuredTargets: [],
|
|
111
|
+
recentChannelId: 'tg-chat-42',
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
116
|
+
task,
|
|
117
|
+
sessions: sessions as SessionFixtureMap,
|
|
118
|
+
connectors,
|
|
119
|
+
running,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
assert.deepEqual(target, {
|
|
123
|
+
connectorId: 'conn-telegram',
|
|
124
|
+
channelId: 'tg-chat-42',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns null when the source connector belongs to a different agent', () => {
|
|
129
|
+
const task = makeTask({ createdInSessionId: 'session-1' })
|
|
130
|
+
const sessions = {
|
|
131
|
+
'session-1': {
|
|
132
|
+
messages: [
|
|
133
|
+
{
|
|
134
|
+
role: 'user',
|
|
135
|
+
text: 'do it',
|
|
136
|
+
source: {
|
|
137
|
+
connectorId: 'conn-wa',
|
|
138
|
+
channelId: '+15551230000',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
const connectors = {
|
|
145
|
+
'conn-wa': {
|
|
146
|
+
id: 'conn-wa',
|
|
147
|
+
platform: 'whatsapp',
|
|
148
|
+
agentId: 'different-agent',
|
|
149
|
+
config: {},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
const running = [
|
|
153
|
+
{
|
|
154
|
+
id: 'conn-wa',
|
|
155
|
+
platform: 'whatsapp',
|
|
156
|
+
agentId: 'different-agent',
|
|
157
|
+
supportsSend: true,
|
|
158
|
+
configuredTargets: [],
|
|
159
|
+
recentChannelId: null,
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
164
|
+
task,
|
|
165
|
+
sessions: sessions as SessionFixtureMap,
|
|
166
|
+
connectors,
|
|
167
|
+
running,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
assert.equal(target, null)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('allows delegated tasks to follow up via the delegating agent connector', () => {
|
|
174
|
+
const task = makeTask({
|
|
175
|
+
agentId: 'worker-agent',
|
|
176
|
+
delegatedByAgentId: 'delegator-agent',
|
|
177
|
+
createdInSessionId: 'session-1',
|
|
178
|
+
})
|
|
179
|
+
const sessions = {
|
|
180
|
+
'session-1': {
|
|
181
|
+
messages: [
|
|
182
|
+
{
|
|
183
|
+
role: 'user',
|
|
184
|
+
text: 'run and update me here',
|
|
185
|
+
source: {
|
|
186
|
+
connectorId: 'conn-wa',
|
|
187
|
+
channelId: '+44 7700 900123',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
const connectors = {
|
|
194
|
+
'conn-wa': {
|
|
195
|
+
id: 'conn-wa',
|
|
196
|
+
platform: 'whatsapp',
|
|
197
|
+
agentId: 'delegator-agent',
|
|
198
|
+
config: {},
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
const running = [
|
|
202
|
+
{
|
|
203
|
+
id: 'conn-wa',
|
|
204
|
+
platform: 'whatsapp',
|
|
205
|
+
agentId: 'delegator-agent',
|
|
206
|
+
supportsSend: true,
|
|
207
|
+
configuredTargets: [],
|
|
208
|
+
recentChannelId: null,
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
213
|
+
task,
|
|
214
|
+
sessions: sessions as SessionFixtureMap,
|
|
215
|
+
connectors,
|
|
216
|
+
running,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
assert.deepEqual(target, {
|
|
220
|
+
connectorId: 'conn-wa',
|
|
221
|
+
channelId: '447700900123@s.whatsapp.net',
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
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'),
|