@swarmclawai/swarmclaw 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -76
- package/package.json +3 -2
- package/skills/swarmclaw.md +17 -0
- package/src/app/api/agents/[id]/dream/route.ts +45 -0
- package/src/app/api/knowledge/[id]/route.ts +48 -49
- package/src/app/api/knowledge/hygiene/route.ts +13 -0
- package/src/app/api/knowledge/route.ts +70 -42
- package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
- package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
- package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
- package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
- package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
- package/src/app/api/knowledge/sources/route.ts +1 -0
- package/src/app/api/knowledge/upload/route.ts +3 -51
- package/src/app/api/memory/dream/[id]/route.ts +19 -0
- package/src/app/api/memory/dream/route.ts +34 -0
- package/src/app/knowledge/layout.tsx +1 -1
- package/src/app/knowledge/page.tsx +2 -22
- package/src/app/protocols/page.tsx +21 -2
- package/src/cli/index.js +16 -0
- package/src/cli/spec.js +5 -0
- package/src/components/agents/agent-sheet.tsx +65 -0
- package/src/components/chat/message-bubble.tsx +10 -0
- package/src/components/knowledge/grounding-panel.tsx +99 -0
- package/src/components/knowledge/knowledge-detail.tsx +402 -0
- package/src/components/knowledge/knowledge-list.tsx +351 -126
- package/src/components/knowledge/knowledge-sheet.tsx +208 -119
- package/src/components/memory/dream-history.tsx +155 -0
- package/src/components/memory/memory-card.tsx +7 -0
- package/src/components/memory/memory-detail.tsx +46 -0
- package/src/components/runs/run-list.tsx +23 -0
- package/src/lib/server/api-routes.test.ts +43 -2
- 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 +108 -0
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
- package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
- package/src/lib/server/chat-execution/chat-execution.ts +1 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
- package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
- package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
- package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
- package/src/lib/server/execution-engine/task-attempt.ts +8 -2
- package/src/lib/server/knowledge-import.ts +159 -0
- package/src/lib/server/knowledge-sources.test.ts +261 -0
- package/src/lib/server/knowledge-sources.ts +1284 -0
- package/src/lib/server/memory/dream-cycles.ts +49 -0
- package/src/lib/server/memory/dream-idle-callback.ts +38 -0
- package/src/lib/server/memory/dream-service.ts +315 -0
- package/src/lib/server/memory/memory-db.ts +37 -2
- package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
- package/src/lib/server/protocols/protocol-service.test.ts +99 -0
- package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
- package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
- package/src/lib/server/protocols/protocol-types.ts +4 -0
- package/src/lib/server/runtime/daemon-state/core.ts +6 -1
- package/src/lib/server/runtime/run-ledger.test.ts +120 -0
- package/src/lib/server/runtime/run-ledger.ts +27 -1
- package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
- package/src/lib/server/storage-normalization.ts +5 -0
- package/src/lib/server/storage.ts +15 -0
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
- package/src/stores/slices/ui-slice.ts +4 -0
- package/src/types/agent.ts +7 -0
- package/src/types/dream.ts +45 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +3 -0
- package/src/types/misc.ts +131 -0
- package/src/types/protocol.ts +4 -0
- package/src/types/run.ts +4 -1
|
@@ -19,6 +19,8 @@ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAva
|
|
|
19
19
|
const [now] = useState(() => Date.now())
|
|
20
20
|
const scope = deriveMemoryScope(entry)
|
|
21
21
|
const tier = getMemoryTier(entry)
|
|
22
|
+
const isDreamOrigin = entry.category === 'dream_reflection'
|
|
23
|
+
|| (entry.metadata as Record<string, unknown> | undefined)?.origin === 'dream'
|
|
22
24
|
|
|
23
25
|
return (
|
|
24
26
|
<div
|
|
@@ -36,6 +38,11 @@ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAva
|
|
|
36
38
|
<span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright/70 bg-accent-soft px-1.5 py-0.5 rounded-[5px]">
|
|
37
39
|
{entry.category || 'note'}
|
|
38
40
|
</span>
|
|
41
|
+
{isDreamOrigin && (
|
|
42
|
+
<span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-violet-300/70 bg-violet-400/10 px-1.5 py-0.5 rounded-[5px]">
|
|
43
|
+
dream
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
39
46
|
{entry.pinned && (
|
|
40
47
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" className="shrink-0 text-amber-400/80">
|
|
41
48
|
<path d="M16 2l-4 4-4-4-2 2 4 4-5 5v1h1l5-5 4 4 2-2-4-4 4-4z" transform="rotate(45 12 12)" />
|
|
@@ -175,6 +175,19 @@ export function MemoryDetail() {
|
|
|
175
175
|
const inputClass = "w-full px-4 py-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] text-text outline-none transition-all duration-200 placeholder:text-text-3/70 focus:border-accent-bright/20 focus:bg-white/[0.03]"
|
|
176
176
|
const refs = entry.references || []
|
|
177
177
|
const showRefsCollapse = refs.length > 3
|
|
178
|
+
const entryMeta = entry.metadata && typeof entry.metadata === 'object'
|
|
179
|
+
? entry.metadata as Record<string, unknown>
|
|
180
|
+
: {}
|
|
181
|
+
const knowledgeSourceId = typeof entryMeta.sourceId === 'string' ? entryMeta.sourceId : null
|
|
182
|
+
const knowledgeSourceTitle = typeof entryMeta.sourceTitle === 'string' ? entryMeta.sourceTitle : null
|
|
183
|
+
const knowledgeSourceKind = typeof entryMeta.sourceKind === 'string' ? entryMeta.sourceKind : null
|
|
184
|
+
const knowledgeSourceLabel = typeof entryMeta.sourceLabel === 'string' ? entryMeta.sourceLabel : null
|
|
185
|
+
const knowledgeSourceUrl = typeof entryMeta.sourceUrl === 'string' ? entryMeta.sourceUrl : null
|
|
186
|
+
const knowledgeChunkIndex = typeof entryMeta.chunkIndex === 'number' ? entryMeta.chunkIndex : null
|
|
187
|
+
const knowledgeChunkCount = typeof entryMeta.chunkCount === 'number' ? entryMeta.chunkCount : null
|
|
188
|
+
const knowledgeSectionLabel = typeof entryMeta.sectionLabel === 'string' ? entryMeta.sectionLabel : null
|
|
189
|
+
const knowledgeCharStart = typeof entryMeta.charStart === 'number' ? entryMeta.charStart : null
|
|
190
|
+
const knowledgeCharEnd = typeof entryMeta.charEnd === 'number' ? entryMeta.charEnd : null
|
|
178
191
|
|
|
179
192
|
return (
|
|
180
193
|
<div className="flex-1 flex flex-col h-full min-h-0">
|
|
@@ -438,6 +451,33 @@ export function MemoryDetail() {
|
|
|
438
451
|
{entry.content || '(empty)'}
|
|
439
452
|
</div>
|
|
440
453
|
|
|
454
|
+
{knowledgeSourceId && (
|
|
455
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-3">
|
|
456
|
+
<label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Source</label>
|
|
457
|
+
<div className="space-y-1.5">
|
|
458
|
+
<p className="text-[13px] text-text-2">
|
|
459
|
+
{knowledgeSourceTitle || entry.title}
|
|
460
|
+
{knowledgeSourceKind ? ` • ${knowledgeSourceKind}` : ''}
|
|
461
|
+
</p>
|
|
462
|
+
{knowledgeSourceLabel && (
|
|
463
|
+
<p className="text-[12px] text-text-3/65">{knowledgeSourceLabel}</p>
|
|
464
|
+
)}
|
|
465
|
+
{knowledgeSourceUrl && (
|
|
466
|
+
<a href={knowledgeSourceUrl} target="_blank" rel="noreferrer" className="text-[12px] text-accent-bright hover:underline break-all">
|
|
467
|
+
{knowledgeSourceUrl}
|
|
468
|
+
</a>
|
|
469
|
+
)}
|
|
470
|
+
<p className="text-[11px] text-text-3/55">
|
|
471
|
+
{knowledgeChunkIndex != null && knowledgeChunkCount != null
|
|
472
|
+
? `Chunk ${knowledgeChunkIndex + 1} of ${knowledgeChunkCount}`
|
|
473
|
+
: 'Source-backed knowledge'}
|
|
474
|
+
{knowledgeSectionLabel ? ` • ${knowledgeSectionLabel}` : ''}
|
|
475
|
+
{knowledgeCharStart != null && knowledgeCharEnd != null ? ` • chars ${knowledgeCharStart}-${knowledgeCharEnd}` : ''}
|
|
476
|
+
</p>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
|
|
441
481
|
{/* Shared with (read mode) */}
|
|
442
482
|
{entry.sharedWith && entry.sharedWith.length > 0 && (
|
|
443
483
|
<div>
|
|
@@ -575,6 +615,12 @@ export function MemoryDetail() {
|
|
|
575
615
|
<span className="text-text-3/70 block mb-1">Tier</span>
|
|
576
616
|
<span className="text-text-3/60 font-mono">{tier}</span>
|
|
577
617
|
</div>
|
|
618
|
+
{knowledgeSourceId && (
|
|
619
|
+
<div>
|
|
620
|
+
<span className="text-text-3/70 block mb-1">Knowledge Source</span>
|
|
621
|
+
<span className="text-text-3/60 font-mono">{knowledgeSourceId}</span>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
578
624
|
{entry.sessionId && (
|
|
579
625
|
<div>
|
|
580
626
|
<span className="text-text-3/70 block mb-1">Chat</span>
|
|
@@ -8,6 +8,7 @@ import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
|
8
8
|
import type { RunEventRecord, SessionRunRecord, SessionRunStatus } from '@/types'
|
|
9
9
|
import { PageLoader } from '@/components/ui/page-loader'
|
|
10
10
|
import { formatElapsed } from '@/lib/format-display'
|
|
11
|
+
import { GroundingPanel } from '@/components/knowledge/grounding-panel'
|
|
11
12
|
|
|
12
13
|
const STATUS_COLORS: Record<SessionRunStatus, { bg: string; text: string }> = {
|
|
13
14
|
queued: { bg: 'bg-yellow-500/10', text: 'text-yellow-400' },
|
|
@@ -84,6 +85,10 @@ export function RunList() {
|
|
|
84
85
|
}, [])
|
|
85
86
|
|
|
86
87
|
const filtered = statusFilter ? runs.filter((r) => r.status === statusFilter) : runs
|
|
88
|
+
const selectedResultGrounding = selectedEvents
|
|
89
|
+
.slice()
|
|
90
|
+
.reverse()
|
|
91
|
+
.find((event) => event.phase === 'status' && ((event.citations?.length || 0) > 0 || event.retrievalTrace?.hits?.length))
|
|
87
92
|
|
|
88
93
|
if (loading) {
|
|
89
94
|
return <PageLoader label="Loading runs..." />
|
|
@@ -240,6 +245,15 @@ export function RunList() {
|
|
|
240
245
|
<pre className="text-[11px] text-text-3/80 font-mono whitespace-pre-wrap break-all bg-white/[0.02] rounded-[12px] p-4 max-h-[200px] overflow-auto border border-white/[0.04]">
|
|
241
246
|
{selected.resultPreview}
|
|
242
247
|
</pre>
|
|
248
|
+
{selectedResultGrounding && (
|
|
249
|
+
<div className="mt-3">
|
|
250
|
+
<GroundingPanel
|
|
251
|
+
citations={selectedResultGrounding.citations}
|
|
252
|
+
retrievalTrace={selectedResultGrounding.retrievalTrace}
|
|
253
|
+
compact
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
243
257
|
</div>
|
|
244
258
|
)}
|
|
245
259
|
|
|
@@ -262,6 +276,15 @@ export function RunList() {
|
|
|
262
276
|
<div className="text-[11px] text-text-2 whitespace-pre-wrap break-words">
|
|
263
277
|
{event.summary || event.event.text || event.event.toolOutput || event.event.toolName || event.event.t}
|
|
264
278
|
</div>
|
|
279
|
+
{(event.citations?.length || event.retrievalTrace?.hits?.length) ? (
|
|
280
|
+
<div className="mt-2">
|
|
281
|
+
<GroundingPanel
|
|
282
|
+
citations={event.citations}
|
|
283
|
+
retrievalTrace={event.retrievalTrace}
|
|
284
|
+
compact
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
) : null}
|
|
265
288
|
</div>
|
|
266
289
|
))}
|
|
267
290
|
</div>
|
|
@@ -78,9 +78,16 @@ function parseKnowledgeQueryParams(url: string) {
|
|
|
78
78
|
const q = searchParams.get('q')
|
|
79
79
|
const tagsParam = searchParams.get('tags')
|
|
80
80
|
const limitParam = searchParams.get('limit')
|
|
81
|
+
const includeArchivedParam = searchParams.get('includeArchived')
|
|
81
82
|
const tags = tagsParam ? tagsParam.split(',').map((t) => t.trim()).filter(Boolean) : undefined
|
|
82
83
|
const limit = limitParam ? Math.max(1, Math.min(500, Number.parseInt(limitParam, 10) || 50)) : undefined
|
|
83
|
-
|
|
84
|
+
const normalizedIncludeArchived = typeof includeArchivedParam === 'string'
|
|
85
|
+
? includeArchivedParam.trim().toLowerCase()
|
|
86
|
+
: ''
|
|
87
|
+
const includeArchived = normalizedIncludeArchived === '1'
|
|
88
|
+
|| normalizedIncludeArchived === 'true'
|
|
89
|
+
|| normalizedIncludeArchived === 'yes'
|
|
90
|
+
return { q, tags, limit, includeArchived }
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// ---------------------------------------------------------------------------
|
|
@@ -91,7 +98,12 @@ const thisFile = new URL(import.meta.url).pathname
|
|
|
91
98
|
const routeDir = path.resolve(path.dirname(thisFile), '../../app/api')
|
|
92
99
|
|
|
93
100
|
function readRoute(...segments: string[]): string {
|
|
94
|
-
|
|
101
|
+
const direct = path.join(routeDir, ...segments)
|
|
102
|
+
if (fs.existsSync(direct)) {
|
|
103
|
+
return fs.readFileSync(direct, 'utf-8')
|
|
104
|
+
}
|
|
105
|
+
const withTs = direct.endsWith('.ts') ? direct : `${direct}.ts`
|
|
106
|
+
return fs.readFileSync(withTs, 'utf-8')
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
// ===========================================================================
|
|
@@ -225,6 +237,12 @@ describe('Knowledge API contract', () => {
|
|
|
225
237
|
it('returns undefined limit when param is absent', () => {
|
|
226
238
|
assert.equal(parseKnowledgeQueryParams('http://localhost/api/knowledge').limit, undefined)
|
|
227
239
|
})
|
|
240
|
+
|
|
241
|
+
it('parses includeArchived as a boolean flag', () => {
|
|
242
|
+
assert.equal(parseKnowledgeQueryParams('http://localhost/api/knowledge?includeArchived=true').includeArchived, true)
|
|
243
|
+
assert.equal(parseKnowledgeQueryParams('http://localhost/api/knowledge?includeArchived=1').includeArchived, true)
|
|
244
|
+
assert.equal(parseKnowledgeQueryParams('http://localhost/api/knowledge?includeArchived=no').includeArchived, false)
|
|
245
|
+
})
|
|
228
246
|
})
|
|
229
247
|
|
|
230
248
|
// --- Route file structure -----------------------------------------------
|
|
@@ -241,6 +259,29 @@ describe('Knowledge API contract', () => {
|
|
|
241
259
|
assert.match(src, /export\s+async\s+function\s+PUT/)
|
|
242
260
|
assert.match(src, /export\s+async\s+function\s+DELETE/)
|
|
243
261
|
})
|
|
262
|
+
|
|
263
|
+
it('knowledge/hygiene/route.ts exports GET and POST', () => {
|
|
264
|
+
const src = readRoute('knowledge', 'hygiene', 'route.ts')
|
|
265
|
+
assert.match(src, /export\s+async\s+function\s+GET/)
|
|
266
|
+
assert.match(src, /export\s+async\s+function\s+POST/)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('knowledge/sources/route.ts re-exports GET and POST', () => {
|
|
270
|
+
const src = readRoute('knowledge', 'sources', 'route.ts')
|
|
271
|
+
assert.match(src, /export\s+\{\s*GET,\s*POST\s*\}/)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('knowledge/sources/[id]/route.ts re-exports GET, PUT, DELETE', () => {
|
|
275
|
+
const src = readRoute('knowledge', 'sources', '[id]', 'route.ts')
|
|
276
|
+
assert.match(src, /export\s+\{\s*GET,\s*PUT,\s*DELETE\s*\}/)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('knowledge source action routes export POST', () => {
|
|
280
|
+
for (const route of ['archive', 'restore', 'supersede', 'sync']) {
|
|
281
|
+
const src = readRoute('knowledge', 'sources', '[id]', route, 'route.ts')
|
|
282
|
+
assert.match(src, /export\s+async\s+function\s+POST/)
|
|
283
|
+
}
|
|
284
|
+
})
|
|
244
285
|
})
|
|
245
286
|
})
|
|
246
287
|
|
|
@@ -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'])
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
4
|
+
|
|
5
|
+
test('executeSessionChatTurn persists citations and retrieval traces on grounded assistant messages', () => {
|
|
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
|
+
}>(`
|
|
15
|
+
const storageMod = await import('@/lib/server/storage')
|
|
16
|
+
const providersMod = await import('@/lib/providers/index')
|
|
17
|
+
const threadMod = await import('@/lib/server/agents/agent-thread-session')
|
|
18
|
+
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
19
|
+
const messageRepoMod = await import('@/lib/server/messages/message-repository')
|
|
20
|
+
const knowledgeMod = await import('@/lib/server/knowledge-sources')
|
|
21
|
+
|
|
22
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
23
|
+
const ensureAgentThreadSession = threadMod.ensureAgentThreadSession
|
|
24
|
+
|| threadMod.default?.ensureAgentThreadSession
|
|
25
|
+
|| threadMod['module.exports']?.ensureAgentThreadSession
|
|
26
|
+
const executeSessionChatTurn = execMod.executeSessionChatTurn
|
|
27
|
+
|| execMod.default?.executeSessionChatTurn
|
|
28
|
+
|| execMod['module.exports']?.executeSessionChatTurn
|
|
29
|
+
const getMessages = messageRepoMod.getMessages
|
|
30
|
+
|| messageRepoMod.default?.getMessages
|
|
31
|
+
|| messageRepoMod['module.exports']?.getMessages
|
|
32
|
+
const knowledge = knowledgeMod.default || knowledgeMod
|
|
33
|
+
const providers = providersMod.PROVIDERS
|
|
34
|
+
|| providersMod.default?.PROVIDERS
|
|
35
|
+
|| providersMod['module.exports']?.PROVIDERS
|
|
36
|
+
|
|
37
|
+
providers['test-provider'] = {
|
|
38
|
+
id: 'test-provider',
|
|
39
|
+
name: 'Test Provider',
|
|
40
|
+
models: ['unit'],
|
|
41
|
+
requiresApiKey: false,
|
|
42
|
+
requiresEndpoint: false,
|
|
43
|
+
handler: {
|
|
44
|
+
async streamChat() {
|
|
45
|
+
return 'Use blue green deployment for the gateway migration so rollback stays simple.'
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
storage.saveAgents({
|
|
52
|
+
molly: {
|
|
53
|
+
id: 'molly',
|
|
54
|
+
name: 'Molly',
|
|
55
|
+
description: 'Grounding test',
|
|
56
|
+
provider: 'test-provider',
|
|
57
|
+
model: 'unit',
|
|
58
|
+
credentialId: null,
|
|
59
|
+
apiEndpoint: null,
|
|
60
|
+
fallbackCredentialIds: [],
|
|
61
|
+
disabled: false,
|
|
62
|
+
proactiveMemory: true,
|
|
63
|
+
extensions: ['memory'],
|
|
64
|
+
createdAt: now,
|
|
65
|
+
updatedAt: now,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await knowledge.createKnowledgeSource({
|
|
70
|
+
kind: 'manual',
|
|
71
|
+
title: 'Gateway Migration Runbook',
|
|
72
|
+
content: 'Use blue green deployment for gateway migrations so rollback stays simple and downtime stays low.',
|
|
73
|
+
tags: ['deploy'],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const session = ensureAgentThreadSession('molly')
|
|
77
|
+
const result = await executeSessionChatTurn({
|
|
78
|
+
sessionId: session.id,
|
|
79
|
+
message: 'gateway blue green rollback',
|
|
80
|
+
runId: 'run-grounding-chat',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const messages = getMessages(session.id)
|
|
84
|
+
const lastMessage = messages[messages.length - 1]
|
|
85
|
+
|
|
86
|
+
console.log(JSON.stringify({
|
|
87
|
+
persisted: result.persisted || false,
|
|
88
|
+
resultCitationCount: Array.isArray(result.citations) ? result.citations.length : 0,
|
|
89
|
+
resultSelectorStatus: result.retrievalTrace?.selectorStatus || null,
|
|
90
|
+
messageCitationCount: Array.isArray(lastMessage?.citations) ? lastMessage.citations.length : 0,
|
|
91
|
+
messageTraceHitCount: Array.isArray(lastMessage?.retrievalTrace?.hits) ? lastMessage.retrievalTrace.hits.length : 0,
|
|
92
|
+
messageSelectorStatus: lastMessage?.retrievalTrace?.selectorStatus || null,
|
|
93
|
+
messageSourceTitle: lastMessage?.citations?.[0]?.sourceTitle || null,
|
|
94
|
+
}))
|
|
95
|
+
`, {
|
|
96
|
+
prefix: 'swarmclaw-chat-grounding-',
|
|
97
|
+
dataDir: 'data',
|
|
98
|
+
browserProfilesDir: 'browser-profiles',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
assert.equal(output.persisted, true)
|
|
102
|
+
assert.equal(output.resultCitationCount >= 1, true)
|
|
103
|
+
assert.equal(output.resultSelectorStatus, 'selected')
|
|
104
|
+
assert.equal(output.messageCitationCount >= 1, true)
|
|
105
|
+
assert.equal(output.messageTraceHitCount >= 1, true)
|
|
106
|
+
assert.equal(output.messageSelectorStatus, 'selected')
|
|
107
|
+
assert.equal(output.messageSourceTitle, 'Gateway Migration Runbook')
|
|
108
|
+
})
|
|
@@ -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')
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
KnowledgeCitation,
|
|
3
|
+
KnowledgeRetrievalTrace,
|
|
4
|
+
MessageToolEvent,
|
|
5
|
+
SSEEvent,
|
|
6
|
+
} from '@/types'
|
|
2
7
|
|
|
3
8
|
export interface ExecuteChatTurnInput {
|
|
4
9
|
sessionId: string
|
|
@@ -33,4 +38,6 @@ export interface ExecuteChatTurnResult {
|
|
|
33
38
|
inputTokens?: number
|
|
34
39
|
outputTokens?: number
|
|
35
40
|
estimatedCost?: number
|
|
41
|
+
citations?: KnowledgeCitation[]
|
|
42
|
+
retrievalTrace?: KnowledgeRetrievalTrace | null
|
|
36
43
|
}
|
|
@@ -112,6 +112,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
112
112
|
responseCacheHit: streamResult.responseCacheHit,
|
|
113
113
|
directUsage: streamResult.directUsage,
|
|
114
114
|
durationMs: streamResult.durationMs,
|
|
115
|
+
knowledgeRetrievalTrace: streamResult.knowledgeRetrievalTrace || null,
|
|
115
116
|
emit: partialPersistence.emit,
|
|
116
117
|
})
|
|
117
118
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
KnowledgeRetrievalTrace,
|
|
3
|
+
Message,
|
|
4
|
+
MessageToolEvent,
|
|
5
|
+
SSEEvent,
|
|
6
|
+
Session,
|
|
7
|
+
UsageRecord,
|
|
8
|
+
} from '@/types'
|
|
2
9
|
import { sendConnectorMessage } from '../connectors/manager'
|
|
3
10
|
import { applyExactOutputContract, classifyExactOutputContract, type ExactOutputContract } from '@/lib/server/chat-execution/exact-output-contract'
|
|
4
11
|
import { stripMainLoopMetaForPersistence } from '@/lib/server/agents/main-agent-loop'
|
|
@@ -43,6 +50,7 @@ import {
|
|
|
43
50
|
import { appendUsage } from '@/lib/server/usage/usage-repository'
|
|
44
51
|
import { synchronizeWorkingStateForTurn } from '@/lib/server/working-state/service'
|
|
45
52
|
import { notify } from '@/lib/server/ws-hub'
|
|
53
|
+
import { selectKnowledgeCitations } from '@/lib/server/knowledge-sources'
|
|
46
54
|
|
|
47
55
|
import type { ExecuteChatTurnInput, ExecuteChatTurnResult } from './chat-execution-types'
|
|
48
56
|
import type { PartialAssistantPersistence } from '@/lib/server/chat-execution/chat-turn-partial-persistence'
|
|
@@ -154,6 +162,7 @@ export async function finalizeChatTurn(params: {
|
|
|
154
162
|
received: boolean
|
|
155
163
|
}
|
|
156
164
|
durationMs: number
|
|
165
|
+
knowledgeRetrievalTrace?: KnowledgeRetrievalTrace | null
|
|
157
166
|
emit: (event: SSEEvent) => void
|
|
158
167
|
}): Promise<ExecuteChatTurnResult> {
|
|
159
168
|
const {
|
|
@@ -164,6 +173,7 @@ export async function finalizeChatTurn(params: {
|
|
|
164
173
|
responseCacheHit,
|
|
165
174
|
directUsage,
|
|
166
175
|
durationMs,
|
|
176
|
+
knowledgeRetrievalTrace,
|
|
167
177
|
emit,
|
|
168
178
|
} = params
|
|
169
179
|
let { fullResponse, errorMessage } = params
|
|
@@ -313,6 +323,12 @@ export async function finalizeChatTurn(params: {
|
|
|
313
323
|
const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
|
|
314
324
|
const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
|
|
315
325
|
const persistedText = getPersistedAssistantText(textForPersistence, persistedToolEvents)
|
|
326
|
+
const grounding = hiddenControlOnly
|
|
327
|
+
? { citations: [], retrievalTrace: knowledgeRetrievalTrace || null }
|
|
328
|
+
: selectKnowledgeCitations({
|
|
329
|
+
responseText: persistedText,
|
|
330
|
+
retrievalTrace: knowledgeRetrievalTrace || null,
|
|
331
|
+
})
|
|
316
332
|
let persistedResponseForHooks = textForPersistence
|
|
317
333
|
|
|
318
334
|
if (isHeartbeatRun && rawTextForPersistence) {
|
|
@@ -413,6 +429,8 @@ export async function finalizeChatTurn(params: {
|
|
|
413
429
|
thinking: thinkingText || undefined,
|
|
414
430
|
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
415
431
|
kind: persistedKind,
|
|
432
|
+
citations: grounding.citations.length > 0 ? grounding.citations : undefined,
|
|
433
|
+
retrievalTrace: grounding.retrievalTrace || undefined,
|
|
416
434
|
},
|
|
417
435
|
enabledIds: extensionsForRun,
|
|
418
436
|
phase: isHeartbeatRun ? 'heartbeat' : 'assistant_final',
|
|
@@ -643,5 +661,7 @@ export async function finalizeChatTurn(params: {
|
|
|
643
661
|
inputTokens: accumulatedUsage.inputTokens || undefined,
|
|
644
662
|
outputTokens: accumulatedUsage.outputTokens || undefined,
|
|
645
663
|
estimatedCost: accumulatedUsage.estimatedCost || undefined,
|
|
664
|
+
citations: grounding.citations.length > 0 ? grounding.citations : undefined,
|
|
665
|
+
retrievalTrace: grounding.retrievalTrace || undefined,
|
|
646
666
|
}
|
|
647
667
|
}
|