@swarmclawai/swarmclaw 0.9.4 → 0.9.6
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 +8 -8
- package/package.json +1 -1
- package/src/app/api/settings/route.ts +1 -0
- package/src/app/api/wallets/[id]/send/route.ts +10 -4
- package/src/app/api/wallets/route.ts +5 -2
- package/src/app/settings/page.tsx +9 -0
- package/src/components/wallets/wallet-panel.tsx +12 -1
- package/src/components/wallets/wallet-section.tsx +4 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +35 -0
- package/src/lib/server/agents/main-agent-loop.ts +12 -1
- package/src/lib/server/agents/subagent-runtime.test.ts +99 -16
- package/src/lib/server/agents/subagent-runtime.ts +115 -19
- package/src/lib/server/agents/subagent-swarm.ts +3 -3
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +112 -0
- package/src/lib/server/chat-execution/chat-execution.ts +357 -152
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +51 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +201 -38
- package/src/lib/server/chat-execution/stream-continuation.ts +46 -0
- package/src/lib/server/connectors/contact-boundaries.ts +70 -8
- package/src/lib/server/connectors/manager.test.ts +129 -7
- package/src/lib/server/plugins.test.ts +263 -0
- package/src/lib/server/plugins.ts +406 -10
- package/src/lib/server/session-tools/context.ts +15 -1
- package/src/lib/server/session-tools/index.ts +42 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +50 -0
- package/src/lib/server/session-tools/subagent.ts +3 -3
- package/src/lib/server/tool-loop-detection.test.ts +21 -0
- package/src/lib/server/tool-loop-detection.ts +79 -0
- package/src/lib/server/wallet/wallet-service.test.ts +25 -1
- package/src/lib/server/wallet/wallet-service.ts +13 -0
- package/src/types/index.ts +134 -1
- package/src/views/settings/section-wallets.tsx +35 -0
|
@@ -13,6 +13,7 @@ import { enqueueSessionRun, type EnqueueSessionRunResult } from '@/lib/server/ru
|
|
|
13
13
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
14
14
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
|
|
15
15
|
import { resolveSubagentBrowserProfileId } from '@/lib/server/session-tools/subagent'
|
|
16
|
+
import { getPluginManager } from '@/lib/server/plugins'
|
|
16
17
|
import {
|
|
17
18
|
appendDelegationCheckpoint,
|
|
18
19
|
completeDelegationJob,
|
|
@@ -179,7 +180,14 @@ export function getSessionDepth(
|
|
|
179
180
|
export function spawnSubagent(
|
|
180
181
|
input: SpawnSubagentInput,
|
|
181
182
|
context: SubagentContext,
|
|
182
|
-
): SubagentHandle {
|
|
183
|
+
): Promise<SubagentHandle> {
|
|
184
|
+
return spawnSubagentImpl(input, context)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function spawnSubagentImpl(
|
|
188
|
+
input: SpawnSubagentInput,
|
|
189
|
+
context: SubagentContext,
|
|
190
|
+
): Promise<SubagentHandle> {
|
|
183
191
|
const runtime = loadRuntimeSettings()
|
|
184
192
|
const maxDepth = runtime.delegationMaxDepth || DEFAULT_DELEGATION_MAX_DEPTH
|
|
185
193
|
const agents = loadAgents()
|
|
@@ -195,6 +203,28 @@ export function spawnSubagent(
|
|
|
195
203
|
if (depth >= maxDepth) {
|
|
196
204
|
throw new Error(`Max subagent depth (${maxDepth}) reached.`)
|
|
197
205
|
}
|
|
206
|
+
const parent = context.sessionId ? sessions[context.sessionId] : null
|
|
207
|
+
const parentPlugins = (
|
|
208
|
+
Array.isArray(parent?.plugins) ? parent.plugins
|
|
209
|
+
: Array.isArray(parent?.tools) ? parent.tools
|
|
210
|
+
: []
|
|
211
|
+
) as string[]
|
|
212
|
+
const pluginManager = getPluginManager()
|
|
213
|
+
const spawningResult = await pluginManager.runSubagentSpawning(
|
|
214
|
+
{
|
|
215
|
+
parentSessionId: context.sessionId || null,
|
|
216
|
+
agentId: input.agentId,
|
|
217
|
+
agentName: agent.name,
|
|
218
|
+
message: input.message,
|
|
219
|
+
cwd: input.cwd || context.cwd,
|
|
220
|
+
mode: 'run',
|
|
221
|
+
threadRequested: false,
|
|
222
|
+
},
|
|
223
|
+
{ enabledIds: parentPlugins },
|
|
224
|
+
)
|
|
225
|
+
if (spawningResult.status === 'error') {
|
|
226
|
+
throw new Error(spawningResult.error || 'Subagent spawn blocked by plugin hook')
|
|
227
|
+
}
|
|
198
228
|
|
|
199
229
|
// 1. Create delegation job
|
|
200
230
|
const job = createDelegationJob({
|
|
@@ -209,7 +239,6 @@ export function spawnSubagent(
|
|
|
209
239
|
// 2. Create child session
|
|
210
240
|
const sid = genId()
|
|
211
241
|
const now = Date.now()
|
|
212
|
-
const parent = context.sessionId ? sessions[context.sessionId] : null
|
|
213
242
|
const browserProfileId = resolveSubagentBrowserProfileId(
|
|
214
243
|
parent,
|
|
215
244
|
sid,
|
|
@@ -276,6 +305,19 @@ export function spawnSubagent(
|
|
|
276
305
|
mode: 'followup',
|
|
277
306
|
executionGroupKey: input.executionGroupKey,
|
|
278
307
|
})
|
|
308
|
+
await pluginManager.runHook(
|
|
309
|
+
'subagentSpawned',
|
|
310
|
+
{
|
|
311
|
+
parentSessionId: context.sessionId || null,
|
|
312
|
+
childSessionId: sid,
|
|
313
|
+
agentId: agent.id,
|
|
314
|
+
agentName: agent.name,
|
|
315
|
+
runId: run.runId,
|
|
316
|
+
mode: 'run',
|
|
317
|
+
threadRequested: false,
|
|
318
|
+
},
|
|
319
|
+
{ enabledIds: parentPlugins },
|
|
320
|
+
)
|
|
279
321
|
|
|
280
322
|
// 8. Register runtime handle for cancellation
|
|
281
323
|
registerDelegationRuntime(job.id, {
|
|
@@ -290,33 +332,87 @@ export function spawnSubagent(
|
|
|
290
332
|
|
|
291
333
|
// 9. Build result promise
|
|
292
334
|
const resultPromise = run.promise
|
|
293
|
-
.then((result): SubagentResult => {
|
|
335
|
+
.then(async (result): Promise<SubagentResult> => {
|
|
294
336
|
const latest = getDelegationJob(job.id)
|
|
295
337
|
const node = getLineageNode(lineageNode.id)
|
|
338
|
+
let subagentResult: SubagentResult
|
|
296
339
|
if (latest?.status === 'cancelled' || node?.status === 'cancelled') {
|
|
297
|
-
|
|
340
|
+
subagentResult = buildResult(job.id, sid, lineageNode, agent, 'cancelled', null, null)
|
|
341
|
+
} else {
|
|
342
|
+
const responseText = (result.text || '').slice(0, 32_000)
|
|
343
|
+
completeLineageNode(lineageNode.id, responseText.slice(0, 1000))
|
|
344
|
+
appendDelegationCheckpoint(job.id, 'Child session completed', 'completed')
|
|
345
|
+
completeDelegationJob(job.id, responseText, { childSessionId: sid })
|
|
346
|
+
|
|
347
|
+
subagentResult = buildResult(job.id, sid, lineageNode, agent, 'completed', responseText, null)
|
|
298
348
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
349
|
+
await pluginManager.runHook(
|
|
350
|
+
'subagentEnded',
|
|
351
|
+
{
|
|
352
|
+
parentSessionId: context.sessionId || null,
|
|
353
|
+
childSessionId: sid,
|
|
354
|
+
agentId: agent.id,
|
|
355
|
+
agentName: agent.name,
|
|
356
|
+
status: subagentResult.status,
|
|
357
|
+
response: subagentResult.response,
|
|
358
|
+
error: subagentResult.error,
|
|
359
|
+
durationMs: subagentResult.durationMs,
|
|
360
|
+
},
|
|
361
|
+
{ enabledIds: parentPlugins },
|
|
362
|
+
)
|
|
363
|
+
await pluginManager.runHook(
|
|
364
|
+
'sessionEnd',
|
|
365
|
+
{
|
|
366
|
+
sessionId: sid,
|
|
367
|
+
session: loadSessions()[sid] || null,
|
|
368
|
+
messageCount: Array.isArray(loadSessions()[sid]?.messages) ? loadSessions()[sid].messages.length : 0,
|
|
369
|
+
durationMs: subagentResult.durationMs,
|
|
370
|
+
reason: subagentResult.status,
|
|
371
|
+
},
|
|
372
|
+
{ enabledIds: parentPlugins },
|
|
373
|
+
)
|
|
374
|
+
return subagentResult
|
|
306
375
|
})
|
|
307
|
-
.catch((err: unknown): SubagentResult => {
|
|
376
|
+
.catch(async (err: unknown): Promise<SubagentResult> => {
|
|
308
377
|
const message = errorMessage(err)
|
|
309
378
|
const latest = getDelegationJob(job.id)
|
|
310
379
|
const node = getLineageNode(lineageNode.id)
|
|
380
|
+
let subagentResult: SubagentResult
|
|
311
381
|
if (latest?.status === 'cancelled' || node?.status === 'cancelled') {
|
|
312
|
-
|
|
313
|
-
}
|
|
382
|
+
subagentResult = buildResult(job.id, sid, lineageNode, agent, 'cancelled', null, null)
|
|
383
|
+
} else {
|
|
384
|
+
failLineageNode(lineageNode.id, message)
|
|
385
|
+
appendDelegationCheckpoint(job.id, `Child session failed: ${message}`, 'failed')
|
|
386
|
+
failDelegationJob(job.id, message, { childSessionId: sid })
|
|
314
387
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
388
|
+
subagentResult = buildResult(job.id, sid, lineageNode, agent, 'failed', null, message)
|
|
389
|
+
}
|
|
390
|
+
await pluginManager.runHook(
|
|
391
|
+
'subagentEnded',
|
|
392
|
+
{
|
|
393
|
+
parentSessionId: context.sessionId || null,
|
|
394
|
+
childSessionId: sid,
|
|
395
|
+
agentId: agent.id,
|
|
396
|
+
agentName: agent.name,
|
|
397
|
+
status: subagentResult.status,
|
|
398
|
+
response: subagentResult.response,
|
|
399
|
+
error: subagentResult.error,
|
|
400
|
+
durationMs: subagentResult.durationMs,
|
|
401
|
+
},
|
|
402
|
+
{ enabledIds: parentPlugins },
|
|
403
|
+
)
|
|
404
|
+
await pluginManager.runHook(
|
|
405
|
+
'sessionEnd',
|
|
406
|
+
{
|
|
407
|
+
sessionId: sid,
|
|
408
|
+
session: loadSessions()[sid] || null,
|
|
409
|
+
messageCount: Array.isArray(loadSessions()[sid]?.messages) ? loadSessions()[sid].messages.length : 0,
|
|
410
|
+
durationMs: subagentResult.durationMs,
|
|
411
|
+
reason: subagentResult.status,
|
|
412
|
+
},
|
|
413
|
+
{ enabledIds: parentPlugins },
|
|
414
|
+
)
|
|
415
|
+
return subagentResult
|
|
320
416
|
})
|
|
321
417
|
|
|
322
418
|
const handle: SubagentHandle = {
|
|
@@ -169,10 +169,10 @@ function notifySwarmChanged() {
|
|
|
169
169
|
*
|
|
170
170
|
* const aggregate = await swarm.allSettled
|
|
171
171
|
*/
|
|
172
|
-
export function spawnSwarm(
|
|
172
|
+
export async function spawnSwarm(
|
|
173
173
|
input: BatchSpawnInput,
|
|
174
174
|
context: SubagentContext,
|
|
175
|
-
): SwarmHandle {
|
|
175
|
+
): Promise<SwarmHandle> {
|
|
176
176
|
const swarmId = genId(10)
|
|
177
177
|
const createdAt = Date.now()
|
|
178
178
|
const members: SwarmMember[] = []
|
|
@@ -190,7 +190,7 @@ export function spawnSwarm(
|
|
|
190
190
|
for (let i = 0; i < input.tasks.length; i++) {
|
|
191
191
|
const task = input.tasks[i]
|
|
192
192
|
try {
|
|
193
|
-
const handle = spawnSubagent(
|
|
193
|
+
const handle = await spawnSubagent(
|
|
194
194
|
{
|
|
195
195
|
agentId: task.agentId,
|
|
196
196
|
message: task.message,
|
|
@@ -230,3 +230,115 @@ test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thr
|
|
|
230
230
|
assert.equal(output.lastMessageText, 'seed user message')
|
|
231
231
|
assert.equal(output.heartbeatKinds, 0)
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
test('executeSessionChatTurn applies lifecycle hooks for model resolution and message persistence', () => {
|
|
235
|
+
const output = runWithTempDataDir(`
|
|
236
|
+
const storageMod = await import('@/lib/server/storage')
|
|
237
|
+
const providersMod = await import('@/lib/providers/index')
|
|
238
|
+
const pluginsMod = await import('@/lib/server/plugins')
|
|
239
|
+
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
240
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
241
|
+
const executeSessionChatTurn = execMod.executeSessionChatTurn
|
|
242
|
+
|| execMod.default?.executeSessionChatTurn
|
|
243
|
+
|| execMod['module.exports']?.executeSessionChatTurn
|
|
244
|
+
const providers = providersMod.PROVIDERS
|
|
245
|
+
|| providersMod.default?.PROVIDERS
|
|
246
|
+
|| providersMod['module.exports']?.PROVIDERS
|
|
247
|
+
const pluginManager = pluginsMod.getPluginManager
|
|
248
|
+
? pluginsMod.getPluginManager()
|
|
249
|
+
: pluginsMod.default?.getPluginManager?.()
|
|
250
|
+
|
|
251
|
+
const lifecycleMarks = []
|
|
252
|
+
pluginManager.registerBuiltin('lifecycle_hooks_test', {
|
|
253
|
+
name: 'Lifecycle Hooks Test',
|
|
254
|
+
hooks: {
|
|
255
|
+
beforeModelResolve: () => ({
|
|
256
|
+
providerOverride: 'claude-cli',
|
|
257
|
+
modelOverride: 'resolved-model',
|
|
258
|
+
}),
|
|
259
|
+
beforeMessageWrite: ({ message, phase }) => {
|
|
260
|
+
lifecycleMarks.push(phase || 'unknown')
|
|
261
|
+
return {
|
|
262
|
+
message: {
|
|
263
|
+
...message,
|
|
264
|
+
text: message.role === 'assistant' ? message.text + ' [stored]' : message.text,
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
sessionStart: () => {
|
|
269
|
+
lifecycleMarks.push('session_start')
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
providers['claude-cli'] = {
|
|
275
|
+
id: 'claude-cli',
|
|
276
|
+
name: 'Resolved Provider',
|
|
277
|
+
models: ['resolved-model'],
|
|
278
|
+
requiresApiKey: false,
|
|
279
|
+
requiresEndpoint: false,
|
|
280
|
+
handler: {
|
|
281
|
+
async streamChat(opts) {
|
|
282
|
+
lifecycleMarks.push('provider:' + opts.session.provider + ':' + opts.session.model)
|
|
283
|
+
return 'resolved response'
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const now = Date.now()
|
|
289
|
+
storage.saveAgents({
|
|
290
|
+
lifecycle: {
|
|
291
|
+
id: 'lifecycle',
|
|
292
|
+
name: 'Lifecycle Agent',
|
|
293
|
+
description: 'Lifecycle hook integration test',
|
|
294
|
+
provider: 'openai',
|
|
295
|
+
model: 'seed-model',
|
|
296
|
+
credentialId: null,
|
|
297
|
+
apiEndpoint: null,
|
|
298
|
+
fallbackCredentialIds: [],
|
|
299
|
+
disabled: false,
|
|
300
|
+
heartbeatEnabled: false,
|
|
301
|
+
heartbeatIntervalSec: null,
|
|
302
|
+
plugins: ['lifecycle_hooks_test'],
|
|
303
|
+
createdAt: now,
|
|
304
|
+
updatedAt: now,
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
storage.saveSessions({
|
|
308
|
+
lifecycle_session: {
|
|
309
|
+
id: 'lifecycle_session',
|
|
310
|
+
name: 'Lifecycle Session',
|
|
311
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
312
|
+
user: 'default',
|
|
313
|
+
provider: 'openai',
|
|
314
|
+
model: 'seed-model',
|
|
315
|
+
claudeSessionId: null,
|
|
316
|
+
messages: [],
|
|
317
|
+
createdAt: now,
|
|
318
|
+
lastActiveAt: now,
|
|
319
|
+
sessionType: 'human',
|
|
320
|
+
agentId: 'lifecycle',
|
|
321
|
+
plugins: ['lifecycle_hooks_test'],
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
await executeSessionChatTurn({
|
|
326
|
+
sessionId: 'lifecycle_session',
|
|
327
|
+
message: 'hello lifecycle',
|
|
328
|
+
runId: 'run-lifecycle-hooks',
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const persisted = storage.loadSession('lifecycle_session')
|
|
332
|
+
console.log(JSON.stringify({
|
|
333
|
+
lastMessageText: persisted?.messages?.at(-1)?.text || null,
|
|
334
|
+
marks: lifecycleMarks,
|
|
335
|
+
}))
|
|
336
|
+
`)
|
|
337
|
+
|
|
338
|
+
assert.equal(output.lastMessageText.startsWith('resolved response'), true)
|
|
339
|
+
assert.equal(output.lastMessageText.endsWith('[stored]'), true)
|
|
340
|
+
assert.equal(output.marks.includes('session_start'), true)
|
|
341
|
+
assert.equal(output.marks.includes('provider:claude-cli:resolved-model'), true)
|
|
342
|
+
assert.equal(output.marks.includes('user'), true)
|
|
343
|
+
assert.equal(output.marks.includes('assistant_final'), true)
|
|
344
|
+
})
|