@swarmclawai/swarmclaw 0.5.3 → 0.6.2
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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- 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/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- 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/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- 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-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -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 +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- 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 +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- 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/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- 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/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- 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 +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -111,3 +111,128 @@ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState
|
|
|
111
111
|
return out
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Lightweight provider ping functions (extracted from check-provider/route.ts)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const PING_TIMEOUT_MS = 8_000
|
|
119
|
+
|
|
120
|
+
async function parseErrorMessage(res: Response, fallback: string): Promise<string> {
|
|
121
|
+
const text = await res.text().catch(() => '')
|
|
122
|
+
if (!text) return fallback
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(text)
|
|
125
|
+
if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
|
|
126
|
+
if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
|
|
127
|
+
if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
|
|
128
|
+
if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
|
|
129
|
+
} catch { /* non-JSON */ }
|
|
130
|
+
return text.slice(0, 300).trim() || fallback
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultEndpoint: string }> = {
|
|
134
|
+
openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
|
|
135
|
+
google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
136
|
+
deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
|
|
137
|
+
groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
|
|
138
|
+
together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
|
|
139
|
+
mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
|
|
140
|
+
xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
|
|
141
|
+
fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function pingOpenAiCompatible(
|
|
145
|
+
apiKey: string,
|
|
146
|
+
endpoint: string,
|
|
147
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
148
|
+
const normalizedEndpoint = endpoint.replace(/\/+$/, '')
|
|
149
|
+
const res = await fetch(`${normalizedEndpoint}/models`, {
|
|
150
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
151
|
+
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
152
|
+
cache: 'no-store',
|
|
153
|
+
})
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
const detail = await parseErrorMessage(res, `Provider returned ${res.status}.`)
|
|
156
|
+
return { ok: false, message: detail }
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, message: 'Connected.' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function pingAnthropic(apiKey: string): Promise<{ ok: boolean; message: string }> {
|
|
162
|
+
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
163
|
+
headers: {
|
|
164
|
+
'x-api-key': apiKey,
|
|
165
|
+
'anthropic-version': '2023-06-01',
|
|
166
|
+
},
|
|
167
|
+
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
168
|
+
cache: 'no-store',
|
|
169
|
+
})
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
|
|
172
|
+
return { ok: false, message: detail }
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, message: 'Connected to Anthropic.' }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function pingOllama(endpoint: string): Promise<{ ok: boolean; message: string }> {
|
|
178
|
+
const normalizedEndpoint = (endpoint || 'http://localhost:11434').replace(/\/+$/, '')
|
|
179
|
+
const res = await fetch(`${normalizedEndpoint}/api/tags`, {
|
|
180
|
+
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
181
|
+
cache: 'no-store',
|
|
182
|
+
})
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const detail = await parseErrorMessage(res, `Ollama returned ${res.status}.`)
|
|
185
|
+
return { ok: false, message: detail }
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, message: 'Connected to Ollama.' }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function pingOpenClaw(
|
|
191
|
+
apiKey: string | undefined,
|
|
192
|
+
endpoint: string,
|
|
193
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
194
|
+
const { wsConnect } = await import('@/lib/providers/openclaw')
|
|
195
|
+
let url = (endpoint || 'http://localhost:18789').replace(/\/+$/, '')
|
|
196
|
+
if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
|
|
197
|
+
const wsUrl = url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
|
|
198
|
+
const result = await wsConnect(wsUrl, apiKey || undefined, true, PING_TIMEOUT_MS)
|
|
199
|
+
if (result.ws) try { result.ws.close() } catch { /* ignore */ }
|
|
200
|
+
return { ok: result.ok, message: result.ok ? 'Connected to OpenClaw.' : result.message }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ping a provider to check reachability. Returns `{ ok, message }`.
|
|
205
|
+
* Skips CLI-based providers (claude-cli, codex-cli, opencode-cli) — returns ok.
|
|
206
|
+
*/
|
|
207
|
+
export async function pingProvider(
|
|
208
|
+
provider: string,
|
|
209
|
+
apiKey: string | undefined,
|
|
210
|
+
endpoint: string | undefined,
|
|
211
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
212
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli']
|
|
213
|
+
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
if (provider === 'anthropic') {
|
|
217
|
+
if (!apiKey) return { ok: false, message: 'No API key configured.' }
|
|
218
|
+
return await pingAnthropic(apiKey)
|
|
219
|
+
}
|
|
220
|
+
if (provider === 'ollama') {
|
|
221
|
+
return await pingOllama(endpoint || 'http://localhost:11434')
|
|
222
|
+
}
|
|
223
|
+
if (provider === 'openclaw') {
|
|
224
|
+
return await pingOpenClaw(apiKey, endpoint || 'http://localhost:18789')
|
|
225
|
+
}
|
|
226
|
+
// OpenAI-compatible providers (openai, google, deepseek, groq, together, mistral, xai, fireworks, custom)
|
|
227
|
+
const defaults = OPENAI_COMPATIBLE_DEFAULTS[provider]
|
|
228
|
+
const resolvedEndpoint = endpoint || defaults?.defaultEndpoint
|
|
229
|
+
if (!resolvedEndpoint) return { ok: false, message: `No endpoint for provider "${provider}".` }
|
|
230
|
+
if (!apiKey) return { ok: false, message: 'No API key configured.' }
|
|
231
|
+
return await pingOpenAiCompatible(apiKey, resolvedEndpoint)
|
|
232
|
+
} catch (err: unknown) {
|
|
233
|
+
const msg = err instanceof Error && err.name === 'TimeoutError'
|
|
234
|
+
? 'Connection timed out.'
|
|
235
|
+
: (err instanceof Error ? err.message : String(err))
|
|
236
|
+
return { ok: false, message: msg }
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -12,13 +12,14 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
|
12
12
|
import { isProtectedMainSession } from './main-session'
|
|
13
13
|
import type { Agent, BoardTask, Message } from '@/types'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// 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, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
|
|
16
17
|
|
|
17
18
|
interface SessionMessageLike {
|
|
18
19
|
role?: string
|
|
19
20
|
text?: string
|
|
20
21
|
time?: number
|
|
21
|
-
kind?: 'chat' | 'heartbeat' | 'system'
|
|
22
|
+
kind?: 'chat' | 'heartbeat' | 'system' | 'context-clear'
|
|
22
23
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -279,19 +280,44 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
279
280
|
changed = true
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
// 2. If delegated, push to delegating agent's thread
|
|
283
|
+
// 2. If delegated, push to delegating agent's thread AND active chat sessions
|
|
283
284
|
const delegatedBy = (task as unknown as Record<string, unknown>).delegatedByAgentId
|
|
284
285
|
if (typeof delegatedBy === 'string' && delegatedBy !== task.agentId) {
|
|
285
286
|
const delegator = agents[delegatedBy]
|
|
287
|
+
const agentName = agent?.name || task.agentId
|
|
288
|
+
const delegationBody = buildResultBlock(`Delegated task ${statusLabel}: **${taskLink}** (by ${agentName})`)
|
|
289
|
+
|
|
290
|
+
// Push to delegating agent's thread
|
|
286
291
|
if (delegator?.threadSessionId && sessions[delegator.threadSessionId]) {
|
|
287
292
|
const thread = sessions[delegator.threadSessionId]
|
|
288
293
|
if (!Array.isArray(thread.messages)) thread.messages = []
|
|
289
|
-
|
|
290
|
-
const body = buildResultBlock(`Delegated task ${statusLabel}: **${taskLink}** (by ${agentName})`)
|
|
291
|
-
thread.messages.push(buildMsg(body))
|
|
294
|
+
thread.messages.push(buildMsg(delegationBody))
|
|
292
295
|
thread.lastActiveAt = now
|
|
293
296
|
changed = true
|
|
294
297
|
}
|
|
298
|
+
|
|
299
|
+
// Push to delegating agent's active user-facing chat sessions
|
|
300
|
+
// so the result is visible in the chat the user is looking at
|
|
301
|
+
if (delegator) {
|
|
302
|
+
for (const session of Object.values(sessions)) {
|
|
303
|
+
if (!session || session.agentId !== delegatedBy) continue
|
|
304
|
+
// Skip thread sessions and orchestrated/subagent sessions
|
|
305
|
+
if (session.id === delegator.threadSessionId) continue
|
|
306
|
+
if (session.sessionType === 'orchestrated') continue
|
|
307
|
+
// Only push to recently-active sessions (within last 30 minutes)
|
|
308
|
+
const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
|
|
309
|
+
if (now - lastActive > 30 * 60_000) continue
|
|
310
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
311
|
+
// Avoid duplicate push
|
|
312
|
+
const lastMsg = session.messages.at(-1)
|
|
313
|
+
if (lastMsg?.text === delegationBody && typeof lastMsg?.time === 'number' && now - lastMsg.time < 30_000) continue
|
|
314
|
+
session.messages.push(buildMsg(delegationBody))
|
|
315
|
+
session.lastActiveAt = now
|
|
316
|
+
changed = true
|
|
317
|
+
// Notify the specific session's message topic for real-time UI update
|
|
318
|
+
notify(`messages:${session.id}`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
295
321
|
}
|
|
296
322
|
|
|
297
323
|
if (changed) saveSessions(sessions)
|
|
@@ -330,6 +356,10 @@ export function enqueueTask(taskId: string) {
|
|
|
330
356
|
text: `Task queued: "${task.title}" (${task.id})`,
|
|
331
357
|
})
|
|
332
358
|
|
|
359
|
+
// If processNext is already running, mark a pending kick so it re-enters after finishing
|
|
360
|
+
if (_queueState.processing) {
|
|
361
|
+
_queueState.pendingKick = true
|
|
362
|
+
}
|
|
333
363
|
// Delay before kicking worker so UI shows the queued state
|
|
334
364
|
setTimeout(() => processNext(), 2000)
|
|
335
365
|
}
|
|
@@ -467,8 +497,8 @@ function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTas
|
|
|
467
497
|
}
|
|
468
498
|
|
|
469
499
|
export async function processNext() {
|
|
470
|
-
if (processing) return
|
|
471
|
-
processing = true
|
|
500
|
+
if (_queueState.processing) return
|
|
501
|
+
_queueState.processing = true
|
|
472
502
|
|
|
473
503
|
try {
|
|
474
504
|
// Recover orphaned tasks: status is 'queued' but missing from the queue array
|
|
@@ -618,10 +648,21 @@ export async function processNext() {
|
|
|
618
648
|
// Save initial assistant message so user sees context when opening the session
|
|
619
649
|
const sessions = loadSessions()
|
|
620
650
|
if (sessions[sessionId]) {
|
|
651
|
+
const isDelegation = (task as unknown as Record<string, unknown>).sourceType === 'delegation'
|
|
652
|
+
let initialText: string
|
|
653
|
+
if (isDelegation) {
|
|
654
|
+
const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
|
|
655
|
+
const delegator = delegatorId ? agents[delegatorId] : null
|
|
656
|
+
const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
|
|
657
|
+
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.`
|
|
658
|
+
} else {
|
|
659
|
+
initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`\n\nI'll begin working on this now.`
|
|
660
|
+
}
|
|
621
661
|
sessions[sessionId].messages.push({
|
|
622
662
|
role: 'assistant',
|
|
623
|
-
text:
|
|
663
|
+
text: initialText,
|
|
624
664
|
time: Date.now(),
|
|
665
|
+
...(isDelegation ? { kind: 'system' as const } : {}),
|
|
625
666
|
})
|
|
626
667
|
saveSessions(sessions)
|
|
627
668
|
}
|
|
@@ -805,7 +846,12 @@ export async function processNext() {
|
|
|
805
846
|
}
|
|
806
847
|
}
|
|
807
848
|
} finally {
|
|
808
|
-
processing = false
|
|
849
|
+
_queueState.processing = false
|
|
850
|
+
// If tasks were enqueued while we were processing, kick another round
|
|
851
|
+
if (_queueState.pendingKick) {
|
|
852
|
+
_queueState.pendingKick = false
|
|
853
|
+
setTimeout(() => processNext(), 500)
|
|
854
|
+
}
|
|
809
855
|
}
|
|
810
856
|
}
|
|
811
857
|
|
|
@@ -4,6 +4,8 @@ import { enqueueTask } from './queue'
|
|
|
4
4
|
import { CronExpressionParser } from 'cron-parser'
|
|
5
5
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
6
6
|
import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
|
|
7
|
+
import { enqueueSystemEvent } from './system-events'
|
|
8
|
+
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
7
9
|
|
|
8
10
|
const TICK_INTERVAL = 60_000 // 60 seconds
|
|
9
11
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
@@ -192,5 +194,11 @@ async function tick() {
|
|
|
192
194
|
type: 'schedule_fired',
|
|
193
195
|
text: `Schedule fired: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
|
|
194
196
|
})
|
|
197
|
+
|
|
198
|
+
// Enqueue system event + heartbeat wake for the schedule's agent
|
|
199
|
+
if (schedule.createdInSessionId) {
|
|
200
|
+
enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
|
|
201
|
+
}
|
|
202
|
+
requestHeartbeatNow({ agentId: schedule.agentId, reason: 'schedule' })
|
|
195
203
|
}
|
|
196
204
|
}
|
|
@@ -37,6 +37,7 @@ interface QueueEntry {
|
|
|
37
37
|
maxRuntimeMs?: number
|
|
38
38
|
modelOverride?: string
|
|
39
39
|
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
40
|
+
replyToId?: string
|
|
40
41
|
resolve: (value: ExecuteChatTurnResult) => void
|
|
41
42
|
reject: (error: Error) => void
|
|
42
43
|
promise: Promise<ExecuteChatTurnResult>
|
|
@@ -245,6 +246,7 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
245
246
|
onEvent: (event) => emitToSubscribers(next, event),
|
|
246
247
|
modelOverride: next.modelOverride,
|
|
247
248
|
heartbeatConfig: next.heartbeatConfig,
|
|
249
|
+
replyToId: next.replyToId,
|
|
248
250
|
})
|
|
249
251
|
|
|
250
252
|
const failed = !!result.error
|
|
@@ -344,6 +346,7 @@ export interface EnqueueSessionRunInput {
|
|
|
344
346
|
maxRuntimeMs?: number
|
|
345
347
|
modelOverride?: string
|
|
346
348
|
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
349
|
+
replyToId?: string
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
export interface EnqueueSessionRunResult {
|
|
@@ -454,6 +457,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
454
457
|
maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|
|
455
458
|
modelOverride: input.modelOverride,
|
|
456
459
|
heartbeatConfig: input.heartbeatConfig,
|
|
460
|
+
replyToId: input.replyToId,
|
|
457
461
|
resolve,
|
|
458
462
|
reject,
|
|
459
463
|
promise,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '../storage'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
|
+
import { notify } from '../ws-hub'
|
|
6
|
+
import type { ToolBuildContext } from './context'
|
|
7
|
+
import type { Chatroom } from '@/types'
|
|
8
|
+
|
|
9
|
+
export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
10
|
+
const tools: StructuredToolInterface[] = []
|
|
11
|
+
const { hasTool } = bctx
|
|
12
|
+
|
|
13
|
+
if (hasTool('manage_chatrooms')) {
|
|
14
|
+
tools.push(
|
|
15
|
+
tool(
|
|
16
|
+
async ({ action, chatroomId, name, description, agentIds, agentId, message }) => {
|
|
17
|
+
try {
|
|
18
|
+
const chatrooms = loadChatrooms() as Record<string, Chatroom>
|
|
19
|
+
|
|
20
|
+
if (action === 'list_chatrooms') {
|
|
21
|
+
const list = Object.values(chatrooms).map((cr) => ({
|
|
22
|
+
id: cr.id,
|
|
23
|
+
name: cr.name,
|
|
24
|
+
description: cr.description,
|
|
25
|
+
memberCount: cr.agentIds.length,
|
|
26
|
+
messageCount: cr.messages.length,
|
|
27
|
+
}))
|
|
28
|
+
return JSON.stringify(list)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (action === 'create_chatroom') {
|
|
32
|
+
const id = genId()
|
|
33
|
+
const agents = loadAgents()
|
|
34
|
+
const validAgentIds = (agentIds || []).filter((aid: string) => agents[aid])
|
|
35
|
+
const chatroom: Chatroom = {
|
|
36
|
+
id,
|
|
37
|
+
name: name || 'New Chatroom',
|
|
38
|
+
description: description || '',
|
|
39
|
+
agentIds: validAgentIds,
|
|
40
|
+
messages: [],
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
updatedAt: Date.now(),
|
|
43
|
+
}
|
|
44
|
+
chatrooms[id] = chatroom
|
|
45
|
+
saveChatrooms(chatrooms)
|
|
46
|
+
notify('chatrooms')
|
|
47
|
+
return JSON.stringify({ ok: true, chatroom: { id, name: chatroom.name, agentIds: validAgentIds } })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!chatroomId) return 'Error: chatroomId is required for this action.'
|
|
51
|
+
const chatroom = chatrooms[chatroomId]
|
|
52
|
+
if (!chatroom) return `Error: chatroom not found: ${chatroomId}`
|
|
53
|
+
|
|
54
|
+
if (action === 'add_agent') {
|
|
55
|
+
if (!agentId) return 'Error: agentId is required.'
|
|
56
|
+
const agents = loadAgents()
|
|
57
|
+
if (!agents[agentId]) return `Error: agent not found: ${agentId}`
|
|
58
|
+
if (!chatroom.agentIds.includes(agentId)) {
|
|
59
|
+
chatroom.agentIds.push(agentId)
|
|
60
|
+
chatroom.updatedAt = Date.now()
|
|
61
|
+
chatrooms[chatroomId] = chatroom
|
|
62
|
+
saveChatrooms(chatrooms)
|
|
63
|
+
notify('chatrooms')
|
|
64
|
+
notify(`chatroom:${chatroomId}`)
|
|
65
|
+
}
|
|
66
|
+
return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (action === 'remove_agent') {
|
|
70
|
+
if (!agentId) return 'Error: agentId is required.'
|
|
71
|
+
chatroom.agentIds = chatroom.agentIds.filter((id: string) => id !== agentId)
|
|
72
|
+
chatroom.updatedAt = Date.now()
|
|
73
|
+
chatrooms[chatroomId] = chatroom
|
|
74
|
+
saveChatrooms(chatrooms)
|
|
75
|
+
notify('chatrooms')
|
|
76
|
+
notify(`chatroom:${chatroomId}`)
|
|
77
|
+
return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === 'list_members') {
|
|
81
|
+
const agents = loadAgents()
|
|
82
|
+
const members = chatroom.agentIds.map((id: string) => {
|
|
83
|
+
const agent = agents[id]
|
|
84
|
+
return agent ? { id, name: agent.name, description: agent.description } : { id, name: 'Unknown' }
|
|
85
|
+
})
|
|
86
|
+
return JSON.stringify(members)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (action === 'send_message') {
|
|
90
|
+
if (!message) return 'Error: message is required.'
|
|
91
|
+
const msgId = genId()
|
|
92
|
+
const senderName = bctx.ctx?.agentId
|
|
93
|
+
? (loadAgents()[bctx.ctx.agentId]?.name || 'Agent')
|
|
94
|
+
: 'Agent'
|
|
95
|
+
chatroom.messages.push({
|
|
96
|
+
id: msgId,
|
|
97
|
+
senderId: bctx.ctx?.agentId || 'agent',
|
|
98
|
+
senderName,
|
|
99
|
+
role: 'assistant' as const,
|
|
100
|
+
text: message,
|
|
101
|
+
mentions: [],
|
|
102
|
+
reactions: [],
|
|
103
|
+
time: Date.now(),
|
|
104
|
+
})
|
|
105
|
+
chatroom.updatedAt = Date.now()
|
|
106
|
+
chatrooms[chatroomId] = chatroom
|
|
107
|
+
saveChatrooms(chatrooms)
|
|
108
|
+
notify(`chatroom:${chatroomId}`)
|
|
109
|
+
return JSON.stringify({ ok: true, messageId: msgId })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `Error: unknown action "${action}". Valid actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message`
|
|
113
|
+
} catch (err: unknown) {
|
|
114
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'manage_chatrooms',
|
|
119
|
+
description: 'Manage chatrooms for multi-agent collaboration. Actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message.',
|
|
120
|
+
schema: z.object({
|
|
121
|
+
action: z.enum(['list_chatrooms', 'create_chatroom', 'add_agent', 'remove_agent', 'list_members', 'send_message'])
|
|
122
|
+
.describe('The action to perform'),
|
|
123
|
+
chatroomId: z.string().optional().describe('Chatroom ID (required for most actions except list/create)'),
|
|
124
|
+
name: z.string().optional().describe('Chatroom name (for create_chatroom)'),
|
|
125
|
+
description: z.string().optional().describe('Chatroom description (for create_chatroom)'),
|
|
126
|
+
agentIds: z.array(z.string()).optional().describe('Initial agent IDs (for create_chatroom)'),
|
|
127
|
+
agentId: z.string().optional().describe('Agent ID (for add_agent/remove_agent)'),
|
|
128
|
+
message: z.string().optional().describe('Message text (for send_message)'),
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return tools
|
|
136
|
+
}
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
|
|
4
6
|
import type { ToolBuildContext } from './context'
|
|
5
7
|
|
|
8
|
+
/** Resolve /api/uploads/filename URLs to actual disk paths */
|
|
9
|
+
function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeType?: string } | null {
|
|
10
|
+
if (!url) return null
|
|
11
|
+
const match = url.match(/^\/api\/uploads\/([^?#]+)/)
|
|
12
|
+
if (!match) return null
|
|
13
|
+
const safeName = match[1].replace(/[^a-zA-Z0-9._-]/g, '')
|
|
14
|
+
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
15
|
+
if (!fs.existsSync(filePath)) return null
|
|
16
|
+
return { mediaPath: filePath }
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
7
20
|
const tools: StructuredToolInterface[] = []
|
|
8
21
|
const { ctx, hasTool } = bctx
|
|
@@ -32,6 +45,29 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
32
45
|
return JSON.stringify(running)
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
if (action === 'start') {
|
|
49
|
+
if (!connectorId) {
|
|
50
|
+
// If no ID given, list available connectors to start
|
|
51
|
+
const allConnectors = loadConnectors()
|
|
52
|
+
const stopped = Object.values(allConnectors)
|
|
53
|
+
.filter((c) => !platform || c.platform === platform)
|
|
54
|
+
.filter((c) => !running.find((r) => r.id === c.id))
|
|
55
|
+
.map((c) => ({ id: c.id, name: c.name, platform: c.platform }))
|
|
56
|
+
if (!stopped.length) return 'All connectors are already running.'
|
|
57
|
+
return `Error: connectorId is required. Stopped connectors available to start: ${JSON.stringify(stopped)}`
|
|
58
|
+
}
|
|
59
|
+
const { startConnector: doStart } = await import('../connectors/manager')
|
|
60
|
+
await doStart(connectorId)
|
|
61
|
+
return JSON.stringify({ status: 'started', connectorId })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (action === 'stop') {
|
|
65
|
+
if (!connectorId) return 'Error: connectorId is required for stop action.'
|
|
66
|
+
const { stopConnector: doStop } = await import('../connectors/manager')
|
|
67
|
+
await doStop(connectorId)
|
|
68
|
+
return JSON.stringify({ status: 'stopped', connectorId })
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
if (action === 'send') {
|
|
36
72
|
const settings = loadSettings()
|
|
37
73
|
if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
|
|
@@ -41,7 +77,15 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
41
77
|
const hasMedia = !!imageUrl?.trim() || !!fileUrl?.trim()
|
|
42
78
|
if (!hasText && !hasMedia) return 'Error: message or media URL is required for send action.'
|
|
43
79
|
if (!running.length) {
|
|
44
|
-
|
|
80
|
+
// Check for configured-but-not-running connectors to give actionable feedback
|
|
81
|
+
const allConnectors = loadConnectors()
|
|
82
|
+
const configured = Object.values(allConnectors)
|
|
83
|
+
.filter((c) => !platform || c.platform === platform)
|
|
84
|
+
.map((c) => ({ id: c.id, name: c.name, platform: c.platform, agentId: c.agentId || null }))
|
|
85
|
+
if (configured.length) {
|
|
86
|
+
return `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}, but ${configured.length} configured connector(s) found: ${JSON.stringify(configured)}. These connectors exist but are not currently started. Ask the user if they'd like you to start one (use action "start" with the connectorId), then retry the send.`
|
|
87
|
+
}
|
|
88
|
+
return `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}. No connectors are configured for this platform either — the user needs to set one up in the Connectors panel first.`
|
|
45
89
|
}
|
|
46
90
|
|
|
47
91
|
const selected = connectorId
|
|
@@ -75,19 +119,49 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
75
119
|
if (allowed.length) channelId = allowed[0]
|
|
76
120
|
}
|
|
77
121
|
if (!channelId) {
|
|
78
|
-
|
|
122
|
+
// Collect any known numbers/targets from config to help the agent suggest them
|
|
123
|
+
const knownTargets: string[] = []
|
|
124
|
+
const jids = connector.config?.allowedJids?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
|
|
125
|
+
const from = connector.config?.allowFrom?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
|
|
126
|
+
const outJid = connector.config?.outboundJid?.trim()
|
|
127
|
+
const outTarget = connector.config?.outboundTarget?.trim()
|
|
128
|
+
if (outJid) knownTargets.push(outJid)
|
|
129
|
+
if (outTarget) knownTargets.push(outTarget)
|
|
130
|
+
knownTargets.push(...jids, ...from)
|
|
131
|
+
const unique = [...new Set(knownTargets)]
|
|
132
|
+
if (unique.length) {
|
|
133
|
+
return `Error: no default outbound target is set, but the connector has ${unique.length} configured number(s)/target(s): ${JSON.stringify(unique)}. Ask the user which one to send to, then re-call with the "to" parameter set to their choice.`
|
|
134
|
+
}
|
|
135
|
+
return `Error: no target recipient configured and no known contacts on this connector. Ask the user for the recipient number/ID, then re-call with the "to" parameter. They can also configure "allowedJids" or "outboundJid" in the connector settings.`
|
|
79
136
|
}
|
|
80
137
|
if (connector.platform === 'whatsapp') {
|
|
81
138
|
channelId = normalizeWhatsAppTarget(channelId)
|
|
82
139
|
}
|
|
83
140
|
|
|
141
|
+
// Resolve /api/uploads/ URLs to actual disk paths so connectors can read the files
|
|
142
|
+
let resolvedMediaPath = mediaPath?.trim() || undefined
|
|
143
|
+
let resolvedImageUrl = imageUrl?.trim() || undefined
|
|
144
|
+
let resolvedFileUrl = fileUrl?.trim() || undefined
|
|
145
|
+
if (!resolvedMediaPath) {
|
|
146
|
+
const fromImage = resolveUploadUrl(resolvedImageUrl)
|
|
147
|
+
if (fromImage) {
|
|
148
|
+
resolvedMediaPath = fromImage.mediaPath
|
|
149
|
+
resolvedImageUrl = undefined
|
|
150
|
+
}
|
|
151
|
+
const fromFile = resolveUploadUrl(resolvedFileUrl)
|
|
152
|
+
if (fromFile) {
|
|
153
|
+
resolvedMediaPath = fromFile.mediaPath
|
|
154
|
+
resolvedFileUrl = undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
84
158
|
const sent = await sendConnectorMessage({
|
|
85
159
|
connectorId: selected.id,
|
|
86
160
|
channelId,
|
|
87
161
|
text: message?.trim() || '',
|
|
88
|
-
imageUrl:
|
|
89
|
-
fileUrl:
|
|
90
|
-
mediaPath:
|
|
162
|
+
imageUrl: resolvedImageUrl,
|
|
163
|
+
fileUrl: resolvedFileUrl,
|
|
164
|
+
mediaPath: resolvedMediaPath,
|
|
91
165
|
mimeType: mimeType?.trim() || undefined,
|
|
92
166
|
fileName: fileName?.trim() || undefined,
|
|
93
167
|
caption: caption?.trim() || undefined,
|
|
@@ -140,16 +214,16 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
140
214
|
}
|
|
141
215
|
}
|
|
142
216
|
|
|
143
|
-
return 'Unknown action. Use list_running, list_targets, or send.'
|
|
217
|
+
return 'Unknown action. Use list_running, list_targets, start, stop, or send.'
|
|
144
218
|
} catch (err: unknown) {
|
|
145
219
|
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
146
220
|
}
|
|
147
221
|
},
|
|
148
222
|
{
|
|
149
223
|
name: 'connector_message_tool',
|
|
150
|
-
description: '
|
|
224
|
+
description: 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, Discord, etc.). Use "start"/"stop" to manage connector lifecycle, "list_running"/"list_targets" to discover available connectors and recipients, "send" to deliver messages, and rich actions (react, edit, delete, pin) for message management. When a send fails because no connector is running, check if one is configured and offer to start it. When no target is set, list available configured numbers and ask the user which to send to.',
|
|
151
225
|
schema: z.object({
|
|
152
|
-
action: z.enum(['list_running', 'list_targets', 'send', 'message_react', 'message_edit', 'message_delete', 'message_pin']).describe('connector messaging action'),
|
|
226
|
+
action: z.enum(['list_running', 'list_targets', 'start', 'stop', 'send', 'message_react', 'message_edit', 'message_delete', 'message_pin']).describe('connector messaging action'),
|
|
153
227
|
connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
|
|
154
228
|
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord, bluebubbles, etc.).'),
|
|
155
229
|
to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
|