@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
|
@@ -22,7 +22,13 @@ import {
|
|
|
22
22
|
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
23
23
|
import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
|
|
24
24
|
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
hasManagedAgentAssignmentInput,
|
|
27
|
+
isDelegationTaskPayload,
|
|
28
|
+
resolveDelegatorAgentId,
|
|
29
|
+
resolveManagedAgentAssignment,
|
|
30
|
+
validateManagedAgentAssignment,
|
|
31
|
+
} from '@/lib/server/agent-assignment'
|
|
26
32
|
import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
|
|
27
33
|
import type { ToolBuildContext } from './context'
|
|
28
34
|
import { safePath, findBinaryOnPath } from './context'
|
|
@@ -137,7 +143,8 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
|
|
|
137
143
|
soul: p.soul || '',
|
|
138
144
|
provider: p.provider || 'claude-cli',
|
|
139
145
|
model: p.model || '',
|
|
140
|
-
|
|
146
|
+
platformAssignScope: p.platformAssignScope === 'all' ? 'all' : 'self',
|
|
147
|
+
isOrchestrator: p.platformAssignScope === 'all',
|
|
141
148
|
tools: p.tools || [],
|
|
142
149
|
skills: p.skills || [],
|
|
143
150
|
skillIds: p.skillIds || [],
|
|
@@ -256,12 +263,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
256
263
|
let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
|
|
257
264
|
if (toolKey === 'manage_tasks') {
|
|
258
265
|
if (assignScope === 'self') {
|
|
259
|
-
description += `\n\
|
|
266
|
+
description += `\n\nYou may create tasks for yourself ("${ctx?.agentId || 'unknown'}") or leave them unassigned to track multi-step work. You cannot assign tasks to other agents unless a user enables "Assign to Other Agents" in your agent settings. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
|
|
260
267
|
} else {
|
|
261
|
-
description += `\n\
|
|
268
|
+
description += `\n\nYou may create tasks for yourself, leave them unassigned, or delegate them to other agents. Your agent ID is "${ctx?.agentId || 'unknown'}". When delegating, set a target agent using "agentId", "assignee", "agent", "assignedAgentId", or "assigned_agent_id". Use the target agent's exact ID when possible. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
|
|
262
269
|
}
|
|
263
270
|
} else if (toolKey === 'manage_agents') {
|
|
264
|
-
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
|
|
271
|
+
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
|
|
265
272
|
} else if (toolKey === 'manage_schedules') {
|
|
266
273
|
if (assignScope === 'self') {
|
|
267
274
|
description += `\n\nSet "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
|
|
@@ -337,13 +344,30 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
337
344
|
if (parsed && typeof parsed === 'object' && 'id' in parsed) {
|
|
338
345
|
delete (parsed as Record<string, unknown>).id
|
|
339
346
|
}
|
|
340
|
-
// Enforce assignment scope for tasks and schedules
|
|
341
|
-
if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
|
|
342
|
-
if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
|
|
343
|
-
return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
347
|
const now = Date.now()
|
|
348
|
+
if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
|
|
349
|
+
const agents = loadAgents()
|
|
350
|
+
const resolution = resolveManagedAgentAssignment(
|
|
351
|
+
parsed as Record<string, unknown>,
|
|
352
|
+
agents,
|
|
353
|
+
toolKey === 'manage_tasks' ? (parsed.agentId || ctx?.agentId || null) : null,
|
|
354
|
+
{ allowDescription: toolKey === 'manage_tasks' },
|
|
355
|
+
)
|
|
356
|
+
const assignmentError = validateManagedAgentAssignment({
|
|
357
|
+
resourceLabel: res.label,
|
|
358
|
+
agents,
|
|
359
|
+
assignScope,
|
|
360
|
+
currentAgentId: ctx?.agentId || null,
|
|
361
|
+
targetAgentId: resolution.agentId,
|
|
362
|
+
unresolvedReference: resolution.unresolvedReference,
|
|
363
|
+
isDelegation: toolKey === 'manage_tasks' ? isDelegationTaskPayload(parsed as Record<string, unknown>) : false,
|
|
364
|
+
delegatorAgentId: toolKey === 'manage_tasks'
|
|
365
|
+
? resolveDelegatorAgentId(parsed as Record<string, unknown>, agents, ctx?.agentId || null)
|
|
366
|
+
: null,
|
|
367
|
+
})
|
|
368
|
+
if (assignmentError) return assignmentError
|
|
369
|
+
parsed.agentId = resolution.agentId
|
|
370
|
+
}
|
|
347
371
|
if (toolKey === 'manage_schedules') {
|
|
348
372
|
const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
|
|
349
373
|
agentId: parsed.agentId || null,
|
|
@@ -387,23 +411,6 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
387
411
|
})
|
|
388
412
|
}
|
|
389
413
|
}
|
|
390
|
-
// @mention agent resolution for tasks
|
|
391
|
-
if (toolKey === 'manage_tasks' && parsed.description) {
|
|
392
|
-
const agents = loadAgents()
|
|
393
|
-
parsed.agentId = resolveTaskAgentFromDescription(
|
|
394
|
-
parsed.description,
|
|
395
|
-
parsed.agentId || ctx?.agentId || '',
|
|
396
|
-
agents,
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
// Agents cannot create tasks for themselves — just do the work directly.
|
|
400
|
-
// Tasks are for delegating to other agents or user-created work items.
|
|
401
|
-
if (toolKey === 'manage_tasks' && ctx?.agentId) {
|
|
402
|
-
const resolvedAgentId = parsed.agentId || ctx.agentId
|
|
403
|
-
if (resolvedAgentId === ctx.agentId) {
|
|
404
|
-
return 'Error: You cannot create tasks for yourself — just do the work directly. Tasks are for delegating work to other agents. If you need to track progress, use memory instead.'
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
414
|
if (toolKey === 'manage_tasks') {
|
|
408
415
|
parsed.title = deriveTaskTitle(parsed)
|
|
409
416
|
if (!parsed.title || /^untitled task$/i.test(parsed.title)) {
|
|
@@ -507,10 +514,41 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
507
514
|
? normalizeTaskQualityGate(parsed.qualityGate, settings)
|
|
508
515
|
: null
|
|
509
516
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
517
|
+
if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
|
|
518
|
+
const agents = loadAgents()
|
|
519
|
+
const requestedClear = Object.prototype.hasOwnProperty.call(parsed, 'agentId') && parsed.agentId == null
|
|
520
|
+
const shouldResolveAssignment = requestedClear
|
|
521
|
+
|| hasManagedAgentAssignmentInput(parsed as Record<string, unknown>)
|
|
522
|
+
if (shouldResolveAssignment) {
|
|
523
|
+
const resolution = resolveManagedAgentAssignment(
|
|
524
|
+
parsed as Record<string, unknown>,
|
|
525
|
+
agents,
|
|
526
|
+
null,
|
|
527
|
+
{ allowDescription: false },
|
|
528
|
+
)
|
|
529
|
+
const assignmentError = validateManagedAgentAssignment({
|
|
530
|
+
resourceLabel: res.label,
|
|
531
|
+
agents,
|
|
532
|
+
assignScope,
|
|
533
|
+
currentAgentId: ctx?.agentId || null,
|
|
534
|
+
targetAgentId: requestedClear ? null : resolution.agentId,
|
|
535
|
+
unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
|
|
536
|
+
isDelegation: toolKey === 'manage_tasks'
|
|
537
|
+
? isDelegationTaskPayload({
|
|
538
|
+
...all[id],
|
|
539
|
+
...parsed,
|
|
540
|
+
agentId: requestedClear ? null : resolution.agentId,
|
|
541
|
+
} as Record<string, unknown>)
|
|
542
|
+
: false,
|
|
543
|
+
delegatorAgentId: toolKey === 'manage_tasks'
|
|
544
|
+
? resolveDelegatorAgentId({
|
|
545
|
+
...all[id],
|
|
546
|
+
...parsed,
|
|
547
|
+
} as Record<string, unknown>, agents, ctx?.agentId || null)
|
|
548
|
+
: null,
|
|
549
|
+
})
|
|
550
|
+
if (assignmentError) return assignmentError
|
|
551
|
+
if (!requestedClear) parsed.agentId = resolution.agentId
|
|
514
552
|
}
|
|
515
553
|
}
|
|
516
554
|
all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
|
|
@@ -0,0 +1,219 @@
|
|
|
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 writeExecutable(dir: string, name: string, source: string) {
|
|
11
|
+
const filePath = path.join(dir, name)
|
|
12
|
+
fs.writeFileSync(filePath, source, { mode: 0o755 })
|
|
13
|
+
return filePath
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runWithFakeDelegates(script: string) {
|
|
17
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegate-fallback-'))
|
|
18
|
+
try {
|
|
19
|
+
writeExecutable(tempDir, 'claude', `#!/bin/sh
|
|
20
|
+
if [ "$1" = "auth" ] && [ "$2" = "status" ]; then
|
|
21
|
+
echo '{"loggedIn":false}'
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
echo "unexpected claude invocation" >&2
|
|
25
|
+
exit 2
|
|
26
|
+
`)
|
|
27
|
+
|
|
28
|
+
writeExecutable(tempDir, 'codex', `#!/bin/sh
|
|
29
|
+
if [ "$1" = "login" ] && [ "$2" = "status" ]; then
|
|
30
|
+
echo 'logged in'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
if [ "$1" = "exec" ]; then
|
|
34
|
+
cat >/dev/null
|
|
35
|
+
printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"codex fallback ok"}}'
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
echo "unexpected codex invocation" >&2
|
|
39
|
+
exit 2
|
|
40
|
+
`)
|
|
41
|
+
|
|
42
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
43
|
+
cwd: repoRoot,
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
PATH: `${tempDir}:${process.env.PATH || ''}`,
|
|
47
|
+
},
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
})
|
|
50
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
51
|
+
const lines = (result.stdout || '')
|
|
52
|
+
.trim()
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map((line) => line.trim())
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') || line.startsWith('['))
|
|
57
|
+
return JSON.parse(jsonLine || '{}')
|
|
58
|
+
} finally {
|
|
59
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('delegate fallback', () => {
|
|
64
|
+
it('falls back to another backend when Claude Code is unavailable', () => {
|
|
65
|
+
const output = runWithFakeDelegates(`
|
|
66
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
67
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
68
|
+
|
|
69
|
+
const tools = buildDelegateTools({
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
72
|
+
hasPlugin: (name) => name === 'delegate',
|
|
73
|
+
hasTool: (name) => name === 'delegate',
|
|
74
|
+
cleanupFns: [],
|
|
75
|
+
commandTimeoutMs: 5000,
|
|
76
|
+
claudeTimeoutMs: 5000,
|
|
77
|
+
cliProcessTimeoutMs: 5000,
|
|
78
|
+
persistDelegateResumeId: () => {},
|
|
79
|
+
readStoredDelegateResumeId: () => null,
|
|
80
|
+
resolveCurrentSession: () => null,
|
|
81
|
+
activePlugins: ['delegate'],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
85
|
+
const raw = await delegateTool.invoke({ task: 'write a helper', backend: 'claude' })
|
|
86
|
+
console.log(raw)
|
|
87
|
+
`)
|
|
88
|
+
|
|
89
|
+
assert.equal(output.backend, 'codex')
|
|
90
|
+
assert.equal(output.status, 'completed')
|
|
91
|
+
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('accepts wrapped function-call payloads with tool_name aliases', () => {
|
|
95
|
+
const output = runWithFakeDelegates(`
|
|
96
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
97
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
98
|
+
|
|
99
|
+
const tools = buildDelegateTools({
|
|
100
|
+
cwd: process.cwd(),
|
|
101
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
102
|
+
hasPlugin: (name) => name === 'delegate',
|
|
103
|
+
hasTool: (name) => name === 'delegate',
|
|
104
|
+
cleanupFns: [],
|
|
105
|
+
commandTimeoutMs: 5000,
|
|
106
|
+
claudeTimeoutMs: 5000,
|
|
107
|
+
cliProcessTimeoutMs: 5000,
|
|
108
|
+
persistDelegateResumeId: () => {},
|
|
109
|
+
readStoredDelegateResumeId: () => null,
|
|
110
|
+
resolveCurrentSession: () => null,
|
|
111
|
+
activePlugins: ['delegate'],
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
115
|
+
const raw = await delegateTool.invoke({
|
|
116
|
+
input: JSON.stringify({
|
|
117
|
+
function: 'delegate',
|
|
118
|
+
parameters: {
|
|
119
|
+
tool_name: 'Claude Code',
|
|
120
|
+
parameters: {
|
|
121
|
+
task: 'Create a proof file',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
console.log(raw)
|
|
127
|
+
`)
|
|
128
|
+
|
|
129
|
+
assert.equal(output.backend, 'codex')
|
|
130
|
+
assert.equal(output.status, 'completed')
|
|
131
|
+
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('synthesizes a delegated task from write-style payloads', () => {
|
|
135
|
+
const output = runWithFakeDelegates(`
|
|
136
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
137
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
138
|
+
|
|
139
|
+
const tools = buildDelegateTools({
|
|
140
|
+
cwd: process.cwd(),
|
|
141
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
142
|
+
hasPlugin: (name) => name === 'delegate',
|
|
143
|
+
hasTool: (name) => name === 'delegate',
|
|
144
|
+
cleanupFns: [],
|
|
145
|
+
commandTimeoutMs: 5000,
|
|
146
|
+
claudeTimeoutMs: 5000,
|
|
147
|
+
cliProcessTimeoutMs: 5000,
|
|
148
|
+
persistDelegateResumeId: () => {},
|
|
149
|
+
readStoredDelegateResumeId: () => null,
|
|
150
|
+
resolveCurrentSession: () => null,
|
|
151
|
+
activePlugins: ['delegate'],
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
155
|
+
const raw = await delegateTool.invoke({
|
|
156
|
+
input: JSON.stringify({
|
|
157
|
+
action: 'write',
|
|
158
|
+
target: 'delegate-proof.md',
|
|
159
|
+
content: 'Proof content',
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
console.log(raw)
|
|
163
|
+
`)
|
|
164
|
+
|
|
165
|
+
assert.equal(output.backend, 'codex')
|
|
166
|
+
assert.equal(output.status, 'completed')
|
|
167
|
+
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('synthesizes a delegated task for action=start payloads that only provide files', () => {
|
|
171
|
+
const output = runWithFakeDelegates(`
|
|
172
|
+
const mod = await import('./src/lib/server/session-tools/delegate.ts')
|
|
173
|
+
const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
|
|
174
|
+
|
|
175
|
+
const tools = buildDelegateTools({
|
|
176
|
+
cwd: process.cwd(),
|
|
177
|
+
ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
|
|
178
|
+
hasPlugin: (name) => name === 'delegate',
|
|
179
|
+
hasTool: (name) => name === 'delegate',
|
|
180
|
+
cleanupFns: [],
|
|
181
|
+
commandTimeoutMs: 5000,
|
|
182
|
+
claudeTimeoutMs: 5000,
|
|
183
|
+
cliProcessTimeoutMs: 5000,
|
|
184
|
+
persistDelegateResumeId: () => {},
|
|
185
|
+
readStoredDelegateResumeId: () => null,
|
|
186
|
+
resolveCurrentSession: () => null,
|
|
187
|
+
activePlugins: ['delegate'],
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const delegateTool = tools.find((tool) => tool.name === 'delegate')
|
|
191
|
+
const raw = await delegateTool.invoke({
|
|
192
|
+
input: JSON.stringify({
|
|
193
|
+
action: 'start',
|
|
194
|
+
name: 'Create Weather Script',
|
|
195
|
+
files: [{
|
|
196
|
+
path: 'weather_update/weather_fetcher.py',
|
|
197
|
+
content: 'print("weather")',
|
|
198
|
+
}],
|
|
199
|
+
}),
|
|
200
|
+
})
|
|
201
|
+
console.log(raw)
|
|
202
|
+
`)
|
|
203
|
+
|
|
204
|
+
assert.equal(output.backend, 'codex')
|
|
205
|
+
assert.equal(output.status, 'completed')
|
|
206
|
+
assert.match(String(output.response || ''), /codex fallback ok/i)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('ranks authenticated delegate backends ahead of unauthenticated ones', () => {
|
|
210
|
+
const output = runWithFakeDelegates(`
|
|
211
|
+
const mod = await import('./src/lib/server/provider-health.ts')
|
|
212
|
+
const { rankDelegatesByHealth } = mod.default || mod['module.exports'] || mod
|
|
213
|
+
const ranked = rankDelegatesByHealth(['delegate_to_claude_code', 'delegate_to_codex_cli'])
|
|
214
|
+
console.log(JSON.stringify(ranked))
|
|
215
|
+
`)
|
|
216
|
+
|
|
217
|
+
assert.deepEqual(output, ['delegate_to_codex_cli', 'delegate_to_claude_code'])
|
|
218
|
+
})
|
|
219
|
+
})
|