@swarmclawai/swarmclaw 0.6.0 → 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 +15 -2
- 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 +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/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 +46 -22
- package/src/components/chat/chat-header.tsx +455 -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 +180 -7
- 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 +68 -16
- 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 +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- 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 +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- 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 +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 +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -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
|
@@ -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
|
+
}
|
|
@@ -9,6 +9,58 @@ import { spawnSync } from 'child_process'
|
|
|
9
9
|
import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
|
|
10
10
|
import { getSearchProvider } from './search-providers'
|
|
11
11
|
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Search result compression — summarize verbose results before injecting into context
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
async function compressSearchResults(
|
|
17
|
+
results: Array<{ title?: string; url?: string; snippet?: string }>,
|
|
18
|
+
query: string,
|
|
19
|
+
bctx: ToolBuildContext,
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
const session = bctx.resolveCurrentSession?.()
|
|
22
|
+
if (!session?.provider || !session?.model) return null
|
|
23
|
+
|
|
24
|
+
const { getProvider } = await import('@/lib/providers')
|
|
25
|
+
const { loadCredentials, decryptKey } = await import('../storage')
|
|
26
|
+
const providerEntry = getProvider(session.provider)
|
|
27
|
+
if (!providerEntry?.handler?.streamChat) return null
|
|
28
|
+
|
|
29
|
+
// Resolve API key
|
|
30
|
+
let apiKey: string | undefined
|
|
31
|
+
if (session.credentialId) {
|
|
32
|
+
const creds = loadCredentials()
|
|
33
|
+
const cred = creds[session.credentialId]
|
|
34
|
+
if (cred) apiKey = decryptKey(cred)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const systemPrompt = 'You are a search result summarizer. Condense search results into a concise reference. Keep key facts, URLs, and data points. Remove filler and redundancy. Output plain text, not JSON.'
|
|
38
|
+
const message = `Query: "${query}"\n\nResults:\n${JSON.stringify(results, null, 1)}\n\nSummarize these results concisely.`
|
|
39
|
+
|
|
40
|
+
let compressed = ''
|
|
41
|
+
await providerEntry.handler.streamChat({
|
|
42
|
+
session: { ...session, messages: [] },
|
|
43
|
+
message,
|
|
44
|
+
apiKey,
|
|
45
|
+
systemPrompt,
|
|
46
|
+
write: (raw: string) => {
|
|
47
|
+
// Extract text data from SSE lines
|
|
48
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.startsWith('data: ')) continue
|
|
51
|
+
try {
|
|
52
|
+
const ev = JSON.parse(line.slice(6))
|
|
53
|
+
if (ev.t === 'd' && ev.text) compressed += ev.text
|
|
54
|
+
} catch { /* skip */ }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
active: new Map(),
|
|
58
|
+
loadHistory: () => [],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return compressed.trim() || null
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
// ---------------------------------------------------------------------------
|
|
13
65
|
// Global registry of active browser instances for cleanup sweeps
|
|
14
66
|
// ---------------------------------------------------------------------------
|
|
@@ -70,9 +122,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
70
122
|
const settings = loadSettings()
|
|
71
123
|
const provider = await getSearchProvider(settings)
|
|
72
124
|
const results = await provider.search(query, limit)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
if (results.length === 0) return 'No results found.'
|
|
126
|
+
const raw = JSON.stringify(results, null, 2)
|
|
127
|
+
// Compress search results if they exceed 2000 chars
|
|
128
|
+
if (raw.length > 2000) {
|
|
129
|
+
try {
|
|
130
|
+
const compressed = await compressSearchResults(results, query, bctx)
|
|
131
|
+
if (compressed) return compressed
|
|
132
|
+
} catch {
|
|
133
|
+
// Compression failed — fall through to raw results
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return raw
|
|
76
137
|
} catch (err: unknown) {
|
|
77
138
|
return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
|
|
78
139
|
}
|
|
@@ -284,6 +345,49 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
284
345
|
return JSON.stringify(result)
|
|
285
346
|
}
|
|
286
347
|
|
|
348
|
+
// Best-effort cookie/consent banner dismissal after navigation
|
|
349
|
+
const dismissCookieBanners = async (
|
|
350
|
+
mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>,
|
|
351
|
+
) => {
|
|
352
|
+
// Wait briefly for consent overlays to appear
|
|
353
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
354
|
+
const js = `
|
|
355
|
+
(() => {
|
|
356
|
+
const sel = [
|
|
357
|
+
// Common "Reject" / "Reject all" / "Decline" buttons
|
|
358
|
+
'button[id*="reject" i]', 'button[class*="reject" i]',
|
|
359
|
+
'a[id*="reject" i]', 'a[class*="reject" i]',
|
|
360
|
+
'[data-testid*="reject" i]', '[data-action="reject"]',
|
|
361
|
+
// OneTrust
|
|
362
|
+
'#onetrust-reject-all-handler',
|
|
363
|
+
// Cookiebot
|
|
364
|
+
'#CybotCookiebotDialogBodyButtonDecline',
|
|
365
|
+
// Didomi
|
|
366
|
+
'#didomi-notice-disagree-button',
|
|
367
|
+
// Quantcast / IAB TCF
|
|
368
|
+
'.qc-cmp2-summary-buttons button:first-child',
|
|
369
|
+
'button.sp_choice_type_12',
|
|
370
|
+
// Generic patterns
|
|
371
|
+
'button[aria-label*="reject" i]', 'button[aria-label*="decline" i]',
|
|
372
|
+
'button[aria-label*="deny" i]', 'button[aria-label*="refuse" i]',
|
|
373
|
+
];
|
|
374
|
+
for (const s of sel) {
|
|
375
|
+
const el = document.querySelector(s);
|
|
376
|
+
if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; }
|
|
377
|
+
}
|
|
378
|
+
// Fallback: find buttons by visible text
|
|
379
|
+
const btns = [...document.querySelectorAll('button, a[role="button"], [class*="cookie"] button, [class*="consent"] button, [id*="cookie"] button')];
|
|
380
|
+
const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
|
|
381
|
+
for (const b of btns) {
|
|
382
|
+
const txt = (b.textContent || '').trim();
|
|
383
|
+
if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; }
|
|
384
|
+
}
|
|
385
|
+
return 'none';
|
|
386
|
+
})()
|
|
387
|
+
`
|
|
388
|
+
await mcpCall('browser_evaluate', { expression: js })
|
|
389
|
+
}
|
|
390
|
+
|
|
287
391
|
// Action-to-MCP tool mapping
|
|
288
392
|
const MCP_TOOL_MAP: Record<string, string> = {
|
|
289
393
|
navigate: 'browser_navigate',
|
|
@@ -315,7 +419,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
315
419
|
const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
|
|
316
420
|
? params.saveTo.trim()
|
|
317
421
|
: undefined
|
|
318
|
-
|
|
422
|
+
const result = await callMcpTool(mcpTool, args, { saveTo })
|
|
423
|
+
|
|
424
|
+
// After navigation, attempt to dismiss cookie consent banners
|
|
425
|
+
if (action === 'navigate') {
|
|
426
|
+
try { await dismissCookieBanners(callMcpTool) } catch { /* best-effort */ }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return result
|
|
319
430
|
} catch (err: unknown) {
|
|
320
431
|
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
321
432
|
}
|
|
@@ -9,6 +9,7 @@ import { getPluginManager } from './plugins'
|
|
|
9
9
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
|
|
10
10
|
import { getMemoryDb } from './memory-db'
|
|
11
11
|
import { logExecution } from './execution-log'
|
|
12
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
12
13
|
import type { Session, Message, UsageRecord } from '@/types'
|
|
13
14
|
import { extractSuggestions } from './suggestions'
|
|
14
15
|
|
|
@@ -227,6 +228,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
227
228
|
stateModifierParts.push(systemPrompt!.trim())
|
|
228
229
|
} else {
|
|
229
230
|
if (settings.userPrompt) stateModifierParts.push(settings.userPrompt)
|
|
231
|
+
stateModifierParts.push(buildCurrentDateTimePromptContext())
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
// Load agent context when a full prompt was not already composed by the route layer.
|
|
@@ -400,15 +402,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
400
402
|
}
|
|
401
403
|
}
|
|
402
404
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
405
|
+
if (settings.suggestionsEnabled !== false) {
|
|
406
|
+
stateModifierParts.push(
|
|
407
|
+
[
|
|
408
|
+
'## Follow-up Suggestions',
|
|
409
|
+
'At the end of every response, include a <suggestions> block with exactly 3 short',
|
|
410
|
+
'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
|
|
411
|
+
'Make them contextual to what you just said. Example:',
|
|
412
|
+
'<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
|
|
413
|
+
].join('\n'),
|
|
414
|
+
)
|
|
415
|
+
}
|
|
412
416
|
|
|
413
417
|
stateModifierParts.push(
|
|
414
418
|
buildAgenticExecutionPolicy({
|
|
@@ -548,8 +552,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
548
552
|
// Context manager failure — continue with full history
|
|
549
553
|
}
|
|
550
554
|
|
|
555
|
+
// Apply context-clear boundary: slice from most recent context-clear marker
|
|
556
|
+
let contextStart = 0
|
|
557
|
+
for (let i = effectiveHistory.length - 1; i >= 0; i--) {
|
|
558
|
+
if (effectiveHistory[i].kind === 'context-clear') {
|
|
559
|
+
contextStart = i + 1
|
|
560
|
+
break
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const postClearHistory = effectiveHistory.slice(contextStart)
|
|
564
|
+
|
|
551
565
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
552
|
-
for (const m of
|
|
566
|
+
for (const m of postClearHistory.slice(-20)) {
|
|
553
567
|
if (m.role === 'user') {
|
|
554
568
|
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
|
|
555
569
|
} else {
|
|
@@ -567,6 +581,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
567
581
|
let totalInputTokens = 0
|
|
568
582
|
let totalOutputTokens = 0
|
|
569
583
|
let lastToolInput: unknown = null
|
|
584
|
+
let accumulatedThinking = ''
|
|
570
585
|
|
|
571
586
|
// Plugin hooks: beforeAgentStart
|
|
572
587
|
const pluginMgr = getPluginManager()
|
|
@@ -603,9 +618,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
603
618
|
for (const block of chunk.content) {
|
|
604
619
|
// Anthropic extended thinking blocks
|
|
605
620
|
if (block.type === 'thinking' && block.thinking) {
|
|
621
|
+
accumulatedThinking += block.thinking
|
|
606
622
|
write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
|
|
607
623
|
// OpenClaw [[thinking]] prefix convention
|
|
608
624
|
} else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
|
|
625
|
+
accumulatedThinking += block.text.slice(12)
|
|
609
626
|
write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
|
|
610
627
|
} else if (block.text) {
|
|
611
628
|
fullText += block.text
|
|
@@ -730,6 +747,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
730
747
|
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
|
|
731
748
|
}
|
|
732
749
|
|
|
750
|
+
// Emit full thinking text as metadata so the client can persist it
|
|
751
|
+
if (accumulatedThinking) {
|
|
752
|
+
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ thinking: accumulatedThinking }) })}\n\n`)
|
|
753
|
+
}
|
|
754
|
+
|
|
733
755
|
// Track cost
|
|
734
756
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
735
757
|
if (totalTokens > 0) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse @AgentName mentions from text and resolve to an agent ID.
|
|
5
|
+
* Uses case-insensitive exact match, then falls back to starts-with.
|
|
6
|
+
*/
|
|
7
|
+
export function parseMentionedAgentId(
|
|
8
|
+
description: string,
|
|
9
|
+
agents: Record<string, Agent>,
|
|
10
|
+
): string | null {
|
|
11
|
+
const mentionRegex = /@(\S+)/g
|
|
12
|
+
const agentList = Object.values(agents)
|
|
13
|
+
let match: RegExpExecArray | null
|
|
14
|
+
|
|
15
|
+
while ((match = mentionRegex.exec(description)) !== null) {
|
|
16
|
+
const mention = match[1].toLowerCase()
|
|
17
|
+
|
|
18
|
+
// Exact name match (case-insensitive)
|
|
19
|
+
const exact = agentList.find((a) => a.name.toLowerCase() === mention)
|
|
20
|
+
if (exact) return exact.id
|
|
21
|
+
|
|
22
|
+
// Starts-with match (for partial names like @code matching "CodeBot")
|
|
23
|
+
const startsWith = agentList.find((a) => a.name.toLowerCase().startsWith(mention))
|
|
24
|
+
if (startsWith) return startsWith.id
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve task agent: if description has an @mention, use that agent.
|
|
32
|
+
* Otherwise fall back to currentAgentId.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveTaskAgentFromDescription(
|
|
35
|
+
description: string,
|
|
36
|
+
currentAgentId: string,
|
|
37
|
+
agents: Record<string, Agent>,
|
|
38
|
+
): string {
|
|
39
|
+
const mentioned = parseMentionedAgentId(description, agents)
|
|
40
|
+
return mentioned || currentAgentId
|
|
41
|
+
}
|
package/src/lib/sessions.ts
CHANGED
|
@@ -34,6 +34,16 @@ export const deleteSession = (id: string) =>
|
|
|
34
34
|
export const fetchMessages = (id: string) =>
|
|
35
35
|
api<Message[]>('GET', `/sessions/${id}/messages`)
|
|
36
36
|
|
|
37
|
+
export interface PaginatedMessages {
|
|
38
|
+
messages: Message[]
|
|
39
|
+
total: number
|
|
40
|
+
hasMore: boolean
|
|
41
|
+
startIndex: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const fetchMessagesPaginated = (id: string, limit: number = 100) =>
|
|
45
|
+
api<PaginatedMessages>('GET', `/sessions/${id}/messages?limit=${limit}`)
|
|
46
|
+
|
|
37
47
|
export const clearMessages = (id: string) =>
|
|
38
48
|
api<string>('POST', `/sessions/${id}/clear`)
|
|
39
49
|
|