@swarmclawai/swarmclaw 0.6.0 → 0.6.3
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 +56 -42
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +16 -35
- package/src/app/api/tts/stream/route.ts +14 -42
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +76 -24
- package/src/components/chat/chat-header.tsx +522 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +113 -8
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +84 -17
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +125 -14
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/connector-routing.test.ts +118 -1
- package/src/lib/server/connectors/discord.ts +31 -8
- package/src/lib/server/connectors/manager.ts +594 -16
- package/src/lib/server/connectors/media.ts +5 -0
- package/src/lib/server/connectors/telegram.ts +12 -2
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +28 -2
- package/src/lib/server/elevenlabs.test.ts +60 -0
- package/src/lib/server/elevenlabs.ts +103 -0
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +182 -8
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +583 -63
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/file.ts +26 -7
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +118 -8
- package/src/lib/server/stream-agent-chat.ts +39 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -160,8 +160,8 @@ export class OpenClawGateway {
|
|
|
160
160
|
this.doConnect().catch(() => {})
|
|
161
161
|
}, this.reconnectDelay)
|
|
162
162
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, maxDelay)
|
|
163
|
-
if (this.consecutiveFailures % 5 === 0) {
|
|
164
|
-
console.log(`[openclaw-gateway] ${this.consecutiveFailures} consecutive
|
|
163
|
+
if (this.consecutiveFailures === 1 || this.consecutiveFailures % 5 === 0) {
|
|
164
|
+
console.log(`[openclaw-gateway] ${this.consecutiveFailures} consecutive failure${this.consecutiveFailures > 1 ? 's' : ''}, next retry in ${Math.round(this.reconnectDelay / 1000)}s`)
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -11,6 +11,7 @@ import { buildChatModel } from './build-llm'
|
|
|
11
11
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
12
12
|
import { notify } from './ws-hub'
|
|
13
13
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
14
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
14
15
|
import { genId } from '@/lib/id'
|
|
15
16
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
16
17
|
import type { Agent, TaskComment, MessageToolEvent } from '@/types'
|
|
@@ -351,6 +352,7 @@ export async function executeLangGraphOrchestrator(
|
|
|
351
352
|
const settings = loadSettings()
|
|
352
353
|
const promptParts: string[] = []
|
|
353
354
|
if (settings.userPrompt) promptParts.push(settings.userPrompt)
|
|
355
|
+
promptParts.push(buildCurrentDateTimePromptContext())
|
|
354
356
|
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
355
357
|
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
356
358
|
// Inject dynamic skills
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { WORKSPACE_DIR } from './data-dir'
|
|
7
7
|
import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
|
|
8
8
|
import { getMemoryDb } from './memory-db'
|
|
9
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
9
10
|
import { getProvider } from '../providers'
|
|
10
11
|
import type { Agent } from '@/types'
|
|
11
12
|
|
|
@@ -109,6 +110,7 @@ async function executeOrchestratorLegacy(
|
|
|
109
110
|
const settings = loadSettings()
|
|
110
111
|
const promptParts: string[] = []
|
|
111
112
|
if (settings.userPrompt) promptParts.push(settings.userPrompt)
|
|
113
|
+
promptParts.push(buildCurrentDateTimePromptContext())
|
|
112
114
|
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
113
115
|
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
114
116
|
if (orchestrator.skillIds?.length) {
|
|
@@ -308,8 +310,8 @@ async function executeSubTask(
|
|
|
308
310
|
|
|
309
311
|
export async function callProvider(
|
|
310
312
|
agent: Agent,
|
|
311
|
-
systemPrompt
|
|
312
|
-
history: { role: string; text: string }[],
|
|
313
|
+
systemPrompt?: string,
|
|
314
|
+
history: { role: string; text: string }[] = [],
|
|
313
315
|
): Promise<string> {
|
|
314
316
|
const provider = getProvider(agent.provider)
|
|
315
317
|
if (!provider) throw new Error(`Unknown provider: ${agent.provider}`)
|
|
@@ -346,6 +348,7 @@ export async function callProvider(
|
|
|
346
348
|
session: mockSession,
|
|
347
349
|
message: history[history.length - 1].text,
|
|
348
350
|
apiKey,
|
|
351
|
+
systemPrompt,
|
|
349
352
|
write: (data: string) => {
|
|
350
353
|
// Parse SSE data to extract text
|
|
351
354
|
if (data.startsWith('data: ')) {
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
import { spawn } from 'child_process'
|
|
8
8
|
import fs from 'fs'
|
|
9
9
|
import path from 'path'
|
|
10
|
-
import os from 'os'
|
|
11
10
|
|
|
12
|
-
const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(
|
|
11
|
+
const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(process.env.DATA_DIR || path.join(process.cwd(), 'data'), 'uploads')
|
|
13
12
|
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
14
13
|
|
|
15
14
|
const child = spawn('npx', ['@playwright/mcp@latest'], {
|
|
@@ -47,7 +46,7 @@ child.stdout.on('data', (chunk) => {
|
|
|
47
46
|
fs.writeFileSync(path.join(UPLOAD_DIR, filename), Buffer.from(block.data, 'base64'))
|
|
48
47
|
newContent.push({
|
|
49
48
|
type: 'text',
|
|
50
|
-
text: `Screenshot saved
|
|
49
|
+
text: `Screenshot saved to /api/uploads/${filename} — it is already displayed inline above (do not repeat it with markdown).`,
|
|
51
50
|
})
|
|
52
51
|
newContent.push(block) // keep image so Claude can see it
|
|
53
52
|
} else {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function resolveLocalTimezone(): string {
|
|
2
|
+
try {
|
|
3
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
4
|
+
} catch {
|
|
5
|
+
return 'UTC'
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatDateTimeInTimezone(date: Date, timezone: string): string | null {
|
|
10
|
+
try {
|
|
11
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
12
|
+
weekday: 'long',
|
|
13
|
+
year: 'numeric',
|
|
14
|
+
month: 'long',
|
|
15
|
+
day: 'numeric',
|
|
16
|
+
hour: '2-digit',
|
|
17
|
+
minute: '2-digit',
|
|
18
|
+
second: '2-digit',
|
|
19
|
+
hour12: false,
|
|
20
|
+
timeZone: timezone,
|
|
21
|
+
timeZoneName: 'short',
|
|
22
|
+
}).format(date)
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildCurrentDateTimePromptContext(preferredTimezone?: string | null): string {
|
|
29
|
+
const now = new Date()
|
|
30
|
+
const utcIso = now.toISOString()
|
|
31
|
+
const utcFormatted = formatDateTimeInTimezone(now, 'UTC') || utcIso
|
|
32
|
+
const localTimezone = resolveLocalTimezone()
|
|
33
|
+
const requestedTimezone = (preferredTimezone || '').trim()
|
|
34
|
+
const chosenTimezone = requestedTimezone || localTimezone
|
|
35
|
+
const chosenFormatted = formatDateTimeInTimezone(now, chosenTimezone)
|
|
36
|
+
|
|
37
|
+
const lines = [
|
|
38
|
+
'## Runtime Date/Time Context',
|
|
39
|
+
`- Current timestamp (UTC): ${utcIso}`,
|
|
40
|
+
`- Current date/time (UTC): ${utcFormatted}`,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
if (chosenFormatted) {
|
|
44
|
+
lines.push(`- Current date/time (${chosenTimezone}): ${chosenFormatted}`)
|
|
45
|
+
} else if (requestedTimezone) {
|
|
46
|
+
lines.push(`- Requested timezone "${requestedTimezone}" could not be resolved. Use UTC time above.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push('- Treat these as authoritative for terms like "today", "yesterday", "tomorrow", and "recent".')
|
|
50
|
+
lines.push('- For time-sensitive answers, use explicit dates (for example, "March 2, 2026").')
|
|
51
|
+
|
|
52
|
+
return lines.join('\n')
|
|
53
|
+
}
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
|
-
import
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { loadTasks, saveTasks, loadQueue, saveQueue, loadAgents, loadSchedules, saveSchedules, loadSessions, saveSessions, loadSettings, loadConnectors, UPLOAD_DIR } from './storage'
|
|
3
5
|
import { notify } from './ws-hub'
|
|
4
6
|
import { WORKSPACE_DIR } from './data-dir'
|
|
5
7
|
import { createOrchestratorSession, executeOrchestrator } from './orchestrator'
|
|
@@ -13,13 +15,13 @@ import { isProtectedMainSession } from './main-session'
|
|
|
13
15
|
import type { Agent, BoardTask, Message } from '@/types'
|
|
14
16
|
|
|
15
17
|
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
16
|
-
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false }) as { processing: boolean }
|
|
18
|
+
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
|
|
17
19
|
|
|
18
20
|
interface SessionMessageLike {
|
|
19
21
|
role?: string
|
|
20
22
|
text?: string
|
|
21
23
|
time?: number
|
|
22
|
-
kind?: 'chat' | 'heartbeat' | 'system'
|
|
24
|
+
kind?: 'chat' | 'heartbeat' | 'system' | 'context-clear'
|
|
23
25
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -114,6 +116,54 @@ function latestAssistantText(session: SessionLike | null | undefined): string {
|
|
|
114
116
|
return ''
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function isEnabledFlag(value: unknown): boolean {
|
|
120
|
+
if (typeof value === 'boolean') return value
|
|
121
|
+
if (typeof value !== 'string') return false
|
|
122
|
+
const normalized = value.trim().toLowerCase()
|
|
123
|
+
return normalized === '1'
|
|
124
|
+
|| normalized === 'true'
|
|
125
|
+
|| normalized === 'yes'
|
|
126
|
+
|| normalized === 'on'
|
|
127
|
+
|| normalized === 'enabled'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeWhatsappTarget(raw: string): string {
|
|
131
|
+
const trimmed = raw.trim()
|
|
132
|
+
if (!trimmed) return trimmed
|
|
133
|
+
if (trimmed.includes('@')) return trimmed
|
|
134
|
+
let cleaned = trimmed.replace(/[^\d+]/g, '')
|
|
135
|
+
if (cleaned.startsWith('+')) cleaned = cleaned.slice(1)
|
|
136
|
+
if (cleaned.startsWith('0') && cleaned.length >= 10) {
|
|
137
|
+
cleaned = `44${cleaned.slice(1)}`
|
|
138
|
+
}
|
|
139
|
+
cleaned = cleaned.replace(/[^\d]/g, '')
|
|
140
|
+
return cleaned ? `${cleaned}@s.whatsapp.net` : trimmed
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function fillTaskFollowupTemplate(template: string, data: {
|
|
144
|
+
status: string
|
|
145
|
+
title: string
|
|
146
|
+
summary: string
|
|
147
|
+
taskId: string
|
|
148
|
+
}): string {
|
|
149
|
+
return template
|
|
150
|
+
.replaceAll('{status}', data.status)
|
|
151
|
+
.replaceAll('{title}', data.title)
|
|
152
|
+
.replaceAll('{summary}', data.summary)
|
|
153
|
+
.replaceAll('{taskId}', data.taskId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function maybeResolveUploadMediaPathFromUrl(url: string | undefined): string | undefined {
|
|
157
|
+
if (!url || !url.startsWith('/api/uploads/')) return undefined
|
|
158
|
+
const rawName = url.slice('/api/uploads/'.length).split(/[?#]/)[0] || ''
|
|
159
|
+
let decoded: string
|
|
160
|
+
try { decoded = decodeURIComponent(rawName) } catch { decoded = rawName }
|
|
161
|
+
const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
162
|
+
if (!safeName) return undefined
|
|
163
|
+
const fullPath = path.join(UPLOAD_DIR, safeName)
|
|
164
|
+
return fs.existsSync(fullPath) ? fullPath : undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
117
167
|
// Task result extraction now uses Zod-validated structured data
|
|
118
168
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
119
169
|
|
|
@@ -215,6 +265,78 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
215
265
|
if (changed) saveSessions(sessions)
|
|
216
266
|
}
|
|
217
267
|
|
|
268
|
+
async function notifyConnectorTaskFollowups(params: {
|
|
269
|
+
task: BoardTask
|
|
270
|
+
statusLabel: string
|
|
271
|
+
summaryText: string
|
|
272
|
+
imageUrl?: string
|
|
273
|
+
}) {
|
|
274
|
+
const { task, statusLabel, summaryText, imageUrl } = params
|
|
275
|
+
|
|
276
|
+
const connectors = loadConnectors()
|
|
277
|
+
const running = (await import('./connectors/manager')).listRunningConnectors()
|
|
278
|
+
const manager = await import('./connectors/manager')
|
|
279
|
+
|
|
280
|
+
const candidates = running.filter((entry) => {
|
|
281
|
+
if (!entry.supportsSend || !entry.id) return false
|
|
282
|
+
const connector = connectors[entry.id]
|
|
283
|
+
if (!connector) return false
|
|
284
|
+
if (connector.agentId !== task.agentId) return false
|
|
285
|
+
return isEnabledFlag(connector.config?.taskFollowups)
|
|
286
|
+
})
|
|
287
|
+
if (!candidates.length) return
|
|
288
|
+
|
|
289
|
+
const summary = summaryText.trim().slice(0, 1400)
|
|
290
|
+
for (const candidate of candidates) {
|
|
291
|
+
const connector = connectors[candidate.id]
|
|
292
|
+
if (!connector) continue
|
|
293
|
+
|
|
294
|
+
const channelTargetRaw = candidate.recentChannelId
|
|
295
|
+
|| candidate.configuredTargets[0]
|
|
296
|
+
|| connector.config?.outboundJid
|
|
297
|
+
|| connector.config?.outboundTarget
|
|
298
|
+
|| ''
|
|
299
|
+
if (!channelTargetRaw) continue
|
|
300
|
+
|
|
301
|
+
const channelId = connector.platform === 'whatsapp'
|
|
302
|
+
? normalizeWhatsappTarget(channelTargetRaw)
|
|
303
|
+
: channelTargetRaw
|
|
304
|
+
|
|
305
|
+
const template = typeof connector.config?.taskFollowupTemplate === 'string'
|
|
306
|
+
? connector.config.taskFollowupTemplate.trim()
|
|
307
|
+
: ''
|
|
308
|
+
const message = template
|
|
309
|
+
? fillTaskFollowupTemplate(template, {
|
|
310
|
+
status: statusLabel,
|
|
311
|
+
title: task.title || task.id,
|
|
312
|
+
summary,
|
|
313
|
+
taskId: task.id,
|
|
314
|
+
})
|
|
315
|
+
: [
|
|
316
|
+
`Task ${statusLabel}: ${task.title}`,
|
|
317
|
+
summary || 'No summary provided.',
|
|
318
|
+
].join('\n\n')
|
|
319
|
+
|
|
320
|
+
const resolvedMediaPath = maybeResolveUploadMediaPathFromUrl(imageUrl)
|
|
321
|
+
try {
|
|
322
|
+
await manager.sendConnectorMessage({
|
|
323
|
+
connectorId: candidate.id,
|
|
324
|
+
channelId,
|
|
325
|
+
text: message,
|
|
326
|
+
...(resolvedMediaPath
|
|
327
|
+
? {
|
|
328
|
+
mediaPath: resolvedMediaPath,
|
|
329
|
+
caption: message,
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
})
|
|
333
|
+
} catch (err: unknown) {
|
|
334
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
335
|
+
console.warn(`[queue] Failed task follow-up send on connector ${candidate.id}: ${errMsg}`)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
218
340
|
/**
|
|
219
341
|
* Notify agent thread sessions when a task completes or fails.
|
|
220
342
|
* - Always pushes to the executing agent's thread
|
|
@@ -280,22 +402,54 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
280
402
|
changed = true
|
|
281
403
|
}
|
|
282
404
|
|
|
283
|
-
// 2. If delegated, push to delegating agent's thread
|
|
405
|
+
// 2. If delegated, push to delegating agent's thread AND active chat sessions
|
|
284
406
|
const delegatedBy = (task as unknown as Record<string, unknown>).delegatedByAgentId
|
|
285
407
|
if (typeof delegatedBy === 'string' && delegatedBy !== task.agentId) {
|
|
286
408
|
const delegator = agents[delegatedBy]
|
|
409
|
+
const agentName = agent?.name || task.agentId
|
|
410
|
+
const delegationBody = buildResultBlock(`Delegated task ${statusLabel}: **${taskLink}** (by ${agentName})`)
|
|
411
|
+
|
|
412
|
+
// Push to delegating agent's thread
|
|
287
413
|
if (delegator?.threadSessionId && sessions[delegator.threadSessionId]) {
|
|
288
414
|
const thread = sessions[delegator.threadSessionId]
|
|
289
415
|
if (!Array.isArray(thread.messages)) thread.messages = []
|
|
290
|
-
|
|
291
|
-
const body = buildResultBlock(`Delegated task ${statusLabel}: **${taskLink}** (by ${agentName})`)
|
|
292
|
-
thread.messages.push(buildMsg(body))
|
|
416
|
+
thread.messages.push(buildMsg(delegationBody))
|
|
293
417
|
thread.lastActiveAt = now
|
|
294
418
|
changed = true
|
|
295
419
|
}
|
|
420
|
+
|
|
421
|
+
// Push to delegating agent's active user-facing chat sessions
|
|
422
|
+
// so the result is visible in the chat the user is looking at
|
|
423
|
+
if (delegator) {
|
|
424
|
+
for (const session of Object.values(sessions)) {
|
|
425
|
+
if (!session || session.agentId !== delegatedBy) continue
|
|
426
|
+
// Skip thread sessions and orchestrated/subagent sessions
|
|
427
|
+
if (session.id === delegator.threadSessionId) continue
|
|
428
|
+
if (session.sessionType === 'orchestrated') continue
|
|
429
|
+
// Only push to recently-active sessions (within last 30 minutes)
|
|
430
|
+
const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
|
|
431
|
+
if (now - lastActive > 30 * 60_000) continue
|
|
432
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
433
|
+
// Avoid duplicate push
|
|
434
|
+
const lastMsg = session.messages.at(-1)
|
|
435
|
+
if (lastMsg?.text === delegationBody && typeof lastMsg?.time === 'number' && now - lastMsg.time < 30_000) continue
|
|
436
|
+
session.messages.push(buildMsg(delegationBody))
|
|
437
|
+
session.lastActiveAt = now
|
|
438
|
+
changed = true
|
|
439
|
+
// Notify the specific session's message topic for real-time UI update
|
|
440
|
+
notify(`messages:${session.id}`)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
296
443
|
}
|
|
297
444
|
|
|
298
445
|
if (changed) saveSessions(sessions)
|
|
446
|
+
|
|
447
|
+
void notifyConnectorTaskFollowups({
|
|
448
|
+
task,
|
|
449
|
+
statusLabel,
|
|
450
|
+
summaryText: resultBody || '',
|
|
451
|
+
imageUrl: firstImage?.url,
|
|
452
|
+
})
|
|
299
453
|
}
|
|
300
454
|
|
|
301
455
|
/** Disable heartbeat on a task's session when the task finishes. */
|
|
@@ -331,6 +485,10 @@ export function enqueueTask(taskId: string) {
|
|
|
331
485
|
text: `Task queued: "${task.title}" (${task.id})`,
|
|
332
486
|
})
|
|
333
487
|
|
|
488
|
+
// If processNext is already running, mark a pending kick so it re-enters after finishing
|
|
489
|
+
if (_queueState.processing) {
|
|
490
|
+
_queueState.pendingKick = true
|
|
491
|
+
}
|
|
334
492
|
// Delay before kicking worker so UI shows the queued state
|
|
335
493
|
setTimeout(() => processNext(), 2000)
|
|
336
494
|
}
|
|
@@ -619,10 +777,21 @@ export async function processNext() {
|
|
|
619
777
|
// Save initial assistant message so user sees context when opening the session
|
|
620
778
|
const sessions = loadSessions()
|
|
621
779
|
if (sessions[sessionId]) {
|
|
780
|
+
const isDelegation = (task as unknown as Record<string, unknown>).sourceType === 'delegation'
|
|
781
|
+
let initialText: string
|
|
782
|
+
if (isDelegation) {
|
|
783
|
+
const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
|
|
784
|
+
const delegator = delegatorId ? agents[delegatorId] : null
|
|
785
|
+
const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
|
|
786
|
+
initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`\n\nI'll begin working on this now.`
|
|
787
|
+
} else {
|
|
788
|
+
initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`\n\nI'll begin working on this now.`
|
|
789
|
+
}
|
|
622
790
|
sessions[sessionId].messages.push({
|
|
623
791
|
role: 'assistant',
|
|
624
|
-
text:
|
|
792
|
+
text: initialText,
|
|
625
793
|
time: Date.now(),
|
|
794
|
+
...(isDelegation ? { kind: 'system' as const } : {}),
|
|
626
795
|
})
|
|
627
796
|
saveSessions(sessions)
|
|
628
797
|
}
|
|
@@ -807,6 +976,11 @@ export async function processNext() {
|
|
|
807
976
|
}
|
|
808
977
|
} finally {
|
|
809
978
|
_queueState.processing = false
|
|
979
|
+
// If tasks were enqueued while we were processing, kick another round
|
|
980
|
+
if (_queueState.pendingKick) {
|
|
981
|
+
_queueState.pendingKick = false
|
|
982
|
+
setTimeout(() => processNext(), 500)
|
|
983
|
+
}
|
|
810
984
|
}
|
|
811
985
|
}
|
|
812
986
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { loadSessions, saveSessions } from '../storage'
|
|
4
|
+
import { notify } from '../ws-hub'
|
|
5
|
+
import type { ToolBuildContext } from './context'
|
|
6
|
+
|
|
7
|
+
export function buildCanvasTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
8
|
+
const { ctx, hasTool } = bctx
|
|
9
|
+
if (!hasTool('canvas')) return []
|
|
10
|
+
|
|
11
|
+
return [
|
|
12
|
+
tool(
|
|
13
|
+
async ({ action, content }) => {
|
|
14
|
+
try {
|
|
15
|
+
const sessionId = ctx?.sessionId
|
|
16
|
+
if (!sessionId) return 'Error: no active session for canvas.'
|
|
17
|
+
|
|
18
|
+
const sessions = loadSessions()
|
|
19
|
+
const session = sessions[sessionId]
|
|
20
|
+
if (!session) return 'Error: session not found.'
|
|
21
|
+
|
|
22
|
+
if (action === 'present') {
|
|
23
|
+
if (!content) return 'Error: content is required for present action.'
|
|
24
|
+
;(session as Record<string, unknown>).canvasContent = content
|
|
25
|
+
session.lastActiveAt = Date.now()
|
|
26
|
+
sessions[sessionId] = session
|
|
27
|
+
saveSessions(sessions)
|
|
28
|
+
notify(`canvas:${sessionId}`)
|
|
29
|
+
return JSON.stringify({ ok: true, action: 'present', contentLength: content.length })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (action === 'hide') {
|
|
33
|
+
;(session as Record<string, unknown>).canvasContent = null
|
|
34
|
+
session.lastActiveAt = Date.now()
|
|
35
|
+
sessions[sessionId] = session
|
|
36
|
+
saveSessions(sessions)
|
|
37
|
+
notify(`canvas:${sessionId}`)
|
|
38
|
+
return JSON.stringify({ ok: true, action: 'hide' })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (action === 'snapshot') {
|
|
42
|
+
const current = (session as Record<string, unknown>).canvasContent
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
ok: true,
|
|
45
|
+
action: 'snapshot',
|
|
46
|
+
hasContent: !!current,
|
|
47
|
+
contentLength: typeof current === 'string' ? current.length : 0,
|
|
48
|
+
preview: typeof current === 'string' ? current.slice(0, 500) : null,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `Unknown canvas action "${action}". Valid: present, hide, snapshot`
|
|
53
|
+
} catch (err: unknown) {
|
|
54
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'canvas',
|
|
59
|
+
description: 'Present live HTML/CSS/JS content to the user in an interactive canvas panel. Use "present" to show content, "hide" to dismiss, "snapshot" to check current state. The canvas renders in a sandboxed iframe alongside the chat.',
|
|
60
|
+
schema: z.object({
|
|
61
|
+
action: z.enum(['present', 'hide', 'snapshot']).describe('Canvas action to perform'),
|
|
62
|
+
content: z.string().optional().describe('HTML content to render (required for "present"). Can include inline CSS and JS.'),
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
]
|
|
67
|
+
}
|