@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.
- package/README.md +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- 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]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- 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 +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/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/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- 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 +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- 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 +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- 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 +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- 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 +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -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 +245 -46
- 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 +74 -1
- 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/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- 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/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 +250 -61
- 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 +45 -5
- 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 +946 -110
- 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/daemon-state.ts +59 -1
- 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 +13 -39
- 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 +27 -967
- 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 +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- 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 +105 -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 +70 -32
- 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.ts +22 -4
- 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 +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +237 -24
- 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 +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- 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 +86 -23
- 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/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -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 +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- 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 +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- 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/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- 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
|
@@ -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
|
|
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 >
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
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(/
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|