@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
|
@@ -11,11 +11,19 @@ import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-setti
|
|
|
11
11
|
|
|
12
12
|
import { logExecution } from './execution-log'
|
|
13
13
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
14
|
-
import { expandPluginIds } from './tool-aliases'
|
|
14
|
+
import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
|
|
15
15
|
import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
|
|
16
16
|
import { extractSuggestions } from './suggestions'
|
|
17
17
|
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
18
18
|
import { enqueueSystemEvent } from './system-events'
|
|
19
|
+
import { resolveActiveProjectContext } from './project-context'
|
|
20
|
+
import {
|
|
21
|
+
getEnabledToolPlanningView,
|
|
22
|
+
getFirstToolForCapability,
|
|
23
|
+
getToolsForCapability,
|
|
24
|
+
matchToolCapabilitiesForMessage,
|
|
25
|
+
TOOL_CAPABILITY,
|
|
26
|
+
} from './tool-planning'
|
|
19
27
|
|
|
20
28
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
21
29
|
interface StreamAgentChatOpts {
|
|
@@ -46,7 +54,8 @@ function buildPluginCapabilityLines(enabledPlugins: string[], opts?: { platformA
|
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
49
|
-
const
|
|
57
|
+
const planning = getEnabledToolPlanningView(enabledPlugins)
|
|
58
|
+
const uniqueTools = planning.displayToolIds
|
|
50
59
|
if (uniqueTools.length === 0) return []
|
|
51
60
|
|
|
52
61
|
const lines = [
|
|
@@ -59,33 +68,26 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
59
68
|
lines.push(`Use direct platform tools exactly as named (${directPlatformTools.map((toolId) => `\`${toolId}\``).join(', ')}). Do not substitute \`manage_platform\` unless it is explicitly enabled.`)
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
lines.push('For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (uniqueTools.includes('shell')) {
|
|
67
|
-
lines.push('For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.')
|
|
68
|
-
}
|
|
71
|
+
lines.push(...planning.disciplineGuidance)
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
lines.push('For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.')
|
|
76
|
-
lines.push('For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.')
|
|
77
|
-
}
|
|
73
|
+
const researchSearchTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.researchSearch)
|
|
74
|
+
const researchFetchTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.researchFetch)
|
|
75
|
+
const browserCaptureTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.browserCapture)
|
|
76
|
+
const deliveryMediaTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.deliveryMedia)
|
|
77
|
+
const deliveryVoiceTools = getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.deliveryVoiceNote)
|
|
78
78
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
79
|
+
if ((researchSearchTools.length || researchFetchTools.length) && browserCaptureTools.length) {
|
|
80
|
+
const researchLabel = [...researchSearchTools, ...researchFetchTools].map((toolName) => `\`${toolName}\``).join('/')
|
|
81
|
+
lines.push(`Research tools like ${researchLabel} gather sources and text, but they do not capture screenshots. Use \`${browserCaptureTools[0]}\` for screenshots or rendered page evidence.`)
|
|
82
|
+
lines.push(`When a task asks for both research and screenshots, use ${researchLabel} first to identify the right source URLs, then use \`${browserCaptureTools[0]}\` to capture the relevant page.`)
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
if (
|
|
84
|
-
lines.push(
|
|
85
|
+
if (browserCaptureTools.length && deliveryMediaTools.length) {
|
|
86
|
+
lines.push(`When the user asks you to send screenshots or other media, capture the artifact first with \`${browserCaptureTools[0]}\`, then deliver that exact file or upload URL through \`${deliveryMediaTools[0]}\` instead of saying the capability is unavailable.`)
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
if (
|
|
88
|
-
lines.push(
|
|
89
|
+
if (deliveryVoiceTools.length) {
|
|
90
|
+
lines.push(`If the user asks for a voice note and \`${deliveryVoiceTools[0]}\` is enabled, try it before saying voice notes are unsupported.`)
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
return lines
|
|
@@ -99,9 +101,36 @@ export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
|
|
|
99
101
|
return isBroadGoal(text) && /(\.md\b|\.txt\b|copy|brief|proposal|plan|report|draft|document)/.test(normalized)
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
104
|
+
export function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
|
|
103
105
|
const normalized = userMessage.toLowerCase()
|
|
104
106
|
const required: string[] = []
|
|
107
|
+
const matchedCapabilities = matchToolCapabilitiesForMessage(enabledPlugins, userMessage)
|
|
108
|
+
|
|
109
|
+
const requireCapability = (capability: string) => {
|
|
110
|
+
const toolName = matchedCapabilities.get(capability)?.[0] || getFirstToolForCapability(enabledPlugins, capability)
|
|
111
|
+
if (toolName && !required.includes(toolName)) required.push(toolName)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (matchedCapabilities.has(TOOL_CAPABILITY.researchSearch)) {
|
|
115
|
+
requireCapability(TOOL_CAPABILITY.researchSearch)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (matchedCapabilities.has(TOOL_CAPABILITY.researchFetch)) {
|
|
119
|
+
requireCapability(TOOL_CAPABILITY.researchFetch)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (matchedCapabilities.has(TOOL_CAPABILITY.browserCapture)) {
|
|
123
|
+
requireCapability(TOOL_CAPABILITY.browserCapture)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (matchedCapabilities.has(TOOL_CAPABILITY.deliveryVoiceNote)) {
|
|
127
|
+
requireCapability(TOOL_CAPABILITY.deliveryVoiceNote)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (matchedCapabilities.has(TOOL_CAPABILITY.deliveryMedia) || matchedCapabilities.has(TOOL_CAPABILITY.deliveryMessage)) {
|
|
131
|
+
requireCapability(TOOL_CAPABILITY.deliveryMedia)
|
|
132
|
+
requireCapability(TOOL_CAPABILITY.deliveryMessage)
|
|
133
|
+
}
|
|
105
134
|
|
|
106
135
|
if (enabledPlugins.includes('ask_human')
|
|
107
136
|
&& (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
|
|
@@ -140,10 +169,10 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
140
169
|
'## Goal Decomposition',
|
|
141
170
|
'When you receive a broad, open-ended goal:',
|
|
142
171
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
143
|
-
'2. If manage_tasks is available,
|
|
144
|
-
'3. Present the plan as a short checklist or numbered list in plain language.',
|
|
145
|
-
'4. Execute the first subtask immediately — do not stop after planning.',
|
|
146
|
-
'5.
|
|
172
|
+
'2. If manage_tasks is available, use it only for durable tracking: multi-turn work, delegation, explicit backlog requests, or work you expect to resume later. Do not create a task for every micro-step.',
|
|
173
|
+
'3. Present the plan as a short checklist or numbered list in plain language. If durable tracking is unnecessary, keep it inline instead of creating tasks.',
|
|
174
|
+
'4. Execute the first substantive subtask immediately — do not stop after planning.',
|
|
175
|
+
'5. Update only the durable tasks you actually created; otherwise just continue executing and report progress plainly.',
|
|
147
176
|
].join('\n')
|
|
148
177
|
|
|
149
178
|
function buildAgenticExecutionPolicy(opts: {
|
|
@@ -275,6 +304,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
275
304
|
let agentMcpServerIds: string[] | undefined
|
|
276
305
|
let agentMcpDisabledTools: string[] | undefined
|
|
277
306
|
let agentHeartbeatEnabled = false
|
|
307
|
+
let agentMemoryScopeMode: 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project' | null = null
|
|
308
|
+
const activeProjectContext = resolveActiveProjectContext(session)
|
|
278
309
|
if (session.agentId) {
|
|
279
310
|
const agents = loadAgents()
|
|
280
311
|
const agent = agents[session.agentId]
|
|
@@ -282,6 +313,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
282
313
|
agentMcpServerIds = agent?.mcpServerIds
|
|
283
314
|
agentMcpDisabledTools = agent?.mcpDisabledTools
|
|
284
315
|
agentHeartbeatEnabled = agent?.heartbeatEnabled === true
|
|
316
|
+
agentMemoryScopeMode = agent?.memoryScopeMode || null
|
|
285
317
|
if (!hasProvidedSystemPrompt) {
|
|
286
318
|
// Identity block — make sure the agent knows who it is
|
|
287
319
|
const identityLines = [`## My Identity`, `My name is ${agent?.name || 'Agent'}.`]
|
|
@@ -336,6 +368,43 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
336
368
|
// Plugin context injection is non-critical
|
|
337
369
|
}
|
|
338
370
|
|
|
371
|
+
if (!hasProvidedSystemPrompt && activeProjectContext.projectId) {
|
|
372
|
+
const projectLines = ['## Current Project']
|
|
373
|
+
if (activeProjectContext.project?.name) {
|
|
374
|
+
projectLines.push(`Active project: ${activeProjectContext.project.name}.`)
|
|
375
|
+
} else {
|
|
376
|
+
projectLines.push(`Active project ID: ${activeProjectContext.projectId}.`)
|
|
377
|
+
}
|
|
378
|
+
if (activeProjectContext.project?.description) {
|
|
379
|
+
projectLines.push(`Project description: ${activeProjectContext.project.description}`)
|
|
380
|
+
projectLines.push('Treat the project description above as authoritative context for who the project is for, what it is focused on, and which pilot priorities matter right now. If the user asks about the active project, answer from that description instead of saying the context is unavailable.')
|
|
381
|
+
}
|
|
382
|
+
if (activeProjectContext.objective) projectLines.push(`Project objective: ${activeProjectContext.objective}`)
|
|
383
|
+
if (activeProjectContext.audience) projectLines.push(`Who it is for: ${activeProjectContext.audience}`)
|
|
384
|
+
if (activeProjectContext.priorities.length > 0) projectLines.push(`Pilot priorities: ${activeProjectContext.priorities.join('; ')}`)
|
|
385
|
+
if (activeProjectContext.openObjectives.length > 0) projectLines.push(`Open objectives: ${activeProjectContext.openObjectives.join('; ')}`)
|
|
386
|
+
if (activeProjectContext.capabilityHints.length > 0) projectLines.push(`Suggested operating modes: ${activeProjectContext.capabilityHints.join('; ')}`)
|
|
387
|
+
if (activeProjectContext.credentialRequirements.length > 0) projectLines.push(`Credential and secret requirements: ${activeProjectContext.credentialRequirements.join('; ')}`)
|
|
388
|
+
if (activeProjectContext.successMetrics.length > 0) projectLines.push(`Success metrics: ${activeProjectContext.successMetrics.join('; ')}`)
|
|
389
|
+
if (activeProjectContext.heartbeatPrompt) projectLines.push(`Preferred heartbeat prompt: ${activeProjectContext.heartbeatPrompt}`)
|
|
390
|
+
if (activeProjectContext.heartbeatIntervalSec != null) projectLines.push(`Preferred heartbeat interval: ${activeProjectContext.heartbeatIntervalSec}s`)
|
|
391
|
+
if (activeProjectContext.resourceSummary) {
|
|
392
|
+
const summary = activeProjectContext.resourceSummary
|
|
393
|
+
const resourceBits = [
|
|
394
|
+
`open tasks ${summary.openTaskCount}`,
|
|
395
|
+
`active schedules ${summary.activeScheduleCount}`,
|
|
396
|
+
`project secrets ${summary.secretCount}`,
|
|
397
|
+
]
|
|
398
|
+
if (summary.topTaskTitles.length > 0) projectLines.push(`Top open tasks: ${summary.topTaskTitles.join('; ')}`)
|
|
399
|
+
if (summary.scheduleNames.length > 0) projectLines.push(`Active schedules: ${summary.scheduleNames.join('; ')}`)
|
|
400
|
+
if (summary.secretNames.length > 0) projectLines.push(`Known project secrets: ${summary.secretNames.join('; ')}`)
|
|
401
|
+
projectLines.push(`Project resource summary: ${resourceBits.join(', ')}.`)
|
|
402
|
+
}
|
|
403
|
+
if (activeProjectContext.projectRoot) projectLines.push(`Workspace root: ${activeProjectContext.projectRoot}`)
|
|
404
|
+
projectLines.push('When creating project tasks, schedules, secrets, memories, or deliverables for this work, default them to the active project unless the user redirects you.')
|
|
405
|
+
stateModifierParts.push(projectLines.join('\n'))
|
|
406
|
+
}
|
|
407
|
+
|
|
339
408
|
// Tell the LLM about available plugins and their access status
|
|
340
409
|
{
|
|
341
410
|
const agentEnabledSet = new Set(sessionPlugins)
|
|
@@ -407,6 +476,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
407
476
|
platformAssignScope: agentPlatformAssignScope,
|
|
408
477
|
mcpServerIds: agentMcpServerIds,
|
|
409
478
|
mcpDisabledTools: agentMcpDisabledTools,
|
|
479
|
+
projectId: activeProjectContext.projectId,
|
|
480
|
+
projectRoot: activeProjectContext.projectRoot,
|
|
481
|
+
projectName: activeProjectContext.project?.name || null,
|
|
482
|
+
projectDescription: activeProjectContext.project?.description || null,
|
|
483
|
+
memoryScopeMode: agentMemoryScopeMode,
|
|
410
484
|
})
|
|
411
485
|
const agent = createReactAgent({ llm, tools, stateModifier })
|
|
412
486
|
const recursionLimit = getAgentLoopRecursionLimit(runtime)
|
|
@@ -707,7 +781,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
707
781
|
needsTextSeparator = true
|
|
708
782
|
lastSegment = ''
|
|
709
783
|
const toolName = event.name || 'unknown'
|
|
710
|
-
usedToolNames.add(toolName)
|
|
784
|
+
usedToolNames.add(canonicalizePluginId(toolName) || toolName)
|
|
711
785
|
const input = event.data?.input
|
|
712
786
|
// Estimate input tokens for plugin invocation tracking
|
|
713
787
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
@@ -851,7 +925,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
851
925
|
langchainMessages.push(new AIMessage({ content: fullText }))
|
|
852
926
|
}
|
|
853
927
|
langchainMessages.push(new HumanMessage({
|
|
854
|
-
content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request,
|
|
928
|
+
content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request, do not replace outbound delivery tools with prose, and do not replace screenshot requests with text-only summaries.`,
|
|
855
929
|
}))
|
|
856
930
|
lastSegment = ''
|
|
857
931
|
} else if (shouldContinue === 'transient') {
|
|
@@ -7,6 +7,7 @@ const PLUGIN_ALIAS_GROUPS: string[][] = [
|
|
|
7
7
|
['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'],
|
|
8
8
|
['manage_platform'],
|
|
9
9
|
['manage_agents'],
|
|
10
|
+
['manage_projects'],
|
|
10
11
|
['manage_tasks'],
|
|
11
12
|
['manage_schedules'],
|
|
12
13
|
['manage_skills'],
|
|
@@ -43,6 +44,7 @@ const PLUGIN_IMPLICATIONS: Record<string, string[]> = {
|
|
|
43
44
|
shell: ['process'],
|
|
44
45
|
manage_platform: [
|
|
45
46
|
'manage_agents',
|
|
47
|
+
'manage_projects',
|
|
46
48
|
'manage_tasks',
|
|
47
49
|
'manage_schedules',
|
|
48
50
|
'manage_skills',
|
|
@@ -56,3 +56,27 @@ test('concrete tool checks inherit blocked family rules', () => {
|
|
|
56
56
|
null,
|
|
57
57
|
)
|
|
58
58
|
})
|
|
59
|
+
|
|
60
|
+
test('task and project management can be disabled from app settings', () => {
|
|
61
|
+
const decision = resolveSessionToolPolicy(
|
|
62
|
+
['manage_platform', 'manage_tasks', 'manage_projects'],
|
|
63
|
+
{
|
|
64
|
+
taskManagementEnabled: false,
|
|
65
|
+
projectManagementEnabled: false,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(decision.enabledPlugins, ['manage_platform'])
|
|
70
|
+
assert.equal(
|
|
71
|
+
decision.blockedPlugins.some((entry) => entry.tool === 'manage_tasks' && /disabled in app settings/.test(entry.reason)),
|
|
72
|
+
true,
|
|
73
|
+
)
|
|
74
|
+
assert.equal(
|
|
75
|
+
decision.blockedPlugins.some((entry) => entry.tool === 'manage_projects' && /disabled in app settings/.test(entry.reason)),
|
|
76
|
+
true,
|
|
77
|
+
)
|
|
78
|
+
assert.match(
|
|
79
|
+
resolveConcreteToolPolicyBlock('manage_tasks', decision, { taskManagementEnabled: false }),
|
|
80
|
+
/task management is disabled/i,
|
|
81
|
+
)
|
|
82
|
+
})
|
|
@@ -64,8 +64,9 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
|
64
64
|
monitor: { categories: ['execution'], concreteTools: ['monitor', 'monitor_tool'] },
|
|
65
65
|
openclaw_workspace: { categories: ['filesystem', 'platform'], concreteTools: ['openclaw_workspace'] },
|
|
66
66
|
openclaw_nodes: { categories: ['platform'], concreteTools: ['openclaw_nodes'] },
|
|
67
|
-
manage_platform: { categories: ['platform'], concreteTools: ['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets'] },
|
|
67
|
+
manage_platform: { categories: ['platform'], concreteTools: ['manage_platform', 'manage_agents', 'manage_projects', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_connectors', 'manage_sessions', 'manage_secrets'] },
|
|
68
68
|
manage_agents: { categories: ['platform'], concreteTools: ['manage_agents'] },
|
|
69
|
+
manage_projects: { categories: ['platform'], concreteTools: ['manage_projects'] },
|
|
69
70
|
manage_tasks: { categories: ['platform'], concreteTools: ['manage_tasks'] },
|
|
70
71
|
manage_schedules: { categories: ['platform'], concreteTools: ['manage_schedules'] },
|
|
71
72
|
schedule_wake: { categories: ['platform'], concreteTools: ['schedule_wake'] },
|
|
@@ -179,6 +180,24 @@ function ensureSettings(settings?: AppSettings | Record<string, unknown> | null)
|
|
|
179
180
|
return settings as Record<string, unknown>
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
export function isTaskManagementEnabled(settings?: AppSettings | Record<string, unknown> | null): boolean {
|
|
184
|
+
return ensureSettings(settings).taskManagementEnabled !== false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function isProjectManagementEnabled(settings?: AppSettings | Record<string, unknown> | null): boolean {
|
|
188
|
+
return ensureSettings(settings).projectManagementEnabled !== false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function settingsBlockReason(toolName: string, settings?: AppSettings | Record<string, unknown> | null): string | null {
|
|
192
|
+
if (toolName === 'manage_tasks' && !isTaskManagementEnabled(settings)) {
|
|
193
|
+
return 'blocked because task management is disabled in app settings'
|
|
194
|
+
}
|
|
195
|
+
if (toolName === 'manage_projects' && !isProjectManagementEnabled(settings)) {
|
|
196
|
+
return 'blocked because project management is disabled in app settings'
|
|
197
|
+
}
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
182
201
|
function parsePolicyConfig(settings: Record<string, unknown>) {
|
|
183
202
|
const mode = normalizeMode(settings.capabilityPolicyMode)
|
|
184
203
|
const safetyBlocked = new Set(getSettingsList(settings, 'safetyBlockedTools'))
|
|
@@ -216,6 +235,12 @@ export function resolveSessionToolPolicy(
|
|
|
216
235
|
|
|
217
236
|
for (const pluginName of requestedPlugins) {
|
|
218
237
|
const descriptor = TOOL_DESCRIPTORS[pluginName]
|
|
238
|
+
const settingsReason = settingsBlockReason(pluginName, normalizedSettings)
|
|
239
|
+
|
|
240
|
+
if (settingsReason) {
|
|
241
|
+
blockedPlugins.push({ tool: pluginName, reason: settingsReason, source: 'policy' })
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
219
244
|
|
|
220
245
|
if (safetyMatchesTool(safetyBlocked, pluginName, descriptor)) {
|
|
221
246
|
blockedPlugins.push({ tool: pluginName, reason: 'blocked by safety policy', source: 'safety' })
|
|
@@ -269,6 +294,9 @@ export function resolveConcreteToolPolicyBlock(
|
|
|
269
294
|
policyBlockedNames,
|
|
270
295
|
policyAllowedNames,
|
|
271
296
|
} = parsePolicyConfig(normalizedSettings)
|
|
297
|
+
const settingsReason = settingsBlockReason(name, normalizedSettings)
|
|
298
|
+
|
|
299
|
+
if (settingsReason) return settingsReason
|
|
272
300
|
|
|
273
301
|
if (safetyBlocked.has(name)) return 'blocked by safety policy'
|
|
274
302
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { getPluginManager } from './plugins'
|
|
4
|
+
import { getEnabledToolPlanningView, getToolsForCapability, TOOL_CAPABILITY } from './tool-planning'
|
|
5
|
+
|
|
6
|
+
let seq = 0
|
|
7
|
+
|
|
8
|
+
function uniquePluginId(prefix: string): string {
|
|
9
|
+
seq += 1
|
|
10
|
+
return `${prefix}_${Date.now()}_${seq}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('tool-planning', () => {
|
|
14
|
+
it('collects core planning metadata for aliased built-in tools', () => {
|
|
15
|
+
const view = getEnabledToolPlanningView(['web_search', 'web_fetch', 'browser', 'manage_connectors'])
|
|
16
|
+
|
|
17
|
+
assert.deepEqual(view.displayToolIds, ['browser', 'manage_connectors', 'web'])
|
|
18
|
+
assert.deepEqual(getToolsForCapability(['web_search'], TOOL_CAPABILITY.researchSearch), ['web_search'])
|
|
19
|
+
assert.deepEqual(getToolsForCapability(['manage_connectors'], TOOL_CAPABILITY.deliveryVoiceNote), ['connector_message_tool'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('collects planning metadata from custom plugin tools', () => {
|
|
23
|
+
const pluginId = uniquePluginId('planner_plugin')
|
|
24
|
+
getPluginManager().registerBuiltin(pluginId, {
|
|
25
|
+
name: 'Planner Plugin',
|
|
26
|
+
tools: [
|
|
27
|
+
{
|
|
28
|
+
name: 'custom_media_sender',
|
|
29
|
+
description: 'Send rendered media somewhere special.',
|
|
30
|
+
planning: {
|
|
31
|
+
capabilities: ['delivery.media', 'delivery.voice_note'],
|
|
32
|
+
disciplineGuidance: ['Use `custom_media_sender` for bespoke outbound media delivery.'],
|
|
33
|
+
},
|
|
34
|
+
parameters: { type: 'object', properties: {} },
|
|
35
|
+
execute: async () => 'ok',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const view = getEnabledToolPlanningView([pluginId])
|
|
41
|
+
assert.deepEqual(getToolsForCapability([pluginId], TOOL_CAPABILITY.deliveryMedia), ['custom_media_sender'])
|
|
42
|
+
assert.equal(view.disciplineGuidance.includes('Use `custom_media_sender` for bespoke outbound media delivery.'), true)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { PluginToolPlanning } from '@/types'
|
|
2
|
+
import { getPluginManager } from './plugins'
|
|
3
|
+
import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
|
|
4
|
+
|
|
5
|
+
export const TOOL_CAPABILITY = {
|
|
6
|
+
researchSearch: 'research.search',
|
|
7
|
+
researchFetch: 'research.fetch',
|
|
8
|
+
browserNavigate: 'browser.navigate',
|
|
9
|
+
browserCapture: 'browser.capture',
|
|
10
|
+
artifactPdf: 'artifact.pdf',
|
|
11
|
+
deliveryMessage: 'delivery.message',
|
|
12
|
+
deliveryMedia: 'delivery.media',
|
|
13
|
+
deliveryVoiceNote: 'delivery.voice_note',
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
export interface ToolPlanningEntry {
|
|
17
|
+
toolName: string
|
|
18
|
+
capabilities: string[]
|
|
19
|
+
disciplineGuidance: string[]
|
|
20
|
+
requestMatchers: NonNullable<PluginToolPlanning['requestMatchers']>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ToolPlanningView {
|
|
24
|
+
displayToolIds: string[]
|
|
25
|
+
expandedPluginIds: string[]
|
|
26
|
+
entries: ToolPlanningEntry[]
|
|
27
|
+
disciplineGuidance: string[]
|
|
28
|
+
capabilityToTools: Map<string, string[]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
32
|
+
files: [
|
|
33
|
+
{
|
|
34
|
+
toolName: 'files',
|
|
35
|
+
capabilities: ['artifact.files'],
|
|
36
|
+
disciplineGuidance: [
|
|
37
|
+
'For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.',
|
|
38
|
+
],
|
|
39
|
+
requestMatchers: [],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
shell: [
|
|
43
|
+
{
|
|
44
|
+
toolName: 'shell',
|
|
45
|
+
capabilities: ['runtime.shell'],
|
|
46
|
+
disciplineGuidance: [
|
|
47
|
+
'For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.',
|
|
48
|
+
],
|
|
49
|
+
requestMatchers: [],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
web: [
|
|
53
|
+
{
|
|
54
|
+
toolName: 'web_search',
|
|
55
|
+
capabilities: [TOOL_CAPABILITY.researchSearch],
|
|
56
|
+
disciplineGuidance: [
|
|
57
|
+
'For `web_search`, use `{"query":"..."}` to research fresh information. For current events, breaking news, or "latest" requests, start with `web_search` before summarizing.',
|
|
58
|
+
],
|
|
59
|
+
requestMatchers: [
|
|
60
|
+
{
|
|
61
|
+
capability: TOOL_CAPABILITY.researchSearch,
|
|
62
|
+
patterns: ['research', 'look up', 'find out', 'search for', 'compare', 'latest', 'news', 'headline', 'current event', 'recent update', "what's new", 'what happened'],
|
|
63
|
+
forbidLiteralUrl: true,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
toolName: 'web_fetch',
|
|
69
|
+
capabilities: [TOOL_CAPABILITY.researchFetch],
|
|
70
|
+
disciplineGuidance: [
|
|
71
|
+
'For `web_fetch`, use `{"url":"https://..."}` to read a specific page or article after you know the URL.',
|
|
72
|
+
],
|
|
73
|
+
requestMatchers: [
|
|
74
|
+
{
|
|
75
|
+
capability: TOOL_CAPABILITY.researchFetch,
|
|
76
|
+
patterns: ['read', 'summarize', 'summarise', 'analyze', 'analyse', 'extract', 'review', 'article', 'page', 'url', 'link'],
|
|
77
|
+
requireLiteralUrl: true,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
browser: [
|
|
83
|
+
{
|
|
84
|
+
toolName: 'browser',
|
|
85
|
+
capabilities: [TOOL_CAPABILITY.browserNavigate, TOOL_CAPABILITY.browserCapture, TOOL_CAPABILITY.artifactPdf],
|
|
86
|
+
disciplineGuidance: [
|
|
87
|
+
'For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.',
|
|
88
|
+
'For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.',
|
|
89
|
+
'Use `browser` when the user asks for screenshots, visual proof, page capture, PDFs, or a rendered view of a page. `navigate` alone is not a screenshot.',
|
|
90
|
+
],
|
|
91
|
+
requestMatchers: [
|
|
92
|
+
{
|
|
93
|
+
capability: TOOL_CAPABILITY.browserNavigate,
|
|
94
|
+
patterns: ['browser', 'click', 'fill form', 'log in', 'login', 'navigate'],
|
|
95
|
+
requireLiteralUrl: true,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
capability: TOOL_CAPABILITY.browserCapture,
|
|
99
|
+
patterns: ['screenshot', 'screen shot', 'snapshot', 'page capture', 'visual proof', 'capture the page', 'rendered view'],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
capability: TOOL_CAPABILITY.artifactPdf,
|
|
103
|
+
patterns: ['pdf', 'save as pdf', 'export pdf'],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
manage_connectors: [
|
|
109
|
+
{
|
|
110
|
+
toolName: 'connector_message_tool',
|
|
111
|
+
capabilities: [TOOL_CAPABILITY.deliveryMessage, TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryVoiceNote],
|
|
112
|
+
disciplineGuidance: [
|
|
113
|
+
'For outbound delivery, inspect available channels with `connector_message_tool` using `{"action":"list_running"}` before claiming something cannot be sent.',
|
|
114
|
+
'Use `connector_message_tool` with `{"action":"send","message":"...","mediaPath":"..."}` for text/media and `{"action":"send_voice_note","voiceText":"..."}` for voice notes.',
|
|
115
|
+
'If no channel or recipient is configured, explain that connector/channel setup is missing rather than claiming the capability does not exist.',
|
|
116
|
+
],
|
|
117
|
+
requestMatchers: [
|
|
118
|
+
{
|
|
119
|
+
capability: TOOL_CAPABILITY.deliveryMessage,
|
|
120
|
+
patterns: ['send', 'share', 'deliver', 'message'],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
capability: TOOL_CAPABILITY.deliveryMedia,
|
|
124
|
+
patterns: ['screenshot', 'screen shot', 'snapshot', 'image', 'photo', 'file', 'pdf', 'attachment'],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
capability: TOOL_CAPABILITY.deliveryVoiceNote,
|
|
128
|
+
patterns: ['voice note', 'voice-note', 'voicenote', 'voice memo', 'voice message', 'audio note', 'audio update', 'ptt'],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
http_request: [
|
|
134
|
+
{
|
|
135
|
+
toolName: 'http_request',
|
|
136
|
+
capabilities: ['network.http'],
|
|
137
|
+
disciplineGuidance: [
|
|
138
|
+
'For `http_request`, send exact literal URLs from the task or from prior tool results. Keep JSON request bodies as raw JSON strings.',
|
|
139
|
+
],
|
|
140
|
+
requestMatchers: [],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
email: [
|
|
144
|
+
{
|
|
145
|
+
toolName: 'email',
|
|
146
|
+
capabilities: ['delivery.email'],
|
|
147
|
+
disciplineGuidance: [
|
|
148
|
+
'For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`. If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.',
|
|
149
|
+
],
|
|
150
|
+
requestMatchers: [],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
ask_human: [
|
|
154
|
+
{
|
|
155
|
+
toolName: 'ask_human',
|
|
156
|
+
capabilities: ['human.input'],
|
|
157
|
+
disciplineGuidance: [
|
|
158
|
+
'For `ask_human`, when a workflow needs a code, approval, or out-of-band value from a person, do not guess or keep re-submitting blank forms. Use `{"action":"request_input","question":"..."}` and, for durable pauses, `{"action":"wait_for_reply","correlationId":"..."}`.',
|
|
159
|
+
],
|
|
160
|
+
requestMatchers: [],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function dedupeStrings(values: string[]): string[] {
|
|
166
|
+
return Array.from(new Set(values.filter((value) => typeof value === 'string' && value.trim()).map((value) => value.trim())))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizePlanningEntry(toolName: string, planning: PluginToolPlanning | null | undefined): ToolPlanningEntry | null {
|
|
170
|
+
if (!planning) return null
|
|
171
|
+
const capabilities = dedupeStrings(Array.isArray(planning.capabilities) ? planning.capabilities : [])
|
|
172
|
+
const disciplineGuidance = dedupeStrings(Array.isArray(planning.disciplineGuidance) ? planning.disciplineGuidance : [])
|
|
173
|
+
const requestMatchers = Array.isArray(planning.requestMatchers)
|
|
174
|
+
? planning.requestMatchers
|
|
175
|
+
.map((matcher) => ({
|
|
176
|
+
capability: typeof matcher?.capability === 'string' ? matcher.capability.trim() : '',
|
|
177
|
+
patterns: dedupeStrings(Array.isArray(matcher?.patterns) ? matcher.patterns : []),
|
|
178
|
+
requireLiteralUrl: matcher?.requireLiteralUrl === true,
|
|
179
|
+
forbidLiteralUrl: matcher?.forbidLiteralUrl === true,
|
|
180
|
+
}))
|
|
181
|
+
.filter((matcher) => matcher.capability || matcher.patterns.length > 0)
|
|
182
|
+
: []
|
|
183
|
+
if (!capabilities.length && !disciplineGuidance.length && !requestMatchers.length) return null
|
|
184
|
+
return {
|
|
185
|
+
toolName,
|
|
186
|
+
capabilities,
|
|
187
|
+
disciplineGuidance,
|
|
188
|
+
requestMatchers,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getEnabledToolPlanningView(enabledPlugins: string[]): ToolPlanningView {
|
|
193
|
+
const displayToolIds = dedupeStrings(enabledPlugins.map((toolId) => canonicalizePluginId(toolId))).sort()
|
|
194
|
+
const expandedPluginIds = dedupeStrings(expandPluginIds(enabledPlugins)).sort()
|
|
195
|
+
const entries: ToolPlanningEntry[] = []
|
|
196
|
+
|
|
197
|
+
for (const pluginId of expandedPluginIds) {
|
|
198
|
+
const coreEntries = CORE_TOOL_PLANNING[pluginId] || []
|
|
199
|
+
for (const entry of coreEntries) {
|
|
200
|
+
entries.push({
|
|
201
|
+
toolName: entry.toolName,
|
|
202
|
+
capabilities: [...entry.capabilities],
|
|
203
|
+
disciplineGuidance: [...entry.disciplineGuidance],
|
|
204
|
+
requestMatchers: [...entry.requestMatchers],
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const entry of getPluginManager().getTools(expandedPluginIds)) {
|
|
210
|
+
const planningEntry = normalizePlanningEntry(entry.tool.name, entry.tool.planning)
|
|
211
|
+
if (planningEntry) entries.push(planningEntry)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const disciplineSet = new Set<string>()
|
|
215
|
+
const capabilityToTools = new Map<string, Set<string>>()
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
for (const line of entry.disciplineGuidance) disciplineSet.add(line)
|
|
218
|
+
for (const capability of entry.capabilities) {
|
|
219
|
+
const current = capabilityToTools.get(capability) || new Set<string>()
|
|
220
|
+
current.add(entry.toolName)
|
|
221
|
+
capabilityToTools.set(capability, current)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
displayToolIds,
|
|
227
|
+
expandedPluginIds,
|
|
228
|
+
entries,
|
|
229
|
+
disciplineGuidance: Array.from(disciplineSet),
|
|
230
|
+
capabilityToTools: new Map(
|
|
231
|
+
Array.from(capabilityToTools.entries()).map(([capability, toolNames]) => [capability, Array.from(toolNames)]),
|
|
232
|
+
),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getToolsForCapability(enabledPlugins: string[], capability: string): string[] {
|
|
237
|
+
return getEnabledToolPlanningView(enabledPlugins).capabilityToTools.get(capability) || []
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getFirstToolForCapability(enabledPlugins: string[], capability: string): string | null {
|
|
241
|
+
return getToolsForCapability(enabledPlugins, capability)[0] || null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function matchToolCapabilitiesForMessage(
|
|
245
|
+
enabledPlugins: string[],
|
|
246
|
+
message: string,
|
|
247
|
+
): Map<string, string[]> {
|
|
248
|
+
const text = String(message || '').toLowerCase()
|
|
249
|
+
const hasLiteralUrl = /https?:\/\/[^\s<>"')]+/i.test(message)
|
|
250
|
+
const matches = new Map<string, Set<string>>()
|
|
251
|
+
|
|
252
|
+
for (const entry of getEnabledToolPlanningView(enabledPlugins).entries) {
|
|
253
|
+
for (const matcher of entry.requestMatchers) {
|
|
254
|
+
const patterns = Array.isArray(matcher.patterns) ? matcher.patterns : []
|
|
255
|
+
if (matcher.requireLiteralUrl === true && !hasLiteralUrl) continue
|
|
256
|
+
if (matcher.forbidLiteralUrl === true && hasLiteralUrl) continue
|
|
257
|
+
if (!patterns.length) continue
|
|
258
|
+
const matched = patterns.some((pattern) => text.includes(pattern.toLowerCase()))
|
|
259
|
+
if (!matched) continue
|
|
260
|
+
const capability = matcher.capability || entry.capabilities[0] || ''
|
|
261
|
+
if (!capability) continue
|
|
262
|
+
const current = matches.get(capability) || new Set<string>()
|
|
263
|
+
current.add(entry.toolName)
|
|
264
|
+
matches.set(capability, current)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return new Map(Array.from(matches.entries()).map(([capability, toolNames]) => [capability, Array.from(toolNames)]))
|
|
269
|
+
}
|