@swarmclawai/swarmclaw 1.3.5 → 1.4.0
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 +37 -1
- package/package.json +10 -3
- package/src/.env.local +4 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +19 -0
- package/src/cli/spec.js +8 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +111 -107
- package/src/lib/providers/openai.ts +146 -142
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
- package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
- package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +15 -34
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/knowledge-sources.test.ts +46 -0
- package/src/lib/server/knowledge-sources.ts +34 -16
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +100 -0
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- package/src/types/swarmfeed.ts +30 -0
|
@@ -1,40 +1,23 @@
|
|
|
1
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
2
|
import test from 'node:test'
|
|
3
|
+
import { runWithTempDataDir as runWithSharedTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cwd: repoRoot,
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
5
|
+
function runWithTempDataDir<T = unknown>(script: string): T {
|
|
6
|
+
return runWithSharedTempDataDir<T>(script, {
|
|
7
|
+
prefix: 'swarmclaw-chat-disabled-',
|
|
8
|
+
dataDir: 'data',
|
|
9
|
+
browserProfilesDir: 'browser-profiles',
|
|
10
|
+
})
|
|
34
11
|
}
|
|
35
12
|
|
|
36
13
|
test('executeSessionChatTurn persists a visible error for disabled agents', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
14
|
+
const output = runWithTempDataDir<{
|
|
15
|
+
error: string | null
|
|
16
|
+
text: string | null
|
|
17
|
+
persisted: boolean
|
|
18
|
+
lastRole: string | null
|
|
19
|
+
lastText: string | null
|
|
20
|
+
}>(`
|
|
38
21
|
const storageMod = await import('@/lib/server/storage')
|
|
39
22
|
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
23
|
const threadMod = await import('@/lib/server/agents/agent-thread-session')
|
|
@@ -1,40 +1,13 @@
|
|
|
1
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
2
|
import test 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-chat-eval-history-'))
|
|
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: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
3
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
35
4
|
|
|
36
5
|
test('executeSessionChatTurn persists internal eval user turns for same-thread recall', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
6
|
+
const output = runWithTempDataDir<{
|
|
7
|
+
recallText: string | null
|
|
8
|
+
roles: string[]
|
|
9
|
+
texts: string[]
|
|
10
|
+
}>(`
|
|
38
11
|
const storageMod = await import('@/lib/server/storage')
|
|
39
12
|
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
13
|
const providersMod = await import('@/lib/providers/index')
|
|
@@ -103,7 +76,11 @@ test('executeSessionChatTurn persists internal eval user turns for same-thread r
|
|
|
103
76
|
roles: storedSession.messages.map((entry) => entry.role),
|
|
104
77
|
texts: storedSession.messages.map((entry) => entry.text),
|
|
105
78
|
}))
|
|
106
|
-
|
|
79
|
+
`, {
|
|
80
|
+
prefix: 'swarmclaw-chat-eval-history-',
|
|
81
|
+
dataDir: 'data',
|
|
82
|
+
browserProfilesDir: 'browser-profiles',
|
|
83
|
+
})
|
|
107
84
|
|
|
108
85
|
assert.match(String(output.recallText || ''), /Sunbird/)
|
|
109
86
|
assert.deepEqual(output.roles, ['user', 'assistant', 'user', 'assistant'])
|
|
@@ -1,40 +1,17 @@
|
|
|
1
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
2
|
import test 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-chat-grounding-'))
|
|
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: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
3
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
35
4
|
|
|
36
5
|
test('executeSessionChatTurn persists citations and retrieval traces on grounded assistant messages', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
6
|
+
const output = runWithTempDataDir<{
|
|
7
|
+
persisted: boolean
|
|
8
|
+
resultCitationCount: number
|
|
9
|
+
resultSelectorStatus: string | null
|
|
10
|
+
messageCitationCount: number
|
|
11
|
+
messageTraceHitCount: number
|
|
12
|
+
messageSelectorStatus: string | null
|
|
13
|
+
messageSourceTitle: string | null
|
|
14
|
+
}>(`
|
|
38
15
|
const storageMod = await import('@/lib/server/storage')
|
|
39
16
|
const providersMod = await import('@/lib/providers/index')
|
|
40
17
|
const threadMod = await import('@/lib/server/agents/agent-thread-session')
|
|
@@ -115,7 +92,11 @@ test('executeSessionChatTurn persists citations and retrieval traces on grounded
|
|
|
115
92
|
messageSelectorStatus: lastMessage?.retrievalTrace?.selectorStatus || null,
|
|
116
93
|
messageSourceTitle: lastMessage?.citations?.[0]?.sourceTitle || null,
|
|
117
94
|
}))
|
|
118
|
-
|
|
95
|
+
`, {
|
|
96
|
+
prefix: 'swarmclaw-chat-grounding-',
|
|
97
|
+
dataDir: 'data',
|
|
98
|
+
browserProfilesDir: 'browser-profiles',
|
|
99
|
+
})
|
|
119
100
|
|
|
120
101
|
assert.equal(output.persisted, true)
|
|
121
102
|
assert.equal(output.resultCitationCount >= 1, true)
|
|
@@ -1,40 +1,24 @@
|
|
|
1
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
2
|
import test from 'node:test'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
3
|
+
import { runWithTempDataDir as runWithSharedTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
4
|
+
|
|
5
|
+
function runWithTempDataDir<T = unknown>(script: string): T {
|
|
6
|
+
return runWithSharedTempDataDir<T>(script, {
|
|
7
|
+
prefix: 'swarmclaw-chat-session-sync-',
|
|
8
|
+
dataDir: 'data',
|
|
9
|
+
browserProfilesDir: 'browser-profiles',
|
|
10
|
+
})
|
|
34
11
|
}
|
|
35
12
|
|
|
36
13
|
test('executeSessionChatTurn syncs updated agent runtime fields onto its thread session', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
14
|
+
const output = runWithTempDataDir<{
|
|
15
|
+
provider: string | null
|
|
16
|
+
model: string | null
|
|
17
|
+
extensions: string[]
|
|
18
|
+
heartbeatEnabled: boolean | null
|
|
19
|
+
heartbeatIntervalSec: number | null
|
|
20
|
+
connectorContext: Record<string, unknown> | null
|
|
21
|
+
}>(`
|
|
38
22
|
const storageMod = await import('@/lib/server/storage')
|
|
39
23
|
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
24
|
const providersMod = await import('@/lib/providers/index')
|
|
@@ -126,7 +110,12 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
|
|
|
126
110
|
})
|
|
127
111
|
|
|
128
112
|
test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thread history and clears stale connector state', () => {
|
|
129
|
-
const output = runWithTempDataDir
|
|
113
|
+
const output = runWithTempDataDir<{
|
|
114
|
+
connectorContext: Record<string, unknown> | null
|
|
115
|
+
messageCount: number
|
|
116
|
+
lastMessageText: string | null
|
|
117
|
+
heartbeatKinds: number
|
|
118
|
+
}>(`
|
|
130
119
|
const storageMod = await import('@/lib/server/storage')
|
|
131
120
|
const providersMod = await import('@/lib/providers/index')
|
|
132
121
|
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
@@ -232,7 +221,11 @@ test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thr
|
|
|
232
221
|
})
|
|
233
222
|
|
|
234
223
|
test('executeSessionChatTurn hides internal main-loop followup output from the visible transcript', () => {
|
|
235
|
-
const output = runWithTempDataDir
|
|
224
|
+
const output = runWithTempDataDir<{
|
|
225
|
+
messageCount: number
|
|
226
|
+
lastMessageText: string | null
|
|
227
|
+
hasStreamingArtifacts: boolean
|
|
228
|
+
}>(`
|
|
236
229
|
const storageMod = await import('@/lib/server/storage')
|
|
237
230
|
const providersMod = await import('@/lib/providers/index')
|
|
238
231
|
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
@@ -324,7 +317,10 @@ test('executeSessionChatTurn hides internal main-loop followup output from the v
|
|
|
324
317
|
})
|
|
325
318
|
|
|
326
319
|
test('executeSessionChatTurn forces external connector sessions onto session-scoped memory', () => {
|
|
327
|
-
const output = runWithTempDataDir
|
|
320
|
+
const output = runWithTempDataDir<{
|
|
321
|
+
memoryScopeMode: string | null
|
|
322
|
+
connectorContext: { isOwnerConversation?: boolean } | null
|
|
323
|
+
}>(`
|
|
328
324
|
const storageMod = await import('@/lib/server/storage')
|
|
329
325
|
const providersMod = await import('@/lib/providers/index')
|
|
330
326
|
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
@@ -418,7 +414,10 @@ test('executeSessionChatTurn forces external connector sessions onto session-sco
|
|
|
418
414
|
})
|
|
419
415
|
|
|
420
416
|
test('executeSessionChatTurn applies lifecycle hooks for model resolution and message persistence', () => {
|
|
421
|
-
const output = runWithTempDataDir
|
|
417
|
+
const output = runWithTempDataDir<{
|
|
418
|
+
lastMessageText: string
|
|
419
|
+
marks: string[]
|
|
420
|
+
}>(`
|
|
422
421
|
const storageMod = await import('@/lib/server/storage')
|
|
423
422
|
const providersMod = await import('@/lib/providers/index')
|
|
424
423
|
const extMod = await import('@/lib/server/extensions')
|
|
@@ -89,14 +89,19 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
89
89
|
return preflight.terminalResult
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
let streamResult: Awaited<ReturnType<typeof executePreparedChatTurn>>
|
|
93
|
+
try {
|
|
94
|
+
streamResult = await executePreparedChatTurn({
|
|
95
|
+
input,
|
|
96
|
+
prepared: preparedTurn,
|
|
97
|
+
partialPersistence,
|
|
98
|
+
preflightToolRoutingResult: preflight?.directMemoryResult || null,
|
|
99
|
+
})
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
await partialPersistence.awaitIdle()
|
|
102
|
+
} finally {
|
|
103
|
+
partialPersistence.stop()
|
|
104
|
+
}
|
|
100
105
|
|
|
101
106
|
if (!streamResult.errorMessage) {
|
|
102
107
|
markProviderSuccess(preparedTurn.providerType, preparedTurn.sessionForRun.credentialId)
|
|
@@ -874,6 +874,17 @@ class ExtensionManager {
|
|
|
874
874
|
try {
|
|
875
875
|
const parsed = JSON.parse(fs.readFileSync(EXTENSION_FAILURES, 'utf8')) as Record<string, ExtensionFailureRecord>
|
|
876
876
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
877
|
+
// Prune records older than 7 days
|
|
878
|
+
const maxAgeMs = 7 * 24 * 60 * 60 * 1000
|
|
879
|
+
const now = Date.now()
|
|
880
|
+
let pruned = false
|
|
881
|
+
for (const key of Object.keys(parsed)) {
|
|
882
|
+
if (now - (parsed[key].lastFailedAt || 0) > maxAgeMs) {
|
|
883
|
+
delete parsed[key]
|
|
884
|
+
pruned = true
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (pruned) this.writeFailureState(parsed)
|
|
877
888
|
return parsed
|
|
878
889
|
} catch {
|
|
879
890
|
return {}
|
|
@@ -213,3 +213,49 @@ test('runKnowledgeHygieneMaintenance reindexes stale file sources and archives e
|
|
|
213
213
|
assert.ok(output.recentActionKinds.includes('archive'))
|
|
214
214
|
assert.ok(output.recentActionKinds.includes('reindex') || output.recentActionKinds.includes('sync'))
|
|
215
215
|
})
|
|
216
|
+
|
|
217
|
+
test('runKnowledgeHygieneMaintenance keeps same-content sources separate when visibility differs', () => {
|
|
218
|
+
const output = runWithTempDataDir<{
|
|
219
|
+
globalArchived: boolean
|
|
220
|
+
agentArchived: boolean
|
|
221
|
+
agent1Hits: number
|
|
222
|
+
agent2Hits: number
|
|
223
|
+
}>(`
|
|
224
|
+
const knowledgeMod = await import('./src/lib/server/knowledge-sources.ts')
|
|
225
|
+
const knowledge = knowledgeMod.default || knowledgeMod
|
|
226
|
+
|
|
227
|
+
const globalSource = await knowledge.createKnowledgeSource({
|
|
228
|
+
kind: 'manual',
|
|
229
|
+
title: 'Global Policy',
|
|
230
|
+
content: 'identical content',
|
|
231
|
+
scope: 'global',
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const agentSource = await knowledge.createKnowledgeSource({
|
|
235
|
+
kind: 'manual',
|
|
236
|
+
title: 'Scoped Policy',
|
|
237
|
+
content: 'identical content',
|
|
238
|
+
scope: 'agent',
|
|
239
|
+
agentIds: ['agent-1'],
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
await knowledge.runKnowledgeHygieneMaintenance()
|
|
243
|
+
|
|
244
|
+
const globalDetail = await knowledge.getKnowledgeSourceDetail(globalSource.source.id)
|
|
245
|
+
const agentDetail = await knowledge.getKnowledgeSourceDetail(agentSource.source.id)
|
|
246
|
+
const agent1Hits = await knowledge.searchKnowledgeHits({ query: 'identical', viewerAgentId: 'agent-1' })
|
|
247
|
+
const agent2Hits = await knowledge.searchKnowledgeHits({ query: 'identical', viewerAgentId: 'agent-2' })
|
|
248
|
+
|
|
249
|
+
console.log(JSON.stringify({
|
|
250
|
+
globalArchived: !!globalDetail?.source?.archivedAt,
|
|
251
|
+
agentArchived: !!agentDetail?.source?.archivedAt,
|
|
252
|
+
agent1Hits: agent1Hits.length,
|
|
253
|
+
agent2Hits: agent2Hits.length,
|
|
254
|
+
}))
|
|
255
|
+
`, { prefix: 'swarmclaw-knowledge-visibility-' })
|
|
256
|
+
|
|
257
|
+
assert.equal(output.globalArchived, false)
|
|
258
|
+
assert.equal(output.agentArchived, false)
|
|
259
|
+
assert.equal(output.agent1Hits, 2)
|
|
260
|
+
assert.equal(output.agent2Hits, 1)
|
|
261
|
+
})
|
|
@@ -1056,6 +1056,38 @@ function sameSourceOrigin(left: KnowledgeSource, right: KnowledgeSource): boolea
|
|
|
1056
1056
|
return false
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
|
+
function duplicateOriginFingerprint(source: KnowledgeSource): string {
|
|
1060
|
+
if (source.sourceUrl) return `url:${source.sourceUrl}`
|
|
1061
|
+
if (source.sourcePath) return `path:${source.sourcePath}`
|
|
1062
|
+
return `kind:${source.kind}`
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function duplicateGroupKey(source: KnowledgeSource): string | null {
|
|
1066
|
+
if (!source.sourceHash) return null
|
|
1067
|
+
const sortedAgentIds = [...source.agentIds].sort()
|
|
1068
|
+
const sortedTags = [...source.tags].map((tag) => tag.toLowerCase()).sort()
|
|
1069
|
+
return [
|
|
1070
|
+
source.sourceHash,
|
|
1071
|
+
source.kind,
|
|
1072
|
+
source.scope,
|
|
1073
|
+
sortedAgentIds.join(','),
|
|
1074
|
+
sortedTags.join(','),
|
|
1075
|
+
duplicateOriginFingerprint(source),
|
|
1076
|
+
].join('|')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function collectDuplicateGroups(sources: KnowledgeSource[]): Map<string, KnowledgeSource[]> {
|
|
1080
|
+
const duplicateGroups = new Map<string, KnowledgeSource[]>()
|
|
1081
|
+
for (const source of sources) {
|
|
1082
|
+
const groupKey = duplicateGroupKey(source)
|
|
1083
|
+
if (!groupKey) continue
|
|
1084
|
+
const group = duplicateGroups.get(groupKey) || []
|
|
1085
|
+
group.push(source)
|
|
1086
|
+
duplicateGroups.set(groupKey, group)
|
|
1087
|
+
}
|
|
1088
|
+
return duplicateGroups
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1059
1091
|
function canonicalSourceForGroup(group: KnowledgeSource[]): KnowledgeSource {
|
|
1060
1092
|
return [...group].sort((left, right) => {
|
|
1061
1093
|
const archiveDelta = Number(sourceIsExcludedByDefault(left)) - Number(sourceIsExcludedByDefault(right))
|
|
@@ -1072,14 +1104,7 @@ function buildHygieneSummary(sources: KnowledgeSource[]): KnowledgeHygieneSummar
|
|
|
1072
1104
|
const pushFinding = (finding: KnowledgeHygieneFinding) => {
|
|
1073
1105
|
if (findings.length < MAX_HYGIENE_FINDINGS) findings.push(finding)
|
|
1074
1106
|
}
|
|
1075
|
-
|
|
1076
|
-
const duplicateGroups = new Map<string, KnowledgeSource[]>()
|
|
1077
|
-
for (const source of sources) {
|
|
1078
|
-
if (!source.sourceHash) continue
|
|
1079
|
-
const group = duplicateGroups.get(source.sourceHash) || []
|
|
1080
|
-
group.push(source)
|
|
1081
|
-
duplicateGroups.set(source.sourceHash, group)
|
|
1082
|
-
}
|
|
1107
|
+
const duplicateGroups = collectDuplicateGroups(sources)
|
|
1083
1108
|
|
|
1084
1109
|
for (const source of sources) {
|
|
1085
1110
|
if (sourceIsArchived(source)) {
|
|
@@ -1185,14 +1210,7 @@ export async function getKnowledgeHygieneSummary(): Promise<KnowledgeHygieneSumm
|
|
|
1185
1210
|
export async function runKnowledgeHygieneMaintenance(): Promise<KnowledgeHygieneSummary> {
|
|
1186
1211
|
await ensureLegacyKnowledgeBackfill()
|
|
1187
1212
|
const sources = listStoredSources()
|
|
1188
|
-
|
|
1189
|
-
const duplicateGroups = new Map<string, KnowledgeSource[]>()
|
|
1190
|
-
for (const source of sources) {
|
|
1191
|
-
if (!source.sourceHash) continue
|
|
1192
|
-
const group = duplicateGroups.get(source.sourceHash) || []
|
|
1193
|
-
group.push(source)
|
|
1194
|
-
duplicateGroups.set(source.sourceHash, group)
|
|
1195
|
-
}
|
|
1213
|
+
const duplicateGroups = collectDuplicateGroups(sources)
|
|
1196
1214
|
|
|
1197
1215
|
for (const source of sources) {
|
|
1198
1216
|
if (sourceIsExcludedByDefault(source)) continue
|
|
@@ -381,7 +381,7 @@ export function syncExtensionsFromOpenClaw(): { imported: number } {
|
|
|
381
381
|
const openclawExtensionDir = path.join(config.workspacePath, 'plugins')
|
|
382
382
|
if (!fs.existsSync(openclawExtensionDir)) return { imported: 0 }
|
|
383
383
|
|
|
384
|
-
const localExtensionDir = path.join(DATA_DIR, '
|
|
384
|
+
const localExtensionDir = path.join(DATA_DIR, 'extensions')
|
|
385
385
|
ensureDir(localExtensionDir)
|
|
386
386
|
|
|
387
387
|
const files = fs.readdirSync(openclawExtensionDir).filter((f) => f.endsWith('.js'))
|
|
@@ -432,7 +432,7 @@ export function setSharedDeviceToken(token: string): void {
|
|
|
432
432
|
|
|
433
433
|
// --- Unified Sync Entry Point ---
|
|
434
434
|
|
|
435
|
-
export type SyncType = 'memory' | 'workspace' | 'schedules' | 'credentials' | '
|
|
435
|
+
export type SyncType = 'memory' | 'workspace' | 'schedules' | 'credentials' | 'extensions'
|
|
436
436
|
|
|
437
437
|
export interface SyncResult {
|
|
438
438
|
type: SyncType
|
|
@@ -467,7 +467,7 @@ export async function runSync(params: {
|
|
|
467
467
|
case 'credentials':
|
|
468
468
|
results.push({ type, action: 'push', result: pushCredentialsToOpenClaw() })
|
|
469
469
|
break
|
|
470
|
-
case '
|
|
470
|
+
case 'extensions':
|
|
471
471
|
// Extensions only pull from OpenClaw
|
|
472
472
|
break
|
|
473
473
|
}
|
|
@@ -492,7 +492,7 @@ export async function runSync(params: {
|
|
|
492
492
|
case 'credentials':
|
|
493
493
|
results.push({ type, action: 'pull', result: await pullCredentialsFromOpenClaw() })
|
|
494
494
|
break
|
|
495
|
-
case '
|
|
495
|
+
case 'extensions':
|
|
496
496
|
results.push({ type, action: 'pull', result: syncExtensionsFromOpenClaw() })
|
|
497
497
|
break
|
|
498
498
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import { log } from '@/lib/server/logger'
|
|
3
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
4
|
+
import { upsertTask } from '@/lib/server/tasks/task-repository'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import { callA2AAgent } from '@/lib/a2a/client'
|
|
7
|
+
import { loadExternalAgents } from '@/lib/server/storage'
|
|
8
|
+
import { appendProtocolEvent, persistRun } from '@/lib/server/protocols/protocol-agent-turn'
|
|
9
|
+
import { now } from '@/lib/server/protocols/protocol-types'
|
|
10
|
+
import type { ProtocolRunDeps } from '@/lib/server/protocols/protocol-types'
|
|
11
|
+
import type { ProtocolPhaseDefinition, ProtocolRun, ProtocolRunPhaseState } from '@/types/protocol'
|
|
12
|
+
import type { BoardTask } from '@/types/task'
|
|
13
|
+
|
|
14
|
+
const TAG = 'protocol-a2a-delegate'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Process an a2a_delegate phase: call a remote A2A agent and wait for the result.
|
|
18
|
+
*
|
|
19
|
+
* Follows the same pattern as processDispatchDelegationPhase:
|
|
20
|
+
* 1. Create a BoardTask for tracking (with protocolRunId so wakeProtocolRunFromTaskCompletion fires)
|
|
21
|
+
* 2. Call the remote agent via HTTP
|
|
22
|
+
* 3. Set protocol run to 'waiting'
|
|
23
|
+
* 4. When the HTTP call completes, update the task → wake machinery resumes the run
|
|
24
|
+
*/
|
|
25
|
+
export function processA2ADelegatePhase(
|
|
26
|
+
run: ProtocolRun,
|
|
27
|
+
phase: ProtocolPhaseDefinition,
|
|
28
|
+
deps?: ProtocolRunDeps,
|
|
29
|
+
): ProtocolRun {
|
|
30
|
+
const config = phase.a2aDelegateConfig
|
|
31
|
+
if (!config?.taskName || !config?.taskMessage) {
|
|
32
|
+
appendProtocolEvent(run.id, {
|
|
33
|
+
type: 'failed',
|
|
34
|
+
phaseId: phase.id,
|
|
35
|
+
summary: `a2a_delegate phase "${phase.label}" missing a2aDelegateConfig`,
|
|
36
|
+
}, deps)
|
|
37
|
+
return persistRun({
|
|
38
|
+
...run,
|
|
39
|
+
status: 'failed',
|
|
40
|
+
lastError: `a2a_delegate phase "${phase.label}" missing a2aDelegateConfig`,
|
|
41
|
+
endedAt: run.endedAt || now(deps),
|
|
42
|
+
updatedAt: now(deps),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve target URL
|
|
47
|
+
let targetUrl = config.targetUrl
|
|
48
|
+
if (!targetUrl && config.targetExternalAgentId) {
|
|
49
|
+
const externalAgents = loadExternalAgents()
|
|
50
|
+
const ea = externalAgents[config.targetExternalAgentId]
|
|
51
|
+
if (ea?.endpoint) {
|
|
52
|
+
targetUrl = ea.endpoint
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!targetUrl) {
|
|
57
|
+
appendProtocolEvent(run.id, {
|
|
58
|
+
type: 'failed',
|
|
59
|
+
phaseId: phase.id,
|
|
60
|
+
summary: `a2a_delegate phase "${phase.label}" — no target URL resolved`,
|
|
61
|
+
}, deps)
|
|
62
|
+
return persistRun({
|
|
63
|
+
...run,
|
|
64
|
+
status: 'failed',
|
|
65
|
+
lastError: `a2a_delegate phase "${phase.label}" — could not resolve target A2A agent URL`,
|
|
66
|
+
endedAt: run.endedAt || now(deps),
|
|
67
|
+
updatedAt: now(deps),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create a BoardTask for tracking
|
|
72
|
+
const taskId = genId()
|
|
73
|
+
const taskData: BoardTask = {
|
|
74
|
+
id: taskId,
|
|
75
|
+
title: `A2A: ${config.taskName}`,
|
|
76
|
+
description: config.taskMessage,
|
|
77
|
+
status: 'queued',
|
|
78
|
+
agentId: run.facilitatorAgentId || run.participantAgentIds?.[0] || '',
|
|
79
|
+
protocolRunId: run.id,
|
|
80
|
+
sourceType: 'delegation',
|
|
81
|
+
externalSource: { source: 'a2a', id: taskId },
|
|
82
|
+
queuedAt: now(deps),
|
|
83
|
+
createdAt: now(deps),
|
|
84
|
+
updatedAt: now(deps),
|
|
85
|
+
}
|
|
86
|
+
upsertTask(taskId, taskData)
|
|
87
|
+
notify('tasks')
|
|
88
|
+
|
|
89
|
+
appendProtocolEvent(run.id, {
|
|
90
|
+
type: 'delegation_dispatched',
|
|
91
|
+
summary: `Dispatched A2A delegation to ${targetUrl}: ${config.taskName}`,
|
|
92
|
+
phaseId: phase.id,
|
|
93
|
+
taskId,
|
|
94
|
+
}, deps)
|
|
95
|
+
|
|
96
|
+
log.info(TAG, `Calling remote A2A agent at ${targetUrl}`, { taskName: config.taskName, taskId })
|
|
97
|
+
|
|
98
|
+
// Fire the HTTP call asynchronously — when it completes, update the task
|
|
99
|
+
// The existing wakeProtocolRunFromTaskCompletion machinery will resume the run
|
|
100
|
+
const resolvedUrl = targetUrl
|
|
101
|
+
callA2AAgent(resolvedUrl, 'executeTask', {
|
|
102
|
+
taskId,
|
|
103
|
+
taskName: config.taskName,
|
|
104
|
+
message: config.taskMessage,
|
|
105
|
+
}, {
|
|
106
|
+
timeout: config.timeoutMs ?? 300_000,
|
|
107
|
+
credentialId: config.credentialId,
|
|
108
|
+
}).then(result => {
|
|
109
|
+
const resultStr = typeof result === 'string' ? result : JSON.stringify(result)
|
|
110
|
+
upsertTask(taskId, { ...taskData, status: 'completed', result: resultStr, updatedAt: Date.now(), completedAt: Date.now() })
|
|
111
|
+
notify('tasks')
|
|
112
|
+
log.info(TAG, `A2A delegation completed for task ${taskId}`)
|
|
113
|
+
// Dynamic import to break circular dependency (protocol-step-processors → protocol-a2a-delegate → protocol-run-lifecycle → protocol-step-processors)
|
|
114
|
+
import('@/lib/server/protocols/protocol-run-lifecycle').then(m => m.wakeProtocolRunFromTaskCompletion(taskId))
|
|
115
|
+
}).catch(err => {
|
|
116
|
+
log.error(TAG, `A2A delegation failed for task ${taskId}: ${errorMessage(err)}`)
|
|
117
|
+
if (config.onFailure === 'advance_with_warning') {
|
|
118
|
+
upsertTask(taskId, { ...taskData, status: 'completed', result: `A2A delegation failed: ${errorMessage(err)}`, error: errorMessage(err), updatedAt: Date.now(), completedAt: Date.now() })
|
|
119
|
+
} else {
|
|
120
|
+
upsertTask(taskId, { ...taskData, status: 'failed', error: errorMessage(err), updatedAt: Date.now() })
|
|
121
|
+
}
|
|
122
|
+
notify('tasks')
|
|
123
|
+
import('@/lib/server/protocols/protocol-run-lifecycle').then(m => m.wakeProtocolRunFromTaskCompletion(taskId))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const createdTaskIds = [...(run.createdTaskIds || []), taskId]
|
|
127
|
+
return persistRun({
|
|
128
|
+
...run,
|
|
129
|
+
status: 'waiting',
|
|
130
|
+
waitingReason: `Waiting for A2A delegation: ${config.taskName}`,
|
|
131
|
+
createdTaskIds,
|
|
132
|
+
phaseState: { ...(run.phaseState || { phaseId: phase.id }), dispatchedTaskId: taskId } as ProtocolRunPhaseState,
|
|
133
|
+
updatedAt: now(deps),
|
|
134
|
+
})
|
|
135
|
+
}
|
|
@@ -48,7 +48,7 @@ describe('protocol-step-helpers', () => {
|
|
|
48
48
|
const kinds = [
|
|
49
49
|
'present', 'collect_independent_inputs', 'round_robin',
|
|
50
50
|
'compare', 'decide', 'summarize', 'emit_tasks', 'wait',
|
|
51
|
-
'dispatch_task', 'dispatch_delegation',
|
|
51
|
+
'dispatch_task', 'dispatch_delegation', 'a2a_delegate',
|
|
52
52
|
]
|
|
53
53
|
for (const kind of kinds) {
|
|
54
54
|
const step = { id: `step-${kind}`, kind, label: kind } as never
|
|
@@ -58,6 +58,7 @@ export function phaseFromStep(step: ProtocolStepDefinition): ProtocolPhaseDefini
|
|
|
58
58
|
completionCriteria: step.completionCriteria || null,
|
|
59
59
|
taskConfig: step.taskConfig || null,
|
|
60
60
|
delegationConfig: step.delegationConfig || null,
|
|
61
|
+
a2aDelegateConfig: step.a2aDelegateConfig || null,
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -23,6 +23,7 @@ import type * as ProtocolRunLifecycle from '@/lib/server/protocols/protocol-run-
|
|
|
23
23
|
import { processForEachStep } from '@/lib/server/protocols/protocol-foreach'
|
|
24
24
|
import { processSubflowStep } from '@/lib/server/protocols/protocol-subflow'
|
|
25
25
|
import { processSwarmStep } from '@/lib/server/protocols/protocol-swarm'
|
|
26
|
+
import { processA2ADelegatePhase } from '@/lib/server/protocols/protocol-a2a-delegate'
|
|
26
27
|
import { findRunStep } from '@/lib/server/protocols/protocol-normalization'
|
|
27
28
|
import {
|
|
28
29
|
appendProtocolEvent,
|
|
@@ -708,6 +709,7 @@ export async function stepProtocolRun(run: ProtocolRun, deps?: ProtocolRunDeps):
|
|
|
708
709
|
if (phase.kind === 'emit_tasks') return processEmitTasksPhase(started, phase, deps)
|
|
709
710
|
if (phase.kind === 'dispatch_task') return processDispatchTaskPhase(started, phase, deps)
|
|
710
711
|
if (phase.kind === 'dispatch_delegation') return processDispatchDelegationPhase(started, phase, deps)
|
|
712
|
+
if (phase.kind === 'a2a_delegate') return processA2ADelegatePhase(started, phase, deps)
|
|
711
713
|
return processWaitPhase(started, phase, deps)
|
|
712
714
|
}
|
|
713
715
|
if (step.kind === 'branch') return processBranchStep(run, step, deps)
|