@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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 +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-main-loop-test-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: tempDir,
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('main-agent-loop', () => {
|
|
36
|
+
it('fans out events to durable main sessions and shapes heartbeat prompts', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
|
+
const mainLoopMod = await import('./src/lib/server/main-agent-loop.ts')
|
|
41
|
+
const mainLoop = mainLoopMod.default || mainLoopMod['module.exports'] || mainLoopMod
|
|
42
|
+
|
|
43
|
+
storage.saveAgents({
|
|
44
|
+
'agent-a': {
|
|
45
|
+
id: 'agent-a',
|
|
46
|
+
name: 'Agent A',
|
|
47
|
+
provider: 'openai',
|
|
48
|
+
model: 'gpt-test',
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
storage.saveSessions({
|
|
53
|
+
main: {
|
|
54
|
+
id: 'main',
|
|
55
|
+
name: 'Main Agent Thread',
|
|
56
|
+
shortcutForAgentId: 'agent-a',
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
user: 'tester',
|
|
59
|
+
provider: 'openai',
|
|
60
|
+
model: 'gpt-test',
|
|
61
|
+
claudeSessionId: null,
|
|
62
|
+
messages: [
|
|
63
|
+
{ role: 'user', text: 'Build me a durable multi-step agent loop.', time: 1 },
|
|
64
|
+
],
|
|
65
|
+
createdAt: 1,
|
|
66
|
+
lastActiveAt: 1,
|
|
67
|
+
sessionType: 'human',
|
|
68
|
+
agentId: 'agent-a',
|
|
69
|
+
heartbeatEnabled: true,
|
|
70
|
+
},
|
|
71
|
+
child: {
|
|
72
|
+
id: 'child',
|
|
73
|
+
name: 'Child Worker',
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
user: 'tester',
|
|
76
|
+
provider: 'openai',
|
|
77
|
+
model: 'gpt-test',
|
|
78
|
+
claudeSessionId: null,
|
|
79
|
+
messages: [],
|
|
80
|
+
createdAt: 1,
|
|
81
|
+
lastActiveAt: 1,
|
|
82
|
+
sessionType: 'orchestrated',
|
|
83
|
+
agentId: 'agent-a',
|
|
84
|
+
parentSessionId: 'main',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const count = mainLoop.pushMainLoopEventToMainSessions({
|
|
89
|
+
type: 'task_completed',
|
|
90
|
+
text: 'Task completed: implement queue follow-ups',
|
|
91
|
+
})
|
|
92
|
+
const state = mainLoop.getMainLoopStateForSession('main')
|
|
93
|
+
const prompt = mainLoop.buildMainLoopHeartbeatPrompt(storage.loadSessions().main, 'fallback heartbeat')
|
|
94
|
+
const childState = mainLoop.getMainLoopStateForSession('child')
|
|
95
|
+
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
count,
|
|
98
|
+
pendingCount: state?.pendingEvents?.length || 0,
|
|
99
|
+
goal: state?.goal || null,
|
|
100
|
+
promptIncludesEvent: prompt.includes('Task completed: implement queue follow-ups'),
|
|
101
|
+
promptIncludesPlanTag: prompt.includes('[MAIN_LOOP_PLAN]'),
|
|
102
|
+
childState,
|
|
103
|
+
}))
|
|
104
|
+
`)
|
|
105
|
+
|
|
106
|
+
assert.equal(output.count, 1)
|
|
107
|
+
assert.equal(output.pendingCount, 1)
|
|
108
|
+
assert.match(output.goal, /durable multi-step agent loop/i)
|
|
109
|
+
assert.equal(output.promptIncludesEvent, true)
|
|
110
|
+
assert.equal(output.promptIncludesPlanTag, true)
|
|
111
|
+
assert.equal(output.childState, null)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('updates state from heartbeat metadata and returns a bounded follow-up', () => {
|
|
115
|
+
const output = runWithTempDataDir(`
|
|
116
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
117
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
118
|
+
const mainLoopMod = await import('./src/lib/server/main-agent-loop.ts')
|
|
119
|
+
const mainLoop = mainLoopMod.default || mainLoopMod['module.exports'] || mainLoopMod
|
|
120
|
+
|
|
121
|
+
storage.saveAgents({
|
|
122
|
+
'agent-a': {
|
|
123
|
+
id: 'agent-a',
|
|
124
|
+
name: 'Agent A',
|
|
125
|
+
provider: 'openai',
|
|
126
|
+
model: 'gpt-test',
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
storage.saveSessions({
|
|
131
|
+
main: {
|
|
132
|
+
id: 'main',
|
|
133
|
+
name: 'Main Agent Thread',
|
|
134
|
+
shortcutForAgentId: 'agent-a',
|
|
135
|
+
cwd: process.cwd(),
|
|
136
|
+
user: 'tester',
|
|
137
|
+
provider: 'openai',
|
|
138
|
+
model: 'gpt-test',
|
|
139
|
+
claudeSessionId: null,
|
|
140
|
+
messages: [
|
|
141
|
+
{ role: 'user', text: 'Build me a durable task runner.', time: 1 },
|
|
142
|
+
],
|
|
143
|
+
createdAt: 1,
|
|
144
|
+
lastActiveAt: 1,
|
|
145
|
+
sessionType: 'human',
|
|
146
|
+
agentId: 'agent-a',
|
|
147
|
+
heartbeatEnabled: true,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
mainLoop.pushMainLoopEventToMainSessions({
|
|
152
|
+
type: 'schedule_fired',
|
|
153
|
+
text: 'Schedule fired: nightly sync',
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const followup = mainLoop.handleMainLoopRunResult({
|
|
157
|
+
sessionId: 'main',
|
|
158
|
+
message: 'Continue the durable task runner objective.',
|
|
159
|
+
internal: true,
|
|
160
|
+
source: 'heartbeat',
|
|
161
|
+
resultText: [
|
|
162
|
+
'Inspected the queue and the heartbeat pipeline.',
|
|
163
|
+
'[MAIN_LOOP_PLAN]{"steps":["inspect queue","wire follow-up scheduling"],"current_step":"inspect queue"}',
|
|
164
|
+
'[MAIN_LOOP_REVIEW]{"note":"queue inspected and next step identified","confidence":0.82,"needs_replan":false}',
|
|
165
|
+
'[AGENT_HEARTBEAT_META]{"goal":"Build a durable task runner","status":"progress","next_action":"wire the follow-up scheduling path"}',
|
|
166
|
+
].join('\\n'),
|
|
167
|
+
toolEvents: [{ name: 'shell', input: '{"action":"execute"}' }],
|
|
168
|
+
inputTokens: 40,
|
|
169
|
+
outputTokens: 20,
|
|
170
|
+
estimatedCost: 0.12,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const state = mainLoop.getMainLoopStateForSession('main')
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
followup,
|
|
176
|
+
status: state?.status || null,
|
|
177
|
+
nextAction: state?.nextAction || null,
|
|
178
|
+
planSteps: state?.planSteps || [],
|
|
179
|
+
currentPlanStep: state?.currentPlanStep || null,
|
|
180
|
+
pendingEvents: state?.pendingEvents?.length || 0,
|
|
181
|
+
followupChainCount: state?.followupChainCount || 0,
|
|
182
|
+
missionTokens: state?.missionTokens || 0,
|
|
183
|
+
}))
|
|
184
|
+
`)
|
|
185
|
+
|
|
186
|
+
assert.equal(output.status, 'progress')
|
|
187
|
+
assert.equal(output.nextAction, 'wire the follow-up scheduling path')
|
|
188
|
+
assert.deepEqual(output.planSteps, ['inspect queue', 'wire follow-up scheduling'])
|
|
189
|
+
assert.equal(output.currentPlanStep, 'inspect queue')
|
|
190
|
+
assert.equal(output.pendingEvents, 0)
|
|
191
|
+
assert.equal(output.followupChainCount, 1)
|
|
192
|
+
assert.equal(output.missionTokens, 60)
|
|
193
|
+
assert.match(output.followup.message, /wire the follow-up scheduling path/i)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('does not keep chaining when the heartbeat explicitly reports ok', () => {
|
|
197
|
+
const output = runWithTempDataDir(`
|
|
198
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
199
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
200
|
+
const mainLoopMod = await import('./src/lib/server/main-agent-loop.ts')
|
|
201
|
+
const mainLoop = mainLoopMod.default || mainLoopMod['module.exports'] || mainLoopMod
|
|
202
|
+
|
|
203
|
+
storage.saveAgents({
|
|
204
|
+
'agent-a': {
|
|
205
|
+
id: 'agent-a',
|
|
206
|
+
name: 'Agent A',
|
|
207
|
+
provider: 'openai',
|
|
208
|
+
model: 'gpt-test',
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
storage.saveSessions({
|
|
213
|
+
main: {
|
|
214
|
+
id: 'main',
|
|
215
|
+
name: 'Main Agent Thread',
|
|
216
|
+
shortcutForAgentId: 'agent-a',
|
|
217
|
+
cwd: process.cwd(),
|
|
218
|
+
user: 'tester',
|
|
219
|
+
provider: 'openai',
|
|
220
|
+
model: 'gpt-test',
|
|
221
|
+
claudeSessionId: null,
|
|
222
|
+
messages: [
|
|
223
|
+
{ role: 'user', text: 'Keep the background project healthy.', time: 1 },
|
|
224
|
+
],
|
|
225
|
+
createdAt: 1,
|
|
226
|
+
lastActiveAt: 1,
|
|
227
|
+
sessionType: 'human',
|
|
228
|
+
agentId: 'agent-a',
|
|
229
|
+
heartbeatEnabled: true,
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
mainLoop.pushMainLoopEventToMainSessions({
|
|
234
|
+
type: 'task_completed',
|
|
235
|
+
text: 'Task completed: health check',
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const followup = mainLoop.handleMainLoopRunResult({
|
|
239
|
+
sessionId: 'main',
|
|
240
|
+
message: 'Heartbeat tick',
|
|
241
|
+
internal: true,
|
|
242
|
+
source: 'heartbeat',
|
|
243
|
+
resultText: 'HEARTBEAT_OK',
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const state = mainLoop.getMainLoopStateForSession('main')
|
|
247
|
+
console.log(JSON.stringify({
|
|
248
|
+
followup,
|
|
249
|
+
status: state?.status || null,
|
|
250
|
+
pendingEvents: state?.pendingEvents?.length || 0,
|
|
251
|
+
followupChainCount: state?.followupChainCount || 0,
|
|
252
|
+
}))
|
|
253
|
+
`)
|
|
254
|
+
|
|
255
|
+
assert.equal(output.followup, null)
|
|
256
|
+
assert.equal(output.status, 'ok')
|
|
257
|
+
assert.equal(output.pendingEvents, 0)
|
|
258
|
+
assert.equal(output.followupChainCount, 0)
|
|
259
|
+
})
|
|
260
|
+
})
|