@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.
Files changed (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. 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
- const { getSecret } = await import('../storage')
264
- const secret = await getSecret('tavily')
265
- if (!secret?.value) throw new Error('Tavily requires an API key. Add a secret named "tavily" in Secrets.')
266
- return new TavilyProvider(secret.value)
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
- const { getSecret } = await import('../storage')
270
- const secret = await getSecret('brave')
271
- if (!secret?.value) throw new Error('Brave Search requires an API key. Add a secret named "brave" in Secrets.')
272
- return new BraveProvider(secret.value)
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
- return results.length > 0
74
- ? JSON.stringify(results, null, 2)
75
- : 'No results found.'
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
- return await callMcpTool(mcpTool, args, { saveTo })
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
- stateModifierParts.push(
404
- [
405
- '## Follow-up Suggestions',
406
- 'At the end of every response, include a <suggestions> block with exactly 3 short',
407
- 'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
408
- 'Make them contextual to what you just said. Example:',
409
- '<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
410
- ].join('\n'),
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 effectiveHistory.slice(-20)) {
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
+ }
@@ -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