Skip to content

Commit 7a8ee1b

Browse files
committed
feat: add bounded web crawl tools
1 parent c2760e3 commit 7a8ee1b

24 files changed

Lines changed: 512 additions & 53 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
409409

410410
## Releases
411411

412+
### v1.9.22 Highlights
413+
414+
Research tools release: agents now get direct `web_extract` and `web_crawl` tools alongside `web_search`, `web_fetch`, and the unified `web` tool.
415+
416+
- **Source-grounded extraction.** `web_extract` returns a page title, canonical URL, and readable content for known source URLs.
417+
- **Bounded crawls.** `web_crawl` walks same-origin links by default with conservative page and depth caps, plus an explicit external-link opt-in.
418+
- **Better routing.** Tool aliases, capability policy, planning hints, continuation recovery, and the chat UI all recognize the granular research tools.
419+
- **Regression coverage.** New tests cover action inference, tool-call translation, direct tool registration, extraction cleanup, and same-origin crawl bounds.
420+
412421
### v1.9.21 Highlights
413422

414423
Provider diagnostics release: connection checks now return a structured step timeline across setup, provider settings, and agent editing.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.9.21",
3+
"version": "1.9.22",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",
@@ -88,7 +88,7 @@
8888
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
8989
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
9090
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
9292
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
9393
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
9494
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",

src/components/chat/activity-moment.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
1919
delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
2020
check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
2121
web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
22+
web_fetch: { label: 'Read a web page', color: '#22C55E', icon: 'search' },
23+
web_extract: { label: 'Extracted a web page', color: '#22C55E', icon: 'search' },
24+
web_crawl: { label: 'Crawled a site', color: '#22C55E', icon: 'search' },
2225
connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
2326
}
2427

@@ -35,6 +38,7 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
3538
if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
3639
if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
3740
if (toolName === 'web_search' && parsed.query) return parsed.query
41+
if ((toolName === 'web_fetch' || toolName === 'web_extract' || toolName === 'web_crawl') && parsed.url) return parsed.url
3842
if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
3943
} catch { /* ignore parse errors */ }
4044
return null

src/components/chat/tool-call-bubble.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const TOOL_COLORS: Record<string, string> = {
2020
create_spreadsheet: '#10B981',
2121
web_search: '#3B82F6',
2222
web_fetch: '#3B82F6',
23+
web_extract: '#3B82F6',
24+
web_crawl: '#3B82F6',
2325
spawn_subagent: '#8B5CF6',
2426
delegate_to_agent: '#6366F1',
2527
check_delegation_status: '#6366F1',
@@ -77,6 +79,8 @@ export const TOOL_LABELS: Record<string, string> = {
7779
create_spreadsheet: 'Create Spreadsheet',
7880
web_search: 'Web Search',
7981
web_fetch: 'Web Fetch',
82+
web_extract: 'Web Extract',
83+
web_crawl: 'Web Crawl',
8084
claude_code: 'Claude Code',
8185
codex_cli: 'Codex CLI',
8286
opencode_cli: 'OpenCode CLI',
@@ -127,6 +131,8 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
127131
create_spreadsheet: 'Create Excel or CSV files from structured data',
128132
web_search: 'Search the web for information',
129133
web_fetch: 'Fetch and read web page content',
134+
web_extract: 'Extract readable content from a source URL',
135+
web_crawl: 'Crawl a bounded set of pages from one site',
130136
claude_code: 'Enable delegation to Claude Code CLI',
131137
codex_cli: 'Enable delegation to OpenAI Codex CLI',
132138
opencode_cli: 'Enable delegation to OpenCode CLI',

src/lib/server/capability-router.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
2626
test('routeTaskIntent keeps hybrid research-plus-media prompts in research intent', () => {
2727
const decision = routeTaskIntent(
2828
'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?',
29-
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
29+
['web_search', 'web_fetch', 'web_crawl', 'browser', 'manage_connectors'],
3030
null,
3131
makeClassification({
3232
taskIntent: 'research',
@@ -39,7 +39,7 @@ test('routeTaskIntent keeps hybrid research-plus-media prompts in research inten
3939
)
4040

4141
assert.equal(decision.intent, 'research')
42-
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'browser', 'connector_message_tool'])
42+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser', 'connector_message_tool'])
4343
})
4444

4545
test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
@@ -72,7 +72,7 @@ test('routeTaskIntent treats keep-watching update requests as research even with
7272
)
7373

7474
assert.equal(decision.intent, 'research')
75-
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch'])
75+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'web_extract', 'web_crawl'])
7676
})
7777

