@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
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
} from '../storage'
|
|
21
21
|
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
22
22
|
import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
|
|
23
|
+
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
24
|
+
import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
|
|
23
25
|
import type { ToolBuildContext } from './context'
|
|
24
26
|
import { safePath, findBinaryOnPath } from './context'
|
|
25
27
|
|
|
@@ -115,6 +117,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
|
|
|
115
117
|
queuedAt: null,
|
|
116
118
|
startedAt: null,
|
|
117
119
|
completedAt: null,
|
|
120
|
+
priority: ['low', 'medium', 'high', 'critical'].includes(p.priority) ? p.priority : undefined,
|
|
118
121
|
...p,
|
|
119
122
|
}),
|
|
120
123
|
manage_schedules: (p) => {
|
|
@@ -342,6 +345,24 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
342
345
|
})
|
|
343
346
|
}
|
|
344
347
|
}
|
|
348
|
+
// @mention agent resolution for tasks
|
|
349
|
+
if (toolKey === 'manage_tasks' && parsed.description) {
|
|
350
|
+
const agents = loadAgents()
|
|
351
|
+
parsed.agentId = resolveTaskAgentFromDescription(
|
|
352
|
+
parsed.description,
|
|
353
|
+
parsed.agentId || ctx?.agentId || '',
|
|
354
|
+
agents,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
// Task dedup
|
|
358
|
+
if (toolKey === 'manage_tasks') {
|
|
359
|
+
const fp = computeTaskFingerprint(parsed.title || 'Untitled Task', parsed.agentId || ctx?.agentId || '')
|
|
360
|
+
parsed.fingerprint = fp
|
|
361
|
+
const dupe = findDuplicateTask(all as Record<string, import('@/types').BoardTask>, { fingerprint: fp })
|
|
362
|
+
if (dupe) {
|
|
363
|
+
return JSON.stringify({ ...dupe, deduplicated: true })
|
|
364
|
+
}
|
|
365
|
+
}
|
|
345
366
|
const newId = genId()
|
|
346
367
|
const entry = {
|
|
347
368
|
id: newId,
|
|
@@ -634,6 +634,69 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
+
// check_delegation_status: lets agents check on tasks they delegated
|
|
638
|
+
if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
|
|
639
|
+
tools.push(
|
|
640
|
+
tool(
|
|
641
|
+
async ({ taskId }) => {
|
|
642
|
+
try {
|
|
643
|
+
const tasks = loadTasks()
|
|
644
|
+
const task = tasks[taskId] as Record<string, unknown> | undefined
|
|
645
|
+
if (!task) return `Error: Task "${taskId}" not found.`
|
|
646
|
+
|
|
647
|
+
const status = task.status as string || 'unknown'
|
|
648
|
+
const result = typeof task.result === 'string' ? task.result : null
|
|
649
|
+
const error = typeof task.error === 'string' ? task.error : null
|
|
650
|
+
const agentId = task.agentId as string || ''
|
|
651
|
+
const agents = loadAgents()
|
|
652
|
+
const agent = agents[agentId]
|
|
653
|
+
const startedAt = typeof task.startedAt === 'number' ? task.startedAt : null
|
|
654
|
+
const completedAt = typeof task.completedAt === 'number' ? task.completedAt : null
|
|
655
|
+
|
|
656
|
+
const info: Record<string, unknown> = {
|
|
657
|
+
taskId,
|
|
658
|
+
status,
|
|
659
|
+
agentId,
|
|
660
|
+
agentName: agent?.name || agentId,
|
|
661
|
+
agentAvatarSeed: agent?.avatarSeed || null,
|
|
662
|
+
title: task.title || '',
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (startedAt) info.startedAt = new Date(startedAt).toISOString()
|
|
666
|
+
if (completedAt) info.completedAt = new Date(completedAt).toISOString()
|
|
667
|
+
if (startedAt && !completedAt && status === 'running') {
|
|
668
|
+
info.runningForSeconds = Math.round((Date.now() - startedAt) / 1000)
|
|
669
|
+
}
|
|
670
|
+
if (result) info.result = result.slice(0, 4000)
|
|
671
|
+
if (error) info.error = error.slice(0, 1000)
|
|
672
|
+
|
|
673
|
+
// Include latest comments for context
|
|
674
|
+
const comments = Array.isArray(task.comments) ? task.comments as Array<{ text: string; author: string; createdAt: number }> : []
|
|
675
|
+
if (comments.length > 0) {
|
|
676
|
+
const latest = comments.slice(-3).map((c) => ({
|
|
677
|
+
author: c.author,
|
|
678
|
+
text: (c.text || '').slice(0, 500),
|
|
679
|
+
time: new Date(c.createdAt).toISOString(),
|
|
680
|
+
}))
|
|
681
|
+
info.latestComments = latest
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return JSON.stringify(info)
|
|
685
|
+
} catch (err: unknown) {
|
|
686
|
+
return `Error checking task: ${err instanceof Error ? err.message : String(err)}`
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: 'check_delegation_status',
|
|
691
|
+
description: 'Check the status and result of a delegated task. Use this after delegate_to_agent to monitor progress. Returns status (todo/queued/running/completed/failed), result if completed, and latest comments.',
|
|
692
|
+
schema: z.object({
|
|
693
|
+
taskId: z.string().describe('The task ID returned by delegate_to_agent'),
|
|
694
|
+
}),
|
|
695
|
+
},
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
|
|
637
700
|
// delegate_to_agent: requires "Assign to Other Agents" (platformAssignScope: 'all')
|
|
638
701
|
if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
|
|
639
702
|
tools.push(
|
|
@@ -698,9 +761,10 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
698
761
|
taskId,
|
|
699
762
|
agentId: resolvedId,
|
|
700
763
|
agentName: target.name,
|
|
764
|
+
agentAvatarSeed: target.avatarSeed || null,
|
|
701
765
|
message: startImmediately
|
|
702
|
-
? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}.`
|
|
703
|
-
: `Task delegated to ${target.name}. Task ID: ${taskId}. Status: todo
|
|
766
|
+
? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}. Use check_delegation_status to monitor progress.`
|
|
767
|
+
: `Task delegated to ${target.name}. Task ID: ${taskId}. Status: todo (not auto-started). Use delegate_to_agent with startImmediately: true to queue it.`,
|
|
704
768
|
})
|
|
705
769
|
} catch (err: unknown) {
|
|
706
770
|
return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -708,12 +772,12 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
708
772
|
},
|
|
709
773
|
{
|
|
710
774
|
name: 'delegate_to_agent',
|
|
711
|
-
description: 'Delegate a task to another agent. Creates a task on the task board
|
|
775
|
+
description: 'Delegate a task to another agent. Creates a task on the task board and queues it for immediate execution by default. Set startImmediately=false if you want the task to go to "todo" status instead.',
|
|
712
776
|
schema: z.object({
|
|
713
777
|
agentId: z.string().describe('ID or name of the target agent to delegate to'),
|
|
714
778
|
task: z.string().describe('What the target agent should do'),
|
|
715
779
|
description: z.string().optional().describe('Optional longer description of the task'),
|
|
716
|
-
startImmediately: z.boolean().optional().default(
|
|
780
|
+
startImmediately: z.boolean().optional().default(true).describe('If true (default), queue the task for immediate execution. Set false to put in todo for manual start.'),
|
|
717
781
|
}),
|
|
718
782
|
},
|
|
719
783
|
),
|
|
@@ -6,6 +6,17 @@ import { UPLOAD_DIR } from '../storage'
|
|
|
6
6
|
import type { ToolBuildContext } from './context'
|
|
7
7
|
import { safePath, truncate, listDirRecursive, MAX_OUTPUT, MAX_FILE } from './context'
|
|
8
8
|
|
|
9
|
+
const SEND_FILE_DEDUPE_TTL_MS = 30_000
|
|
10
|
+
const recentSendFileResults = new Map<string, { at: number; output: string; uploadPath: string }>()
|
|
11
|
+
|
|
12
|
+
function pruneRecentSendFileCache(now: number): void {
|
|
13
|
+
for (const [key, entry] of recentSendFileResults.entries()) {
|
|
14
|
+
if (now - entry.at > SEND_FILE_DEDUPE_TTL_MS || !fs.existsSync(entry.uploadPath)) {
|
|
15
|
+
recentSendFileResults.delete(key)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
10
21
|
const tools: StructuredToolInterface[] = []
|
|
11
22
|
|
|
@@ -197,6 +208,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
197
208
|
tool(
|
|
198
209
|
async ({ filePath: rawPath }) => {
|
|
199
210
|
try {
|
|
211
|
+
const now = Date.now()
|
|
212
|
+
pruneRecentSendFileCache(now)
|
|
200
213
|
// Resolve relative to cwd, but also allow absolute paths
|
|
201
214
|
const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
|
|
202
215
|
if (!fs.existsSync(resolved)) return `Error: file not found: ${rawPath}`
|
|
@@ -204,6 +217,13 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
204
217
|
if (stat.isDirectory()) return `Error: cannot send a directory. Send individual files instead.`
|
|
205
218
|
if (stat.size > 100 * 1024 * 1024) return `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 100MB.`
|
|
206
219
|
|
|
220
|
+
const sessionId = bctx.ctx?.sessionId || 'no-session'
|
|
221
|
+
const dedupeKey = `${sessionId}|${resolved}`
|
|
222
|
+
const cached = recentSendFileResults.get(dedupeKey)
|
|
223
|
+
if (cached && now - cached.at <= SEND_FILE_DEDUPE_TTL_MS && fs.existsSync(cached.uploadPath)) {
|
|
224
|
+
return cached.output
|
|
225
|
+
}
|
|
226
|
+
|
|
207
227
|
const ext = path.extname(resolved).slice(1).toLowerCase()
|
|
208
228
|
const basename = path.basename(resolved)
|
|
209
229
|
const filename = `${Date.now()}-${basename}`
|
|
@@ -212,14 +232,13 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
212
232
|
|
|
213
233
|
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']
|
|
214
234
|
const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv']
|
|
235
|
+
const AUDIO_EXTS = ['mp3', 'ogg', 'wav', 'aac', 'm4a', 'opus']
|
|
215
236
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return `[Download ${basename}](/api/uploads/${filename})`
|
|
222
|
-
}
|
|
237
|
+
const output = (IMAGE_EXTS.includes(ext) || VIDEO_EXTS.includes(ext) || AUDIO_EXTS.includes(ext))
|
|
238
|
+
? ``
|
|
239
|
+
: `[Download ${basename}](/api/uploads/${filename})`
|
|
240
|
+
recentSendFileResults.set(dedupeKey, { at: now, output, uploadPath: dest })
|
|
241
|
+
return output
|
|
223
242
|
} catch (err: unknown) {
|
|
224
243
|
return `Error sending file: ${err instanceof Error ? err.message : String(err)}`
|
|
225
244
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { execFile } from 'child_process'
|
|
4
|
+
import { promisify } from 'util'
|
|
5
|
+
import type { ToolBuildContext } from './context'
|
|
6
|
+
import { findBinaryOnPath, safePath, truncate, MAX_OUTPUT } from './context'
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile)
|
|
9
|
+
|
|
10
|
+
const GIT_ACTIONS = [
|
|
11
|
+
'status', 'log', 'diff', 'commit', 'add', 'push', 'pull',
|
|
12
|
+
'branch', 'checkout', 'stash', 'merge', 'clone', 'remote',
|
|
13
|
+
'tag', 'reset', 'show',
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
export function buildGitTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
17
|
+
if (!bctx.hasTool('git')) return []
|
|
18
|
+
|
|
19
|
+
const gitPath = findBinaryOnPath('git')
|
|
20
|
+
if (!gitPath) return []
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
tool(
|
|
24
|
+
async ({ action, args, repoPath, timeoutSec }) => {
|
|
25
|
+
try {
|
|
26
|
+
const cwd = repoPath ? safePath(bctx.cwd, repoPath) : bctx.cwd
|
|
27
|
+
const timeout = Math.max(5, Math.min(timeoutSec ?? 60, 300)) * 1000
|
|
28
|
+
|
|
29
|
+
// Verify we're in a git repo (except for clone)
|
|
30
|
+
if (action !== 'clone') {
|
|
31
|
+
try {
|
|
32
|
+
await execFileAsync(gitPath, ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 })
|
|
33
|
+
} catch {
|
|
34
|
+
return JSON.stringify({ error: `Not a git repository: ${cwd}` })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cmdArgs = [action, ...(args ?? [])]
|
|
39
|
+
const result = await execFileAsync(gitPath, cmdArgs, {
|
|
40
|
+
cwd,
|
|
41
|
+
timeout,
|
|
42
|
+
maxBuffer: MAX_OUTPUT,
|
|
43
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
44
|
+
})
|
|
45
|
+
return JSON.stringify({
|
|
46
|
+
exitCode: 0,
|
|
47
|
+
stdout: truncate(result.stdout ?? '', MAX_OUTPUT),
|
|
48
|
+
stderr: truncate(result.stderr ?? '', MAX_OUTPUT),
|
|
49
|
+
})
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
const execErr = err as { code?: number; stdout?: string; stderr?: string; message?: string }
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
exitCode: execErr.code ?? 1,
|
|
54
|
+
stdout: truncate(execErr.stdout ?? '', MAX_OUTPUT),
|
|
55
|
+
stderr: truncate(execErr.stderr ?? execErr.message ?? String(err), MAX_OUTPUT),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'git',
|
|
61
|
+
description: 'Run git operations. Verify the repo exists before committing or pushing. Use args for subcommand flags (e.g. args: ["-m", "message"] for commit).',
|
|
62
|
+
schema: z.object({
|
|
63
|
+
action: z.enum(GIT_ACTIONS).describe('Git subcommand to run'),
|
|
64
|
+
args: z.array(z.string()).optional().describe('Additional arguments (e.g. ["-m", "fix: typo"], ["--oneline", "-n", "5"])'),
|
|
65
|
+
repoPath: z.string().optional().describe('Relative path to git repo (defaults to working directory)'),
|
|
66
|
+
timeoutSec: z.number().optional().describe('Timeout in seconds (default 60, max 300)'),
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { ToolBuildContext } from './context'
|
|
4
|
+
import { truncate, MAX_OUTPUT } from './context'
|
|
5
|
+
|
|
6
|
+
export function buildHttpTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
7
|
+
if (!bctx.hasTool('http_request')) return []
|
|
8
|
+
|
|
9
|
+
return [
|
|
10
|
+
tool(
|
|
11
|
+
async ({ method, url, headers, body, timeoutSec, followRedirects }) => {
|
|
12
|
+
try {
|
|
13
|
+
const timeout = Math.max(1, Math.min(timeoutSec ?? 30, 120)) * 1000
|
|
14
|
+
const init: RequestInit = {
|
|
15
|
+
method,
|
|
16
|
+
headers: (headers ?? undefined) as Record<string, string> | undefined,
|
|
17
|
+
signal: AbortSignal.timeout(timeout),
|
|
18
|
+
}
|
|
19
|
+
if (body && method !== 'GET' && method !== 'HEAD') {
|
|
20
|
+
init.body = body
|
|
21
|
+
}
|
|
22
|
+
if (followRedirects === false) {
|
|
23
|
+
init.redirect = 'manual'
|
|
24
|
+
}
|
|
25
|
+
const res = await fetch(url, init)
|
|
26
|
+
const resHeaders: Record<string, string> = {}
|
|
27
|
+
for (const key of ['content-type', 'location', 'x-request-id', 'retry-after', 'content-length']) {
|
|
28
|
+
const val = res.headers.get(key)
|
|
29
|
+
if (val) resHeaders[key] = val
|
|
30
|
+
}
|
|
31
|
+
let resBody: string
|
|
32
|
+
const ct = res.headers.get('content-type') ?? ''
|
|
33
|
+
if (ct.includes('image/') || ct.includes('audio/') || ct.includes('video/') || ct.includes('application/octet-stream')) {
|
|
34
|
+
resBody = `[binary content, ${res.headers.get('content-length') ?? 'unknown'} bytes]`
|
|
35
|
+
} else {
|
|
36
|
+
resBody = truncate(await res.text(), MAX_OUTPUT)
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify({ status: res.status, statusText: res.statusText, headers: resHeaders, body: resBody })
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'http_request',
|
|
45
|
+
description: 'Make an HTTP API request. Supports all methods, custom headers, and request bodies. Returns status, headers, and body.',
|
|
46
|
+
schema: z.object({
|
|
47
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).describe('HTTP method'),
|
|
48
|
+
url: z.string().describe('Full URL to request'),
|
|
49
|
+
headers: z.record(z.string(), z.string()).optional().describe('Request headers as key-value pairs'),
|
|
50
|
+
body: z.string().optional().describe('Request body (JSON string, form data, or plain text). Ignored for GET/HEAD.'),
|
|
51
|
+
timeoutSec: z.number().optional().describe('Timeout in seconds (default 30, max 120)'),
|
|
52
|
+
followRedirects: z.boolean().optional().describe('Follow redirects (default true). Set false to inspect redirect responses.'),
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -17,6 +17,10 @@ import { buildContextTools } from './context-mgmt'
|
|
|
17
17
|
import { buildSandboxTools } from './sandbox'
|
|
18
18
|
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
19
19
|
import { buildChatroomTools } from './chatroom'
|
|
20
|
+
import { buildSubagentTools } from './subagent'
|
|
21
|
+
import { buildCanvasTools } from './canvas'
|
|
22
|
+
import { buildHttpTools } from './http'
|
|
23
|
+
import { buildGitTools } from './git'
|
|
20
24
|
|
|
21
25
|
export type { ToolContext, SessionToolsResult }
|
|
22
26
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
@@ -99,6 +103,10 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
99
103
|
...buildSandboxTools(bctx),
|
|
100
104
|
...buildOpenClawNodeTools(bctx),
|
|
101
105
|
...buildChatroomTools(bctx),
|
|
106
|
+
...buildSubagentTools(bctx),
|
|
107
|
+
...buildCanvasTools(bctx),
|
|
108
|
+
...buildHttpTools(bctx),
|
|
109
|
+
...buildGitTools(bctx),
|
|
102
110
|
)
|
|
103
111
|
|
|
104
112
|
// ---------------------------------------------------------------------------
|
|
@@ -60,6 +60,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
60
60
|
|
|
61
61
|
const formatEntry = (m: any) => {
|
|
62
62
|
let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
|
|
63
|
+
if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
|
|
63
64
|
if (m.references?.length) {
|
|
64
65
|
line += `\n refs: ${m.references.map((r: any) => {
|
|
65
66
|
const core = r.path || r.title || r.type
|
|
@@ -260,16 +260,24 @@ export async function getSearchProvider(settings: Partial<AppSettings>): Promise
|
|
|
260
260
|
return new SearXNGProvider(url)
|
|
261
261
|
}
|
|
262
262
|
case 'tavily': {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
let apiKey = settings.tavilyApiKey
|
|
264
|
+
if (!apiKey) {
|
|
265
|
+
const { getSecret } = await import('../storage')
|
|
266
|
+
const secret = await getSecret('tavily')
|
|
267
|
+
apiKey = secret?.value ?? null
|
|
268
|
+
}
|
|
269
|
+
if (!apiKey) throw new Error('Tavily requires an API key. Set one in Settings > Web Search.')
|
|
270
|
+
return new TavilyProvider(apiKey)
|
|
267
271
|
}
|
|
268
272
|
case 'brave': {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
let apiKey = settings.braveApiKey
|
|
274
|
+
if (!apiKey) {
|
|
275
|
+
const { getSecret } = await import('../storage')
|
|
276
|
+
const secret = await getSecret('brave')
|
|
277
|
+
apiKey = secret?.value ?? null
|
|
278
|
+
}
|
|
279
|
+
if (!apiKey) throw new Error('Brave Search requires an API key. Set one in Settings > Web Search.')
|
|
280
|
+
return new BraveProvider(apiKey)
|
|
273
281
|
}
|
|
274
282
|
default:
|
|
275
283
|
return new DuckDuckGoProvider()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
|
+
import { loadAgents, loadSessions, saveSessions } from '../storage'
|
|
5
|
+
import { executeSessionChatTurn } from '../chat-execution'
|
|
6
|
+
import { log } from '../logger'
|
|
7
|
+
import type { ToolBuildContext } from './context'
|
|
8
|
+
|
|
9
|
+
const MAX_RECURSION_DEPTH = 3
|
|
10
|
+
|
|
11
|
+
function getSessionDepth(sessionId: string | undefined): number {
|
|
12
|
+
if (!sessionId) return 0
|
|
13
|
+
const sessions = loadSessions()
|
|
14
|
+
let depth = 0
|
|
15
|
+
let current = sessionId
|
|
16
|
+
while (current && depth < MAX_RECURSION_DEPTH + 1) {
|
|
17
|
+
const session = sessions[current]
|
|
18
|
+
if (!session?.parentSessionId) break
|
|
19
|
+
current = session.parentSessionId
|
|
20
|
+
depth++
|
|
21
|
+
}
|
|
22
|
+
return depth
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
26
|
+
const { ctx, hasTool } = bctx
|
|
27
|
+
if (!hasTool('spawn_subagent')) return []
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
tool(
|
|
31
|
+
async ({ agentId, message, cwd }) => {
|
|
32
|
+
try {
|
|
33
|
+
// Validate agent exists
|
|
34
|
+
const agents = loadAgents()
|
|
35
|
+
const agent = agents[agentId]
|
|
36
|
+
if (!agent) return `Error: Agent "${agentId}" not found. Available agents: ${Object.values(agents).map((a) => `"${a.id}" (${a.name})`).join(', ')}`
|
|
37
|
+
|
|
38
|
+
// Check recursion depth
|
|
39
|
+
const depth = getSessionDepth(ctx?.sessionId ?? undefined)
|
|
40
|
+
if (depth >= MAX_RECURSION_DEPTH) {
|
|
41
|
+
return `Error: Maximum subagent recursion depth (${MAX_RECURSION_DEPTH}) reached. Cannot spawn further subagents.`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create ephemeral session
|
|
45
|
+
const sessionId = genId()
|
|
46
|
+
const now = Date.now()
|
|
47
|
+
const sessions = loadSessions()
|
|
48
|
+
sessions[sessionId] = {
|
|
49
|
+
id: sessionId,
|
|
50
|
+
name: `subagent-${agent.name}-${sessionId.slice(0, 6)}`,
|
|
51
|
+
cwd: cwd || bctx.cwd,
|
|
52
|
+
user: 'agent',
|
|
53
|
+
provider: agent.provider,
|
|
54
|
+
model: agent.model,
|
|
55
|
+
credentialId: agent.credentialId || null,
|
|
56
|
+
fallbackCredentialIds: agent.fallbackCredentialIds || [],
|
|
57
|
+
apiEndpoint: agent.apiEndpoint || null,
|
|
58
|
+
claudeSessionId: null,
|
|
59
|
+
messages: [],
|
|
60
|
+
createdAt: now,
|
|
61
|
+
lastActiveAt: now,
|
|
62
|
+
sessionType: 'orchestrated',
|
|
63
|
+
agentId: agent.id,
|
|
64
|
+
parentSessionId: ctx?.sessionId || null,
|
|
65
|
+
tools: agent.tools || [],
|
|
66
|
+
}
|
|
67
|
+
saveSessions(sessions)
|
|
68
|
+
|
|
69
|
+
log.info('subagent', `Spawning subagent "${agent.name}" (depth=${depth + 1})`, {
|
|
70
|
+
parentSessionId: ctx?.sessionId,
|
|
71
|
+
childSessionId: sessionId,
|
|
72
|
+
agentId,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Execute the chat turn
|
|
76
|
+
const result = await executeSessionChatTurn({
|
|
77
|
+
sessionId,
|
|
78
|
+
message,
|
|
79
|
+
internal: true,
|
|
80
|
+
source: 'subagent',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
agentId,
|
|
85
|
+
agentName: agent.name,
|
|
86
|
+
sessionId,
|
|
87
|
+
response: result.text.slice(0, 8000),
|
|
88
|
+
toolEvents: result.toolEvents?.length || 0,
|
|
89
|
+
error: result.error || null,
|
|
90
|
+
})
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
return `Error spawning subagent: ${err instanceof Error ? err.message : String(err)}`
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'spawn_subagent',
|
|
97
|
+
description: `Delegate a task to another agent. The subagent runs independently and returns its response. Use this to leverage specialized agents for subtasks. Max recursion depth: ${MAX_RECURSION_DEPTH}.`,
|
|
98
|
+
schema: z.object({
|
|
99
|
+
agentId: z.string().describe('ID of the agent to delegate to'),
|
|
100
|
+
message: z.string().describe('The message/task to send to the subagent'),
|
|
101
|
+
cwd: z.string().optional().describe('Optional working directory for the subagent (defaults to current)'),
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
}
|