@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
|
@@ -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 'list'
|
|
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,18 @@ 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
|
|
420
|
+
getCapabilityDescription: () => 'I can manage files with the unified `files` tool (actions: `read`, `write`, `list`, `copy`, `move`, `delete`) and deliver finished artifacts with `send_file`.',
|
|
421
|
+
getOperatingGuidance: () => [
|
|
422
|
+
'The `files` tool always works best with an explicit action. Use `{"action":"list","dirPath":"."}` to inspect the workspace, `{"action":"read","filePath":"path/to/file.md"}` to inspect a file, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}` to create or overwrite content.',
|
|
423
|
+
'For follow-up revision requests, read the current file first, then overwrite it with the improved version or use `edit_file` for a surgical change.',
|
|
424
|
+
'If a `files` call fails, correct the arguments and retry. Do not conclude that the workspace is inaccessible until an explicit read/list/write attempt with a path fails.',
|
|
425
|
+
'When `send_file` returns a download link, copy that link exactly instead of rewriting it.',
|
|
426
|
+
],
|
|
208
427
|
} as PluginHooks,
|
|
209
428
|
tools: [
|
|
210
429
|
{
|
|
211
430
|
name: 'files',
|
|
212
|
-
description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. Supports bulk writes via "files" array.',
|
|
431
|
+
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
432
|
parameters: {
|
|
214
433
|
type: 'object',
|
|
215
434
|
properties: {
|
|
@@ -238,7 +457,7 @@ const FilePlugin: Plugin = {
|
|
|
238
457
|
},
|
|
239
458
|
{
|
|
240
459
|
name: 'send_file',
|
|
241
|
-
description: 'Send a file to the user in chat.',
|
|
460
|
+
description: 'Send a file to the user in chat. Use the returned /api/uploads/... links exactly as provided.',
|
|
242
461
|
parameters: {
|
|
243
462
|
type: 'object',
|
|
244
463
|
properties: {
|
|
@@ -68,12 +68,18 @@ async function executeHttpAction(args: HttpRequestArgs) {
|
|
|
68
68
|
*/
|
|
69
69
|
const HttpPlugin: Plugin = {
|
|
70
70
|
name: 'Core HTTP',
|
|
71
|
-
description: 'Make direct HTTP API calls
|
|
72
|
-
hooks: {
|
|
71
|
+
description: 'Make direct HTTP API calls without generating throwaway code.',
|
|
72
|
+
hooks: {
|
|
73
|
+
getCapabilityDescription: () => 'I can make direct HTTP requests (`http_request`) without writing code. Use this for straightforward API calls or fetching JSON.',
|
|
74
|
+
getOperatingGuidance: () => [
|
|
75
|
+
'Prefer `http_request` over `sandbox_exec` for straightforward REST or JSON API calls.',
|
|
76
|
+
'Keep API keys in plugin settings or SwarmClaw secrets instead of hardcoding them in generated code.',
|
|
77
|
+
],
|
|
78
|
+
} as PluginHooks,
|
|
73
79
|
tools: [
|
|
74
80
|
{
|
|
75
81
|
name: 'http_request',
|
|
76
|
-
description: 'Make an HTTP API request.',
|
|
82
|
+
description: 'Make an HTTP API request without generating code.',
|
|
77
83
|
parameters: {
|
|
78
84
|
type: 'object',
|
|
79
85
|
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) || '',
|