@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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 +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-task-tool-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('manage_tasks tool', () => {
|
|
36
|
+
it('inherits continuation context from continueFromTaskId', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
import fs from 'node:fs'
|
|
39
|
+
import path from 'node:path'
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
41
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
42
|
+
const storage = storageMod.default || storageMod
|
|
43
|
+
const crud = crudMod.default || crudMod
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
const workspaceDir = process.env.WORKSPACE_DIR
|
|
47
|
+
const projectDir = path.join(workspaceDir, 'projects', 'project-1')
|
|
48
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
49
|
+
|
|
50
|
+
storage.saveAgents({
|
|
51
|
+
default: {
|
|
52
|
+
id: 'default',
|
|
53
|
+
name: 'Molly',
|
|
54
|
+
description: '',
|
|
55
|
+
systemPrompt: '',
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
model: 'gpt-test',
|
|
58
|
+
createdAt: now,
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
},
|
|
61
|
+
worker: {
|
|
62
|
+
id: 'worker',
|
|
63
|
+
name: 'Worker',
|
|
64
|
+
description: '',
|
|
65
|
+
systemPrompt: '',
|
|
66
|
+
provider: 'openai',
|
|
67
|
+
model: 'gpt-test',
|
|
68
|
+
createdAt: now,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
storage.saveTasks({
|
|
74
|
+
'task-source': {
|
|
75
|
+
id: 'task-source',
|
|
76
|
+
title: 'Source task',
|
|
77
|
+
description: 'Original work',
|
|
78
|
+
status: 'completed',
|
|
79
|
+
agentId: 'worker',
|
|
80
|
+
projectId: 'project-1',
|
|
81
|
+
cwd: projectDir,
|
|
82
|
+
sessionId: 'session-source',
|
|
83
|
+
codexResumeId: 'codex-thread-1',
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const tools = crud.buildCrudTools({
|
|
90
|
+
cwd: workspaceDir,
|
|
91
|
+
ctx: { sessionId: 'session-creator', agentId: 'default', platformAssignScope: 'all' },
|
|
92
|
+
hasPlugin: (name) => name === 'manage_tasks',
|
|
93
|
+
})
|
|
94
|
+
const tool = tools.find((entry) => entry.name === 'manage_tasks')
|
|
95
|
+
const raw = await tool.invoke({
|
|
96
|
+
action: 'create',
|
|
97
|
+
title: 'Follow-up task',
|
|
98
|
+
description: 'Continue the previous task with the next deliverable.',
|
|
99
|
+
status: 'backlog',
|
|
100
|
+
continueFromTaskId: 'task-source',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const tasks = storage.loadTasks()
|
|
104
|
+
const created = Object.values(tasks).find((entry) => entry.id !== 'task-source')
|
|
105
|
+
console.log(JSON.stringify({ raw, created }))
|
|
106
|
+
`)
|
|
107
|
+
|
|
108
|
+
assert.equal(output.created.projectId, 'project-1')
|
|
109
|
+
assert.equal(output.created.agentId, 'worker')
|
|
110
|
+
assert.equal(output.created.sessionId, 'session-source')
|
|
111
|
+
assert.equal(output.created.codexResumeId, 'codex-thread-1')
|
|
112
|
+
assert.deepEqual(output.created.blockedBy, ['task-source'])
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -42,7 +42,8 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
42
42
|
? ctx.id
|
|
43
43
|
: null
|
|
44
44
|
const currentSession = ctx && typeof ctx === 'object' && Array.isArray(ctx.messages) ? ctx : null
|
|
45
|
-
const
|
|
45
|
+
const configuredScope = typeof ctx?.memoryScopeMode === 'string' ? ctx.memoryScopeMode : 'auto'
|
|
46
|
+
const rawScope = typeof scope === 'string' ? scope : configuredScope
|
|
46
47
|
const scopeMode = normalizeMemoryScopeMode(rawScope === 'shared' ? 'global' : rawScope)
|
|
47
48
|
const rerankMode = rerank === 'semantic' || rerank === 'lexical' ? rerank : 'balanced'
|
|
48
49
|
|
|
@@ -50,7 +51,11 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
50
51
|
mode: scopeMode,
|
|
51
52
|
agentId: currentAgentId,
|
|
52
53
|
sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : currentSessionId,
|
|
53
|
-
projectRoot: (typeof projectRoot === 'string' && projectRoot.trim())
|
|
54
|
+
projectRoot: (typeof projectRoot === 'string' && projectRoot.trim())
|
|
55
|
+
? projectRoot.trim()
|
|
56
|
+
: ((project && typeof project === 'object' && 'rootPath' in project && typeof (project as Record<string, unknown>).rootPath === 'string')
|
|
57
|
+
? (project as Record<string, unknown>).rootPath as string
|
|
58
|
+
: (typeof ctx?.projectRoot === 'string' && ctx.projectRoot.trim() ? ctx.projectRoot.trim() : null)),
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
const filterScope = (rows: MemoryEntry[]) => filterMemoriesByScope(rows, scopeFilter)
|
|
@@ -80,12 +85,19 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
80
85
|
if (imagePath && fs.existsSync(imagePath)) {
|
|
81
86
|
storedImage = await storeMemoryImageAsset(imagePath, genId(6))
|
|
82
87
|
}
|
|
88
|
+
const metadata = n.metadata && typeof n.metadata === 'object' && !Array.isArray(n.metadata)
|
|
89
|
+
? { ...(n.metadata as Record<string, unknown>) }
|
|
90
|
+
: {}
|
|
91
|
+
if (scopeMode === 'project' && scopeFilter.projectRoot && !metadata.projectRoot) {
|
|
92
|
+
metadata.projectRoot = scopeFilter.projectRoot
|
|
93
|
+
}
|
|
83
94
|
const entry = memDb.add({
|
|
84
95
|
agentId: scopeMode === 'global' ? null : currentAgentId,
|
|
85
96
|
sessionId: ctx?.sessionId || null,
|
|
86
97
|
category: category || 'note',
|
|
87
98
|
title: key,
|
|
88
99
|
content: value || '',
|
|
100
|
+
metadata,
|
|
89
101
|
references: Array.isArray(references) ? references : [],
|
|
90
102
|
filePaths: filePaths as any,
|
|
91
103
|
imagePath: storedImage?.path || undefined,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { afterEach, describe, it } from 'node:test'
|
|
3
|
+
import type { ToolBuildContext } from './context'
|
|
4
|
+
import { buildPlatformTools } from './platform'
|
|
5
|
+
import { loadSettings, saveSettings } from '../storage'
|
|
6
|
+
|
|
7
|
+
const originalSettings = loadSettings()
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
saveSettings(originalSettings)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function buildTestContext(hasPlugin: (name: string) => boolean): ToolBuildContext {
|
|
14
|
+
return {
|
|
15
|
+
cwd: process.cwd(),
|
|
16
|
+
ctx: undefined,
|
|
17
|
+
hasPlugin,
|
|
18
|
+
hasTool: hasPlugin,
|
|
19
|
+
cleanupFns: [],
|
|
20
|
+
commandTimeoutMs: 1_000,
|
|
21
|
+
claudeTimeoutMs: 1_000,
|
|
22
|
+
cliProcessTimeoutMs: 1_000,
|
|
23
|
+
persistDelegateResumeId: () => {},
|
|
24
|
+
readStoredDelegateResumeId: () => null,
|
|
25
|
+
resolveCurrentSession: () => null,
|
|
26
|
+
activePlugins: ['manage_platform'],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('buildPlatformTools', () => {
|
|
31
|
+
it('blocks task resources when task management is disabled', async () => {
|
|
32
|
+
saveSettings({
|
|
33
|
+
...originalSettings,
|
|
34
|
+
taskManagementEnabled: false,
|
|
35
|
+
projectManagementEnabled: true,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const [toolEntry] = buildPlatformTools(buildTestContext((name) => name === 'manage_platform'))
|
|
39
|
+
assert.ok(toolEntry)
|
|
40
|
+
|
|
41
|
+
const result = await toolEntry.invoke({ resource: 'tasks', action: 'list' })
|
|
42
|
+
assert.match(String(result), /task management is disabled/i)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('allows project resources through manage_platform when project management is enabled', async () => {
|
|
46
|
+
saveSettings({
|
|
47
|
+
...originalSettings,
|
|
48
|
+
taskManagementEnabled: true,
|
|
49
|
+
projectManagementEnabled: true,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const [toolEntry] = buildPlatformTools(buildTestContext((name) => name === 'manage_platform'))
|
|
53
|
+
assert.ok(toolEntry)
|
|
54
|
+
|
|
55
|
+
const result = await toolEntry.invoke({ resource: 'projects', action: 'list' })
|
|
56
|
+
assert.doesNotMatch(String(result), /unknown resource|disabled/i)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -2,9 +2,13 @@ import { z } from 'zod'
|
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import { buildCrudTools } from './crud'
|
|
4
4
|
import type { ToolBuildContext } from './context'
|
|
5
|
-
import type { Plugin, PluginHooks } from '@/types'
|
|
5
|
+
import type { Plugin, PluginHooks, Session } from '@/types'
|
|
6
6
|
import { getPluginManager } from '../plugins'
|
|
7
7
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
8
|
+
import { loadSettings } from '../storage'
|
|
9
|
+
import { resolveSessionToolPolicy } from '../tool-capability-policy'
|
|
10
|
+
import { loadRuntimeSettings } from '../runtime-settings'
|
|
11
|
+
import { expandPluginIds } from '../tool-aliases'
|
|
8
12
|
|
|
9
13
|
function parsePlatformData(value: unknown): Record<string, unknown> | null {
|
|
10
14
|
if (!value) return null
|
|
@@ -39,6 +43,7 @@ function normalizePlatformResourceName(value: unknown): string | undefined {
|
|
|
39
43
|
if (!normalized) return undefined
|
|
40
44
|
const singularMap: Record<string, string> = {
|
|
41
45
|
agent: 'agents',
|
|
46
|
+
project: 'projects',
|
|
42
47
|
task: 'tasks',
|
|
43
48
|
backlog_task: 'tasks',
|
|
44
49
|
'backlog-task': 'tasks',
|
|
@@ -144,33 +149,69 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
|
|
|
144
149
|
return [...new Set(values.filter((value): value is string => Boolean(value)))]
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
function resolvePlatformResourceAccess(toolId: string, bctx: ToolBuildContext): { allowed: boolean; reason: string | null } {
|
|
153
|
+
if (bctx.hasPlugin(toolId)) return { allowed: true, reason: null }
|
|
154
|
+
if (!bctx.hasPlugin('manage_platform')) return { allowed: false, reason: null }
|
|
155
|
+
const settings = loadSettings()
|
|
156
|
+
const decision = resolveSessionToolPolicy(['manage_platform', toolId], settings)
|
|
157
|
+
const allowed = decision.enabledPlugins.includes(toolId)
|
|
158
|
+
const blocked = decision.blockedPlugins.find((entry) => entry.tool === toolId)
|
|
159
|
+
return { allowed, reason: blocked?.reason || null }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function buildPlatformContextFromSession(session: Session): ToolBuildContext {
|
|
163
|
+
const runtime = loadRuntimeSettings()
|
|
164
|
+
const sessionPlugins = Array.isArray(session.plugins) ? session.plugins : []
|
|
165
|
+
const legacyTools = Array.isArray(session.tools) ? session.tools : []
|
|
166
|
+
const activePlugins = expandPluginIds([...sessionPlugins, ...legacyTools, 'manage_platform'])
|
|
167
|
+
const activePluginSet = new Set(activePlugins)
|
|
168
|
+
const hasPlugin = (name: string) => activePluginSet.has(name)
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
cwd: session.cwd || process.cwd(),
|
|
172
|
+
ctx: {
|
|
173
|
+
sessionId: session.id,
|
|
174
|
+
agentId: session.agentId ?? null,
|
|
175
|
+
},
|
|
176
|
+
hasPlugin,
|
|
177
|
+
hasTool: hasPlugin,
|
|
178
|
+
cleanupFns: [],
|
|
179
|
+
commandTimeoutMs: runtime.shellCommandTimeoutMs,
|
|
180
|
+
claudeTimeoutMs: runtime.claudeCodeTimeoutMs,
|
|
181
|
+
cliProcessTimeoutMs: runtime.cliProcessTimeoutMs,
|
|
182
|
+
persistDelegateResumeId: () => {},
|
|
183
|
+
readStoredDelegateResumeId: () => null,
|
|
184
|
+
resolveCurrentSession: () => session,
|
|
185
|
+
activePlugins,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
147
189
|
/**
|
|
148
190
|
* Unified Platform Execution Logic
|
|
149
191
|
*/
|
|
150
|
-
async function executePlatformAction(args: any, bctx:
|
|
192
|
+
async function executePlatformAction(args: any, bctx: ToolBuildContext) {
|
|
151
193
|
const normalized = normalizePlatformActionArgs((args ?? {}) as Record<string, unknown>)
|
|
152
194
|
const { resource, action, id, data } = normalized
|
|
195
|
+
const resourceName = typeof resource === 'string' ? resource : ''
|
|
153
196
|
|
|
154
197
|
// We reuse the existing CRUD tool logic but expose it via a single tool
|
|
155
198
|
const crudTools = buildCrudTools({
|
|
156
199
|
...bctx,
|
|
157
|
-
hasPlugin: (
|
|
158
|
-
'manage_agents',
|
|
159
|
-
'manage_tasks',
|
|
160
|
-
'manage_schedules',
|
|
161
|
-
'manage_skills',
|
|
162
|
-
'manage_documents',
|
|
163
|
-
'manage_secrets',
|
|
164
|
-
'manage_connectors',
|
|
165
|
-
'manage_sessions'
|
|
166
|
-
].includes(id)
|
|
200
|
+
hasPlugin: (toolId: string) => resolvePlatformResourceAccess(toolId, bctx).allowed,
|
|
167
201
|
})
|
|
168
202
|
|
|
169
|
-
const targetToolName = `manage_${
|
|
203
|
+
const targetToolName = `manage_${resourceName}`
|
|
170
204
|
const targetTool = crudTools.find(t => t.name === targetToolName)
|
|
171
205
|
|
|
172
206
|
if (!targetTool) {
|
|
173
|
-
|
|
207
|
+
const knownResources = ['agents', 'projects', 'tasks', 'schedules', 'skills', 'documents', 'secrets', 'connectors', 'sessions']
|
|
208
|
+
if (resourceName && knownResources.includes(resourceName)) {
|
|
209
|
+
const toolId = `manage_${resourceName}`
|
|
210
|
+
const access = resolvePlatformResourceAccess(toolId, bctx)
|
|
211
|
+
const suffix = access.reason ? ` (${access.reason})` : ''
|
|
212
|
+
return `Error: Resource "${resourceName}" is disabled by app settings or capability policy in this chat${suffix}.`
|
|
213
|
+
}
|
|
214
|
+
return `Error: Unknown resource type "${resourceName || resource}". Valid resources: ${knownResources.join(', ')}.`
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
// Forward to the specific CRUD tool implementation
|
|
@@ -182,10 +223,10 @@ async function executePlatformAction(args: any, bctx: any) {
|
|
|
182
223
|
*/
|
|
183
224
|
const PlatformPlugin: Plugin = {
|
|
184
225
|
name: 'Core Platform',
|
|
185
|
-
description: 'Unified management of agents, tasks, schedules, skills, documents, and secrets.',
|
|
226
|
+
description: 'Unified management of agents, projects, tasks, schedules, skills, documents, and secrets.',
|
|
186
227
|
hooks: {
|
|
187
|
-
getCapabilityDescription: () => 'I can
|
|
188
|
-
getOperatingGuidance: () => ['Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
|
|
228
|
+
getCapabilityDescription: () => 'I can manage durable execution context across agents, projects, tasks, schedules, documents, skills, webhooks, connectors, sessions, and encrypted secrets.',
|
|
229
|
+
getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
|
|
189
230
|
} as PluginHooks,
|
|
190
231
|
tools: [
|
|
191
232
|
{
|
|
@@ -194,14 +235,14 @@ const PlatformPlugin: Plugin = {
|
|
|
194
235
|
parameters: {
|
|
195
236
|
type: 'object',
|
|
196
237
|
properties: {
|
|
197
|
-
resource: { type: 'string', enum: ['agents', 'tasks', 'schedules', 'skills', 'documents', 'secrets', 'connectors', 'sessions'] },
|
|
238
|
+
resource: { type: 'string', enum: ['agents', 'projects', 'tasks', 'schedules', 'skills', 'documents', 'secrets', 'connectors', 'sessions'] },
|
|
198
239
|
action: { type: 'string', enum: ['list', 'get', 'create', 'update', 'delete'] },
|
|
199
240
|
id: { type: 'string' },
|
|
200
241
|
data: { type: 'string' }
|
|
201
242
|
},
|
|
202
243
|
required: ['resource', 'action']
|
|
203
244
|
},
|
|
204
|
-
execute: async (args, context) => executePlatformAction(args,
|
|
245
|
+
execute: async (args, context) => executePlatformAction(args, buildPlatformContextFromSession(context.session))
|
|
205
246
|
}
|
|
206
247
|
]
|
|
207
248
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { inferWebActionFromArgs } from './web'
|
|
4
|
+
|
|
5
|
+
describe('inferWebActionFromArgs', () => {
|
|
6
|
+
it('defaults to search when only query text is provided', () => {
|
|
7
|
+
assert.equal(inferWebActionFromArgs({ query: 'latest US-Iran news' }), 'search')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('defaults to fetch when the url is an absolute http url', () => {
|
|
11
|
+
assert.equal(inferWebActionFromArgs({ url: 'https://example.com/article' }), 'fetch')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('preserves an explicit action when present', () => {
|
|
15
|
+
assert.equal(inferWebActionFromArgs({ action: 'search', url: 'https://example.com/article' }), 'search')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -159,13 +159,30 @@ export function cleanupSessionBrowser(sessionId: string): void {
|
|
|
159
159
|
export function getActiveBrowserCount(): number { return activeBrowsers.size }
|
|
160
160
|
export function hasActiveBrowser(sessionId: string): boolean { return activeBrowsers.has(sessionId) }
|
|
161
161
|
|
|
162
|
+
export function inferWebActionFromArgs(params: {
|
|
163
|
+
action?: string
|
|
164
|
+
query?: string
|
|
165
|
+
url?: string
|
|
166
|
+
}): 'search' | 'fetch' | undefined {
|
|
167
|
+
if (params.action === 'search' || params.action === 'fetch') return params.action
|
|
168
|
+
if (typeof params.url === 'string' && /^https?:\/\//i.test(params.url.trim())) return 'fetch'
|
|
169
|
+
if (typeof params.query === 'string' && params.query.trim()) return 'search'
|
|
170
|
+
if (typeof params.url === 'string' && params.url.trim()) return 'search'
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
/**
|
|
163
175
|
* Unified Web Execution Logic
|
|
164
176
|
*/
|
|
165
177
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
178
|
async function executeWebAction(args: Record<string, unknown>, bctx: any) {
|
|
167
179
|
const normalized = normalizeToolInputArgs(args)
|
|
168
|
-
const {
|
|
180
|
+
const { query, url, maxResults } = normalized as { query?: string; url?: string; maxResults?: number }
|
|
181
|
+
const action = inferWebActionFromArgs({
|
|
182
|
+
action: (normalized as { action?: string }).action,
|
|
183
|
+
query,
|
|
184
|
+
url,
|
|
185
|
+
})
|
|
169
186
|
try {
|
|
170
187
|
if (action === 'search') {
|
|
171
188
|
const searchQuery = query || url
|
|
@@ -650,12 +667,141 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
650
667
|
}
|
|
651
668
|
|
|
652
669
|
const dismissCookieBanners = async (mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>) => {
|
|
653
|
-
await new Promise((r) => setTimeout(r,
|
|
670
|
+
await new Promise((r) => setTimeout(r, 1200))
|
|
654
671
|
const js = `() => {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
|
|
672
|
+
const docs = [document];
|
|
673
|
+
const roots = [document];
|
|
674
|
+
const seenRoots = new Set([document]);
|
|
675
|
+
const pushRoot = (root) => {
|
|
676
|
+
if (!root || seenRoots.has(root)) return;
|
|
677
|
+
seenRoots.add(root);
|
|
678
|
+
roots.push(root);
|
|
679
|
+
};
|
|
680
|
+
const collectFrames = (doc) => {
|
|
681
|
+
try {
|
|
682
|
+
const frames = doc.querySelectorAll('iframe');
|
|
683
|
+
for (const frame of frames) {
|
|
684
|
+
try {
|
|
685
|
+
const child = frame.contentDocument || frame.contentWindow?.document;
|
|
686
|
+
if (child && !docs.includes(child)) {
|
|
687
|
+
docs.push(child);
|
|
688
|
+
pushRoot(child);
|
|
689
|
+
}
|
|
690
|
+
} catch {}
|
|
691
|
+
}
|
|
692
|
+
} catch {}
|
|
693
|
+
};
|
|
694
|
+
const collectShadowRoots = () => {
|
|
695
|
+
for (const root of [...roots]) {
|
|
696
|
+
try {
|
|
697
|
+
const all = root.querySelectorAll('*');
|
|
698
|
+
for (const el of all) {
|
|
699
|
+
if (el.shadowRoot) pushRoot(el.shadowRoot);
|
|
700
|
+
}
|
|
701
|
+
} catch {}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
collectFrames(document);
|
|
705
|
+
collectShadowRoots();
|
|
706
|
+
const allRoots = [...new Set([...docs, ...roots])];
|
|
707
|
+
const visible = (el) => {
|
|
708
|
+
if (!el) return false;
|
|
709
|
+
const style = window.getComputedStyle(el);
|
|
710
|
+
if (!style || style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
711
|
+
const rect = el.getBoundingClientRect();
|
|
712
|
+
return rect.width > 0 && rect.height > 0;
|
|
713
|
+
};
|
|
714
|
+
const normalizedText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
715
|
+
const candidateSelectors = [
|
|
716
|
+
'#onetrust-reject-all-handler',
|
|
717
|
+
'#CybotCookiebotDialogBodyButtonDecline',
|
|
718
|
+
'#didomi-notice-disagree-button',
|
|
719
|
+
'.qc-cmp2-summary-buttons button:first-child',
|
|
720
|
+
'button.sp_choice_type_12',
|
|
721
|
+
'button[id*="reject" i]',
|
|
722
|
+
'button[class*="reject" i]',
|
|
723
|
+
'button[id*="decline" i]',
|
|
724
|
+
'button[class*="decline" i]',
|
|
725
|
+
'button[id*="consent" i]',
|
|
726
|
+
'button[class*="consent" i]',
|
|
727
|
+
'a[id*="reject" i]',
|
|
728
|
+
'a[class*="reject" i]',
|
|
729
|
+
'a[id*="decline" i]',
|
|
730
|
+
'a[class*="decline" i]'
|
|
731
|
+
];
|
|
732
|
+
for (const root of allRoots) {
|
|
733
|
+
for (const selector of candidateSelectors) {
|
|
734
|
+
try {
|
|
735
|
+
const el = root.querySelector(selector);
|
|
736
|
+
if (el && visible(el)) {
|
|
737
|
+
el.click();
|
|
738
|
+
return 'clicked:' + selector;
|
|
739
|
+
}
|
|
740
|
+
} catch {}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const buttonSelector = 'button, a[role="button"], [role="button"], input[type="button"], input[type="submit"]';
|
|
744
|
+
const rejectRe = /^(reject|reject all|decline|decline all|deny|deny all|refuse|no,? thanks|only necessary|necessary only|use necessary cookies only)$/i;
|
|
745
|
+
const acceptRe = /^(accept|accept all|allow all|agree|i agree|okay|ok|got it|continue|consent)$/i;
|
|
746
|
+
const closeRe = /^(close|dismiss|skip|not now|x|×)$/i;
|
|
747
|
+
const clickMatching = (matcher, label) => {
|
|
748
|
+
for (const root of allRoots) {
|
|
749
|
+
let buttons = [];
|
|
750
|
+
try { buttons = [...root.querySelectorAll(buttonSelector)]; } catch {}
|
|
751
|
+
for (const button of buttons) {
|
|
752
|
+
const txt = normalizedText(button.textContent || button.getAttribute?.('aria-label') || button.getAttribute?.('value'));
|
|
753
|
+
if (!txt || !matcher.test(txt) || !visible(button)) continue;
|
|
754
|
+
try {
|
|
755
|
+
button.click();
|
|
756
|
+
return label + ':' + txt.slice(0, 80);
|
|
757
|
+
} catch {}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
};
|
|
762
|
+
const clicked = clickMatching(rejectRe, 'reject') || clickMatching(acceptRe, 'accept') || clickMatching(closeRe, 'close');
|
|
763
|
+
if (clicked) return clicked;
|
|
764
|
+
const overlaySelectors = [
|
|
765
|
+
'#onetrust-banner-sdk',
|
|
766
|
+
'#onetrust-consent-sdk',
|
|
767
|
+
'#CybotCookiebotDialog',
|
|
768
|
+
'.didomi-popup-container',
|
|
769
|
+
'.fc-consent-root',
|
|
770
|
+
'[id*="cookie" i]',
|
|
771
|
+
'[class*="cookie" i]',
|
|
772
|
+
'[id*="consent" i]',
|
|
773
|
+
'[class*="consent" i]',
|
|
774
|
+
'[id*="privacy" i]',
|
|
775
|
+
'[class*="privacy" i]',
|
|
776
|
+
'[id*="sp_message" i]',
|
|
777
|
+
'[class*="sp_message" i]'
|
|
778
|
+
];
|
|
779
|
+
const hidden = [];
|
|
780
|
+
for (const root of allRoots) {
|
|
781
|
+
for (const selector of overlaySelectors) {
|
|
782
|
+
let nodes = [];
|
|
783
|
+
try { nodes = [...root.querySelectorAll(selector)]; } catch {}
|
|
784
|
+
for (const node of nodes) {
|
|
785
|
+
if (!visible(node)) continue;
|
|
786
|
+
const text = normalizedText(node.textContent).toLowerCase();
|
|
787
|
+
const attrs = normalizedText(node.id + ' ' + node.className).toLowerCase();
|
|
788
|
+
if (!text.includes('cookie') && !text.includes('consent') && !text.includes('privacy') && !attrs.includes('cookie') && !attrs.includes('consent') && !attrs.includes('privacy') && !attrs.includes('onetrust') && !attrs.includes('didomi') && !attrs.includes('sp_message')) continue;
|
|
789
|
+
try {
|
|
790
|
+
node.style.setProperty('display', 'none', 'important');
|
|
791
|
+
node.style.setProperty('visibility', 'hidden', 'important');
|
|
792
|
+
node.style.setProperty('pointer-events', 'none', 'important');
|
|
793
|
+
hidden.push(selector);
|
|
794
|
+
} catch {}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (hidden.length) {
|
|
799
|
+
try {
|
|
800
|
+
document.documentElement.style.removeProperty('overflow');
|
|
801
|
+
document.body.style.removeProperty('overflow');
|
|
802
|
+
} catch {}
|
|
803
|
+
return 'hidden:' + hidden[0];
|
|
804
|
+
}
|
|
659
805
|
return 'none';
|
|
660
806
|
}`
|
|
661
807
|
await mcpCall('browser_evaluate', { function: js })
|
|
@@ -1139,6 +1285,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1139
1285
|
} catch {
|
|
1140
1286
|
await new Promise((r) => setTimeout(r, 1200))
|
|
1141
1287
|
}
|
|
1288
|
+
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
1142
1289
|
}
|
|
1143
1290
|
|
|
1144
1291
|
let result = await callMcpTool(mcpTool, args, { saveTo: typeof params.saveTo === 'string' ? params.saveTo : undefined })
|
|
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { describe, it } from 'node:test'
|
|
5
|
-
import { buildToolDisciplineLines, looksLikeOpenEndedDeliverableTask } from './stream-agent-chat'
|
|
5
|
+
import { buildToolDisciplineLines, getExplicitRequiredToolNames, looksLikeOpenEndedDeliverableTask } from './stream-agent-chat'
|
|
6
6
|
|
|
7
7
|
const streamAgentChatSource = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'stream-agent-chat.ts'), 'utf-8')
|
|
8
8
|
|
|
@@ -27,19 +27,44 @@ describe('buildToolDisciplineLines', () => {
|
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('warns browser tasks to use literal urls and the supported form schema', () => {
|
|
30
|
-
const lines = buildToolDisciplineLines(['browser', 'http_request', 'email', 'ask_human'])
|
|
30
|
+
const lines = buildToolDisciplineLines(['web_search', 'web_fetch', 'browser', 'manage_connectors', 'http_request', 'email', 'ask_human'])
|
|
31
31
|
|
|
32
32
|
assert.ok(lines.some((line) => line.includes('Do not invent placeholder URLs')))
|
|
33
33
|
assert.ok(lines.some((line) => line.includes('A shorthand `form` object keyed by input id/name also works')))
|
|
34
|
+
assert.ok(lines.some((line) => line.includes('For current events, breaking news, or "latest" requests, start with `web_search`')))
|
|
35
|
+
assert.ok(lines.some((line) => line.includes('Use `browser` when the user asks for screenshots')))
|
|
36
|
+
assert.ok(lines.some((line) => line.includes('do not capture screenshots') && line.includes('`browser`')))
|
|
37
|
+
assert.ok(lines.some((line) => line.includes('connector_message_tool') && line.includes('list_running')))
|
|
38
|
+
assert.ok(lines.some((line) => line.includes('connector/channel setup is missing')))
|
|
39
|
+
assert.ok(lines.some((line) => line.includes('capture the artifact first with `browser`') && line.includes('`connector_message_tool`')))
|
|
34
40
|
assert.ok(lines.some((line) => line.includes('Keep JSON request bodies as raw JSON strings')))
|
|
35
41
|
assert.ok(lines.some((line) => line.includes('{"action":"send","to":"user@example.com","subject":"...","body":"..."}')))
|
|
36
42
|
assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
|
|
37
43
|
})
|
|
38
44
|
|
|
45
|
+
it('requires research, browser, and connector tools for hybrid news delivery requests', () => {
|
|
46
|
+
const required = getExplicitRequiredToolNames(
|
|
47
|
+
'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
|
|
48
|
+
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(required, ['web_search', 'browser', 'connector_message_tool'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('requires connector delivery for explicit channel requests', () => {
|
|
55
|
+
const required = getExplicitRequiredToolNames(
|
|
56
|
+
'Research the latest launch news, take a screenshot, and send it to me over Slack.',
|
|
57
|
+
['web_search', 'browser', 'manage_connectors'],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(required, ['web_search', 'browser', 'connector_message_tool'])
|
|
61
|
+
})
|
|
62
|
+
|
|
39
63
|
it('tells the agent that named enabled tools are completion requirements', () => {
|
|
40
64
|
assert.ok(streamAgentChatSource.includes('If a task explicitly names an enabled tool, use that tool before declaring success.'))
|
|
41
65
|
assert.ok(streamAgentChatSource.includes('collect required human input through the tool'))
|
|
42
66
|
assert.ok(streamAgentChatSource.includes('You have not yet completed the required explicit tool step(s):'))
|
|
67
|
+
assert.ok(streamAgentChatSource.includes('do not replace screenshot requests with text-only summaries'))
|
|
43
68
|
assert.ok(streamAgentChatSource.includes('[Loop Budget Reached]'))
|
|
44
69
|
})
|
|
45
70
|
})
|