7878
test('routeTaskIntent uses structured classification when available', () => {
@@ -99,7 +99,7 @@ test('routeTaskIntent uses structured classification when available', () => {
9999
)
100100

101101
assert.equal(decision.intent, 'browsing')
102-
assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch'])
102+
assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch', 'web_extract'])
103103
})
104104

105105
function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {

src/lib/server/capability-router.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export function routeTaskIntent(
144144
[
145145
TOOL_CAPABILITY.researchSearch,
146146
TOOL_CAPABILITY.researchFetch,
147+
TOOL_CAPABILITY.researchCrawl,
147148
...(wantsScreenshots ? [TOOL_CAPABILITY.browserCapture] : []),
148149
...(wantsVoiceDelivery ? [TOOL_CAPABILITY.deliveryVoiceNote] : []),
149150
...(wantsOutboundDelivery ? [TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryMessage] : []),

src/lib/server/chat-execution/chat-execution-advanced.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,33 @@ describe('translateRequestedToolInvocation advanced', () => {
407407
assert.equal(args.action, 'search')
408408
assert.equal(args.query, 'test query')
409409
})
410+
411+
it('maps web_extract to web with action=extract', () => {
412+
const { toolName, args } = translateRequestedToolInvocation(
413+
'web_extract',
414+
{ url: 'https://example.com/source' },
415+
'',
416+
['web'],
417+
)
418+
assert.equal(toolName, 'web')
419+
assert.equal(args.action, 'extract')
420+
assert.equal(args.url, 'https://example.com/source')
421+
})
422+
423+
it('maps web_crawl to web with bounded crawl arguments', () => {
424+
const { toolName, args } = translateRequestedToolInvocation(
425+
'web_crawl',
426+
{ url: 'https://example.com/', maxPages: 4, maxDepth: 1, includeExternal: false },
427+
'',
428+
['web'],
429+
)
430+
assert.equal(toolName, 'web')
431+
assert.equal(args.action, 'crawl')
432+
assert.equal(args.url, 'https://example.com/')
433+
assert.equal(args.maxPages, 4)
434+
assert.equal(args.maxDepth, 1)
435+
assert.equal(args.includeExternal, false)
436+
})
410437
})
411438

412439
// ---------------------------------------------------------------------------

src/lib/server/chat-execution/chat-execution-utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,27 @@ export function translateRequestedToolInvocation(
127127
},
128128
}
129129
}
130+
if (requestedName === 'web_extract') {
131+
return {
132+
toolName: 'web',
133+
args: {
134+
action: 'extract',
135+
url: rawArgs.url,
136+
},
137+
}
138+
}
139+
if (requestedName === 'web_crawl') {
140+
return {
141+
toolName: 'web',
142+
args: {
143+
action: 'crawl',
144+
url: rawArgs.url || rawArgs.query,
145+
maxPages: rawArgs.maxPages ?? rawArgs.maxResults,
146+
maxDepth: rawArgs.maxDepth,
147+
includeExternal: rawArgs.includeExternal,
148+
},
149+
}
150+
}
130151
if (requestedName === 'delegate_to_claude_code') {
131152
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
132153
}

src/lib/server/chat-execution/iteration-event-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ export async function processIterationEvents(opts: ProcessIterationEventsOpts):
349349
}
350350
if (
351351
boundedExternalExecutionTask
352-
&& ['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(toolName)
352+
&& ['http_request', 'web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser'].includes(toolName)
353353
&& countExternalExecutionResearchSteps(state.streamedToolEvents) >= 5
354354
&& countDistinctExternalResearchHosts(state.streamedToolEvents) >= 3
355355
) {

0 commit comments

Comments
 (0)