@swarmclawai/swarmclaw 0.7.2 → 0.7.3
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 +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- 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]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- 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/files/open/route.ts +16 -14
- 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/skills/route.ts +11 -3
- 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/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- 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 +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- 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 +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- 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 +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -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 +245 -46
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- 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/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -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 +36 -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 +134 -0
- package/src/lib/server/chat-execution.ts +250 -61
- 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 +67 -2
- package/src/lib/server/chatroom-helpers.ts +45 -5
- 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 +946 -110
- 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 +188 -9
- 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 +59 -1
- 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/heartbeat-service.ts +13 -39
- 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 +27 -967
- 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 +5 -6
- 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 +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- 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 +105 -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 +70 -32
- 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.ts +22 -4
- 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 +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +237 -24
- 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 +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- 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 +86 -23
- 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/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -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 +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- 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 -10
- package/src/lib/server/tool-aliases.ts +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- 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/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- 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
|
@@ -1,14 +1,127 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
+
import os from 'os'
|
|
4
5
|
import path from 'path'
|
|
5
6
|
import { UPLOAD_DIR } from '../storage'
|
|
7
|
+
import { WORKSPACE_DIR } from '../data-dir'
|
|
6
8
|
import type { ToolBuildContext } from './context'
|
|
7
9
|
import { safePath, truncate, listDirRecursive, MAX_FILE } from './context'
|
|
8
10
|
import type { Plugin, PluginHooks } from '@/types'
|
|
9
11
|
import { getPluginManager } from '../plugins'
|
|
10
12
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
11
13
|
|
|
14
|
+
function pickNonEmptyString(...values: unknown[]): string | undefined {
|
|
15
|
+
for (const value of values) {
|
|
16
|
+
if (typeof value !== 'string') continue
|
|
17
|
+
const trimmed = value.trim()
|
|
18
|
+
if (trimmed) return trimmed
|
|
19
|
+
}
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pickStringValue(...values: unknown[]): string | undefined {
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
if (typeof value === 'string') return value
|
|
26
|
+
}
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getFileEntryPath(entry: Record<string, unknown> | undefined): string | undefined {
|
|
31
|
+
if (!entry) return undefined
|
|
32
|
+
return pickNonEmptyString(
|
|
33
|
+
entry.path,
|
|
34
|
+
entry.filePath,
|
|
35
|
+
entry.filename,
|
|
36
|
+
entry.fileName,
|
|
37
|
+
entry.name,
|
|
38
|
+
entry.targetPath,
|
|
39
|
+
entry.target,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getFileEntryContent(entry: Record<string, unknown> | undefined): string | undefined {
|
|
44
|
+
if (!entry) return undefined
|
|
45
|
+
const raw = entry.content ?? entry.text ?? entry.contents ?? entry.value ?? entry.body
|
|
46
|
+
if (raw === undefined || raw === null) return undefined
|
|
47
|
+
return typeof raw === 'string' ? raw : JSON.stringify(raw)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function inferFileAction(
|
|
51
|
+
normalized: Record<string, unknown>,
|
|
52
|
+
files: Array<Record<string, unknown>> | undefined,
|
|
53
|
+
filePath: string | undefined,
|
|
54
|
+
dirPath: string | undefined,
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const fileHasContent = Array.isArray(files) && files.some((entry) => getFileEntryContent(entry) !== undefined)
|
|
57
|
+
if (fileHasContent) return 'write'
|
|
58
|
+
if (getFileEntryContent(normalized) !== undefined) return 'write'
|
|
59
|
+
if (dirPath) return 'list'
|
|
60
|
+
if (filePath) return 'read'
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeFileArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
|
|
65
|
+
const normalized = normalizeToolInputArgs(rawArgs)
|
|
66
|
+
const actionPayload = ['read', 'write', 'list', 'copy', 'move', 'delete']
|
|
67
|
+
.map((candidate) => {
|
|
68
|
+
const value = normalized[candidate]
|
|
69
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
70
|
+
? { action: candidate, value: value as Record<string, unknown> }
|
|
71
|
+
: null
|
|
72
|
+
})
|
|
73
|
+
.find(Boolean)
|
|
74
|
+
const merged = {
|
|
75
|
+
...normalized,
|
|
76
|
+
...(actionPayload?.value || {}),
|
|
77
|
+
}
|
|
78
|
+
const files = Array.isArray(merged.files)
|
|
79
|
+
? merged.files.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
82
|
+
let action = pickNonEmptyString(normalized.action, actionPayload?.action)
|
|
83
|
+
if (!action && Array.isArray(files) && files.length > 0) {
|
|
84
|
+
action = pickNonEmptyString(files[0].action)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filePath = pickNonEmptyString(
|
|
88
|
+
merged.filePath,
|
|
89
|
+
merged.filepath,
|
|
90
|
+
merged.path,
|
|
91
|
+
merged.name,
|
|
92
|
+
merged.filename,
|
|
93
|
+
merged.fileName,
|
|
94
|
+
merged.file,
|
|
95
|
+
merged.targetPath,
|
|
96
|
+
merged.target,
|
|
97
|
+
)
|
|
98
|
+
const dirPath = pickNonEmptyString(
|
|
99
|
+
merged.dirPath,
|
|
100
|
+
merged.directory,
|
|
101
|
+
merged.directoryPath,
|
|
102
|
+
merged.dir,
|
|
103
|
+
merged.folder,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if (!action) {
|
|
107
|
+
action = inferFileAction(merged, files, filePath, dirPath)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
action,
|
|
112
|
+
files,
|
|
113
|
+
encoding: pickNonEmptyString(merged.encoding),
|
|
114
|
+
filePath,
|
|
115
|
+
content: pickStringValue(merged.content, merged.text, merged.contents, merged.value, merged.body),
|
|
116
|
+
dirPath,
|
|
117
|
+
sourcePath: pickNonEmptyString(merged.sourcePath, merged.source, merged.from, merged.src),
|
|
118
|
+
destinationPath: pickNonEmptyString(merged.destinationPath, merged.destination, merged.to, merged.dest),
|
|
119
|
+
overwrite: !!merged.overwrite,
|
|
120
|
+
recursive: !!merged.recursive,
|
|
121
|
+
force: !!merged.force,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
12
125
|
function resolveFileToolPath(cwd: string, target: string): string {
|
|
13
126
|
try {
|
|
14
127
|
return safePath(cwd, target)
|
|
@@ -21,23 +134,16 @@ function resolveFileToolPath(cwd: string, target: string): string {
|
|
|
21
134
|
/**
|
|
22
135
|
* Unified File Execution Logic
|
|
23
136
|
*/
|
|
24
|
-
async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: string }) {
|
|
25
|
-
const normalized =
|
|
26
|
-
// Normalize filePath/content for single-file mode
|
|
137
|
+
export async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: string }) {
|
|
138
|
+
const normalized = normalizeFileArgs(args)
|
|
27
139
|
const files = normalized.files as Array<Record<string, unknown>> | undefined
|
|
28
|
-
|
|
140
|
+
const action = normalized.action as string | undefined
|
|
29
141
|
const encoding = normalized.encoding as string | undefined
|
|
30
|
-
|
|
31
|
-
// Resiliency: check if action is buried in the files array
|
|
32
|
-
if (!action && Array.isArray(files) && files.length > 0) {
|
|
33
|
-
action = files[0].action as string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const filePath = (normalized.filePath || normalized.path) as string | undefined
|
|
142
|
+
const filePath = normalized.filePath as string | undefined
|
|
37
143
|
const content = normalized.content as string | undefined
|
|
38
|
-
const dirPath =
|
|
39
|
-
const sourcePath =
|
|
40
|
-
const destinationPath =
|
|
144
|
+
const dirPath = normalized.dirPath as string | undefined
|
|
145
|
+
const sourcePath = normalized.sourcePath as string | undefined
|
|
146
|
+
const destinationPath = normalized.destinationPath as string | undefined
|
|
41
147
|
const overwrite = !!normalized.overwrite
|
|
42
148
|
const recursive = !!normalized.recursive
|
|
43
149
|
const force = !!normalized.force
|
|
@@ -45,7 +151,7 @@ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: str
|
|
|
45
151
|
try {
|
|
46
152
|
switch (action) {
|
|
47
153
|
case 'read': {
|
|
48
|
-
const target = filePath || (files?.[0]
|
|
154
|
+
const target = filePath || getFileEntryPath(files?.[0])
|
|
49
155
|
if (!target) return 'Error: no filePath or path provided.'
|
|
50
156
|
const resolved = resolveFileToolPath(bctx.cwd, target)
|
|
51
157
|
return truncate(fs.readFileSync(resolved, 'utf-8'), MAX_FILE)
|
|
@@ -57,9 +163,15 @@ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: str
|
|
|
57
163
|
const results: string[] = []
|
|
58
164
|
|
|
59
165
|
for (const file of filesToWrite) {
|
|
60
|
-
const targetPath = (file
|
|
166
|
+
const targetPath = getFileEntryPath(file)
|
|
61
167
|
if (!targetPath) continue
|
|
62
|
-
const fileContent = (file
|
|
168
|
+
const fileContent = getFileEntryContent(file) ?? ''
|
|
169
|
+
if (/[\\/]$/.test(targetPath)) {
|
|
170
|
+
const resolvedDir = resolveFileToolPath(bctx.cwd, targetPath)
|
|
171
|
+
fs.mkdirSync(resolvedDir, { recursive: true })
|
|
172
|
+
results.push(`Created directory ${targetPath}`)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
63
175
|
|
|
64
176
|
const resolved = resolveFileToolPath(bctx.cwd, targetPath)
|
|
65
177
|
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
@@ -106,7 +218,7 @@ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: str
|
|
|
106
218
|
}
|
|
107
219
|
|
|
108
220
|
case 'delete': {
|
|
109
|
-
const target = filePath || (files?.[0]
|
|
221
|
+
const target = filePath || getFileEntryPath(files?.[0])
|
|
110
222
|
if (!target) return 'Error: no filePath or path provided.'
|
|
111
223
|
const resolved = resolveFileToolPath(bctx.cwd, target)
|
|
112
224
|
if (resolved === path.resolve(bctx.cwd) || resolved === path.resolve(process.cwd())) return 'Error: cannot delete root'
|
|
@@ -126,7 +238,17 @@ function collectSendFilePaths(payload: unknown, into: string[]): void {
|
|
|
126
238
|
if (!payload) return
|
|
127
239
|
if (typeof payload === 'string') {
|
|
128
240
|
const trimmed = payload.trim()
|
|
129
|
-
if (trimmed)
|
|
241
|
+
if (trimmed) {
|
|
242
|
+
const extracted = new Set<string>()
|
|
243
|
+
const uploadMatches = trimmed.match(/(?:sandbox:)?\/api\/uploads\/[^\s)\]]+/g) || []
|
|
244
|
+
for (const match of uploadMatches) extracted.add(match)
|
|
245
|
+
const markdownMatches = [...trimmed.matchAll(/\]\(((?:sandbox:)?\/api\/uploads\/[^\s)]+|(?:\.{1,2}\/|\/)?[^\s)]+\.(?:png|jpg|jpeg|gif|webp|pdf|md|txt|html|json|csv|yml|yaml))\)/gi)]
|
|
246
|
+
for (const match of markdownMatches) {
|
|
247
|
+
if (typeof match[1] === 'string' && match[1].trim()) extracted.add(match[1].trim())
|
|
248
|
+
}
|
|
249
|
+
if (extracted.size === 0) extracted.add(trimmed)
|
|
250
|
+
for (const candidate of extracted) into.push(candidate)
|
|
251
|
+
}
|
|
130
252
|
return
|
|
131
253
|
}
|
|
132
254
|
if (Array.isArray(payload)) {
|
|
@@ -135,15 +257,29 @@ function collectSendFilePaths(payload: unknown, into: string[]): void {
|
|
|
135
257
|
}
|
|
136
258
|
if (typeof payload !== 'object') return
|
|
137
259
|
const record = payload as Record<string, unknown>
|
|
260
|
+
if (record.filePaths !== undefined) collectSendFilePaths(record.filePaths, into)
|
|
138
261
|
if (typeof record.filePath === 'string') into.push(record.filePath)
|
|
262
|
+
if (typeof record.filepath === 'string') into.push(record.filepath)
|
|
263
|
+
if (typeof record.fileId === 'string') into.push(record.fileId)
|
|
264
|
+
if (typeof record.id === 'string') into.push(record.id)
|
|
139
265
|
if (typeof record.path === 'string') into.push(record.path)
|
|
266
|
+
if (typeof record.filename === 'string') into.push(record.filename)
|
|
267
|
+
if (typeof record.fileName === 'string') into.push(record.fileName)
|
|
268
|
+
if (typeof record.name === 'string') into.push(record.name)
|
|
269
|
+
if (typeof record.targetPath === 'string') into.push(record.targetPath)
|
|
270
|
+
if (typeof record.target === 'string') into.push(record.target)
|
|
140
271
|
if (record.files !== undefined) collectSendFilePaths(record.files, into)
|
|
141
272
|
}
|
|
142
273
|
|
|
143
274
|
export function normalizeSendFilePaths(args: Record<string, unknown>): string[] {
|
|
144
275
|
const candidates: string[] = []
|
|
276
|
+
collectSendFilePaths(args.filePaths, candidates)
|
|
145
277
|
collectSendFilePaths(args.filePath, candidates)
|
|
278
|
+
collectSendFilePaths(args.filepath, candidates)
|
|
146
279
|
collectSendFilePaths(args.path, candidates)
|
|
280
|
+
collectSendFilePaths(args.filename, candidates)
|
|
281
|
+
collectSendFilePaths(args.fileName, candidates)
|
|
282
|
+
collectSendFilePaths(args.name, candidates)
|
|
147
283
|
collectSendFilePaths(args.file, candidates)
|
|
148
284
|
collectSendFilePaths(args.files, candidates)
|
|
149
285
|
|
|
@@ -167,17 +303,94 @@ export function normalizeSendFilePaths(args: Record<string, unknown>): string[]
|
|
|
167
303
|
return [...deduped]
|
|
168
304
|
}
|
|
169
305
|
|
|
306
|
+
function collectRecentFiles(
|
|
307
|
+
root: string,
|
|
308
|
+
currentDir: string,
|
|
309
|
+
maxAgeMs: number,
|
|
310
|
+
into: string[],
|
|
311
|
+
depth: number,
|
|
312
|
+
): void {
|
|
313
|
+
if (depth > 3) return
|
|
314
|
+
let entries: fs.Dirent[] = []
|
|
315
|
+
try {
|
|
316
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
317
|
+
} catch {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (entry.name.startsWith('.')) continue
|
|
323
|
+
if (entry.isDirectory()) {
|
|
324
|
+
if (entry.name === 'node_modules' || entry.name === '.next') continue
|
|
325
|
+
collectRecentFiles(root, path.join(currentDir, entry.name), maxAgeMs, into, depth + 1)
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
if (!entry.isFile()) continue
|
|
329
|
+
const absolute = path.join(currentDir, entry.name)
|
|
330
|
+
let stat: fs.Stats | null = null
|
|
331
|
+
try {
|
|
332
|
+
stat = fs.statSync(absolute)
|
|
333
|
+
} catch {
|
|
334
|
+
stat = null
|
|
335
|
+
}
|
|
336
|
+
if (!stat) continue
|
|
337
|
+
if (Date.now() - stat.mtimeMs > maxAgeMs) continue
|
|
338
|
+
into.push(path.relative(root, absolute))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function findRecentSendFileFallbackPaths(cwd: string, maxAgeMs = 10 * 60 * 1000): string[] {
|
|
343
|
+
const resolvedRoot = path.resolve(cwd)
|
|
344
|
+
const candidates: string[] = []
|
|
345
|
+
collectRecentFiles(resolvedRoot, resolvedRoot, maxAgeMs, candidates, 0)
|
|
346
|
+
return [...new Set(candidates)]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function resolveSendFileSourcePath(cwd: string, rawPath: string): string {
|
|
350
|
+
const trimmed = rawPath.trim()
|
|
351
|
+
const uploadMatch = trimmed.match(/^(?:sandbox:)?\/api\/uploads\/(.+)$/)
|
|
352
|
+
if (uploadMatch) {
|
|
353
|
+
return path.join(UPLOAD_DIR, path.basename(uploadMatch[1]))
|
|
354
|
+
}
|
|
355
|
+
const browserProfileIdx = trimmed.lastIndexOf('.swarmclaw/browser-profiles/')
|
|
356
|
+
if (browserProfileIdx !== -1) {
|
|
357
|
+
const relative = trimmed.slice(browserProfileIdx)
|
|
358
|
+
return path.join(os.homedir(), relative)
|
|
359
|
+
}
|
|
360
|
+
if (trimmed.startsWith('browser-profiles/')) {
|
|
361
|
+
const candidate = path.join(os.homedir(), '.swarmclaw', trimmed)
|
|
362
|
+
if (fs.existsSync(candidate)) return candidate
|
|
363
|
+
}
|
|
364
|
+
if (trimmed === '/workspace' || trimmed === 'workspace') return cwd
|
|
365
|
+
if (trimmed.startsWith('/workspace/') || trimmed.startsWith('workspace/')) {
|
|
366
|
+
const relative = trimmed.replace(/^\/?workspace\/?/, '')
|
|
367
|
+
const sessionScoped = path.resolve(cwd, relative)
|
|
368
|
+
if (fs.existsSync(sessionScoped)) return sessionScoped
|
|
369
|
+
return path.resolve(WORKSPACE_DIR, relative)
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
return safePath(cwd, trimmed)
|
|
373
|
+
} catch (err: unknown) {
|
|
374
|
+
if (path.isAbsolute(trimmed)) return trimmed
|
|
375
|
+
throw err
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
170
379
|
async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: string }) {
|
|
171
380
|
try {
|
|
172
|
-
const
|
|
381
|
+
const explicitPaths = normalizeSendFilePaths(args)
|
|
382
|
+
const paths = explicitPaths.length > 0 ? explicitPaths : findRecentSendFileFallbackPaths(bctx.cwd)
|
|
173
383
|
if (paths.length === 0) {
|
|
174
384
|
return 'Error: filePath/path is required (or provide files[] / input.files[]).'
|
|
175
385
|
}
|
|
386
|
+
if (explicitPaths.length === 0 && paths.length !== 1) {
|
|
387
|
+
return 'Error: filePath/path is required (or provide files[] / input.files[]).'
|
|
388
|
+
}
|
|
176
389
|
|
|
177
390
|
const links: string[] = []
|
|
178
391
|
const errors: string[] = []
|
|
179
392
|
for (const rawPath of paths) {
|
|
180
|
-
const resolved =
|
|
393
|
+
const resolved = resolveSendFileSourcePath(bctx.cwd, rawPath)
|
|
181
394
|
if (!fs.existsSync(resolved)) {
|
|
182
395
|
errors.push(`file not found: ${rawPath}`)
|
|
183
396
|
continue
|
|
@@ -204,12 +417,12 @@ const FilePlugin: Plugin = {
|
|
|
204
417
|
name: 'Core Files',
|
|
205
418
|
description: 'Complete file management: read, write, list, move, copy, delete, and send.',
|
|
206
419
|
hooks: {
|
|
207
|
-
getCapabilityDescription: () => 'I can read, write, copy, move, and send files (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). Deleting files is destructive, so that may need explicit permission.',
|
|
420
|
+
getCapabilityDescription: () => 'I can read, write, copy, move, and send files (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). When writing, I should always provide a target path (`filePath`, `path`, `filename`, or `name`) and the content (`content`, `text`, or `body`). When `send_file` returns a download link, I should copy that link exactly instead of rewriting it. Deleting files is destructive, so that may need explicit permission.',
|
|
208
421
|
} as PluginHooks,
|
|
209
422
|
tools: [
|
|
210
423
|
{
|
|
211
424
|
name: 'files',
|
|
212
|
-
description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. Supports bulk writes via "files" array.',
|
|
425
|
+
description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. For writes, include a target path (`filePath`, `path`, `filename`, or `name`) plus content (`content`, `text`, or `body`). Supports bulk writes via "files" array.',
|
|
213
426
|
parameters: {
|
|
214
427
|
type: 'object',
|
|
215
428
|
properties: {
|
|
@@ -238,7 +451,7 @@ const FilePlugin: Plugin = {
|
|
|
238
451
|
},
|
|
239
452
|
{
|
|
240
453
|
name: 'send_file',
|
|
241
|
-
description: 'Send a file to the user in chat.',
|
|
454
|
+
description: 'Send a file to the user in chat. Use the returned /api/uploads/... links exactly as provided.',
|
|
242
455
|
parameters: {
|
|
243
456
|
type: 'object',
|
|
244
457
|
properties: {
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { Plugin, PluginHooks } from '@/types'
|
|
4
|
+
import type { ToolBuildContext } from './context'
|
|
5
|
+
import { getPluginManager } from '../plugins'
|
|
6
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
7
|
+
import { ackMailboxEnvelope, listMailbox, sendMailboxEnvelope } from '../session-mailbox'
|
|
8
|
+
import { loadApprovals } from '../storage'
|
|
9
|
+
import { requestApprovalMaybeAutoApprove } from '../approvals'
|
|
10
|
+
import { createWatchJob, getWatchJob } from '../watch-jobs'
|
|
11
|
+
|
|
12
|
+
async function executeHumanLoopAction(args: Record<string, unknown>, bctx: { sessionId?: string | null; agentId?: string | null }) {
|
|
13
|
+
const normalized = normalizeToolInputArgs(args)
|
|
14
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (action === 'request_input') {
|
|
18
|
+
const toSessionId = typeof normalized.toSessionId === 'string' ? normalized.toSessionId : bctx.sessionId
|
|
19
|
+
if (!toSessionId) return 'Error: toSessionId or current session is required.'
|
|
20
|
+
const question = typeof normalized.question === 'string' ? normalized.question.trim() : ''
|
|
21
|
+
if (!question) return 'Error: question is required.'
|
|
22
|
+
const correlationId = typeof normalized.correlationId === 'string' ? normalized.correlationId.trim() : `human-${Date.now()}`
|
|
23
|
+
const options = Array.isArray(normalized.options)
|
|
24
|
+
? normalized.options.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
25
|
+
: []
|
|
26
|
+
const envelope = sendMailboxEnvelope({
|
|
27
|
+
toSessionId,
|
|
28
|
+
type: typeof normalized.type === 'string' ? normalized.type : 'human_request',
|
|
29
|
+
payload: JSON.stringify({
|
|
30
|
+
question,
|
|
31
|
+
options,
|
|
32
|
+
expectedFormat: typeof normalized.expectedFormat === 'string' ? normalized.expectedFormat : null,
|
|
33
|
+
notes: typeof normalized.notes === 'string' ? normalized.notes : null,
|
|
34
|
+
}),
|
|
35
|
+
fromSessionId: bctx.sessionId || null,
|
|
36
|
+
fromAgentId: bctx.agentId || null,
|
|
37
|
+
correlationId,
|
|
38
|
+
ttlSec: typeof normalized.ttlSec === 'number' ? normalized.ttlSec : null,
|
|
39
|
+
})
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
ok: true,
|
|
42
|
+
envelope,
|
|
43
|
+
correlationId,
|
|
44
|
+
hint: `A human can answer via POST /api/chats/${toSessionId}/mailbox with action="send", type="human_reply", correlationId="${correlationId}", and payload set to the response.`,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (action === 'list_mailbox') {
|
|
49
|
+
const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
|
|
50
|
+
if (!sessionId) return 'Error: sessionId or current session is required.'
|
|
51
|
+
const includeAcked = normalized.includeAcked === true
|
|
52
|
+
return JSON.stringify(listMailbox(sessionId, {
|
|
53
|
+
includeAcked,
|
|
54
|
+
limit: typeof normalized.limit === 'number' ? normalized.limit : undefined,
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (action === 'ack_mailbox') {
|
|
59
|
+
const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
|
|
60
|
+
if (!sessionId) return 'Error: sessionId or current session is required.'
|
|
61
|
+
const envelopeId = typeof normalized.envelopeId === 'string' ? normalized.envelopeId.trim() : ''
|
|
62
|
+
if (!envelopeId) return 'Error: envelopeId is required.'
|
|
63
|
+
const envelope = ackMailboxEnvelope(sessionId, envelopeId)
|
|
64
|
+
return envelope ? JSON.stringify(envelope) : `Error: mailbox envelope "${envelopeId}" not found.`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (action === 'request_approval') {
|
|
68
|
+
const title = typeof normalized.title === 'string' && normalized.title.trim()
|
|
69
|
+
? normalized.title.trim()
|
|
70
|
+
: 'Human approval requested'
|
|
71
|
+
const approval = await requestApprovalMaybeAutoApprove({
|
|
72
|
+
category: 'human_loop',
|
|
73
|
+
title,
|
|
74
|
+
description: typeof normalized.description === 'string' ? normalized.description : undefined,
|
|
75
|
+
agentId: bctx.agentId || null,
|
|
76
|
+
sessionId: bctx.sessionId || null,
|
|
77
|
+
data: {
|
|
78
|
+
question: typeof normalized.question === 'string' ? normalized.question : title,
|
|
79
|
+
options: Array.isArray(normalized.options) ? normalized.options : undefined,
|
|
80
|
+
metadata: normalized.metadata,
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
return JSON.stringify(approval)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (action === 'wait_for_reply') {
|
|
87
|
+
const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
|
|
88
|
+
if (!sessionId) return 'Error: sessionId or current session is required.'
|
|
89
|
+
const job = await createWatchJob({
|
|
90
|
+
type: 'mailbox',
|
|
91
|
+
sessionId,
|
|
92
|
+
agentId: bctx.agentId || null,
|
|
93
|
+
createdByAgentId: bctx.agentId || null,
|
|
94
|
+
resumeMessage: typeof normalized.resumeMessage === 'string' && normalized.resumeMessage.trim()
|
|
95
|
+
? normalized.resumeMessage.trim()
|
|
96
|
+
: 'A human reply arrived in the mailbox. Read it and continue the task.',
|
|
97
|
+
description: typeof normalized.description === 'string' ? normalized.description : 'Wait for mailbox reply',
|
|
98
|
+
timeoutAt: typeof normalized.timeoutMinutes === 'number'
|
|
99
|
+
? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
|
|
100
|
+
: undefined,
|
|
101
|
+
target: {
|
|
102
|
+
sessionId,
|
|
103
|
+
},
|
|
104
|
+
condition: {
|
|
105
|
+
type: typeof normalized.type === 'string' ? normalized.type : 'human_reply',
|
|
106
|
+
correlationId: typeof normalized.correlationId === 'string' ? normalized.correlationId : undefined,
|
|
107
|
+
fromSessionId: typeof normalized.fromSessionId === 'string' ? normalized.fromSessionId : undefined,
|
|
108
|
+
containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
return JSON.stringify(job)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (action === 'wait_for_approval') {
|
|
115
|
+
const approvalId = typeof normalized.approvalId === 'string' ? normalized.approvalId.trim() : ''
|
|
116
|
+
if (!approvalId) return 'Error: approvalId is required.'
|
|
117
|
+
const job = await createWatchJob({
|
|
118
|
+
type: 'approval',
|
|
119
|
+
sessionId: bctx.sessionId || null,
|
|
120
|
+
agentId: bctx.agentId || null,
|
|
121
|
+
createdByAgentId: bctx.agentId || null,
|
|
122
|
+
resumeMessage: typeof normalized.resumeMessage === 'string' && normalized.resumeMessage.trim()
|
|
123
|
+
? normalized.resumeMessage.trim()
|
|
124
|
+
: 'A human approval decision was made. Inspect it and continue the task.',
|
|
125
|
+
description: typeof normalized.description === 'string' ? normalized.description : 'Wait for approval decision',
|
|
126
|
+
timeoutAt: typeof normalized.timeoutMinutes === 'number'
|
|
127
|
+
? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
|
|
128
|
+
: undefined,
|
|
129
|
+
target: {
|
|
130
|
+
approvalId,
|
|
131
|
+
},
|
|
132
|
+
condition: {
|
|
133
|
+
statusIn: Array.isArray(normalized.statusIn)
|
|
134
|
+
? normalized.statusIn.filter((value): value is string => typeof value === 'string')
|
|
135
|
+
: ['approved', 'rejected'],
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
return JSON.stringify(job)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (action === 'status') {
|
|
142
|
+
const approvalId = typeof normalized.approvalId === 'string' ? normalized.approvalId.trim() : ''
|
|
143
|
+
const watchJobId = typeof normalized.watchJobId === 'string' ? normalized.watchJobId.trim() : ''
|
|
144
|
+
if (approvalId) {
|
|
145
|
+
const approvals = loadApprovals()
|
|
146
|
+
const approval = approvals[approvalId]
|
|
147
|
+
return approval ? JSON.stringify(approval) : `Error: approval "${approvalId}" not found.`
|
|
148
|
+
}
|
|
149
|
+
if (watchJobId) {
|
|
150
|
+
const watch = getWatchJob(watchJobId)
|
|
151
|
+
return watch ? JSON.stringify(watch) : `Error: watch job "${watchJobId}" not found.`
|
|
152
|
+
}
|
|
153
|
+
return 'Error: approvalId or watchJobId is required for status.'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return `Error: Unknown action "${action}".`
|
|
157
|
+
} catch (err: unknown) {
|
|
158
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const HumanLoopPlugin: Plugin = {
|
|
163
|
+
name: 'Human Loop',
|
|
164
|
+
enabledByDefault: false,
|
|
165
|
+
description: 'Request structured human input or approvals, then wait durably for the response.',
|
|
166
|
+
hooks: {
|
|
167
|
+
getCapabilityDescription: () =>
|
|
168
|
+
'I can request structured human input or explicit approvals with `ask_human`, then pause on durable wait handles until the response arrives.',
|
|
169
|
+
} as PluginHooks,
|
|
170
|
+
tools: [
|
|
171
|
+
{
|
|
172
|
+
name: 'ask_human',
|
|
173
|
+
description: 'Human-loop tool. Actions: request_input, request_approval, wait_for_reply, wait_for_approval, list_mailbox, ack_mailbox, status.',
|
|
174
|
+
parameters: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
action: { type: 'string', enum: ['request_input', 'request_approval', 'wait_for_reply', 'wait_for_approval', 'list_mailbox', 'ack_mailbox', 'status'] },
|
|
178
|
+
question: { type: 'string' },
|
|
179
|
+
title: { type: 'string' },
|
|
180
|
+
description: { type: 'string' },
|
|
181
|
+
options: { type: 'array', items: { type: 'string' } },
|
|
182
|
+
correlationId: { type: 'string' },
|
|
183
|
+
expectedFormat: { type: 'string' },
|
|
184
|
+
notes: { type: 'string' },
|
|
185
|
+
envelopeId: { type: 'string' },
|
|
186
|
+
sessionId: { type: 'string' },
|
|
187
|
+
toSessionId: { type: 'string' },
|
|
188
|
+
approvalId: { type: 'string' },
|
|
189
|
+
watchJobId: { type: 'string' },
|
|
190
|
+
statusIn: { type: 'array', items: { type: 'string' } },
|
|
191
|
+
type: { type: 'string' },
|
|
192
|
+
fromSessionId: { type: 'string' },
|
|
193
|
+
containsText: { type: 'string' },
|
|
194
|
+
ttlSec: { type: 'number' },
|
|
195
|
+
timeoutMinutes: { type: 'number' },
|
|
196
|
+
resumeMessage: { type: 'string' },
|
|
197
|
+
limit: { type: 'number' },
|
|
198
|
+
includeAcked: { type: 'boolean' },
|
|
199
|
+
},
|
|
200
|
+
required: ['action'],
|
|
201
|
+
},
|
|
202
|
+
execute: async (args, context) => executeHumanLoopAction(args, {
|
|
203
|
+
sessionId: context.session.id,
|
|
204
|
+
agentId: context.session.agentId || null,
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getPluginManager().registerBuiltin('ask_human', HumanLoopPlugin)
|
|
211
|
+
|
|
212
|
+
export function buildHumanLoopTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
213
|
+
if (!bctx.hasPlugin('ask_human')) return []
|
|
214
|
+
return [
|
|
215
|
+
tool(
|
|
216
|
+
async (args) => executeHumanLoopAction(args, {
|
|
217
|
+
sessionId: bctx.ctx?.sessionId || null,
|
|
218
|
+
agentId: bctx.ctx?.agentId || null,
|
|
219
|
+
}),
|
|
220
|
+
{
|
|
221
|
+
name: 'ask_human',
|
|
222
|
+
description: HumanLoopPlugin.tools![0].description,
|
|
223
|
+
schema: z.object({}).passthrough(),
|
|
224
|
+
},
|
|
225
|
+
),
|
|
226
|
+
]
|
|
227
|
+
}
|
|
@@ -5,7 +5,6 @@ import path from 'path'
|
|
|
5
5
|
import type { Plugin, PluginHooks } from '@/types'
|
|
6
6
|
import { getPluginManager } from '../plugins'
|
|
7
7
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
8
|
-
import { loadSettings } from '../storage'
|
|
9
8
|
import { UPLOAD_DIR } from '../storage'
|
|
10
9
|
import type { ToolBuildContext } from './context'
|
|
11
10
|
|
|
@@ -20,8 +19,7 @@ interface PluginConfig {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
function getConfig(): PluginConfig {
|
|
23
|
-
const
|
|
24
|
-
const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.image_gen ?? {}
|
|
22
|
+
const ps = getPluginManager().getPluginSettings('image_gen')
|
|
25
23
|
return {
|
|
26
24
|
provider: (ps.provider as ImageProvider) || 'openai',
|
|
27
25
|
apiKey: (ps.apiKey as string) || '',
|
|
@@ -37,6 +37,12 @@ import { buildImageGenTools } from './image-gen'
|
|
|
37
37
|
import { buildEmailTools } from './email'
|
|
38
38
|
import { buildCalendarTools } from './calendar'
|
|
39
39
|
import { buildReplicateTools } from './replicate'
|
|
40
|
+
import { buildMailboxTools } from './mailbox'
|
|
41
|
+
import { buildHumanLoopTools } from './human-loop'
|
|
42
|
+
import { buildDocumentTools } from './document'
|
|
43
|
+
import { buildExtractTools } from './extract'
|
|
44
|
+
import { buildTableTools } from './table'
|
|
45
|
+
import { buildCrawlTools } from './crawl'
|
|
40
46
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
41
47
|
|
|
42
48
|
import { getPluginManager } from '../plugins'
|
|
@@ -157,6 +163,12 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
157
163
|
['email', buildEmailTools],
|
|
158
164
|
['calendar', buildCalendarTools],
|
|
159
165
|
['replicate', buildReplicateTools],
|
|
166
|
+
['mailbox', buildMailboxTools],
|
|
167
|
+
['ask_human', buildHumanLoopTools],
|
|
168
|
+
['document', buildDocumentTools],
|
|
169
|
+
['extract', buildExtractTools],
|
|
170
|
+
['table', buildTableTools],
|
|
171
|
+
['crawl', buildCrawlTools],
|
|
160
172
|
]
|
|
161
173
|
|
|
162
174
|
for (const [pluginId, builder] of nativeBuilders) {
|
|
@@ -272,8 +284,51 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
272
284
|
),
|
|
273
285
|
)
|
|
274
286
|
|
|
287
|
+
const buildFallbackHookSession = (): Session => ({
|
|
288
|
+
id: ctx?.sessionId || 'plugin-hook-session',
|
|
289
|
+
name: 'Plugin Hook Session',
|
|
290
|
+
cwd,
|
|
291
|
+
user: 'system',
|
|
292
|
+
provider: 'openai',
|
|
293
|
+
model: 'unknown',
|
|
294
|
+
claudeSessionId: null,
|
|
295
|
+
messages: [],
|
|
296
|
+
createdAt: Date.now(),
|
|
297
|
+
lastActiveAt: Date.now(),
|
|
298
|
+
agentId: ctx?.agentId || null,
|
|
299
|
+
plugins: [...activePlugins],
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const wrappedTools = tools.map((candidate) => {
|
|
303
|
+
const schema = (candidate as unknown as { schema?: z.ZodTypeAny }).schema || z.object({}).passthrough()
|
|
304
|
+
return tool(
|
|
305
|
+
async (args) => {
|
|
306
|
+
const normalizedArgs = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
307
|
+
const nextArgs = await pluginManager.runBeforeToolExec(
|
|
308
|
+
{ toolName: candidate.name, input: normalizedArgs },
|
|
309
|
+
{ enabledIds: activePlugins },
|
|
310
|
+
)
|
|
311
|
+
const effectiveArgs = nextArgs ?? normalizedArgs
|
|
312
|
+
const result = await candidate.invoke(effectiveArgs)
|
|
313
|
+
const outputText = typeof result === 'string' ? result : JSON.stringify(result)
|
|
314
|
+
const hookSession = resolveCurrentSession() || buildFallbackHookSession()
|
|
315
|
+
await pluginManager.runHook(
|
|
316
|
+
'afterToolExec',
|
|
317
|
+
{ session: hookSession, toolName: candidate.name, input: effectiveArgs, output: outputText },
|
|
318
|
+
{ enabledIds: activePlugins },
|
|
319
|
+
)
|
|
320
|
+
return outputText
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: candidate.name,
|
|
324
|
+
description: candidate.description,
|
|
325
|
+
schema,
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
})
|
|
329
|
+
|
|
275
330
|
return {
|
|
276
|
-
tools,
|
|
331
|
+
tools: wrappedTools,
|
|
277
332
|
cleanup: async () => {
|
|
278
333
|
for (const fn of cleanupFns) {
|
|
279
334
|
try { await fn() } catch { /* ignore */ }
|