@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { URL } from 'url'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
5
|
+
import * as cheerio from 'cheerio'
|
|
6
|
+
import type { Plugin, PluginHooks } from '@/types'
|
|
7
|
+
import { getPluginManager } from '../plugins'
|
|
8
|
+
import { runStructuredExtraction } from '../structured-extract'
|
|
9
|
+
import type { ToolBuildContext } from './context'
|
|
10
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
11
|
+
|
|
12
|
+
interface CrawledPage {
|
|
13
|
+
url: string
|
|
14
|
+
status: number
|
|
15
|
+
title: string | null
|
|
16
|
+
depth: number
|
|
17
|
+
textPreview: string
|
|
18
|
+
headings: string[]
|
|
19
|
+
links: string[]
|
|
20
|
+
hash: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanText(value: string, max = 1200): string {
|
|
24
|
+
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
25
|
+
return normalized.length <= max ? normalized : `${normalized.slice(0, max)}...`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeUrl(input: string, base?: string): string {
|
|
29
|
+
const resolved = base ? new URL(input, base) : new URL(input)
|
|
30
|
+
resolved.hash = ''
|
|
31
|
+
if (resolved.pathname.endsWith('/') && resolved.pathname !== '/') {
|
|
32
|
+
resolved.pathname = resolved.pathname.replace(/\/+$/, '')
|
|
33
|
+
}
|
|
34
|
+
return resolved.toString()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shouldIncludeUrl(url: string, params: { includePattern?: string | null; excludePattern?: string | null }) {
|
|
38
|
+
if (params.includePattern) {
|
|
39
|
+
try {
|
|
40
|
+
if (!new RegExp(params.includePattern, 'i').test(url)) return false
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (params.excludePattern) {
|
|
46
|
+
try {
|
|
47
|
+
if (new RegExp(params.excludePattern, 'i').test(url)) return false
|
|
48
|
+
} catch {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pageHash(text: string): string {
|
|
56
|
+
return crypto.createHash('sha1').update(text).digest('hex')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchCrawlPage(url: string, depth: number): Promise<CrawledPage> {
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
62
|
+
signal: AbortSignal.timeout(15_000),
|
|
63
|
+
})
|
|
64
|
+
const html = await res.text()
|
|
65
|
+
const $ = cheerio.load(html)
|
|
66
|
+
$('script, style, noscript').remove()
|
|
67
|
+
|
|
68
|
+
const title = cleanText($('title').first().text(), 200) || null
|
|
69
|
+
const headings = $('h1, h2, h3')
|
|
70
|
+
.toArray()
|
|
71
|
+
.map((node) => cleanText($(node).text(), 200))
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.slice(0, 12)
|
|
74
|
+
const textPreview = cleanText($('body').text() || $.text(), 1600)
|
|
75
|
+
const links = $('a[href]')
|
|
76
|
+
.toArray()
|
|
77
|
+
.map((node) => $(node).attr('href') || '')
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((href) => {
|
|
80
|
+
try {
|
|
81
|
+
return normalizeUrl(href, url)
|
|
82
|
+
} catch {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.filter((href): href is string => !!href)
|
|
87
|
+
.slice(0, 200)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
url,
|
|
91
|
+
status: res.status,
|
|
92
|
+
title,
|
|
93
|
+
depth,
|
|
94
|
+
textPreview,
|
|
95
|
+
headings,
|
|
96
|
+
links: Array.from(new Set(links)),
|
|
97
|
+
hash: pageHash(`${title || ''}\n${textPreview}`),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function crawlSite(params: {
|
|
102
|
+
startUrl: string
|
|
103
|
+
limit: number
|
|
104
|
+
maxDepth: number
|
|
105
|
+
sameOrigin: boolean
|
|
106
|
+
includePattern?: string | null
|
|
107
|
+
excludePattern?: string | null
|
|
108
|
+
}): Promise<CrawledPage[]> {
|
|
109
|
+
const startUrl = normalizeUrl(params.startUrl)
|
|
110
|
+
const startOrigin = new URL(startUrl).origin
|
|
111
|
+
const queue: Array<{ url: string; depth: number }> = [{ url: startUrl, depth: 0 }]
|
|
112
|
+
const visited = new Set<string>()
|
|
113
|
+
const pages: CrawledPage[] = []
|
|
114
|
+
|
|
115
|
+
while (queue.length > 0 && pages.length < params.limit) {
|
|
116
|
+
const current = queue.shift()!
|
|
117
|
+
if (visited.has(current.url)) continue
|
|
118
|
+
visited.add(current.url)
|
|
119
|
+
if (!shouldIncludeUrl(current.url, params)) continue
|
|
120
|
+
if (params.sameOrigin && new URL(current.url).origin !== startOrigin) continue
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const page = await fetchCrawlPage(current.url, current.depth)
|
|
124
|
+
pages.push(page)
|
|
125
|
+
if (current.depth >= params.maxDepth) continue
|
|
126
|
+
for (const link of page.links) {
|
|
127
|
+
if (visited.has(link)) continue
|
|
128
|
+
if (params.sameOrigin && new URL(link).origin !== startOrigin) continue
|
|
129
|
+
queue.push({ url: link, depth: current.depth + 1 })
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// skip failed pages and continue crawling
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return pages
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function followPagination(params: {
|
|
140
|
+
startUrl: string
|
|
141
|
+
limit: number
|
|
142
|
+
}): Promise<CrawledPage[]> {
|
|
143
|
+
const pages: CrawledPage[] = []
|
|
144
|
+
const visited = new Set<string>()
|
|
145
|
+
let currentUrl = normalizeUrl(params.startUrl)
|
|
146
|
+
let depth = 0
|
|
147
|
+
|
|
148
|
+
while (currentUrl && pages.length < params.limit && !visited.has(currentUrl)) {
|
|
149
|
+
visited.add(currentUrl)
|
|
150
|
+
const page = await fetchCrawlPage(currentUrl, depth)
|
|
151
|
+
pages.push(page)
|
|
152
|
+
|
|
153
|
+
const res = await fetch(currentUrl, {
|
|
154
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
155
|
+
signal: AbortSignal.timeout(15_000),
|
|
156
|
+
})
|
|
157
|
+
const html = await res.text()
|
|
158
|
+
const $ = cheerio.load(html)
|
|
159
|
+
const nextHref = $('link[rel="next"]').attr('href')
|
|
160
|
+
|| $('a[rel="next"]').attr('href')
|
|
161
|
+
|| $('a').toArray().map((node) => ({
|
|
162
|
+
href: $(node).attr('href') || '',
|
|
163
|
+
text: cleanText($(node).text(), 80).toLowerCase(),
|
|
164
|
+
})).find((candidate) => /^(next|next page|older|more|continue)/i.test(candidate.text))?.href
|
|
165
|
+
|
|
166
|
+
if (!nextHref) break
|
|
167
|
+
try {
|
|
168
|
+
currentUrl = normalizeUrl(nextHref, currentUrl)
|
|
169
|
+
} catch {
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
depth += 1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return pages
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function dedupePages(input: CrawledPage[]): CrawledPage[] {
|
|
179
|
+
const seen = new Set<string>()
|
|
180
|
+
const out: CrawledPage[] = []
|
|
181
|
+
for (const page of input) {
|
|
182
|
+
const key = `${page.url}|${page.hash}`
|
|
183
|
+
if (seen.has(key)) continue
|
|
184
|
+
seen.add(key)
|
|
185
|
+
out.push(page)
|
|
186
|
+
}
|
|
187
|
+
return out
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function fetchSitemapUrls(sitemapUrl: string): Promise<string[]> {
|
|
191
|
+
const res = await fetch(sitemapUrl, {
|
|
192
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
193
|
+
signal: AbortSignal.timeout(15_000),
|
|
194
|
+
})
|
|
195
|
+
const xml = await res.text()
|
|
196
|
+
const matches = Array.from(xml.matchAll(/<loc>\s*([^<]+)\s*<\/loc>/gi))
|
|
197
|
+
return Array.from(new Set(matches.map((match) => match[1]?.trim()).filter((value): value is string => !!value)))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeSelectorMap(value: unknown): Record<string, string> {
|
|
201
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
202
|
+
const entries: Array<readonly [string, string]> = []
|
|
203
|
+
for (const [key, selector] of Object.entries(value as Record<string, unknown>)) {
|
|
204
|
+
if (typeof selector !== 'string') continue
|
|
205
|
+
const trimmed = selector.trim()
|
|
206
|
+
if (!trimmed) continue
|
|
207
|
+
entries.push([key, trimmed] as const)
|
|
208
|
+
}
|
|
209
|
+
return Object.fromEntries(entries)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function extractSelectorRows(urls: string[], selectors: Record<string, string>) {
|
|
213
|
+
const rows: Array<Record<string, unknown>> = []
|
|
214
|
+
for (const url of urls) {
|
|
215
|
+
const res = await fetch(url, {
|
|
216
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
|
|
217
|
+
signal: AbortSignal.timeout(15_000),
|
|
218
|
+
})
|
|
219
|
+
const html = await res.text()
|
|
220
|
+
const $ = cheerio.load(html)
|
|
221
|
+
$('script, style, noscript').remove()
|
|
222
|
+
const row: Record<string, unknown> = { url }
|
|
223
|
+
for (const [key, selector] of Object.entries(selectors)) {
|
|
224
|
+
row[key] = cleanText($(selector).first().text(), 800)
|
|
225
|
+
}
|
|
226
|
+
rows.push(row)
|
|
227
|
+
}
|
|
228
|
+
return rows
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizePagesInput(value: unknown): CrawledPage[] {
|
|
232
|
+
if (typeof value === 'string' && value.trim()) {
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(value) as CrawledPage[]
|
|
235
|
+
} catch {
|
|
236
|
+
return []
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(value)) return value as CrawledPage[]
|
|
240
|
+
return []
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveExtractionSession(bctx: ToolBuildContext) {
|
|
244
|
+
const session = bctx.resolveCurrentSession?.()
|
|
245
|
+
if (!session) throw new Error('crawl batch_extract requires an active session context.')
|
|
246
|
+
return session
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function executeCrawlAction(args: Record<string, unknown>, bctx: ToolBuildContext) {
|
|
250
|
+
const normalized = normalizeToolInputArgs(args)
|
|
251
|
+
const action = String(normalized.action || 'crawl_site').trim().toLowerCase()
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
if (action === 'status') {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
supports: ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract'],
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (action === 'dedupe_pages') {
|
|
261
|
+
const pages = dedupePages(normalizePagesInput(normalized.pages))
|
|
262
|
+
return JSON.stringify({ count: pages.length, pages })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const startUrl = typeof normalized.url === 'string'
|
|
266
|
+
? normalized.url
|
|
267
|
+
: typeof normalized.startUrl === 'string'
|
|
268
|
+
? normalized.startUrl
|
|
269
|
+
: ''
|
|
270
|
+
|
|
271
|
+
const limit = typeof normalized.limit === 'number' ? Math.max(1, Math.min(normalized.limit, 100)) : 20
|
|
272
|
+
const maxDepth = typeof normalized.maxDepth === 'number' ? Math.max(0, Math.min(normalized.maxDepth, 5)) : 2
|
|
273
|
+
const sameOrigin = normalized.sameOrigin !== false
|
|
274
|
+
|
|
275
|
+
if (action === 'crawl_site' || action === 'extract_sitemap') {
|
|
276
|
+
const sitemapUrl = typeof normalized.sitemapUrl === 'string' && normalized.sitemapUrl.trim()
|
|
277
|
+
? normalized.sitemapUrl.trim()
|
|
278
|
+
: null
|
|
279
|
+
const pages = action === 'extract_sitemap' && sitemapUrl
|
|
280
|
+
? dedupePages(await Promise.all(
|
|
281
|
+
(await fetchSitemapUrls(sitemapUrl))
|
|
282
|
+
.slice(0, limit)
|
|
283
|
+
.map((url) => fetchCrawlPage(normalizeUrl(url), 0)),
|
|
284
|
+
))
|
|
285
|
+
: dedupePages(await crawlSite({
|
|
286
|
+
startUrl,
|
|
287
|
+
limit,
|
|
288
|
+
maxDepth,
|
|
289
|
+
sameOrigin,
|
|
290
|
+
includePattern: typeof normalized.includePattern === 'string' ? normalized.includePattern : null,
|
|
291
|
+
excludePattern: typeof normalized.excludePattern === 'string' ? normalized.excludePattern : null,
|
|
292
|
+
}))
|
|
293
|
+
if (action === 'extract_sitemap') {
|
|
294
|
+
return JSON.stringify({
|
|
295
|
+
startUrl: normalizeUrl(startUrl),
|
|
296
|
+
count: pages.length,
|
|
297
|
+
urlCount: pages.length,
|
|
298
|
+
urls: pages.map((page) => page.url),
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
return JSON.stringify({
|
|
302
|
+
startUrl: normalizeUrl(startUrl),
|
|
303
|
+
count: pages.length,
|
|
304
|
+
pageCount: pages.length,
|
|
305
|
+
pages,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (action === 'follow_pagination') {
|
|
310
|
+
const pages = dedupePages(await followPagination({ startUrl, limit }))
|
|
311
|
+
return JSON.stringify({
|
|
312
|
+
startUrl: normalizeUrl(startUrl),
|
|
313
|
+
count: pages.length,
|
|
314
|
+
pageCount: pages.length,
|
|
315
|
+
pages,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (action === 'batch_extract') {
|
|
320
|
+
const seededPages = normalizePagesInput(normalized.pages)
|
|
321
|
+
if (seededPages.length === 0 && !startUrl) return 'Error: url/startUrl or pages is required.'
|
|
322
|
+
const pages = seededPages.length > 0
|
|
323
|
+
? dedupePages(seededPages)
|
|
324
|
+
: dedupePages(await crawlSite({
|
|
325
|
+
startUrl,
|
|
326
|
+
limit,
|
|
327
|
+
maxDepth,
|
|
328
|
+
sameOrigin,
|
|
329
|
+
includePattern: typeof normalized.includePattern === 'string' ? normalized.includePattern : null,
|
|
330
|
+
excludePattern: typeof normalized.excludePattern === 'string' ? normalized.excludePattern : null,
|
|
331
|
+
}))
|
|
332
|
+
const selectors = normalizeSelectorMap(normalized.selectors)
|
|
333
|
+
if (Object.keys(selectors).length > 0) {
|
|
334
|
+
const rows = await extractSelectorRows(pages.map((page) => page.url), selectors)
|
|
335
|
+
return JSON.stringify({
|
|
336
|
+
count: pages.length,
|
|
337
|
+
pageCount: pages.length,
|
|
338
|
+
rowCount: rows.length,
|
|
339
|
+
urls: pages.map((page) => page.url),
|
|
340
|
+
rows,
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
const session = resolveExtractionSession(bctx)
|
|
344
|
+
const sourceText = pages
|
|
345
|
+
.map((page) => `URL: ${page.url}\nTitle: ${page.title || ''}\nHeadings: ${page.headings.join(' | ')}\nText: ${page.textPreview}`)
|
|
346
|
+
.join('\n\n---\n\n')
|
|
347
|
+
const extracted = await runStructuredExtraction({
|
|
348
|
+
session,
|
|
349
|
+
text: sourceText,
|
|
350
|
+
schema: normalized.schema,
|
|
351
|
+
instruction: typeof normalized.instruction === 'string'
|
|
352
|
+
? normalized.instruction
|
|
353
|
+
: 'Aggregate the crawled pages and extract the requested structured information.',
|
|
354
|
+
maxChars: typeof normalized.maxChars === 'number' ? Math.max(10_000, normalized.maxChars) : 120_000,
|
|
355
|
+
})
|
|
356
|
+
return JSON.stringify({
|
|
357
|
+
count: pages.length,
|
|
358
|
+
pageCount: pages.length,
|
|
359
|
+
urls: pages.map((page) => page.url),
|
|
360
|
+
object: extracted.object,
|
|
361
|
+
validationErrors: extracted.validationErrors,
|
|
362
|
+
provider: extracted.provider,
|
|
363
|
+
model: extracted.model,
|
|
364
|
+
raw: normalized.includeRaw === true ? extracted.raw : undefined,
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!startUrl) return 'Error: url or startUrl is required.'
|
|
369
|
+
|
|
370
|
+
return `Error: Unknown action "${action}".`
|
|
371
|
+
} catch (err: unknown) {
|
|
372
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const CrawlPlugin: Plugin = {
|
|
377
|
+
name: 'Crawl',
|
|
378
|
+
enabledByDefault: false,
|
|
379
|
+
description: 'Research whole sites by crawling pages, following pagination, deduping results, and batch-extracting structure.',
|
|
380
|
+
hooks: {
|
|
381
|
+
getCapabilityDescription: () =>
|
|
382
|
+
'I can crawl websites with `crawl`, including sitemap extraction, pagination following, page deduping, and batch structured extraction over many pages.',
|
|
383
|
+
} as PluginHooks,
|
|
384
|
+
tools: [
|
|
385
|
+
{
|
|
386
|
+
name: 'crawl',
|
|
387
|
+
description: 'Site crawler. Actions: crawl_site, follow_pagination, extract_sitemap, dedupe_pages, batch_extract, status.',
|
|
388
|
+
parameters: {
|
|
389
|
+
type: 'object',
|
|
390
|
+
properties: {
|
|
391
|
+
action: {
|
|
392
|
+
type: 'string',
|
|
393
|
+
enum: ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract', 'status'],
|
|
394
|
+
},
|
|
395
|
+
url: { type: 'string' },
|
|
396
|
+
startUrl: { type: 'string' },
|
|
397
|
+
sitemapUrl: { type: 'string' },
|
|
398
|
+
pages: {},
|
|
399
|
+
limit: { type: 'number' },
|
|
400
|
+
maxDepth: { type: 'number' },
|
|
401
|
+
sameOrigin: { type: 'boolean' },
|
|
402
|
+
includePattern: { type: 'string' },
|
|
403
|
+
excludePattern: { type: 'string' },
|
|
404
|
+
selectors: {},
|
|
405
|
+
schema: {},
|
|
406
|
+
instruction: { type: 'string' },
|
|
407
|
+
maxChars: { type: 'number' },
|
|
408
|
+
includeRaw: { type: 'boolean' },
|
|
409
|
+
},
|
|
410
|
+
required: ['action'],
|
|
411
|
+
},
|
|
412
|
+
execute: async (args, context) => {
|
|
413
|
+
const syntheticBuildContext = {
|
|
414
|
+
cwd: context.session.cwd || process.cwd(),
|
|
415
|
+
ctx: { sessionId: context.session.id, agentId: context.session.agentId || null },
|
|
416
|
+
hasPlugin: () => true,
|
|
417
|
+
hasTool: () => true,
|
|
418
|
+
cleanupFns: [],
|
|
419
|
+
commandTimeoutMs: 0,
|
|
420
|
+
claudeTimeoutMs: 0,
|
|
421
|
+
cliProcessTimeoutMs: 0,
|
|
422
|
+
persistDelegateResumeId: () => undefined,
|
|
423
|
+
readStoredDelegateResumeId: () => null,
|
|
424
|
+
resolveCurrentSession: () => context.session,
|
|
425
|
+
activePlugins: context.session.plugins || [],
|
|
426
|
+
} as ToolBuildContext
|
|
427
|
+
return executeCrawlAction(args, syntheticBuildContext)
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
getPluginManager().registerBuiltin('crawl', CrawlPlugin)
|
|
434
|
+
|
|
435
|
+
export function buildCrawlTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
436
|
+
if (!bctx.hasPlugin('crawl')) return []
|
|
437
|
+
return [
|
|
438
|
+
tool(
|
|
439
|
+
async (args) => executeCrawlAction(args, bctx),
|
|
440
|
+
{
|
|
441
|
+
name: 'crawl',
|
|
442
|
+
description: CrawlPlugin.tools![0].description,
|
|
443
|
+
schema: z.object({}).passthrough(),
|
|
444
|
+
},
|
|
445
|
+
),
|
|
446
|
+
]
|
|
447
|
+
}
|
|
@@ -22,8 +22,15 @@ import {
|
|
|
22
22
|
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
23
23
|
import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
|
|
24
24
|
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
hasManagedAgentAssignmentInput,
|
|
27
|
+
isDelegationTaskPayload,
|
|
28
|
+
resolveDelegatorAgentId,
|
|
29
|
+
resolveManagedAgentAssignment,
|
|
30
|
+
validateManagedAgentAssignment,
|
|
31
|
+
} from '@/lib/server/agent-assignment'
|
|
26
32
|
import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
|
|
33
|
+
import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
|
|
27
34
|
import type { ToolBuildContext } from './context'
|
|
28
35
|
import { safePath, findBinaryOnPath } from './context'
|
|
29
36
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
@@ -137,7 +144,8 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
|
|
|
137
144
|
soul: p.soul || '',
|
|
138
145
|
provider: p.provider || 'claude-cli',
|
|
139
146
|
model: p.model || '',
|
|
140
|
-
|
|
147
|
+
platformAssignScope: p.platformAssignScope === 'all' ? 'all' : 'self',
|
|
148
|
+
isOrchestrator: p.platformAssignScope === 'all',
|
|
141
149
|
tools: p.tools || [],
|
|
142
150
|
skills: p.skills || [],
|
|
143
151
|
skillIds: p.skillIds || [],
|
|
@@ -254,19 +262,22 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
254
262
|
if (!hasPlugin(toolKey)) continue
|
|
255
263
|
|
|
256
264
|
let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
|
|
265
|
+
if (toolKey.startsWith('manage_') && toolKey !== 'manage_platform') {
|
|
266
|
+
description += `\n\nUse this direct tool name exactly as shown (\`${toolKey}\`). Do not swap it for \`manage_platform\` unless that umbrella tool is separately enabled in the current session.`
|
|
267
|
+
}
|
|
257
268
|
if (toolKey === 'manage_tasks') {
|
|
258
269
|
if (assignScope === 'self') {
|
|
259
|
-
description += `\n\
|
|
270
|
+
description += `\n\nYou may create tasks for yourself ("${ctx?.agentId || 'unknown'}") or leave them unassigned to track multi-step work. You cannot assign tasks to other agents unless a user enables "Assign to Other Agents" in your agent settings. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
|
|
260
271
|
} else {
|
|
261
|
-
description += `\n\
|
|
272
|
+
description += `\n\nYou may create tasks for yourself, leave them unassigned, or delegate them to other agents. Your agent ID is "${ctx?.agentId || 'unknown'}". When delegating, set a target agent using "agentId", "assignee", "agent", "assignedAgentId", or "assigned_agent_id". Use the target agent's exact ID when possible. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
|
|
262
273
|
}
|
|
263
274
|
} else if (toolKey === 'manage_agents') {
|
|
264
|
-
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
|
|
275
|
+
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
|
|
265
276
|
} else if (toolKey === 'manage_schedules') {
|
|
266
277
|
if (assignScope === 'self') {
|
|
267
|
-
description += `\n\
|
|
278
|
+
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
|
|
268
279
|
} else {
|
|
269
|
-
description += `\n\
|
|
280
|
+
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
|
|
270
281
|
}
|
|
271
282
|
} else if (toolKey === 'manage_webhooks') {
|
|
272
283
|
description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
|
|
@@ -337,14 +348,39 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
337
348
|
if (parsed && typeof parsed === 'object' && 'id' in parsed) {
|
|
338
349
|
delete (parsed as Record<string, unknown>).id
|
|
339
350
|
}
|
|
340
|
-
// Enforce assignment scope for tasks and schedules
|
|
341
|
-
if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
|
|
342
|
-
if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
|
|
343
|
-
return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
351
|
const now = Date.now()
|
|
352
|
+
if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
|
|
353
|
+
const agents = loadAgents()
|
|
354
|
+
const resolution = resolveManagedAgentAssignment(
|
|
355
|
+
parsed as Record<string, unknown>,
|
|
356
|
+
agents,
|
|
357
|
+
toolKey === 'manage_tasks' || toolKey === 'manage_schedules'
|
|
358
|
+
? (parsed.agentId || ctx?.agentId || null)
|
|
359
|
+
: null,
|
|
360
|
+
{ allowDescription: toolKey === 'manage_tasks' },
|
|
361
|
+
)
|
|
362
|
+
const assignmentError = validateManagedAgentAssignment({
|
|
363
|
+
resourceLabel: res.label,
|
|
364
|
+
agents,
|
|
365
|
+
assignScope,
|
|
366
|
+
currentAgentId: ctx?.agentId || null,
|
|
367
|
+
targetAgentId: resolution.agentId,
|
|
368
|
+
unresolvedReference: resolution.unresolvedReference,
|
|
369
|
+
isDelegation: toolKey === 'manage_tasks' ? isDelegationTaskPayload(parsed as Record<string, unknown>) : false,
|
|
370
|
+
delegatorAgentId: toolKey === 'manage_tasks'
|
|
371
|
+
? resolveDelegatorAgentId(parsed as Record<string, unknown>, agents, ctx?.agentId || null)
|
|
372
|
+
: null,
|
|
373
|
+
})
|
|
374
|
+
if (assignmentError) return assignmentError
|
|
375
|
+
parsed.agentId = resolution.agentId
|
|
376
|
+
}
|
|
347
377
|
if (toolKey === 'manage_schedules') {
|
|
378
|
+
const normalizedSchedule = normalizeSchedulePayload(parsed as Record<string, unknown>, {
|
|
379
|
+
cwd,
|
|
380
|
+
now,
|
|
381
|
+
})
|
|
382
|
+
if (!normalizedSchedule.ok) return normalizedSchedule.error
|
|
383
|
+
Object.assign(parsed, normalizedSchedule.value)
|
|
348
384
|
const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
|
|
349
385
|
agentId: parsed.agentId || null,
|
|
350
386
|
taskPrompt: parsed.taskPrompt || '',
|
|
@@ -387,23 +423,6 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
387
423
|
})
|
|
388
424
|
}
|
|
389
425
|
}
|
|
390
|
-
// @mention agent resolution for tasks
|
|
391
|
-
if (toolKey === 'manage_tasks' && parsed.description) {
|
|
392
|
-
const agents = loadAgents()
|
|
393
|
-
parsed.agentId = resolveTaskAgentFromDescription(
|
|
394
|
-
parsed.description,
|
|
395
|
-
parsed.agentId || ctx?.agentId || '',
|
|
396
|
-
agents,
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
// Agents cannot create tasks for themselves — just do the work directly.
|
|
400
|
-
// Tasks are for delegating to other agents or user-created work items.
|
|
401
|
-
if (toolKey === 'manage_tasks' && ctx?.agentId) {
|
|
402
|
-
const resolvedAgentId = parsed.agentId || ctx.agentId
|
|
403
|
-
if (resolvedAgentId === ctx.agentId) {
|
|
404
|
-
return 'Error: You cannot create tasks for yourself — just do the work directly. Tasks are for delegating work to other agents. If you need to track progress, use memory instead.'
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
426
|
if (toolKey === 'manage_tasks') {
|
|
408
427
|
parsed.title = deriveTaskTitle(parsed)
|
|
409
428
|
if (!parsed.title || /^untitled task$/i.test(parsed.title)) {
|
|
@@ -507,13 +526,56 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
507
526
|
? normalizeTaskQualityGate(parsed.qualityGate, settings)
|
|
508
527
|
: null
|
|
509
528
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
529
|
+
if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
|
|
530
|
+
const agents = loadAgents()
|
|
531
|
+
const requestedClear = Object.prototype.hasOwnProperty.call(parsed, 'agentId') && parsed.agentId == null
|
|
532
|
+
const shouldResolveAssignment = requestedClear
|
|
533
|
+
|| hasManagedAgentAssignmentInput(parsed as Record<string, unknown>)
|
|
534
|
+
if (shouldResolveAssignment) {
|
|
535
|
+
const resolution = resolveManagedAgentAssignment(
|
|
536
|
+
parsed as Record<string, unknown>,
|
|
537
|
+
agents,
|
|
538
|
+
null,
|
|
539
|
+
{ allowDescription: false },
|
|
540
|
+
)
|
|
541
|
+
const assignmentError = validateManagedAgentAssignment({
|
|
542
|
+
resourceLabel: res.label,
|
|
543
|
+
agents,
|
|
544
|
+
assignScope,
|
|
545
|
+
currentAgentId: ctx?.agentId || null,
|
|
546
|
+
targetAgentId: requestedClear ? null : resolution.agentId,
|
|
547
|
+
unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
|
|
548
|
+
isDelegation: toolKey === 'manage_tasks'
|
|
549
|
+
? isDelegationTaskPayload({
|
|
550
|
+
...all[id],
|
|
551
|
+
...parsed,
|
|
552
|
+
agentId: requestedClear ? null : resolution.agentId,
|
|
553
|
+
} as Record<string, unknown>)
|
|
554
|
+
: false,
|
|
555
|
+
delegatorAgentId: toolKey === 'manage_tasks'
|
|
556
|
+
? resolveDelegatorAgentId({
|
|
557
|
+
...all[id],
|
|
558
|
+
...parsed,
|
|
559
|
+
} as Record<string, unknown>, agents, ctx?.agentId || null)
|
|
560
|
+
: null,
|
|
561
|
+
})
|
|
562
|
+
if (assignmentError) return assignmentError
|
|
563
|
+
if (!requestedClear) parsed.agentId = resolution.agentId
|
|
514
564
|
}
|
|
515
565
|
}
|
|
516
566
|
all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
|
|
567
|
+
if (toolKey === 'manage_schedules') {
|
|
568
|
+
const normalizedSchedule = normalizeSchedulePayload(all[id] as Record<string, unknown>, {
|
|
569
|
+
cwd,
|
|
570
|
+
now: Date.now(),
|
|
571
|
+
})
|
|
572
|
+
if (!normalizedSchedule.ok) return normalizedSchedule.error
|
|
573
|
+
all[id] = {
|
|
574
|
+
...all[id],
|
|
575
|
+
...normalizedSchedule.value,
|
|
576
|
+
updatedAt: Date.now(),
|
|
577
|
+
}
|
|
578
|
+
}
|
|
517
579
|
if (toolKey === 'manage_secrets') {
|
|
518
580
|
if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
|
|
519
581
|
const nextScope = parsed.scope === 'agent'
|