@swarmclawai/swarmclaw 0.7.1 → 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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- 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 +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- 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/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- 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 +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- 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 +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- 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 +244 -56
- 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 +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- 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/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- 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/capability-router.ts +10 -8
- 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 +285 -165
- 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 +48 -8
- 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 +948 -112
- 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/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -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/heartbeat-service.ts +14 -40
- 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 +28 -1103
- 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 +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- 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 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- 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 +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- 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 +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- 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/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -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,30 @@ 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)
|
|
283
|
+
collectSendFilePaths(args.file, candidates)
|
|
147
284
|
collectSendFilePaths(args.files, candidates)
|
|
148
285
|
|
|
149
286
|
const nestedInput = args.input
|
|
@@ -166,17 +303,94 @@ export function normalizeSendFilePaths(args: Record<string, unknown>): string[]
|
|
|
166
303
|
return [...deduped]
|
|
167
304
|
}
|
|
168
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
|
+
|
|
169
379
|
async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: string }) {
|
|
170
380
|
try {
|
|
171
|
-
const
|
|
381
|
+
const explicitPaths = normalizeSendFilePaths(args)
|
|
382
|
+
const paths = explicitPaths.length > 0 ? explicitPaths : findRecentSendFileFallbackPaths(bctx.cwd)
|
|
172
383
|
if (paths.length === 0) {
|
|
173
384
|
return 'Error: filePath/path is required (or provide files[] / input.files[]).'
|
|
174
385
|
}
|
|
386
|
+
if (explicitPaths.length === 0 && paths.length !== 1) {
|
|
387
|
+
return 'Error: filePath/path is required (or provide files[] / input.files[]).'
|
|
388
|
+
}
|
|
175
389
|
|
|
176
390
|
const links: string[] = []
|
|
177
391
|
const errors: string[] = []
|
|
178
392
|
for (const rawPath of paths) {
|
|
179
|
-
const resolved =
|
|
393
|
+
const resolved = resolveSendFileSourcePath(bctx.cwd, rawPath)
|
|
180
394
|
if (!fs.existsSync(resolved)) {
|
|
181
395
|
errors.push(`file not found: ${rawPath}`)
|
|
182
396
|
continue
|
|
@@ -202,11 +416,13 @@ async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: strin
|
|
|
202
416
|
const FilePlugin: Plugin = {
|
|
203
417
|
name: 'Core Files',
|
|
204
418
|
description: 'Complete file management: read, write, list, move, copy, delete, and send.',
|
|
205
|
-
hooks: {
|
|
419
|
+
hooks: {
|
|
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.',
|
|
421
|
+
} as PluginHooks,
|
|
206
422
|
tools: [
|
|
207
423
|
{
|
|
208
424
|
name: 'files',
|
|
209
|
-
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.',
|
|
210
426
|
parameters: {
|
|
211
427
|
type: 'object',
|
|
212
428
|
properties: {
|
|
@@ -235,7 +451,7 @@ const FilePlugin: Plugin = {
|
|
|
235
451
|
},
|
|
236
452
|
{
|
|
237
453
|
name: 'send_file',
|
|
238
|
-
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.',
|
|
239
455
|
parameters: {
|
|
240
456
|
type: 'object',
|
|
241
457
|
properties: {
|
|
@@ -265,7 +481,7 @@ getPluginManager().registerBuiltin('files', FilePlugin)
|
|
|
265
481
|
* Legacy Bridge
|
|
266
482
|
*/
|
|
267
483
|
export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
268
|
-
if (!bctx.
|
|
484
|
+
if (!bctx.hasPlugin('files')) return []
|
|
269
485
|
|
|
270
486
|
return [
|
|
271
487
|
tool(
|
|
@@ -97,7 +97,7 @@ getPluginManager().registerBuiltin('git', GitPlugin)
|
|
|
97
97
|
* Legacy Bridge
|
|
98
98
|
*/
|
|
99
99
|
export function buildGitTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
100
|
-
if (!bctx.
|
|
100
|
+
if (!bctx.hasPlugin('git')) return []
|
|
101
101
|
return [
|
|
102
102
|
tool(
|
|
103
103
|
async (args) => executeGitAction(args, { cwd: bctx.cwd }),
|
|
@@ -97,7 +97,7 @@ getPluginManager().registerBuiltin('http', HttpPlugin)
|
|
|
97
97
|
* Legacy Bridge
|
|
98
98
|
*/
|
|
99
99
|
export function buildHttpTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
100
|
-
if (!bctx.
|
|
100
|
+
if (!bctx.hasPlugin('http_request')) return []
|
|
101
101
|
|
|
102
102
|
return [
|
|
103
103
|
tool(
|
|
@@ -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
|
+
}
|