@swarmclawai/swarmclaw 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -0,0 +1,257 @@
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 { after, before, describe, it } from 'node:test'
6
+ import type { Session } from '@/types'
7
+
8
+ const originalEnv = {
9
+ DATA_DIR: process.env.DATA_DIR,
10
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
11
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
12
+ }
13
+
14
+ let tempDir = ''
15
+ let workspaceDir = ''
16
+ let buildDocumentTools: typeof import('./document').buildDocumentTools
17
+ let buildExtractTools: typeof import('./extract').buildExtractTools
18
+ let buildTableTools: typeof import('./table').buildTableTools
19
+ let buildMailboxTools: typeof import('./mailbox').buildMailboxTools
20
+ let buildHumanLoopTools: typeof import('./human-loop').buildHumanLoopTools
21
+ let buildCrawlTools: typeof import('./crawl').buildCrawlTools
22
+ let sessionMailbox: typeof import('../session-mailbox')
23
+ let watchJobs: typeof import('../watch-jobs')
24
+ let storage: typeof import('../storage')
25
+
26
+ function makeSession(overrides?: Partial<Session>): Session {
27
+ return {
28
+ id: 'session_1',
29
+ name: 'Test Session',
30
+ cwd: workspaceDir,
31
+ user: 'tester',
32
+ provider: 'ollama',
33
+ model: 'qwen3.5',
34
+ apiEndpoint: 'http://localhost:11434',
35
+ claudeSessionId: null,
36
+ messages: [],
37
+ createdAt: Date.now(),
38
+ lastActiveAt: Date.now(),
39
+ plugins: [],
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ function makeBuildContext(overrides?: {
45
+ cwd?: string
46
+ session?: Session
47
+ }) {
48
+ const session = overrides?.session || makeSession()
49
+ return {
50
+ cwd: overrides?.cwd || workspaceDir,
51
+ ctx: {
52
+ sessionId: session.id,
53
+ agentId: session.agentId || 'agent_1',
54
+ },
55
+ hasPlugin: () => true,
56
+ hasTool: () => true,
57
+ cleanupFns: [],
58
+ commandTimeoutMs: 5000,
59
+ claudeTimeoutMs: 5000,
60
+ cliProcessTimeoutMs: 5000,
61
+ persistDelegateResumeId: () => {},
62
+ readStoredDelegateResumeId: () => null,
63
+ resolveCurrentSession: () => session,
64
+ activePlugins: [],
65
+ }
66
+ }
67
+
68
+ before(async () => {
69
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-primitive-tools-'))
70
+ workspaceDir = path.join(tempDir, 'workspace')
71
+ fs.mkdirSync(workspaceDir, { recursive: true })
72
+ process.env.DATA_DIR = path.join(tempDir, 'data')
73
+ process.env.WORKSPACE_DIR = workspaceDir
74
+ process.env.SWARMCLAW_BUILD_MODE = '1'
75
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
76
+
77
+ ;({ buildDocumentTools } = await import('./document'))
78
+ ;({ buildExtractTools } = await import('./extract'))
79
+ ;({ buildTableTools } = await import('./table'))
80
+ ;({ buildMailboxTools } = await import('./mailbox'))
81
+ ;({ buildHumanLoopTools } = await import('./human-loop'))
82
+ ;({ buildCrawlTools } = await import('./crawl'))
83
+ sessionMailbox = await import('../session-mailbox')
84
+ watchJobs = await import('../watch-jobs')
85
+ storage = await import('../storage')
86
+ })
87
+
88
+ after(() => {
89
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
90
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
91
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
92
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
93
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
94
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
95
+ fs.rmSync(tempDir, { recursive: true, force: true })
96
+ })
97
+
98
+ describe('primitive tools', () => {
99
+ it('document tool reads, stores, and searches extracted text', async () => {
100
+ const sourcePath = path.join(workspaceDir, 'note.txt')
101
+ fs.writeFileSync(sourcePath, 'Invoice 42 for ACME\nTotal: $120.50\n')
102
+
103
+ const [documentTool] = buildDocumentTools(makeBuildContext())
104
+ const read = JSON.parse(String(await documentTool.invoke({ action: 'read', filePath: 'note.txt' })))
105
+ assert.match(read.text, /Invoice 42/)
106
+
107
+ const stored = JSON.parse(String(await documentTool.invoke({ action: 'store', filePath: 'note.txt', title: 'Invoice Note' })))
108
+ const search = JSON.parse(String(await documentTool.invoke({ action: 'search', query: 'ACME' })))
109
+ const fetched = JSON.parse(String(await documentTool.invoke({ action: 'get', id: stored.id })))
110
+
111
+ assert.equal(search.matches[0].id, stored.id)
112
+ assert.equal(fetched.title, 'Invoice Note')
113
+ })
114
+
115
+ it('table tool transforms inline data and writes results', async () => {
116
+ const [tableTool] = buildTableTools(makeBuildContext())
117
+ const rows = [
118
+ { id: '1', name: 'Ada', score: 10 },
119
+ { id: '2', name: 'Grace', score: 25 },
120
+ { id: '2', name: 'Grace', score: 25 },
121
+ ]
122
+
123
+ const filtered = JSON.parse(String(await tableTool.invoke({
124
+ action: 'filter',
125
+ rows,
126
+ where: [{ column: 'score', op: 'gt', value: 15 }],
127
+ })))
128
+ assert.equal(filtered.rowCount, 2)
129
+
130
+ const deduped = JSON.parse(String(await tableTool.invoke({
131
+ action: 'dedupe',
132
+ rows,
133
+ on: ['id'],
134
+ })))
135
+ assert.equal(deduped.rowCount, 2)
136
+
137
+ const joined = JSON.parse(String(await tableTool.invoke({
138
+ action: 'join',
139
+ leftRows: [{ id: '1', team: 'red' }],
140
+ rightRows: [{ id: '1', email: 'ada@example.com' }],
141
+ on: 'id',
142
+ })))
143
+ assert.equal(joined.rows[0].email, 'ada@example.com')
144
+
145
+ const writeResult = JSON.parse(String(await tableTool.invoke({
146
+ action: 'write',
147
+ rows,
148
+ outputPath: 'exports/report.csv',
149
+ })))
150
+ assert.equal(fs.existsSync(writeResult.output.filePath), true)
151
+ })
152
+
153
+ it('human-loop tool creates durable mailbox and approval waits', async () => {
154
+ const [humanTool] = buildHumanLoopTools(makeBuildContext())
155
+ const sessions = storage.loadSessions()
156
+ sessions.session_1 = makeSession({ id: 'session_1', agentId: 'agent_1' })
157
+ storage.saveSessions(sessions)
158
+
159
+ const requestInput = JSON.parse(String(await humanTool.invoke({
160
+ action: 'request_input',
161
+ question: 'Ship it?',
162
+ correlationId: 'corr_123',
163
+ })))
164
+ assert.equal(requestInput.ok, true)
165
+
166
+ const replyWatch = JSON.parse(String(await humanTool.invoke({
167
+ action: 'wait_for_reply',
168
+ correlationId: 'corr_123',
169
+ })))
170
+ const replyEnvelope = sessionMailbox.sendMailboxEnvelope({
171
+ toSessionId: 'session_1',
172
+ type: 'human_reply',
173
+ payload: 'yes',
174
+ correlationId: 'corr_123',
175
+ })
176
+ watchJobs.triggerMailboxWatchJobs({ sessionId: 'session_1', envelope: replyEnvelope })
177
+ assert.equal(watchJobs.getWatchJob(replyWatch.id)?.status, 'triggered')
178
+
179
+ const approval = JSON.parse(String(await humanTool.invoke({
180
+ action: 'request_approval',
181
+ title: 'Need signoff',
182
+ question: 'Allow publish?',
183
+ })))
184
+ const approvalWatch = JSON.parse(String(await humanTool.invoke({
185
+ action: 'wait_for_approval',
186
+ approvalId: approval.id,
187
+ })))
188
+ watchJobs.triggerApprovalWatchJobs({ approvalId: approval.id, status: 'approved' })
189
+ assert.equal(watchJobs.getWatchJob(approvalWatch.id)?.status, 'triggered')
190
+ })
191
+
192
+ it('mailbox tool reports configuration status without requiring network', async () => {
193
+ const [mailboxTool] = buildMailboxTools(makeBuildContext())
194
+ const status = JSON.parse(String(await mailboxTool.invoke({ action: 'status' })))
195
+ assert.equal(status.configured, false)
196
+ assert.equal(status.folder, 'INBOX')
197
+ })
198
+
199
+ it('extract tool reports active model context', async () => {
200
+ const [extractTool] = buildExtractTools(makeBuildContext({
201
+ session: makeSession({
202
+ provider: 'ollama',
203
+ model: 'qwen3.5',
204
+ apiEndpoint: 'http://localhost:11434',
205
+ }),
206
+ }))
207
+ const status = JSON.parse(String(await extractTool.invoke({ action: 'status' })))
208
+ assert.equal(status.provider, 'ollama')
209
+ assert.equal(Array.isArray(status.supports), true)
210
+ })
211
+
212
+ it('crawl tool crawls and dedupes fetched pages without a live server', async () => {
213
+ const originalFetch = global.fetch
214
+ global.fetch = (async (input: RequestInfo | URL) => {
215
+ const url = typeof input === 'string' ? input : input.toString()
216
+ if (url.endsWith('/page-2')) {
217
+ return new Response('<html><head><title>Page Two</title></head><body><article><h1>Second</h1><p>Next content</p></article></body></html>', {
218
+ status: 200,
219
+ headers: { 'content-type': 'text/html' },
220
+ })
221
+ }
222
+ return new Response('<html><head><title>Root</title></head><body><article><h1>Home</h1><p>Welcome</p></article><a href="/page-2" rel="next">Next</a></body></html>', {
223
+ status: 200,
224
+ headers: { 'content-type': 'text/html' },
225
+ })
226
+ }) as typeof fetch
227
+
228
+ try {
229
+ const [crawlTool] = buildCrawlTools(makeBuildContext())
230
+ const baseUrl = 'https://example.test/'
231
+
232
+ const crawled = JSON.parse(String(await crawlTool.invoke({
233
+ action: 'crawl_site',
234
+ url: baseUrl,
235
+ limit: 2,
236
+ })))
237
+ assert.equal(crawled.count, 2)
238
+ assert.equal(crawled.pages[0].title, 'Root')
239
+
240
+ const extracted = JSON.parse(String(await crawlTool.invoke({
241
+ action: 'extract_sitemap',
242
+ url: baseUrl,
243
+ limit: 2,
244
+ })))
245
+ assert.equal(extracted.count, 2)
246
+ assert.equal(extracted.urls.includes('https://example.test/page-2'), true)
247
+
248
+ const deduped = JSON.parse(String(await crawlTool.invoke({
249
+ action: 'dedupe_pages',
250
+ pages: [crawled.pages[0], crawled.pages[0], crawled.pages[1]],
251
+ })))
252
+ assert.equal(deduped.count, 2)
253
+ } finally {
254
+ global.fetch = originalFetch
255
+ }
256
+ })
257
+ })
@@ -3,7 +3,6 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { Plugin, PluginHooks } from '@/types'
4
4
  import { getPluginManager } from '../plugins'
5
5
  import { normalizeToolInputArgs } from './normalize-tool-args'
6
- import { loadSettings } from '../storage'
7
6
  import type { ToolBuildContext } from './context'
8
7
 
9
8
  interface ReplicateConfig {
@@ -14,8 +13,7 @@ interface ReplicateConfig {
14
13
  }
15
14
 
16
15
  function getConfig(): ReplicateConfig {
17
- const settings = loadSettings()
18
- const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.replicate ?? {}
16
+ const ps = getPluginManager().getPluginSettings('replicate')
19
17
  return {
20
18
  apiToken: (ps.apiToken as string) || '',
21
19
  defaultModel: (ps.defaultModel as string) || '',
@@ -6,6 +6,7 @@ import type { ToolBuildContext } from './context'
6
6
  import type { Plugin, PluginHooks } from '@/types'
7
7
  import { getPluginManager } from '../plugins'
8
8
  import { normalizeToolInputArgs } from './normalize-tool-args'
9
+ import { createWatchJob } from '../watch-jobs'
9
10
 
10
11
  /**
11
12
  * Core Schedule Execution Logic
@@ -15,7 +16,7 @@ async function executeScheduleWake(args: { delayMinutes: number; message: string
15
16
  const delayMinutes = normalized.delayMinutes as number
16
17
  const message = normalized.message as string
17
18
  if (!context.sessionId) return 'Cannot schedule wake: no session context.'
18
- if (delayMinutes < 0 || delayMinutes > 1440) return 'delayMinutes must be between 0 and 1440 (24 hours).'
19
+ if (delayMinutes < 0 || delayMinutes > 43_200) return 'delayMinutes must be between 0 and 43200 (30 days).'
19
20
 
20
21
  if (delayMinutes === 0) {
21
22
  enqueueSystemEvent(context.sessionId, `[Scheduled Wake Event / Reminder] ${message}`)
@@ -23,15 +24,24 @@ async function executeScheduleWake(args: { delayMinutes: number; message: string
23
24
  return 'Successfully scheduled an immediate wake event.'
24
25
  }
25
26
 
26
- const delayMs = delayMinutes * 60 * 1000
27
- setTimeout(() => {
28
- if (context.sessionId) {
29
- enqueueSystemEvent(context.sessionId, `[Scheduled Wake Event / Reminder] ${message}`)
30
- requestHeartbeatNow({ sessionId: context.sessionId, reason: 'scheduled_wake' })
31
- }
32
- }, delayMs)
27
+ const runAt = Date.now() + delayMinutes * 60 * 1000
28
+ const watch = await createWatchJob({
29
+ type: 'time',
30
+ sessionId: context.sessionId,
31
+ resumeMessage: message,
32
+ description: `Scheduled wake in ${delayMinutes} minutes`,
33
+ target: { source: 'schedule_wake' },
34
+ condition: {},
35
+ runAt,
36
+ })
33
37
 
34
- return `Successfully scheduled a wake event in ${delayMinutes} minutes.`
38
+ return JSON.stringify({
39
+ ok: true,
40
+ jobId: watch.id,
41
+ delayMinutes,
42
+ runAt,
43
+ message,
44
+ })
35
45
  }
36
46
 
37
47
  /**
@@ -39,7 +49,7 @@ async function executeScheduleWake(args: { delayMinutes: number; message: string
39
49
  */
40
50
  const SchedulePlugin: Plugin = {
41
51
  name: 'Core Scheduler',
42
- description: 'Schedule wake events and reminders for agents.',
52
+ description: 'Schedule durable wake events and reminders for agents.',
43
53
  hooks: {
44
54
  getCapabilityDescription: () => 'I can set a conversational timer (`schedule_wake`) to remind myself to check back on something later in this chat.',
45
55
  } as PluginHooks,
@@ -33,6 +33,7 @@ async function executeSessionsAction(args: any, context: { sessionId?: string; a
33
33
  const limit = normalized.limit as number | undefined
34
34
  const agentId = (normalized.agentId ?? normalized.agent_id) as string | undefined
35
35
  const name = normalized.name as string | undefined
36
+ const updates = normalized.updates as Record<string, unknown> | undefined
36
37
  try {
37
38
  const sessions = loadSessions()
38
39
  if (action === 'list') {
@@ -53,12 +54,43 @@ async function executeSessionsAction(args: any, context: { sessionId?: string; a
53
54
  sessions[id] = {
54
55
  id, name: (name || `${agent.name} Chat`).trim(), cwd: context.cwd, user: 'system',
55
56
  provider: agent.provider, model: agent.model, credentialId: agent.credentialId || null,
56
- messages: [], createdAt: now, lastActiveAt: now, sessionType: 'orchestrated',
57
+ messages: [], createdAt: now, lastActiveAt: now, sessionType: 'human',
57
58
  agentId: agent.id, parentSessionId: context.sessionId || undefined, plugins: agent.plugins || agent.tools || [],
58
59
  }
59
60
  saveSessions(sessions)
60
61
  return JSON.stringify({ sessionId: id, name: agent.name })
61
62
  }
63
+ if (action === 'update') {
64
+ const targetId = sessionId || context.sessionId || ''
65
+ if (!targetId) return 'sessionId required.'
66
+ const target = sessions[targetId]
67
+ if (!target) return 'Not found.'
68
+ const allowedKeys = new Set([
69
+ 'thinkingLevel',
70
+ 'connectorThinkLevel',
71
+ 'sessionResetMode',
72
+ 'sessionIdleTimeoutSec',
73
+ 'sessionMaxAgeSec',
74
+ 'sessionDailyResetAt',
75
+ 'sessionResetTimezone',
76
+ 'connectorSessionScope',
77
+ 'connectorReplyMode',
78
+ 'connectorThreadBinding',
79
+ 'connectorGroupPolicy',
80
+ 'connectorIdleTimeoutSec',
81
+ 'connectorMaxAgeSec',
82
+ 'identityState',
83
+ 'provider',
84
+ 'model',
85
+ ])
86
+ const patch = updates && typeof updates === 'object' ? updates : {}
87
+ for (const [key, value] of Object.entries(patch)) {
88
+ if (!allowedKeys.has(key)) continue
89
+ target[key] = value
90
+ }
91
+ saveSessions(sessions)
92
+ return JSON.stringify({ sessionId: targetId, updated: Object.keys(patch).filter((key) => allowedKeys.has(key)) })
93
+ }
62
94
  return `Unknown action "${action}".`
63
95
  } catch (err: any) { return `Error: ${err.message}` }
64
96
  }
@@ -86,11 +118,12 @@ const SessionInfoPlugin: Plugin = {
86
118
  parameters: {
87
119
  type: 'object',
88
120
  properties: {
89
- action: { type: 'string', enum: ['list', 'history', 'spawn', 'status', 'stop'] },
121
+ action: { type: 'string', enum: ['list', 'history', 'spawn', 'status', 'stop', 'update'] },
90
122
  sessionId: { type: 'string' },
91
123
  agentId: { type: 'string' },
92
124
  message: { type: 'string' },
93
- limit: { type: 'number' }
125
+ limit: { type: 'number' },
126
+ updates: { type: 'object' },
94
127
  },
95
128
  required: ['action']
96
129
  },
@@ -24,6 +24,21 @@ describe('module exports', () => {
24
24
  const mem = await import('./memory')
25
25
  assert.equal(typeof mem.buildMemoryTools, 'function')
26
26
  })
27
+
28
+ it('primitive tool builders are exported', async () => {
29
+ const document = await import('./document')
30
+ const extract = await import('./extract')
31
+ const table = await import('./table')
32
+ const crawl = await import('./crawl')
33
+ const mailbox = await import('./mailbox')
34
+ const humanLoop = await import('./human-loop')
35
+ assert.equal(typeof document.buildDocumentTools, 'function')
36
+ assert.equal(typeof extract.buildExtractTools, 'function')
37
+ assert.equal(typeof table.buildTableTools, 'function')
38
+ assert.equal(typeof crawl.buildCrawlTools, 'function')
39
+ assert.equal(typeof mailbox.buildMailboxTools, 'function')
40
+ assert.equal(typeof humanLoop.buildHumanLoopTools, 'function')
41
+ })
27
42
  })
28
43
 
29
44
  // ---------------------------------------------------------------------------
@@ -60,46 +75,45 @@ describe('buildSessionTools signature', () => {
60
75
  })
61
76
 
62
77
  // ---------------------------------------------------------------------------
63
- // 4. Memory tool schema — knowledge actions
78
+ // 4. Memory tool schema
64
79
  // buildMemoryTools calls getMemoryDb() eagerly so we cannot invoke it
65
80
  // without a real SQLite DB. Instead we read the source and verify the
66
- // action enum includes the knowledge actions.
81
+ // declared action enum matches the current JSON schema definition.
67
82
  // ---------------------------------------------------------------------------
68
83
  describe('memory tool knowledge actions (source verification)', () => {
69
- it('action enum in memory.ts includes knowledge_store and knowledge_search', async () => {
84
+ it('action enum in memory.ts includes the declared base actions', async () => {
70
85
  const fs = await import('fs')
71
86
  const src = fs.readFileSync(
72
87
  new URL('./memory.ts', import.meta.url).pathname,
73
88
  'utf-8',
74
89
  )
75
90
 
76
- // Find the z.enum([...]) for the action field
77
- const enumMatch = src.match(/z\.enum\(\[([^\]]+)\]\)\.describe\([^)]*action/s)
78
- assert.ok(enumMatch, 'Should find a z.enum() for the action field')
91
+ const enumMatch = src.match(/action:\s*\{\s*type:\s*'string',\s*enum:\s*\[([^\]]+)\]/s)
92
+ assert.ok(enumMatch, 'Should find the action enum in the memory tool schema')
79
93
 
80
94
  const enumBody = enumMatch![1]
81
- assert.ok(enumBody.includes("'knowledge_store'"), 'action enum should include knowledge_store')
82
- assert.ok(enumBody.includes("'knowledge_search'"), 'action enum should include knowledge_search')
95
+ const expectedActions = ['store', 'get', 'search', 'list', 'delete']
96
+ for (const action of expectedActions) {
97
+ assert.ok(
98
+ enumBody.includes(`'${action}'`),
99
+ `action enum should include '${action}'`,
100
+ )
101
+ }
83
102
  })
84
103
 
85
- it('action enum includes all expected base actions', async () => {
104
+ it('action enum does not advertise removed knowledge actions', async () => {
86
105
  const fs = await import('fs')
87
106
  const src = fs.readFileSync(
88
107
  new URL('./memory.ts', import.meta.url).pathname,
89
108
  'utf-8',
90
109
  )
91
110
 
92
- const enumMatch = src.match(/z\.enum\(\[([^\]]+)\]\)/)
111
+ const enumMatch = src.match(/action:\s*\{\s*type:\s*'string',\s*enum:\s*\[([^\]]+)\]/s)
93
112
  assert.ok(enumMatch)
94
113
  const enumBody = enumMatch![1]
95
114
 
96
- const expectedActions = ['store', 'get', 'search', 'list', 'delete', 'link', 'unlink', 'knowledge_store', 'knowledge_search']
97
- for (const action of expectedActions) {
98
- assert.ok(
99
- enumBody.includes(`'${action}'`),
100
- `action enum should include '${action}'`,
101
- )
102
- }
115
+ assert.equal(enumBody.includes("'knowledge_store'"), false)
116
+ assert.equal(enumBody.includes("'knowledge_search'"), false)
103
117
  })
104
118
  })
105
119