@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
|
@@ -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
|
+
}
|
|
@@ -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})`
|
|
@@ -127,7 +139,164 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
127
139
|
const MemoryPlugin: Plugin = {
|
|
128
140
|
name: 'Core Memory',
|
|
129
141
|
description: 'Advanced database-backed long-term memory with semantic search and graph linking.',
|
|
130
|
-
hooks: {
|
|
142
|
+
hooks: {
|
|
143
|
+
getAgentContext: async (ctx) => {
|
|
144
|
+
const agentId = ctx.session.agentId
|
|
145
|
+
if (!agentId) return null
|
|
146
|
+
|
|
147
|
+
try { syncSessionArchiveMemory(ctx.session) } catch { /* archive sync is best-effort */ }
|
|
148
|
+
|
|
149
|
+
const memDb = getMemoryDb()
|
|
150
|
+
const memoryQuerySeed = [
|
|
151
|
+
ctx.message,
|
|
152
|
+
...ctx.history
|
|
153
|
+
.slice(-4)
|
|
154
|
+
.filter((h) => h.role === 'user')
|
|
155
|
+
.map((h) => h.text),
|
|
156
|
+
].join('\n')
|
|
157
|
+
|
|
158
|
+
const seen = new Set<string>()
|
|
159
|
+
const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
|
|
160
|
+
const category = String(m.category || 'note')
|
|
161
|
+
const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
|
|
162
|
+
const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
163
|
+
const pin = m.pinned ? ' [pinned]' : ''
|
|
164
|
+
return `- [${category}]${pin} ${title}: ${snippet}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const pinned = memDb.listPinned(agentId, 5)
|
|
168
|
+
const pinnedLines = pinned
|
|
169
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
170
|
+
.map(formatMemoryLine)
|
|
171
|
+
|
|
172
|
+
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
173
|
+
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
|
|
174
|
+
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
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)
|
|
182
|
+
|
|
183
|
+
const archiveLines = relevantByTier.archive
|
|
184
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
185
|
+
.map(formatMemoryLine)
|
|
186
|
+
|
|
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
|
|
192
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
193
|
+
.map(formatMemoryLine)
|
|
194
|
+
|
|
195
|
+
const parts: string[] = []
|
|
196
|
+
if (pinnedLines.length) {
|
|
197
|
+
parts.push(['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'))
|
|
198
|
+
}
|
|
199
|
+
if (relevantLines.length) {
|
|
200
|
+
parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
|
|
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
|
+
}
|
|
205
|
+
if (recentLines.length) {
|
|
206
|
+
parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
|
|
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
|
+
}
|
|
211
|
+
|
|
212
|
+
// Memory Policy
|
|
213
|
+
parts.push([
|
|
214
|
+
'## My Memory',
|
|
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.',
|
|
217
|
+
'',
|
|
218
|
+
'**Things worth remembering:**',
|
|
219
|
+
'- What the user likes, dislikes, or has corrected me on',
|
|
220
|
+
'- Important decisions, outcomes, and lessons learned',
|
|
221
|
+
'- What I\'ve discovered about projects, codebases, or environments',
|
|
222
|
+
'- Problems I\'ve hit and how I solved them',
|
|
223
|
+
'- Who people are and how they relate to each other',
|
|
224
|
+
'- Configuration details and environment specifics that I\'ll need again',
|
|
225
|
+
'',
|
|
226
|
+
'**Not worth cluttering my memory with:**',
|
|
227
|
+
'- Throwaway acknowledgments or small talk',
|
|
228
|
+
'- Work-in-progress that\'ll change soon (use category "working" for scratch notes)',
|
|
229
|
+
'- Things already in my system prompt',
|
|
230
|
+
'- Something I\'ve already stored',
|
|
231
|
+
'',
|
|
232
|
+
'**Good habits:**',
|
|
233
|
+
'- Give memories clear titles ("User prefers dark mode" not "Note 1")',
|
|
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',
|
|
236
|
+
'- Check what I already know before storing something new',
|
|
237
|
+
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
238
|
+
].join('\n'))
|
|
239
|
+
|
|
240
|
+
// Pre-compaction consolidation nudge
|
|
241
|
+
const msgCount = ctx.history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
242
|
+
if (msgCount > 20) {
|
|
243
|
+
parts.push([
|
|
244
|
+
'## Reflection & Consolidation Reminder',
|
|
245
|
+
'This conversation is getting long and I might lose older context soon.',
|
|
246
|
+
'Save anything important I\'ve learned, decided, or discovered to memory now. Only what matters, not every detail.',
|
|
247
|
+
].join('\n'))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return parts.join('\n\n') || null
|
|
251
|
+
},
|
|
252
|
+
afterToolExec: (ctx) => {
|
|
253
|
+
const agentId = ctx.session.agentId
|
|
254
|
+
if (!agentId) return
|
|
255
|
+
const inp = ctx.input
|
|
256
|
+
if (!inp || typeof inp !== 'object') return
|
|
257
|
+
const action = typeof inp.action === 'string' ? inp.action : ''
|
|
258
|
+
let title: string | null = null
|
|
259
|
+
if (ctx.toolName === 'manage_tasks') {
|
|
260
|
+
if (action === 'create') title = `Created task: ${inp.title || 'Untitled'}`
|
|
261
|
+
else if (ctx.output && /status.*completed|completed.*successfully/i.test(ctx.output)) title = `Completed task: ${inp.title || inp.taskId || 'unknown'}`
|
|
262
|
+
}
|
|
263
|
+
if (ctx.toolName === 'manage_schedules' && action === 'create') title = `Created schedule: ${inp.name || 'Untitled'}`
|
|
264
|
+
if (ctx.toolName === 'manage_agents' && action === 'create') title = `Created agent: ${inp.name || 'Untitled'}`
|
|
265
|
+
if (!title) return
|
|
266
|
+
try {
|
|
267
|
+
const memDb = getMemoryDb()
|
|
268
|
+
memDb.add({ agentId, sessionId: ctx.session.id, category: 'breadcrumb', title, content: '' })
|
|
269
|
+
} catch { /* breadcrumbs are best-effort */ }
|
|
270
|
+
},
|
|
271
|
+
afterChatTurn: (ctx) => {
|
|
272
|
+
if (ctx.internal) return
|
|
273
|
+
if (ctx.source !== 'chat' && ctx.source !== 'connector') return
|
|
274
|
+
const agentId = ctx.session.agentId
|
|
275
|
+
if (!agentId) return
|
|
276
|
+
const msg = (ctx.message || '').trim()
|
|
277
|
+
const resp = (ctx.response || '').trim()
|
|
278
|
+
if (msg.length < 20 || resp.length < 40) return
|
|
279
|
+
if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return
|
|
280
|
+
if (resp === 'HEARTBEAT_OK') return
|
|
281
|
+
const now = Date.now()
|
|
282
|
+
const last = typeof ctx.session.lastAutoMemoryAt === 'number' ? ctx.session.lastAutoMemoryAt : 0
|
|
283
|
+
if (last > 0 && now - last < 5 * 60 * 1000) return
|
|
284
|
+
try {
|
|
285
|
+
const memDb = getMemoryDb()
|
|
286
|
+
const compactMessage = msg.replace(/\s+/g, ' ').slice(0, 220)
|
|
287
|
+
const compactResponse = resp.replace(/\s+/g, ' ').slice(0, 700)
|
|
288
|
+
const autoTitle = `[auto] ${compactMessage.slice(0, 90)}`
|
|
289
|
+
const content = `source: ${ctx.source}\nuser_request: ${compactMessage}\nassistant_outcome: ${compactResponse}`
|
|
290
|
+
memDb.add({ agentId, sessionId: ctx.session.id, category: 'execution', title: autoTitle, content })
|
|
291
|
+
ctx.session.lastAutoMemoryAt = now
|
|
292
|
+
} catch { /* auto-memory is best-effort */ }
|
|
293
|
+
},
|
|
294
|
+
getCapabilityDescription: () => 'I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.',
|
|
295
|
+
getOperatingGuidance: () => [
|
|
296
|
+
'Memory: search before major tasks, store concise notes after meaningful steps. Platform preloads context each turn.',
|
|
297
|
+
'For open goals, form a hypothesis and execute — do not keep re-asking broad questions.',
|
|
298
|
+
],
|
|
299
|
+
} as PluginHooks,
|
|
131
300
|
tools: [
|
|
132
301
|
{
|
|
133
302
|
name: 'memory_tool',
|
|
@@ -155,7 +324,7 @@ const MemoryPlugin: Plugin = {
|
|
|
155
324
|
getPluginManager().registerBuiltin('memory', MemoryPlugin)
|
|
156
325
|
|
|
157
326
|
export function buildMemoryTools(bctx: ToolBuildContext) {
|
|
158
|
-
if (!bctx.
|
|
327
|
+
if (!bctx.hasPlugin('memory')) return []
|
|
159
328
|
|
|
160
329
|
return [
|
|
161
330
|
tool(
|