@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
|
@@ -25,6 +25,7 @@ import { buildWalletTools } from './wallet'
|
|
|
25
25
|
import { buildOpenClawWorkspaceTools } from './openclaw-workspace'
|
|
26
26
|
import { buildScheduleTools } from './schedule'
|
|
27
27
|
import { buildPlatformTools } from './platform'
|
|
28
|
+
import { buildCrudTools } from './crud'
|
|
28
29
|
import { buildSessionInfoTools } from './session-info'
|
|
29
30
|
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
30
31
|
import { buildContextTools } from './context-mgmt'
|
|
@@ -37,6 +38,12 @@ import { buildImageGenTools } from './image-gen'
|
|
|
37
38
|
import { buildEmailTools } from './email'
|
|
38
39
|
import { buildCalendarTools } from './calendar'
|
|
39
40
|
import { buildReplicateTools } from './replicate'
|
|
41
|
+
import { buildMailboxTools } from './mailbox'
|
|
42
|
+
import { buildHumanLoopTools } from './human-loop'
|
|
43
|
+
import { buildDocumentTools } from './document'
|
|
44
|
+
import { buildExtractTools } from './extract'
|
|
45
|
+
import { buildTableTools } from './table'
|
|
46
|
+
import { buildCrawlTools } from './crawl'
|
|
40
47
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
41
48
|
|
|
42
49
|
import { getPluginManager } from '../plugins'
|
|
@@ -157,6 +164,12 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
157
164
|
['email', buildEmailTools],
|
|
158
165
|
['calendar', buildCalendarTools],
|
|
159
166
|
['replicate', buildReplicateTools],
|
|
167
|
+
['mailbox', buildMailboxTools],
|
|
168
|
+
['ask_human', buildHumanLoopTools],
|
|
169
|
+
['document', buildDocumentTools],
|
|
170
|
+
['extract', buildExtractTools],
|
|
171
|
+
['table', buildTableTools],
|
|
172
|
+
['crawl', buildCrawlTools],
|
|
160
173
|
]
|
|
161
174
|
|
|
162
175
|
for (const [pluginId, builder] of nativeBuilders) {
|
|
@@ -167,6 +180,12 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
167
180
|
tools.push(...builtTools)
|
|
168
181
|
}
|
|
169
182
|
|
|
183
|
+
const crudTools = buildCrudTools(bctx)
|
|
184
|
+
for (const toolEntry of crudTools) {
|
|
185
|
+
toolToPluginMap[toolEntry.name] = toolEntry.name
|
|
186
|
+
}
|
|
187
|
+
tools.push(...crudTools)
|
|
188
|
+
|
|
170
189
|
// 2. Build Plugin Tools (Built-in + External)
|
|
171
190
|
try {
|
|
172
191
|
const pluginTools = pluginManager.getTools(activePlugins)
|
|
@@ -254,11 +273,34 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
254
273
|
const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
255
274
|
const toolId = normalized.toolId as string | undefined
|
|
256
275
|
const reason = normalized.reason as string | undefined
|
|
276
|
+
if (!toolId?.trim()) {
|
|
277
|
+
return JSON.stringify({
|
|
278
|
+
error: 'toolId is required',
|
|
279
|
+
message: 'Specify the exact plugin ID to request access for.',
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
const { requestApprovalMaybeAutoApprove } = await import('../approvals')
|
|
283
|
+
const approval = await requestApprovalMaybeAutoApprove({
|
|
284
|
+
category: 'tool_access',
|
|
285
|
+
title: `Enable Plugin: ${toolId}`,
|
|
286
|
+
description: reason || `Agent is requesting access to "${toolId}".`,
|
|
287
|
+
data: { toolId, pluginId: toolId, reason: reason || '' },
|
|
288
|
+
agentId: ctx?.agentId,
|
|
289
|
+
sessionId: ctx?.sessionId,
|
|
290
|
+
})
|
|
291
|
+
if (approval.status === 'approved') {
|
|
292
|
+
return JSON.stringify({
|
|
293
|
+
type: 'tool_request',
|
|
294
|
+
toolId,
|
|
295
|
+
autoApproved: true,
|
|
296
|
+
message: `Tool access for "${toolId}" was granted. Proceed to use it directly.`,
|
|
297
|
+
})
|
|
298
|
+
}
|
|
257
299
|
return JSON.stringify({
|
|
258
300
|
type: 'tool_request',
|
|
259
301
|
toolId,
|
|
260
302
|
reason,
|
|
261
|
-
message: `Tool access request sent to user for "${toolId}".
|
|
303
|
+
message: `Tool access request sent to user for "${toolId}". Once granted, continue immediately with the original task using the newly available tool.`,
|
|
262
304
|
})
|
|
263
305
|
},
|
|
264
306
|
{
|
|
@@ -272,8 +314,51 @@ export async function buildSessionTools(cwd: string, enabledPlugins: string[], c
|
|
|
272
314
|
),
|
|
273
315
|
)
|
|
274
316
|
|
|
317
|
+
const buildFallbackHookSession = (): Session => ({
|
|
318
|
+
id: ctx?.sessionId || 'plugin-hook-session',
|
|
319
|
+
name: 'Plugin Hook Session',
|
|
320
|
+
cwd,
|
|
321
|
+
user: 'system',
|
|
322
|
+
provider: 'openai',
|
|
323
|
+
model: 'unknown',
|
|
324
|
+
claudeSessionId: null,
|
|
325
|
+
messages: [],
|
|
326
|
+
createdAt: Date.now(),
|
|
327
|
+
lastActiveAt: Date.now(),
|
|
328
|
+
agentId: ctx?.agentId || null,
|
|
329
|
+
plugins: [...activePlugins],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const wrappedTools = tools.map((candidate) => {
|
|
333
|
+
const schema = (candidate as unknown as { schema?: z.ZodTypeAny }).schema || z.object({}).passthrough()
|
|
334
|
+
return tool(
|
|
335
|
+
async (args) => {
|
|
336
|
+
const normalizedArgs = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
337
|
+
const nextArgs = await pluginManager.runBeforeToolExec(
|
|
338
|
+
{ toolName: candidate.name, input: normalizedArgs },
|
|
339
|
+
{ enabledIds: activePlugins },
|
|
340
|
+
)
|
|
341
|
+
const effectiveArgs = nextArgs ?? normalizedArgs
|
|
342
|
+
const result = await candidate.invoke(effectiveArgs)
|
|
343
|
+
const outputText = typeof result === 'string' ? result : JSON.stringify(result)
|
|
344
|
+
const hookSession = resolveCurrentSession() || buildFallbackHookSession()
|
|
345
|
+
await pluginManager.runHook(
|
|
346
|
+
'afterToolExec',
|
|
347
|
+
{ session: hookSession, toolName: candidate.name, input: effectiveArgs, output: outputText },
|
|
348
|
+
{ enabledIds: activePlugins },
|
|
349
|
+
)
|
|
350
|
+
return outputText
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: candidate.name,
|
|
354
|
+
description: candidate.description,
|
|
355
|
+
schema,
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
})
|
|
359
|
+
|
|
275
360
|
return {
|
|
276
|
-
tools,
|
|
361
|
+
tools: wrappedTools,
|
|
277
362
|
cleanup: async () => {
|
|
278
363
|
for (const fn of cleanupFns) {
|
|
279
364
|
try { await fn() } catch { /* ignore */ }
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { Plugin, PluginHooks } from '@/types'
|
|
4
|
+
import { getPluginManager } from '../plugins'
|
|
5
|
+
import type { ToolBuildContext } from './context'
|
|
6
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
7
|
+
import {
|
|
8
|
+
downloadMailboxAttachment,
|
|
9
|
+
fetchMailboxMessageByUid,
|
|
10
|
+
fetchMailboxMessages,
|
|
11
|
+
getMailboxConfig,
|
|
12
|
+
replyMailboxMessage,
|
|
13
|
+
} from '../mailbox-utils'
|
|
14
|
+
import { createWatchJob } from '../watch-jobs'
|
|
15
|
+
|
|
16
|
+
function parseMessageUid(value: unknown): number {
|
|
17
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number.parseInt(value, 10) : Number.NaN
|
|
18
|
+
return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function executeMailboxAction(args: Record<string, unknown>, bctx: { cwd: string; sessionId?: string | null; agentId?: string | null }) {
|
|
22
|
+
const normalized = normalizeToolInputArgs(args)
|
|
23
|
+
const action = String(normalized.action || 'status').trim().toLowerCase()
|
|
24
|
+
const folder = typeof normalized.folder === 'string' ? normalized.folder.trim() : undefined
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (action === 'status') {
|
|
28
|
+
const config = getMailboxConfig()
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
configured: !!(config.imapHost && config.user && config.password),
|
|
31
|
+
imapHost: config.imapHost || null,
|
|
32
|
+
smtpHost: config.smtpHost || null,
|
|
33
|
+
folder: config.folder || 'INBOX',
|
|
34
|
+
fromAddress: config.fromAddress || null,
|
|
35
|
+
subjectPrefix: config.subjectPrefix || null,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (action === 'list_messages' || action === 'search_messages') {
|
|
40
|
+
const messages = await fetchMailboxMessages({
|
|
41
|
+
folder,
|
|
42
|
+
query: typeof normalized.query === 'string' ? normalized.query : undefined,
|
|
43
|
+
from: typeof normalized.from === 'string' ? normalized.from : undefined,
|
|
44
|
+
subjectContains: typeof normalized.subjectContains === 'string' ? normalized.subjectContains : undefined,
|
|
45
|
+
bodyContains: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
|
|
46
|
+
unreadOnly: normalized.unreadOnly === true,
|
|
47
|
+
hasAttachments: normalized.hasAttachments === true,
|
|
48
|
+
limit: typeof normalized.limit === 'number' ? normalized.limit : undefined,
|
|
49
|
+
})
|
|
50
|
+
return JSON.stringify(messages.map((message) => ({
|
|
51
|
+
uid: message.uid,
|
|
52
|
+
messageId: message.messageId,
|
|
53
|
+
subject: message.subject,
|
|
54
|
+
from: message.from,
|
|
55
|
+
fromName: message.fromName,
|
|
56
|
+
date: message.date,
|
|
57
|
+
snippet: message.snippet,
|
|
58
|
+
hasAttachments: message.hasAttachments,
|
|
59
|
+
attachmentCount: message.attachments.length,
|
|
60
|
+
threadKey: message.threadKey,
|
|
61
|
+
})))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (action === 'list_threads') {
|
|
65
|
+
const messages = await fetchMailboxMessages({
|
|
66
|
+
folder,
|
|
67
|
+
limit: typeof normalized.limit === 'number' ? Math.max(10, normalized.limit * 4) : 80,
|
|
68
|
+
})
|
|
69
|
+
const threads = new Map<string, {
|
|
70
|
+
threadKey: string
|
|
71
|
+
subject: string
|
|
72
|
+
participants: Set<string>
|
|
73
|
+
latestUid: number
|
|
74
|
+
latestDate: string | null
|
|
75
|
+
messageCount: number
|
|
76
|
+
unreadCount: number
|
|
77
|
+
snippet: string
|
|
78
|
+
}>()
|
|
79
|
+
for (const message of messages) {
|
|
80
|
+
const current = threads.get(message.threadKey) || {
|
|
81
|
+
threadKey: message.threadKey,
|
|
82
|
+
subject: message.subject,
|
|
83
|
+
participants: new Set<string>(),
|
|
84
|
+
latestUid: message.uid,
|
|
85
|
+
latestDate: message.date,
|
|
86
|
+
messageCount: 0,
|
|
87
|
+
unreadCount: 0,
|
|
88
|
+
snippet: message.snippet,
|
|
89
|
+
}
|
|
90
|
+
current.messageCount += 1
|
|
91
|
+
current.participants.add(message.from)
|
|
92
|
+
if (!message.flags.includes('\\Seen')) current.unreadCount += 1
|
|
93
|
+
if (message.uid >= current.latestUid) {
|
|
94
|
+
current.latestUid = message.uid
|
|
95
|
+
current.latestDate = message.date
|
|
96
|
+
current.subject = message.subject
|
|
97
|
+
current.snippet = message.snippet
|
|
98
|
+
}
|
|
99
|
+
threads.set(message.threadKey, current)
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify(Array.from(threads.values())
|
|
102
|
+
.map((thread) => ({
|
|
103
|
+
threadKey: thread.threadKey,
|
|
104
|
+
subject: thread.subject,
|
|
105
|
+
participants: Array.from(thread.participants),
|
|
106
|
+
latestUid: thread.latestUid,
|
|
107
|
+
latestDate: thread.latestDate,
|
|
108
|
+
messageCount: thread.messageCount,
|
|
109
|
+
unreadCount: thread.unreadCount,
|
|
110
|
+
snippet: thread.snippet,
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => b.latestUid - a.latestUid)
|
|
113
|
+
.slice(0, Math.max(1, Math.min(typeof normalized.limit === 'number' ? normalized.limit : 20, 100))))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (action === 'read_message') {
|
|
117
|
+
const uid = parseMessageUid(normalized.uid ?? normalized.id)
|
|
118
|
+
if (!uid) return 'Error: uid is required.'
|
|
119
|
+
const message = await fetchMailboxMessageByUid(uid, folder)
|
|
120
|
+
if (!message) return `Error: mailbox message "${uid}" not found.`
|
|
121
|
+
return JSON.stringify(message)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (action === 'download_attachment') {
|
|
125
|
+
const uid = parseMessageUid(normalized.uid ?? normalized.id)
|
|
126
|
+
if (!uid) return 'Error: uid is required.'
|
|
127
|
+
const result = await downloadMailboxAttachment({
|
|
128
|
+
uid,
|
|
129
|
+
folder,
|
|
130
|
+
attachmentId: typeof normalized.attachmentId === 'string' ? normalized.attachmentId : undefined,
|
|
131
|
+
attachmentName: typeof normalized.attachmentName === 'string' ? normalized.attachmentName : undefined,
|
|
132
|
+
saveTo: typeof normalized.saveTo === 'string' ? normalized.saveTo : undefined,
|
|
133
|
+
cwd: bctx.cwd,
|
|
134
|
+
})
|
|
135
|
+
return JSON.stringify(result)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (action === 'reply') {
|
|
139
|
+
const uid = parseMessageUid(normalized.uid ?? normalized.id)
|
|
140
|
+
if (!uid) return 'Error: uid is required.'
|
|
141
|
+
const text = typeof normalized.text === 'string'
|
|
142
|
+
? normalized.text
|
|
143
|
+
: typeof normalized.body === 'string'
|
|
144
|
+
? normalized.body
|
|
145
|
+
: ''
|
|
146
|
+
if (!text.trim()) return 'Error: text is required.'
|
|
147
|
+
const result = await replyMailboxMessage({
|
|
148
|
+
uid,
|
|
149
|
+
folder,
|
|
150
|
+
text,
|
|
151
|
+
html: typeof normalized.html === 'string' ? normalized.html : undefined,
|
|
152
|
+
subject: typeof normalized.subject === 'string' ? normalized.subject : undefined,
|
|
153
|
+
})
|
|
154
|
+
return JSON.stringify({ ok: true, ...result, uid })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (action === 'wait_for_email') {
|
|
158
|
+
if (!bctx.sessionId && !bctx.agentId) return 'Error: email waits require a session or agent context.'
|
|
159
|
+
const resumeMessage = typeof normalized.resumeMessage === 'string' && normalized.resumeMessage.trim()
|
|
160
|
+
? normalized.resumeMessage.trim()
|
|
161
|
+
: 'A matching email arrived. Read it, decide what to do next, and continue the task.'
|
|
162
|
+
const intervalMs = typeof normalized.intervalSec === 'number'
|
|
163
|
+
? Math.max(30, normalized.intervalSec) * 1000
|
|
164
|
+
: 60_000
|
|
165
|
+
const timeoutAt = typeof normalized.timeoutMinutes === 'number'
|
|
166
|
+
? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
|
|
167
|
+
: undefined
|
|
168
|
+
const job = await createWatchJob({
|
|
169
|
+
type: 'email',
|
|
170
|
+
sessionId: bctx.sessionId || null,
|
|
171
|
+
agentId: bctx.agentId || null,
|
|
172
|
+
createdByAgentId: bctx.agentId || null,
|
|
173
|
+
resumeMessage,
|
|
174
|
+
description: typeof normalized.description === 'string' ? normalized.description : 'Wait for email',
|
|
175
|
+
intervalMs,
|
|
176
|
+
timeoutAt,
|
|
177
|
+
target: {
|
|
178
|
+
folder: folder || getMailboxConfig().folder || 'INBOX',
|
|
179
|
+
},
|
|
180
|
+
condition: {
|
|
181
|
+
from: typeof normalized.from === 'string' ? normalized.from : undefined,
|
|
182
|
+
subjectContains: typeof normalized.subjectContains === 'string' ? normalized.subjectContains : undefined,
|
|
183
|
+
containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
|
|
184
|
+
query: typeof normalized.query === 'string' ? normalized.query : undefined,
|
|
185
|
+
unreadOnly: normalized.unreadOnly === true,
|
|
186
|
+
hasAttachments: normalized.hasAttachments === true,
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
return JSON.stringify(job)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return `Error: Unknown action "${action}".`
|
|
193
|
+
} catch (err: unknown) {
|
|
194
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const MailboxPlugin: Plugin = {
|
|
199
|
+
name: 'Mailbox',
|
|
200
|
+
enabledByDefault: false,
|
|
201
|
+
description: 'Read/search/reply to inbox messages over IMAP/SMTP, download attachments, and wait for matching inbound email.',
|
|
202
|
+
hooks: {
|
|
203
|
+
getCapabilityDescription: () =>
|
|
204
|
+
'I can inspect inboxes with `mailbox`, read and search messages, download attachments, reply to emails, and wait for specific inbound messages.',
|
|
205
|
+
} as PluginHooks,
|
|
206
|
+
tools: [
|
|
207
|
+
{
|
|
208
|
+
name: 'mailbox',
|
|
209
|
+
description: 'Work with email inboxes. Actions: status, list_messages, list_threads, search_messages, read_message, download_attachment, reply, wait_for_email.',
|
|
210
|
+
parameters: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
action: { type: 'string', enum: ['status', 'list_messages', 'list_threads', 'search_messages', 'read_message', 'download_attachment', 'reply', 'wait_for_email'] },
|
|
214
|
+
uid: { type: 'number' },
|
|
215
|
+
query: { type: 'string' },
|
|
216
|
+
from: { type: 'string' },
|
|
217
|
+
subjectContains: { type: 'string' },
|
|
218
|
+
containsText: { type: 'string' },
|
|
219
|
+
attachmentId: { type: 'string' },
|
|
220
|
+
attachmentName: { type: 'string' },
|
|
221
|
+
text: { type: 'string' },
|
|
222
|
+
body: { type: 'string' },
|
|
223
|
+
html: { type: 'string' },
|
|
224
|
+
subject: { type: 'string' },
|
|
225
|
+
folder: { type: 'string' },
|
|
226
|
+
unreadOnly: { type: 'boolean' },
|
|
227
|
+
hasAttachments: { type: 'boolean' },
|
|
228
|
+
limit: { type: 'number' },
|
|
229
|
+
saveTo: { type: 'string' },
|
|
230
|
+
resumeMessage: { type: 'string' },
|
|
231
|
+
intervalSec: { type: 'number' },
|
|
232
|
+
timeoutMinutes: { type: 'number' },
|
|
233
|
+
},
|
|
234
|
+
required: ['action'],
|
|
235
|
+
},
|
|
236
|
+
execute: async (args, context) => executeMailboxAction(args, {
|
|
237
|
+
cwd: context.session.cwd || process.cwd(),
|
|
238
|
+
sessionId: context.session.id,
|
|
239
|
+
agentId: context.session.agentId || null,
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
ui: {
|
|
244
|
+
settingsFields: [
|
|
245
|
+
{ key: 'imapHost', label: 'IMAP Host', type: 'text', placeholder: 'imap.gmail.com', help: 'Inbound mailbox host.' },
|
|
246
|
+
{ key: 'imapPort', label: 'IMAP Port', type: 'number', defaultValue: 993, help: '993 for TLS IMAP.' },
|
|
247
|
+
{ key: 'smtpHost', label: 'SMTP Host', type: 'text', placeholder: 'smtp.gmail.com', help: 'Outbound mail host for replies.' },
|
|
248
|
+
{ key: 'smtpPort', label: 'SMTP Port', type: 'number', defaultValue: 587, help: '587 for STARTTLS, 465 for SSL.' },
|
|
249
|
+
{ key: 'user', label: 'Mailbox Username', type: 'text', placeholder: 'agent@example.com' },
|
|
250
|
+
{ key: 'password', label: 'Mailbox Password', type: 'secret', help: 'IMAP password or app password.' },
|
|
251
|
+
{ key: 'folder', label: 'Folder', type: 'text', defaultValue: 'INBOX', placeholder: 'INBOX' },
|
|
252
|
+
{ key: 'fromAddress', label: 'Reply From Address', type: 'text', placeholder: 'agent@example.com' },
|
|
253
|
+
{ key: 'fromName', label: 'Reply From Name', type: 'text', defaultValue: 'SwarmClaw Agent' },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getPluginManager().registerBuiltin('mailbox', MailboxPlugin)
|
|
259
|
+
|
|
260
|
+
export function buildMailboxTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
261
|
+
if (!bctx.hasPlugin('mailbox')) return []
|
|
262
|
+
return [
|
|
263
|
+
tool(
|
|
264
|
+
async (args) => executeMailboxAction(args, {
|
|
265
|
+
cwd: bctx.cwd,
|
|
266
|
+
sessionId: bctx.ctx?.sessionId || null,
|
|
267
|
+
agentId: bctx.ctx?.agentId || null,
|
|
268
|
+
}),
|
|
269
|
+
{
|
|
270
|
+
name: 'mailbox',
|
|
271
|
+
description: MailboxPlugin.tools![0].description,
|
|
272
|
+
schema: z.object({}).passthrough(),
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
]
|
|
276
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-schedule-tool-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('manage_schedules tool', () => {
|
|
36
|
+
it('defaults schedules to the current agent and derives a runnable taskPrompt from run_script payloads', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
import fs from 'node:fs'
|
|
39
|
+
import path from 'node:path'
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
41
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
42
|
+
const storage = storageMod.default || storageMod
|
|
43
|
+
const crud = crudMod.default || crudMod
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
storage.saveAgents({
|
|
47
|
+
default: {
|
|
48
|
+
id: 'default',
|
|
49
|
+
name: 'Molly',
|
|
50
|
+
description: '',
|
|
51
|
+
systemPrompt: '',
|
|
52
|
+
provider: 'openai',
|
|
53
|
+
model: 'gpt-test',
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const cwd = process.env.WORKSPACE_DIR
|
|
60
|
+
fs.mkdirSync(path.join(cwd, 'weather_workspace'), { recursive: true })
|
|
61
|
+
fs.writeFileSync(path.join(cwd, 'weather_workspace', 'weather_fetch.py'), 'print("weather")\\n')
|
|
62
|
+
|
|
63
|
+
const tools = crud.buildCrudTools({
|
|
64
|
+
cwd,
|
|
65
|
+
ctx: { sessionId: 'session-1', agentId: 'default', platformAssignScope: 'self' },
|
|
66
|
+
hasPlugin: (name) => name === 'manage_schedules',
|
|
67
|
+
})
|
|
68
|
+
const tool = tools.find((entry) => entry.name === 'manage_schedules')
|
|
69
|
+
const raw = await tool.invoke({
|
|
70
|
+
action: 'create',
|
|
71
|
+
data: JSON.stringify({
|
|
72
|
+
name: 'Daily Weather Update',
|
|
73
|
+
scheduleType: 'interval',
|
|
74
|
+
intervalMs: 60000,
|
|
75
|
+
action: 'run_script',
|
|
76
|
+
path: 'weather_workspace/weather_fetch.py',
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const schedule = Object.values(storage.loadSchedules())[0]
|
|
81
|
+
console.log(JSON.stringify({
|
|
82
|
+
raw,
|
|
83
|
+
schedule,
|
|
84
|
+
}))
|
|
85
|
+
`)
|
|
86
|
+
|
|
87
|
+
assert.equal(output.schedule.agentId, 'default')
|
|
88
|
+
assert.equal(output.schedule.path, 'weather_workspace/weather_fetch.py')
|
|
89
|
+
assert.match(output.schedule.taskPrompt, /weather_workspace\/weather_fetch\.py/)
|
|
90
|
+
assert.equal(output.schedule.status, 'active')
|
|
91
|
+
assert.equal(typeof output.schedule.nextRunAt, 'number')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('rejects schedules whose referenced script path does not exist', () => {
|
|
95
|
+
const output = runWithTempDataDir(`
|
|
96
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
97
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
98
|
+
const storage = storageMod.default || storageMod
|
|
99
|
+
const crud = crudMod.default || crudMod
|
|
100
|
+
|
|
101
|
+
const now = Date.now()
|
|
102
|
+
storage.saveAgents({
|
|
103
|
+
default: {
|
|
104
|
+
id: 'default',
|
|
105
|
+
name: 'Molly',
|
|
106
|
+
description: '',
|
|
107
|
+
systemPrompt: '',
|
|
108
|
+
provider: 'openai',
|
|
109
|
+
model: 'gpt-test',
|
|
110
|
+
createdAt: now,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const tools = crud.buildCrudTools({
|
|
116
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
117
|
+
ctx: { sessionId: 'session-2', agentId: 'default', platformAssignScope: 'self' },
|
|
118
|
+
hasPlugin: (name) => name === 'manage_schedules',
|
|
119
|
+
})
|
|
120
|
+
const tool = tools.find((entry) => entry.name === 'manage_schedules')
|
|
121
|
+
const raw = await tool.invoke({
|
|
122
|
+
action: 'create',
|
|
123
|
+
data: JSON.stringify({
|
|
124
|
+
name: 'Broken Weather Update',
|
|
125
|
+
scheduleType: 'interval',
|
|
126
|
+
intervalMs: 60000,
|
|
127
|
+
action: 'run_script',
|
|
128
|
+
path: 'weather_workspace/missing.py',
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
console.log(JSON.stringify({ raw }))
|
|
133
|
+
`)
|
|
134
|
+
|
|
135
|
+
assert.match(String(output.raw), /schedule path not found: weather_workspace\/missing\.py/i)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -15,6 +15,8 @@ import type { MemoryEntry, Plugin, PluginHooks } from '@/types'
|
|
|
15
15
|
import type { ToolBuildContext } from './context'
|
|
16
16
|
import { getPluginManager } from '../plugins'
|
|
17
17
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
18
|
+
import { partitionMemoriesByTier } from '../memory-tiers'
|
|
19
|
+
import { syncSessionArchiveMemory } from '../session-archive-memory'
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Advanced Database-Backed Memory logic.
|
|
@@ -34,6 +36,12 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
34
36
|
|
|
35
37
|
const memDb = getMemoryDb()
|
|
36
38
|
const currentAgentId = ctx?.agentId || null
|
|
39
|
+
const currentSessionId = typeof ctx?.sessionId === 'string'
|
|
40
|
+
? ctx.sessionId
|
|
41
|
+
: typeof ctx?.id === 'string'
|
|
42
|
+
? ctx.id
|
|
43
|
+
: null
|
|
44
|
+
const currentSession = ctx && typeof ctx === 'object' && Array.isArray(ctx.messages) ? ctx : null
|
|
37
45
|
const rawScope = typeof scope === 'string' ? scope : 'auto'
|
|
38
46
|
const scopeMode = normalizeMemoryScopeMode(rawScope === 'shared' ? 'global' : rawScope)
|
|
39
47
|
const rerankMode = rerank === 'semantic' || rerank === 'lexical' ? rerank : 'balanced'
|
|
@@ -41,7 +49,7 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
41
49
|
const scopeFilter = {
|
|
42
50
|
mode: scopeMode,
|
|
43
51
|
agentId: currentAgentId,
|
|
44
|
-
sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() :
|
|
52
|
+
sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : currentSessionId,
|
|
45
53
|
projectRoot: (typeof projectRoot === 'string' && projectRoot.trim()) ? projectRoot.trim() : ((project && typeof project === 'object' && 'rootPath' in project && typeof (project as Record<string, unknown>).rootPath === 'string') ? (project as Record<string, unknown>).rootPath as string : null),
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -52,6 +60,10 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
52
60
|
const limits = getMemoryLookupLimits(loadSettings())
|
|
53
61
|
const maxPerLookup = limits.maxPerLookup
|
|
54
62
|
|
|
63
|
+
if ((action === 'search' || action === 'list') && currentSession) {
|
|
64
|
+
try { syncSessionArchiveMemory(currentSession) } catch { /* archive sync is best-effort */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
const formatEntry = (m: any) => {
|
|
56
68
|
let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
|
|
57
69
|
if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
|
|
@@ -132,6 +144,8 @@ const MemoryPlugin: Plugin = {
|
|
|
132
144
|
const agentId = ctx.session.agentId
|
|
133
145
|
if (!agentId) return null
|
|
134
146
|
|
|
147
|
+
try { syncSessionArchiveMemory(ctx.session) } catch { /* archive sync is best-effort */ }
|
|
148
|
+
|
|
135
149
|
const memDb = getMemoryDb()
|
|
136
150
|
const memoryQuerySeed = [
|
|
137
151
|
ctx.message,
|
|
@@ -159,12 +173,22 @@ const MemoryPlugin: Plugin = {
|
|
|
159
173
|
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
|
|
160
174
|
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
161
175
|
const recent = memDb.list(agentId, 12).slice(0, 6)
|
|
176
|
+
const relevantByTier = partitionMemoriesByTier(relevant)
|
|
177
|
+
const recentByTier = partitionMemoriesByTier(recent)
|
|
178
|
+
|
|
179
|
+
const relevantLines = relevantByTier.durable
|
|
180
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
181
|
+
.map(formatMemoryLine)
|
|
162
182
|
|
|
163
|
-
const
|
|
183
|
+
const archiveLines = relevantByTier.archive
|
|
164
184
|
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
165
185
|
.map(formatMemoryLine)
|
|
166
186
|
|
|
167
|
-
const recentLines =
|
|
187
|
+
const recentLines = recentByTier.durable
|
|
188
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
189
|
+
.map(formatMemoryLine)
|
|
190
|
+
|
|
191
|
+
const recentArchiveLines = recentByTier.archive
|
|
168
192
|
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
169
193
|
.map(formatMemoryLine)
|
|
170
194
|
|
|
@@ -175,14 +199,21 @@ const MemoryPlugin: Plugin = {
|
|
|
175
199
|
if (relevantLines.length) {
|
|
176
200
|
parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
|
|
177
201
|
}
|
|
202
|
+
if (archiveLines.length) {
|
|
203
|
+
parts.push(['## Session Archive Hits', 'Past conversation snapshots that may restore context from older chats.', ...archiveLines].join('\n'))
|
|
204
|
+
}
|
|
178
205
|
if (recentLines.length) {
|
|
179
206
|
parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
|
|
180
207
|
}
|
|
208
|
+
if (recentArchiveLines.length) {
|
|
209
|
+
parts.push(['## Recent Session Archives', 'Recently synced conversation archives you can search instead of relying on stale live context.', ...recentArchiveLines].join('\n'))
|
|
210
|
+
}
|
|
181
211
|
|
|
182
212
|
// Memory Policy
|
|
183
213
|
parts.push([
|
|
184
214
|
'## My Memory',
|
|
185
215
|
'I have long-term memory that persists across conversations. I use it naturally — I don\'t wait to be asked to remember things.',
|
|
216
|
+
'Memory tiers: working memory is short-lived, durable memory stores stable facts and decisions, and session archives capture older conversation context for search.',
|
|
186
217
|
'',
|
|
187
218
|
'**Things worth remembering:**',
|
|
188
219
|
'- What the user likes, dislikes, or has corrected me on',
|
|
@@ -201,6 +232,7 @@ const MemoryPlugin: Plugin = {
|
|
|
201
232
|
'**Good habits:**',
|
|
202
233
|
'- Give memories clear titles ("User prefers dark mode" not "Note 1")',
|
|
203
234
|
'- Use categories: preference, fact, learning, project, identity, decision',
|
|
235
|
+
'- Search session archives before assuming older conversation context is still in the live chat history',
|
|
204
236
|
'- Check what I already know before storing something new',
|
|
205
237
|
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
206
238
|
].join('\n'))
|