@swarmclawai/swarmclaw 0.7.1 → 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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- 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 +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- 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/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- 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 +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- 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 +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- 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 +244 -56
- 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 +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- 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/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- 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/capability-router.ts +10 -8
- 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 +285 -165
- 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 +48 -8
- 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 +948 -112
- 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/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- 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 +14 -40
- 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 +28 -1103
- 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 +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- 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 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- 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 +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- 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 +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- 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/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -43,7 +43,7 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Add MCP servers for enabled tools
|
|
46
|
-
const tools: string[] = session.
|
|
46
|
+
const tools: string[] = session.plugins || []
|
|
47
47
|
let mcpConfigPath: string | null = null
|
|
48
48
|
if (tools.includes('browser')) {
|
|
49
49
|
const proxyScript = path.join(process.cwd(), 'src/lib/server/playwright-proxy.mjs')
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { Agent } from '@/types'
|
|
4
|
+
import {
|
|
5
|
+
isDelegationTaskPayload,
|
|
6
|
+
resolveDelegatorAgentId,
|
|
7
|
+
resolveManagedAgentAssignment,
|
|
8
|
+
validateManagedAgentAssignment,
|
|
9
|
+
} from './agent-assignment'
|
|
10
|
+
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
const agents: Record<string, Agent> = {
|
|
13
|
+
molly: {
|
|
14
|
+
id: 'molly',
|
|
15
|
+
name: 'Molly',
|
|
16
|
+
description: '',
|
|
17
|
+
systemPrompt: '',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
model: 'gpt-4o',
|
|
20
|
+
createdAt: now,
|
|
21
|
+
updatedAt: now,
|
|
22
|
+
},
|
|
23
|
+
writer: {
|
|
24
|
+
id: 'writer',
|
|
25
|
+
name: 'Writer',
|
|
26
|
+
description: '',
|
|
27
|
+
systemPrompt: '',
|
|
28
|
+
provider: 'openai',
|
|
29
|
+
model: 'gpt-4o',
|
|
30
|
+
createdAt: now,
|
|
31
|
+
updatedAt: now,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('resolveManagedAgentAssignment', () => {
|
|
36
|
+
it('resolves explicit aliases to concrete agent ids', () => {
|
|
37
|
+
const resolved = resolveManagedAgentAssignment({ assignee: 'Writer' }, agents, 'molly')
|
|
38
|
+
assert.equal(resolved.agentId, 'writer')
|
|
39
|
+
assert.equal(resolved.source, 'explicit')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('resolves description-based delegation before scope checks', () => {
|
|
43
|
+
const resolved = resolveManagedAgentAssignment(
|
|
44
|
+
{ description: 'Please delegate this to @Writer and let them handle the draft.' },
|
|
45
|
+
agents,
|
|
46
|
+
'molly',
|
|
47
|
+
)
|
|
48
|
+
assert.equal(resolved.agentId, 'writer')
|
|
49
|
+
assert.equal(resolved.source, 'description')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('validateManagedAgentAssignment', () => {
|
|
54
|
+
it('blocks assigning another agent when scope is self', () => {
|
|
55
|
+
const resolved = resolveManagedAgentAssignment({ assignee: 'writer' }, agents, 'molly')
|
|
56
|
+
const error = validateManagedAgentAssignment({
|
|
57
|
+
resourceLabel: 'tasks',
|
|
58
|
+
agents,
|
|
59
|
+
assignScope: 'self',
|
|
60
|
+
currentAgentId: 'molly',
|
|
61
|
+
targetAgentId: resolved.agentId,
|
|
62
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
63
|
+
})
|
|
64
|
+
assert.match(error || '', /only assign tasks to yourself/i)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('allows self-assignment in self scope', () => {
|
|
68
|
+
const resolved = resolveManagedAgentAssignment({ agentId: 'molly' }, agents, 'molly')
|
|
69
|
+
const error = validateManagedAgentAssignment({
|
|
70
|
+
resourceLabel: 'tasks',
|
|
71
|
+
agents,
|
|
72
|
+
assignScope: 'self',
|
|
73
|
+
currentAgentId: 'molly',
|
|
74
|
+
targetAgentId: resolved.agentId,
|
|
75
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
76
|
+
})
|
|
77
|
+
assert.equal(error, null)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('rejects unknown explicit agent references', () => {
|
|
81
|
+
const resolved = resolveManagedAgentAssignment({ agentId: 'missing-agent' }, agents, 'molly')
|
|
82
|
+
const error = validateManagedAgentAssignment({
|
|
83
|
+
resourceLabel: 'tasks',
|
|
84
|
+
agents,
|
|
85
|
+
assignScope: 'all',
|
|
86
|
+
currentAgentId: 'molly',
|
|
87
|
+
targetAgentId: resolved.agentId,
|
|
88
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
89
|
+
})
|
|
90
|
+
assert.match(error || '', /unknown agent "missing-agent"/i)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('rejects self-delegation using resolved agent ids', () => {
|
|
94
|
+
const payload = {
|
|
95
|
+
agentId: 'molly',
|
|
96
|
+
sourceType: 'delegation',
|
|
97
|
+
delegatedByAgentId: 'Molly',
|
|
98
|
+
}
|
|
99
|
+
const resolved = resolveManagedAgentAssignment(payload, agents, 'molly')
|
|
100
|
+
const error = validateManagedAgentAssignment({
|
|
101
|
+
resourceLabel: 'tasks',
|
|
102
|
+
agents,
|
|
103
|
+
assignScope: 'all',
|
|
104
|
+
currentAgentId: 'molly',
|
|
105
|
+
targetAgentId: resolved.agentId,
|
|
106
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
107
|
+
isDelegation: isDelegationTaskPayload(payload),
|
|
108
|
+
delegatorAgentId: resolveDelegatorAgentId(payload, agents, 'molly'),
|
|
109
|
+
})
|
|
110
|
+
assert.match(error || '', /different agent id/i)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
import { resolveAgentReference, resolveTaskAgentFromDescription } from './task-mention'
|
|
3
|
+
|
|
4
|
+
export const MANAGED_AGENT_REFERENCE_KEYS = [
|
|
5
|
+
'agentId',
|
|
6
|
+
'agent_id',
|
|
7
|
+
'assignedAgentId',
|
|
8
|
+
'assigned_agent_id',
|
|
9
|
+
'assignedToAgentId',
|
|
10
|
+
'assigned_to_agent_id',
|
|
11
|
+
'assigneeId',
|
|
12
|
+
'assignee_id',
|
|
13
|
+
'assignedAgent',
|
|
14
|
+
'assigned_agent',
|
|
15
|
+
'assignedTo',
|
|
16
|
+
'assigned_to',
|
|
17
|
+
'assignee',
|
|
18
|
+
'agent',
|
|
19
|
+
'owner',
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
type AssignmentSource = 'explicit' | 'description' | 'fallback' | 'none'
|
|
23
|
+
|
|
24
|
+
export interface ManagedAgentAssignmentResolution {
|
|
25
|
+
agentId: string | null
|
|
26
|
+
explicitReference: string | null
|
|
27
|
+
unresolvedReference: string | null
|
|
28
|
+
source: AssignmentSource
|
|
29
|
+
hadExplicitInput: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function firstNonEmptyString(
|
|
33
|
+
parsed: Record<string, unknown>,
|
|
34
|
+
keys: readonly string[],
|
|
35
|
+
): string | null {
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const raw = parsed[key]
|
|
38
|
+
if (typeof raw !== 'string') continue
|
|
39
|
+
const trimmed = raw.trim()
|
|
40
|
+
if (trimmed) return trimmed
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasManagedAgentAssignmentInput(
|
|
46
|
+
parsed: Record<string, unknown>,
|
|
47
|
+
keys: readonly string[] = MANAGED_AGENT_REFERENCE_KEYS,
|
|
48
|
+
): boolean {
|
|
49
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(parsed, key))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveManagedAgentAssignment(
|
|
53
|
+
parsed: Record<string, unknown>,
|
|
54
|
+
agents: Record<string, Agent>,
|
|
55
|
+
fallbackAgentId?: string | null,
|
|
56
|
+
opts?: {
|
|
57
|
+
allowDescription?: boolean
|
|
58
|
+
keys?: readonly string[]
|
|
59
|
+
},
|
|
60
|
+
): ManagedAgentAssignmentResolution {
|
|
61
|
+
const keys = opts?.keys ?? MANAGED_AGENT_REFERENCE_KEYS
|
|
62
|
+
const explicitReference = firstNonEmptyString(parsed, keys)
|
|
63
|
+
const hadExplicitInput = hasManagedAgentAssignmentInput(parsed, keys)
|
|
64
|
+
if (explicitReference) {
|
|
65
|
+
const resolved = resolveAgentReference(explicitReference, agents)
|
|
66
|
+
return {
|
|
67
|
+
agentId: resolved,
|
|
68
|
+
explicitReference,
|
|
69
|
+
unresolvedReference: resolved ? null : explicitReference,
|
|
70
|
+
source: 'explicit',
|
|
71
|
+
hadExplicitInput,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (opts?.allowDescription !== false) {
|
|
76
|
+
const description = typeof parsed.description === 'string' ? parsed.description.trim() : ''
|
|
77
|
+
if (description) {
|
|
78
|
+
const resolvedFromDescription = resolveTaskAgentFromDescription(description, '', agents).trim()
|
|
79
|
+
if (resolvedFromDescription) {
|
|
80
|
+
return {
|
|
81
|
+
agentId: resolvedFromDescription,
|
|
82
|
+
explicitReference: null,
|
|
83
|
+
unresolvedReference: null,
|
|
84
|
+
source: 'description',
|
|
85
|
+
hadExplicitInput,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
|
|
92
|
+
if (fallback) {
|
|
93
|
+
return {
|
|
94
|
+
agentId: fallback,
|
|
95
|
+
explicitReference: null,
|
|
96
|
+
unresolvedReference: null,
|
|
97
|
+
source: 'fallback',
|
|
98
|
+
hadExplicitInput,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
agentId: null,
|
|
104
|
+
explicitReference: null,
|
|
105
|
+
unresolvedReference: null,
|
|
106
|
+
source: 'none',
|
|
107
|
+
hadExplicitInput,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveDelegatorAgentId(
|
|
112
|
+
parsed: Record<string, unknown>,
|
|
113
|
+
agents: Record<string, Agent>,
|
|
114
|
+
fallbackAgentId?: string | null,
|
|
115
|
+
): string | null {
|
|
116
|
+
const explicitDelegator = typeof parsed.delegatedByAgentId === 'string'
|
|
117
|
+
? parsed.delegatedByAgentId.trim()
|
|
118
|
+
: ''
|
|
119
|
+
if (explicitDelegator) {
|
|
120
|
+
return resolveAgentReference(explicitDelegator, agents) || explicitDelegator
|
|
121
|
+
}
|
|
122
|
+
const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
|
|
123
|
+
return fallback || null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isDelegationTaskPayload(parsed: Record<string, unknown>): boolean {
|
|
127
|
+
const sourceType = typeof parsed.sourceType === 'string' ? parsed.sourceType.trim().toLowerCase() : ''
|
|
128
|
+
if (sourceType === 'delegation') return true
|
|
129
|
+
if (typeof parsed.delegatedFromTaskId === 'string' && parsed.delegatedFromTaskId.trim()) return true
|
|
130
|
+
if (typeof parsed.delegatedByAgentId === 'string' && parsed.delegatedByAgentId.trim()) return true
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function validateManagedAgentAssignment(params: {
|
|
135
|
+
resourceLabel: string
|
|
136
|
+
agents: Record<string, Agent>
|
|
137
|
+
assignScope: 'self' | 'all'
|
|
138
|
+
currentAgentId?: string | null
|
|
139
|
+
targetAgentId?: string | null
|
|
140
|
+
unresolvedReference?: string | null
|
|
141
|
+
isDelegation?: boolean
|
|
142
|
+
delegatorAgentId?: string | null
|
|
143
|
+
}): string | null {
|
|
144
|
+
const currentAgentId = typeof params.currentAgentId === 'string' ? params.currentAgentId.trim() : ''
|
|
145
|
+
const targetAgentId = typeof params.targetAgentId === 'string' ? params.targetAgentId.trim() : ''
|
|
146
|
+
const unresolvedReference = typeof params.unresolvedReference === 'string' ? params.unresolvedReference.trim() : ''
|
|
147
|
+
const delegatorAgentId = typeof params.delegatorAgentId === 'string' ? params.delegatorAgentId.trim() : ''
|
|
148
|
+
|
|
149
|
+
if (unresolvedReference) {
|
|
150
|
+
return `Error: Unknown agent "${unresolvedReference}". Use an existing agent ID or exact agent name.`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (targetAgentId && !params.agents[targetAgentId]) {
|
|
154
|
+
return `Error: Unknown agent "${targetAgentId}". Use an existing agent ID or exact agent name.`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (params.assignScope === 'self' && currentAgentId && targetAgentId && targetAgentId !== currentAgentId) {
|
|
158
|
+
return `Error: You can only assign ${params.resourceLabel} to yourself ("${currentAgentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (params.isDelegation && targetAgentId) {
|
|
162
|
+
const comparisonId = delegatorAgentId || currentAgentId
|
|
163
|
+
if (comparisonId && targetAgentId === comparisonId) {
|
|
164
|
+
return 'Error: Delegation target must be a different agent ID. Create a normal self-task instead of delegating to yourself.'
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
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-approval-connector-'))
|
|
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('approval connector reminders', () => {
|
|
36
|
+
it('resolves a due approval to the session connector target and records one-shot delivery state', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
40
|
+
const storage = storageMod.default || storageMod
|
|
41
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
42
|
+
|
|
43
|
+
const now = Date.now()
|
|
44
|
+
storage.saveSettings({
|
|
45
|
+
approvalConnectorNotifyEnabled: true,
|
|
46
|
+
approvalConnectorNotifyDelaySec: 60,
|
|
47
|
+
})
|
|
48
|
+
storage.saveAgents({
|
|
49
|
+
agent_1: {
|
|
50
|
+
id: 'agent_1',
|
|
51
|
+
name: 'Molly',
|
|
52
|
+
description: 'Test agent',
|
|
53
|
+
systemPrompt: 'You are Molly.',
|
|
54
|
+
provider: 'openai',
|
|
55
|
+
model: 'gpt-test',
|
|
56
|
+
plugins: [],
|
|
57
|
+
createdAt: now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
storage.saveSessions({
|
|
62
|
+
session_1: {
|
|
63
|
+
id: 'session_1',
|
|
64
|
+
name: 'Connector session',
|
|
65
|
+
cwd: process.cwd(),
|
|
66
|
+
user: 'tester',
|
|
67
|
+
provider: 'openai',
|
|
68
|
+
model: 'gpt-test',
|
|
69
|
+
credentialId: null,
|
|
70
|
+
apiEndpoint: null,
|
|
71
|
+
claudeSessionId: null,
|
|
72
|
+
codexThreadId: null,
|
|
73
|
+
opencodeSessionId: null,
|
|
74
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'user',
|
|
78
|
+
text: 'Please ask me before spending money.',
|
|
79
|
+
time: now - 1_000,
|
|
80
|
+
source: { connectorId: 'conn-1', channelId: 'chat-42', threadId: 'topic-7' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
createdAt: now - 120_000,
|
|
84
|
+
lastActiveAt: now - 1_000,
|
|
85
|
+
sessionType: 'human',
|
|
86
|
+
agentId: 'agent_1',
|
|
87
|
+
plugins: [],
|
|
88
|
+
connectorContext: {
|
|
89
|
+
connectorId: 'conn-1',
|
|
90
|
+
platform: 'telegram',
|
|
91
|
+
channelId: 'chat-42',
|
|
92
|
+
threadId: 'topic-7',
|
|
93
|
+
lastInboundAt: now - 1_000,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const approval = approvals.requestApproval({
|
|
99
|
+
category: 'human_loop',
|
|
100
|
+
title: 'Approve plugin install',
|
|
101
|
+
description: 'Need permission to install a plugin.',
|
|
102
|
+
data: {},
|
|
103
|
+
sessionId: 'session_1',
|
|
104
|
+
agentId: 'agent_1',
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const dueAt = approval.createdAt + 61_000
|
|
108
|
+
const reminders = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
109
|
+
now: dueAt,
|
|
110
|
+
runningConnectors: [
|
|
111
|
+
{ id: 'conn-1', agentId: 'agent_1', supportsSend: true, configuredTargets: [], recentChannelId: 'chat-42' },
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
approvals.markApprovalConnectorNotificationSent(approval.id, {
|
|
116
|
+
at: dueAt,
|
|
117
|
+
connectorId: 'conn-1',
|
|
118
|
+
channelId: 'chat-42',
|
|
119
|
+
threadId: 'topic-7',
|
|
120
|
+
messageId: 'msg-9',
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const afterSend = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
124
|
+
now: dueAt + 1_000,
|
|
125
|
+
runningConnectors: [
|
|
126
|
+
{ id: 'conn-1', agentId: 'agent_1', supportsSend: true, configuredTargets: [], recentChannelId: 'chat-42' },
|
|
127
|
+
],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const storedApproval = storage.loadApprovals()[approval.id]
|
|
131
|
+
console.log(JSON.stringify({
|
|
132
|
+
reminderCount: reminders.length,
|
|
133
|
+
reminder: reminders[0],
|
|
134
|
+
afterSendCount: afterSend.length,
|
|
135
|
+
storedApproval,
|
|
136
|
+
}))
|
|
137
|
+
`)
|
|
138
|
+
|
|
139
|
+
assert.equal(output.reminderCount, 1)
|
|
140
|
+
assert.equal(output.reminder.connectorId, 'conn-1')
|
|
141
|
+
assert.equal(output.reminder.channelId, 'chat-42')
|
|
142
|
+
assert.equal(output.reminder.threadId, 'topic-7')
|
|
143
|
+
assert.match(output.reminder.text, /Molly is waiting for your approval/i)
|
|
144
|
+
assert.equal(output.afterSendCount, 0)
|
|
145
|
+
assert.equal(output.storedApproval.connectorNotification.sentAt > 0, true)
|
|
146
|
+
assert.equal(output.storedApproval.connectorNotification.messageId, 'msg-9')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('falls back to a running owned connector and respects retry cooldowns after failed sends', () => {
|
|
150
|
+
const output = runWithTempDataDir(`
|
|
151
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
152
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
153
|
+
const storage = storageMod.default || storageMod
|
|
154
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
155
|
+
|
|
156
|
+
const now = Date.now()
|
|
157
|
+
storage.saveSettings({
|
|
158
|
+
approvalConnectorNotifyEnabled: true,
|
|
159
|
+
approvalConnectorNotifyDelaySec: 30,
|
|
160
|
+
})
|
|
161
|
+
storage.saveAgents({
|
|
162
|
+
agent_2: {
|
|
163
|
+
id: 'agent_2',
|
|
164
|
+
name: 'Writer',
|
|
165
|
+
description: 'Test agent',
|
|
166
|
+
systemPrompt: 'You are Writer.',
|
|
167
|
+
provider: 'openai',
|
|
168
|
+
model: 'gpt-test',
|
|
169
|
+
plugins: [],
|
|
170
|
+
createdAt: now,
|
|
171
|
+
updatedAt: now,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
storage.saveSessions({
|
|
175
|
+
session_plain: {
|
|
176
|
+
id: 'session_plain',
|
|
177
|
+
name: 'Non-connector session',
|
|
178
|
+
cwd: process.cwd(),
|
|
179
|
+
user: 'tester',
|
|
180
|
+
provider: 'openai',
|
|
181
|
+
model: 'gpt-test',
|
|
182
|
+
credentialId: null,
|
|
183
|
+
apiEndpoint: null,
|
|
184
|
+
claudeSessionId: null,
|
|
185
|
+
codexThreadId: null,
|
|
186
|
+
opencodeSessionId: null,
|
|
187
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
188
|
+
messages: [],
|
|
189
|
+
createdAt: now - 60_000,
|
|
190
|
+
lastActiveAt: now - 1_000,
|
|
191
|
+
sessionType: 'human',
|
|
192
|
+
agentId: 'agent_2',
|
|
193
|
+
plugins: [],
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const approval = approvals.requestApproval({
|
|
198
|
+
category: 'task_tool',
|
|
199
|
+
title: 'Approve outbound outreach',
|
|
200
|
+
description: 'Need your approval before sending a message.',
|
|
201
|
+
data: {},
|
|
202
|
+
sessionId: 'session_plain',
|
|
203
|
+
agentId: 'agent_2',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const dueAt = approval.createdAt + 31_000
|
|
207
|
+
const first = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
208
|
+
now: dueAt,
|
|
209
|
+
runningConnectors: [
|
|
210
|
+
{ id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
|
|
211
|
+
],
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
approvals.markApprovalConnectorNotificationAttempt(approval.id, {
|
|
215
|
+
at: dueAt,
|
|
216
|
+
connectorId: 'conn-fallback',
|
|
217
|
+
channelId: 'dm-88',
|
|
218
|
+
lastError: 'connector temporarily unavailable',
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const withinCooldown = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
222
|
+
now: dueAt + 5_000,
|
|
223
|
+
runningConnectors: [
|
|
224
|
+
{ id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
|
|
225
|
+
],
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const afterCooldown = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
229
|
+
now: dueAt + (10 * 60_000) + 1_000,
|
|
230
|
+
runningConnectors: [
|
|
231
|
+
{ id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const storedApproval = storage.loadApprovals()[approval.id]
|
|
236
|
+
console.log(JSON.stringify({
|
|
237
|
+
firstCount: first.length,
|
|
238
|
+
fallbackConnectorId: first[0]?.connectorId || null,
|
|
239
|
+
fallbackChannelId: first[0]?.channelId || null,
|
|
240
|
+
withinCooldownCount: withinCooldown.length,
|
|
241
|
+
afterCooldownCount: afterCooldown.length,
|
|
242
|
+
storedApproval,
|
|
243
|
+
}))
|
|
244
|
+
`)
|
|
245
|
+
|
|
246
|
+
assert.equal(output.firstCount, 1)
|
|
247
|
+
assert.equal(output.fallbackConnectorId, 'conn-fallback')
|
|
248
|
+
assert.equal(output.fallbackChannelId, 'dm-88')
|
|
249
|
+
assert.equal(output.withinCooldownCount, 0)
|
|
250
|
+
assert.equal(output.afterCooldownCount, 1)
|
|
251
|
+
assert.equal(output.storedApproval.connectorNotification.lastError, 'connector temporarily unavailable')
|
|
252
|
+
})
|
|
253
|
+
})
|