@swarmclawai/swarmclaw 1.3.3 → 1.3.5
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 +19 -76
- package/package.json +1 -1
- 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/providers/cli-utils.ts +3 -4
- package/src/lib/providers/index.ts +12 -22
- package/src/lib/providers/openclaw.ts +1 -2
- package/src/lib/server/agents/subagent-swarm.ts +2 -7
- package/src/lib/server/api-routes.test.ts +43 -2
- package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +127 -0
- 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 +23 -6
- 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-builder.ts +4 -6
- 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/connectors/openclaw.ts +1 -2
- 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 +215 -0
- package/src/lib/server/knowledge-sources.ts +1266 -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/provider-health.ts +2 -7
- 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 +16 -1
- 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>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from 'fs'
|
|
10
10
|
import os from 'os'
|
|
11
|
+
import { findBinaryOnPath } from '../server/session-tools/context'
|
|
11
12
|
import path from 'path'
|
|
12
13
|
import { spawnSync, type ChildProcess } from 'child_process'
|
|
13
14
|
import { log } from '../server/logger'
|
|
@@ -63,10 +64,8 @@ function getNvmBinaryPaths(name: string): string[] {
|
|
|
63
64
|
* then falls back to known paths + nvm paths.
|
|
64
65
|
*/
|
|
65
66
|
export function resolveCliBinary(name: string, extraPaths?: string[]): string | null {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const { findBinaryOnPath } = require('../server/session-tools/context')
|
|
69
|
-
const fromPath = findBinaryOnPath(name) as string | null
|
|
67
|
+
const fromPath = findBinaryOnPath(name)
|
|
68
|
+
|
|
70
69
|
if (fromPath) return fromPath
|
|
71
70
|
|
|
72
71
|
const paths = [
|
|
@@ -11,6 +11,8 @@ import { errorMessage, sleep, jitteredBackoff } from '@/lib/shared-utils'
|
|
|
11
11
|
import { classifyProviderError } from './error-classification'
|
|
12
12
|
import { log } from '@/lib/server/logger'
|
|
13
13
|
import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType, ProviderId } from '../../types'
|
|
14
|
+
import { loadProviderConfigs, loadModelOverrides, loadStoredItem, loadCredentials, decryptKey } from '@/lib/server/storage'
|
|
15
|
+
import { getExtensionManager } from '@/lib/server/extensions'
|
|
14
16
|
|
|
15
17
|
const TAG = 'providers'
|
|
16
18
|
|
|
@@ -289,8 +291,6 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
289
291
|
/** Merge built-in providers with custom providers from storage */
|
|
290
292
|
function getCustomProviders(): Record<string, CustomProviderConfig> {
|
|
291
293
|
try {
|
|
292
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
293
|
-
const { loadProviderConfigs } = require('../server/storage') as typeof import('@/lib/server/storage')
|
|
294
294
|
const configs = loadProviderConfigs() as Record<string, CustomProviderConfig>
|
|
295
295
|
return Object.fromEntries(
|
|
296
296
|
Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
|
|
@@ -303,8 +303,6 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
|
|
|
303
303
|
|
|
304
304
|
function getModelOverrides(): Record<string, string[]> {
|
|
305
305
|
try {
|
|
306
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
307
|
-
const { loadModelOverrides } = require('../server/storage') as typeof import('@/lib/server/storage')
|
|
308
306
|
return loadModelOverrides()
|
|
309
307
|
} catch {
|
|
310
308
|
return {}
|
|
@@ -342,17 +340,15 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
342
340
|
|
|
343
341
|
let extensionProviders: ProviderInfo[] = []
|
|
344
342
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
requiresEndpoint: Boolean(p.requiresEndpoint),
|
|
355
|
-
defaultEndpoint: p.defaultEndpoint as string | undefined,
|
|
343
|
+
extensionProviders = getExtensionManager().getProviders().map((p) => ({
|
|
344
|
+
id: p.id as ProviderId,
|
|
345
|
+
name: p.name,
|
|
346
|
+
models: p.models,
|
|
347
|
+
defaultModels: p.models,
|
|
348
|
+
supportsModelDiscovery: false,
|
|
349
|
+
requiresApiKey: p.requiresApiKey,
|
|
350
|
+
requiresEndpoint: p.requiresEndpoint,
|
|
351
|
+
defaultEndpoint: p.defaultEndpoint,
|
|
356
352
|
}))
|
|
357
353
|
} catch { /* ignore if running somewhere extensions aren't available */ }
|
|
358
354
|
|
|
@@ -391,8 +387,6 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
|
|
|
391
387
|
// Fallback: direct single-item DB lookup for custom-* providers
|
|
392
388
|
if (id.startsWith('custom-') && !custom) {
|
|
393
389
|
try {
|
|
394
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
395
|
-
const { loadStoredItem } = require('../server/storage') as typeof import('@/lib/server/storage')
|
|
396
390
|
const directConfig = loadStoredItem('provider_configs', id) as CustomProviderConfig | null
|
|
397
391
|
if (directConfig?.type === 'custom' && directConfig.isEnabled) {
|
|
398
392
|
log.info(TAG, `Resolved custom provider '${id}' via direct DB lookup (batch load missed it)`)
|
|
@@ -405,10 +399,8 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
|
|
|
405
399
|
|
|
406
400
|
// Check Extension Providers
|
|
407
401
|
try {
|
|
408
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
409
|
-
const { getExtensionManager } = require('../server/extensions')
|
|
410
402
|
const extensionProviders = getExtensionManager().getProviders()
|
|
411
|
-
const found = extensionProviders.find((p
|
|
403
|
+
const found = extensionProviders.find((p) => p.id === id)
|
|
412
404
|
if (found) {
|
|
413
405
|
return {
|
|
414
406
|
id: found.id as ProviderId,
|
|
@@ -455,8 +447,6 @@ export async function streamChatWithFailover(
|
|
|
455
447
|
let apiKey: string | null = opts.apiKey || null
|
|
456
448
|
if (credId && i > 0) {
|
|
457
449
|
// Need to decrypt fallback credential
|
|
458
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
459
|
-
const { loadCredentials, decryptKey } = require('../server/storage') as typeof import('@/lib/server/storage')
|
|
460
450
|
const creds = loadCredentials()
|
|
461
451
|
const cred = creds[credId]
|
|
462
452
|
if (cred?.encryptedKey) {
|
|
@@ -8,6 +8,7 @@ import type { Agent } from '@/types'
|
|
|
8
8
|
import { deriveOpenClawWsUrl } from '@/lib/openclaw/openclaw-endpoint'
|
|
9
9
|
import { normalizeOpenClawAgentId } from '@/lib/openclaw/openclaw-agent-id'
|
|
10
10
|
import { loadAgents } from '../server/storage'
|
|
11
|
+
import { getSharedDeviceToken } from '../server/openclaw/sync'
|
|
11
12
|
import {
|
|
12
13
|
resolveOpenClawGatewayAgentIdFromList,
|
|
13
14
|
type OpenClawGatewayAgentSummary,
|
|
@@ -74,8 +75,6 @@ function tryLoadIdentityFile(filePath: string): DeviceIdentity | null {
|
|
|
74
75
|
function loadOrCreateDeviceIdentity(): DeviceIdentity {
|
|
75
76
|
// 0. Check shared device token for cross-synced identity
|
|
76
77
|
try {
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
78
|
-
const { getSharedDeviceToken } = require('../server/openclaw/sync')
|
|
79
78
|
const sharedToken = getSharedDeviceToken()
|
|
80
79
|
if (sharedToken) {
|
|
81
80
|
// Shared token exists — the connector has already paired.
|
|
@@ -17,6 +17,7 @@ import { logActivity } from '@/lib/server/activity/activity-log'
|
|
|
17
17
|
import { createNotification } from '@/lib/server/create-notification'
|
|
18
18
|
import { notify } from '@/lib/server/ws-hub'
|
|
19
19
|
import { loadAgents } from '@/lib/server/agents/agent-repository'
|
|
20
|
+
import { upsertStoredItem, loadStoredItem, loadCollection } from '../storage'
|
|
20
21
|
import {
|
|
21
22
|
spawnSubagent,
|
|
22
23
|
type SubagentContext,
|
|
@@ -159,8 +160,6 @@ function notifySwarmChanged() {
|
|
|
159
160
|
|
|
160
161
|
function persistSwarmSnapshot(swarm: SwarmHandle): void {
|
|
161
162
|
try {
|
|
162
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
163
|
-
const { upsertStoredItem } = require('../storage')
|
|
164
163
|
upsertStoredItem('swarm_snapshots', swarm.swarmId, {
|
|
165
164
|
swarmId: swarm.swarmId,
|
|
166
165
|
parentSessionId: swarm.parentSessionId,
|
|
@@ -549,8 +548,6 @@ export function getSwarmSnapshot(swarmId: string): SwarmSnapshot | null {
|
|
|
549
548
|
if (swarm) return buildSwarmSnapshot(swarm)
|
|
550
549
|
// Fallback to persisted store for swarms from previous process lifetimes
|
|
551
550
|
try {
|
|
552
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
553
|
-
const { loadStoredItem } = require('../storage')
|
|
554
551
|
const persisted = loadStoredItem('swarm_snapshots', swarmId)
|
|
555
552
|
return persisted ? (persisted as SwarmSnapshot) : null
|
|
556
553
|
} catch { return null }
|
|
@@ -640,9 +637,7 @@ function buildSwarmSnapshot(swarm: SwarmHandle): SwarmSnapshot {
|
|
|
640
637
|
*/
|
|
641
638
|
export function restoreSwarmRegistry(): number {
|
|
642
639
|
try {
|
|
643
|
-
|
|
644
|
-
const { loadCollection, upsertStoredItem } = require('../storage')
|
|
645
|
-
const persisted = loadCollection('swarm_snapshots') as Record<string, SwarmSnapshot>
|
|
640
|
+
const persisted = loadCollection('swarm_snapshots') as unknown as Record<string, SwarmSnapshot>
|
|
646
641
|
let lost = 0
|
|
647
642
|
for (const [id, record] of Object.entries(persisted)) {
|
|
648
643
|
if (swarmRegistry.has(id)) continue
|
|
@@ -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
|
|
|
@@ -0,0 +1,127 @@
|
|
|
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 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
|
+
}
|
|
35
|
+
|
|
36
|
+
test('executeSessionChatTurn persists citations and retrieval traces on grounded assistant messages', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('@/lib/server/storage')
|
|
39
|
+
const providersMod = await import('@/lib/providers/index')
|
|
40
|
+
const threadMod = await import('@/lib/server/agents/agent-thread-session')
|
|
41
|
+
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
42
|
+
const messageRepoMod = await import('@/lib/server/messages/message-repository')
|
|
43
|
+
const knowledgeMod = await import('@/lib/server/knowledge-sources')
|
|
44
|
+
|
|
45
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
46
|
+
const ensureAgentThreadSession = threadMod.ensureAgentThreadSession
|
|
47
|
+
|| threadMod.default?.ensureAgentThreadSession
|
|
48
|
+
|| threadMod['module.exports']?.ensureAgentThreadSession
|
|
49
|
+
const executeSessionChatTurn = execMod.executeSessionChatTurn
|
|
50
|
+
|| execMod.default?.executeSessionChatTurn
|
|
51
|
+
|| execMod['module.exports']?.executeSessionChatTurn
|
|
52
|
+
const getMessages = messageRepoMod.getMessages
|
|
53
|
+
|| messageRepoMod.default?.getMessages
|
|
54
|
+
|| messageRepoMod['module.exports']?.getMessages
|
|
55
|
+
const knowledge = knowledgeMod.default || knowledgeMod
|
|
56
|
+
const providers = providersMod.PROVIDERS
|
|
57
|
+
|| providersMod.default?.PROVIDERS
|
|
58
|
+
|| providersMod['module.exports']?.PROVIDERS
|
|
59
|
+
|
|
60
|
+
providers['test-provider'] = {
|
|
61
|
+
id: 'test-provider',
|
|
62
|
+
name: 'Test Provider',
|
|
63
|
+
models: ['unit'],
|
|
64
|
+
requiresApiKey: false,
|
|
65
|
+
requiresEndpoint: false,
|
|
66
|
+
handler: {
|
|
67
|
+
async streamChat() {
|
|
68
|
+
return 'Use blue green deployment for the gateway migration so rollback stays simple.'
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const now = Date.now()
|
|
74
|
+
storage.saveAgents({
|
|
75
|
+
molly: {
|
|
76
|
+
id: 'molly',
|
|
77
|
+
name: 'Molly',
|
|
78
|
+
description: 'Grounding test',
|
|
79
|
+
provider: 'test-provider',
|
|
80
|
+
model: 'unit',
|
|
81
|
+
credentialId: null,
|
|
82
|
+
apiEndpoint: null,
|
|
83
|
+
fallbackCredentialIds: [],
|
|
84
|
+
disabled: false,
|
|
85
|
+
proactiveMemory: true,
|
|
86
|
+
extensions: ['memory'],
|
|
87
|
+
createdAt: now,
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await knowledge.createKnowledgeSource({
|
|
93
|
+
kind: 'manual',
|
|
94
|
+
title: 'Gateway Migration Runbook',
|
|
95
|
+
content: 'Use blue green deployment for gateway migrations so rollback stays simple and downtime stays low.',
|
|
96
|
+
tags: ['deploy'],
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const session = ensureAgentThreadSession('molly')
|
|
100
|
+
const result = await executeSessionChatTurn({
|
|
101
|
+
sessionId: session.id,
|
|
102
|
+
message: 'gateway blue green rollback',
|
|
103
|
+
runId: 'run-grounding-chat',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const messages = getMessages(session.id)
|
|
107
|
+
const lastMessage = messages[messages.length - 1]
|
|
108
|
+
|
|
109
|
+
console.log(JSON.stringify({
|
|
110
|
+
persisted: result.persisted || false,
|
|
111
|
+
resultCitationCount: Array.isArray(result.citations) ? result.citations.length : 0,
|
|
112
|
+
resultSelectorStatus: result.retrievalTrace?.selectorStatus || null,
|
|
113
|
+
messageCitationCount: Array.isArray(lastMessage?.citations) ? lastMessage.citations.length : 0,
|
|
114
|
+
messageTraceHitCount: Array.isArray(lastMessage?.retrievalTrace?.hits) ? lastMessage.retrievalTrace.hits.length : 0,
|
|
115
|
+
messageSelectorStatus: lastMessage?.retrievalTrace?.selectorStatus || null,
|
|
116
|
+
messageSourceTitle: lastMessage?.citations?.[0]?.sourceTitle || null,
|
|
117
|
+
}))
|
|
118
|
+
`)
|
|
119
|
+
|
|
120
|
+
assert.equal(output.persisted, true)
|
|
121
|
+
assert.equal(output.resultCitationCount >= 1, true)
|
|
122
|
+
assert.equal(output.resultSelectorStatus, 'selected')
|
|
123
|
+
assert.equal(output.messageCitationCount >= 1, true)
|
|
124
|
+
assert.equal(output.messageTraceHitCount >= 1, true)
|
|
125
|
+
assert.equal(output.messageSelectorStatus, 'selected')
|
|
126
|
+
assert.equal(output.messageSourceTitle, 'Gateway Migration Runbook')
|
|
127
|
+
})
|
|
@@ -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,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
KnowledgeRetrievalTrace,
|
|
3
|
+
Message,
|
|
4
|
+
MessageToolEvent,
|
|
5
|
+
SSEEvent,
|
|
6
|
+
Session,
|
|
7
|
+
UsageRecord,
|
|
8
|
+
} from '@/types'
|
|
9
|
+
import { sendConnectorMessage } from '../connectors/manager'
|
|
2
10
|
import { applyExactOutputContract, classifyExactOutputContract, type ExactOutputContract } from '@/lib/server/chat-execution/exact-output-contract'
|
|
3
11
|
import { stripMainLoopMetaForPersistence } from '@/lib/server/agents/main-agent-loop'
|
|
4
12
|
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
|
|
@@ -42,6 +50,7 @@ import {
|
|
|
42
50
|
import { appendUsage } from '@/lib/server/usage/usage-repository'
|
|
43
51
|
import { synchronizeWorkingStateForTurn } from '@/lib/server/working-state/service'
|
|
44
52
|
import { notify } from '@/lib/server/ws-hub'
|
|
53
|
+
import { selectKnowledgeCitations } from '@/lib/server/knowledge-sources'
|
|
45
54
|
|
|
46
55
|
import type { ExecuteChatTurnInput, ExecuteChatTurnResult } from './chat-execution-types'
|
|
47
56
|
import type { PartialAssistantPersistence } from '@/lib/server/chat-execution/chat-turn-partial-persistence'
|
|
@@ -153,6 +162,7 @@ export async function finalizeChatTurn(params: {
|
|
|
153
162
|
received: boolean
|
|
154
163
|
}
|
|
155
164
|
durationMs: number
|
|
165
|
+
knowledgeRetrievalTrace?: KnowledgeRetrievalTrace | null
|
|
156
166
|
emit: (event: SSEEvent) => void
|
|
157
167
|
}): Promise<ExecuteChatTurnResult> {
|
|
158
168
|
const {
|
|
@@ -163,6 +173,7 @@ export async function finalizeChatTurn(params: {
|
|
|
163
173
|
responseCacheHit,
|
|
164
174
|
directUsage,
|
|
165
175
|
durationMs,
|
|
176
|
+
knowledgeRetrievalTrace,
|
|
166
177
|
emit,
|
|
167
178
|
} = params
|
|
168
179
|
let { fullResponse, errorMessage } = params
|
|
@@ -312,6 +323,12 @@ export async function finalizeChatTurn(params: {
|
|
|
312
323
|
const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
|
|
313
324
|
const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
|
|
314
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
|
+
})
|
|
315
332
|
let persistedResponseForHooks = textForPersistence
|
|
316
333
|
|
|
317
334
|
if (isHeartbeatRun && rawTextForPersistence) {
|
|
@@ -412,6 +429,8 @@ export async function finalizeChatTurn(params: {
|
|
|
412
429
|
thinking: thinkingText || undefined,
|
|
413
430
|
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
414
431
|
kind: persistedKind,
|
|
432
|
+
citations: grounding.citations.length > 0 ? grounding.citations : undefined,
|
|
433
|
+
retrievalTrace: grounding.retrievalTrace || undefined,
|
|
415
434
|
},
|
|
416
435
|
enabledIds: extensionsForRun,
|
|
417
436
|
phase: isHeartbeatRun ? 'heartbeat' : 'assistant_final',
|
|
@@ -478,8 +497,6 @@ export async function finalizeChatTurn(params: {
|
|
|
478
497
|
&& heartbeatConfig.target !== 'none'
|
|
479
498
|
) {
|
|
480
499
|
try {
|
|
481
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
482
|
-
const { sendConnectorMessage } = require('../connectors/manager')
|
|
483
500
|
let connectorId: string | undefined
|
|
484
501
|
let channelId: string | undefined
|
|
485
502
|
if (heartbeatConfig.target === 'last') {
|
|
@@ -527,9 +544,7 @@ export async function finalizeChatTurn(params: {
|
|
|
527
544
|
: ''
|
|
528
545
|
if (!recentInbound && channelId) {
|
|
529
546
|
try {
|
|
530
|
-
|
|
531
|
-
const { sendConnectorMessage: sendMsg } = require('../connectors/manager')
|
|
532
|
-
sendMsg({ connectorId: connectorId || undefined, channelId, text: nextAssistantMessage.text }).catch((err: unknown) => {
|
|
547
|
+
sendConnectorMessage({ connectorId: connectorId || undefined, channelId, text: nextAssistantMessage.text }).catch((err: unknown) => {
|
|
533
548
|
log.warn('connector', 'Auto-route connector delivery failed', {
|
|
534
549
|
connectorId,
|
|
535
550
|
channelId,
|
|
@@ -646,5 +661,7 @@ export async function finalizeChatTurn(params: {
|
|
|
646
661
|
inputTokens: accumulatedUsage.inputTokens || undefined,
|
|
647
662
|
outputTokens: accumulatedUsage.outputTokens || undefined,
|
|
648
663
|
estimatedCost: accumulatedUsage.estimatedCost || undefined,
|
|
664
|
+
citations: grounding.citations.length > 0 ? grounding.citations : undefined,
|
|
665
|
+
retrievalTrace: grounding.retrievalTrace || undefined,
|
|
649
666
|
}
|
|
650
667
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CONTEXT_OVERFLOW_RE } from '@/lib/providers/error-classification'
|
|
2
|
-
import type { ProviderType } from '@/types'
|
|
2
|
+
import type { KnowledgeRetrievalTrace, ProviderType } from '@/types'
|
|
3
3
|
import { getEnabledCapabilityIds } from '@/lib/capability-selection'
|
|
4
4
|
import { isLocalOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
5
5
|
import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
|
|
@@ -42,6 +42,7 @@ export interface ExecutedPreparedChatTurn {
|
|
|
42
42
|
outputTokens: number
|
|
43
43
|
received: boolean
|
|
44
44
|
}
|
|
45
|
+
knowledgeRetrievalTrace?: KnowledgeRetrievalTrace | null
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export async function executePreparedChatTurn(params: {
|
|
@@ -90,6 +91,7 @@ export async function executePreparedChatTurn(params: {
|
|
|
90
91
|
let responseCacheHit = false
|
|
91
92
|
let responseCacheInput: LlmResponseCacheKeyInput | null = null
|
|
92
93
|
let durationMs = 0
|
|
94
|
+
let knowledgeRetrievalTrace: KnowledgeRetrievalTrace | null = null
|
|
93
95
|
const startTs = Date.now()
|
|
94
96
|
const endLlmPerf = perf.start('chat-execution', 'llm-round-trip', {
|
|
95
97
|
sessionId,
|
|
@@ -111,6 +113,7 @@ export async function executePreparedChatTurn(params: {
|
|
|
111
113
|
responseCacheHit,
|
|
112
114
|
durationMs,
|
|
113
115
|
directUsage,
|
|
116
|
+
knowledgeRetrievalTrace: null,
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -157,6 +160,7 @@ export async function executePreparedChatTurn(params: {
|
|
|
157
160
|
promptMode,
|
|
158
161
|
})
|
|
159
162
|
fullResponse = result.finalResponse || result.fullText
|
|
163
|
+
knowledgeRetrievalTrace = result.knowledgeRetrievalTrace || null
|
|
160
164
|
} else {
|
|
161
165
|
let directHistorySnapshot = isAutoRunNoHistory
|
|
162
166
|
? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
|
|
@@ -298,5 +302,6 @@ export async function executePreparedChatTurn(params: {
|
|
|
298
302
|
responseCacheHit,
|
|
299
303
|
durationMs,
|
|
300
304
|
directUsage,
|
|
305
|
+
knowledgeRetrievalTrace,
|
|
301
306
|
}
|
|
302
307
|
}
|