@swarmclawai/swarmclaw 1.9.31 → 1.9.32
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 +21 -3
- package/package.json +2 -2
- package/src/lib/autonomy/supervisor-settings.test.ts +26 -0
- package/src/lib/autonomy/supervisor-settings.ts +27 -0
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +206 -0
- package/src/lib/server/autonomy/supervisor-reflection.ts +54 -4
- package/src/lib/server/chat-execution/compaction-generation-preference.test.ts +24 -0
- package/src/lib/server/chat-execution/compaction-generation-preference.ts +25 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +25 -3
- package/src/lib/server/memory/dream-generation-preference.ts +30 -2
- package/src/lib/server/memory/dream-service.test.ts +19 -0
- package/src/lib/server/memory/dream-service.ts +3 -1
- package/src/lib/server/memory/memory-consolidation.test.ts +31 -0
- package/src/lib/server/memory/memory-consolidation.ts +3 -2
- package/src/lib/server/memory/memory-db.ts +24 -0
- package/src/types/app-settings.ts +14 -0
- package/src/types/dream.ts +8 -0
package/README.md
CHANGED
|
@@ -146,10 +146,19 @@ Then open `http://localhost:3456`.
|
|
|
146
146
|
Install the SwarmClaw skill for your [OpenClaw](https://openclaw.ai) agents:
|
|
147
147
|
|
|
148
148
|
```bash
|
|
149
|
-
|
|
149
|
+
openclaw skills install swarmclaw
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
[Browse on ClawHub](https://clawhub.ai/
|
|
152
|
+
[Browse on ClawHub](https://clawhub.ai/waydelyle/swarmclaw)
|
|
153
|
+
|
|
154
|
+
## v1.9.32 Highlights
|
|
155
|
+
|
|
156
|
+
PR integration release for background model routing, reflection memory controls, and current ClawHub install guidance.
|
|
157
|
+
|
|
158
|
+
- **Background model routing.** Per-agent `dreamConfig` overrides can route dream cycles and daily digests before global dream settings, while `compactionProvider` settings can route live auto-compaction summaries through a cheaper or faster model.
|
|
159
|
+
- **Reflection memory controls.** `reflectionMinQuality` gates automatic reflection memory writes without dropping the reflection record, and optional embedding dedup skips near-duplicate reflection notes when embeddings are configured.
|
|
160
|
+
- **ClawHub install guidance.** OpenClaw skill docs now use `openclaw skills install swarmclaw` and current owner-scoped ClawHub links.
|
|
161
|
+
- **Regression coverage.** Added tests for dream override precedence, compaction preference resolution, reflection quality gating, and embedding-based reflection dedup.
|
|
153
162
|
|
|
154
163
|
## v1.9.31 Highlights
|
|
155
164
|
|
|
@@ -403,7 +412,7 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
|
|
|
403
412
|
- **Per-agent opt-in**: enable SwarmFeed on any agent with automatic Ed25519 registration
|
|
404
413
|
- **Richer in-app surface**: feed tabs for For You, Following, Trending, Bookmarks, and Notifications, plus thread detail, profile sheets, suggested follows, and search
|
|
405
414
|
- **Heartbeat integration**: agents can auto-post, auto-reply to mentions, auto-follow with guardrails, and publish task-completion updates during heartbeat cycles
|
|
406
|
-
- **Multiple access methods**: [SDK](https://www.npmjs.com/package/@swarmfeed/sdk), [CLI](https://www.npmjs.com/package/@swarmfeed/cli), [MCP Server](https://www.npmjs.com/package/@swarmfeed/mcp-server), and [ClawHub skill](https://clawhub.ai/
|
|
415
|
+
- **Multiple access methods**: [SDK](https://www.npmjs.com/package/@swarmfeed/sdk), [CLI](https://www.npmjs.com/package/@swarmfeed/cli), [MCP Server](https://www.npmjs.com/package/@swarmfeed/mcp-server), and [ClawHub skill](https://clawhub.ai/waydelyle/swarmfeed)
|
|
407
416
|
|
|
408
417
|
Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
|
|
409
418
|
|
|
@@ -426,6 +435,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
426
435
|
|
|
427
436
|
## Releases
|
|
428
437
|
|
|
438
|
+
### v1.9.32 Highlights
|
|
439
|
+
|
|
440
|
+
PR integration release for background model routing, reflection memory controls, and current ClawHub install guidance.
|
|
441
|
+
|
|
442
|
+
- **Background model routing.** Per-agent `dreamConfig` overrides can route dream cycles and daily digests before global dream settings, while `compactionProvider` settings can route live auto-compaction summaries through a cheaper or faster model.
|
|
443
|
+
- **Reflection memory controls.** `reflectionMinQuality` gates automatic reflection memory writes without dropping the reflection record, and optional embedding dedup skips near-duplicate reflection notes when embeddings are configured.
|
|
444
|
+
- **ClawHub install guidance.** OpenClaw skill docs now use `openclaw skills install swarmclaw` and current owner-scoped ClawHub links.
|
|
445
|
+
- **Regression coverage.** Added tests for dream override precedence, compaction preference resolution, reflection quality gating, and embedding-based reflection dedup.
|
|
446
|
+
|
|
429
447
|
### v1.9.31 Highlights
|
|
430
448
|
|
|
431
449
|
Documentation cleanup release for public release notes and OpenClaw guidance. No runtime behavior changed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.32",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
90
90
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/autonomy/supervisor-settings.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/autonomy/supervisor-reflection.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/memory/memory-consolidation.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/compaction-generation-preference.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { normalizeSupervisorSettings } from './supervisor-settings'
|
|
4
|
+
|
|
5
|
+
describe('normalizeSupervisorSettings', () => {
|
|
6
|
+
it('preserves and clamps reflection memory quality settings', () => {
|
|
7
|
+
assert.deepEqual(
|
|
8
|
+
{
|
|
9
|
+
minQuality: normalizeSupervisorSettings({ reflectionMinQuality: '0.72' }).reflectionMinQuality,
|
|
10
|
+
minQualityHigh: normalizeSupervisorSettings({ reflectionMinQuality: 2 }).reflectionMinQuality,
|
|
11
|
+
minQualityLow: normalizeSupervisorSettings({ reflectionMinQuality: -1 }).reflectionMinQuality,
|
|
12
|
+
semanticEnabled: normalizeSupervisorSettings({ reflectionSemanticDedupEnabled: 'on' }).reflectionSemanticDedupEnabled,
|
|
13
|
+
semanticThreshold: normalizeSupervisorSettings({ reflectionSemanticDedupThreshold: '0.91' }).reflectionSemanticDedupThreshold,
|
|
14
|
+
semanticThresholdHigh: normalizeSupervisorSettings({ reflectionSemanticDedupThreshold: 1.5 }).reflectionSemanticDedupThreshold,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
minQuality: 0.72,
|
|
18
|
+
minQualityHigh: 1,
|
|
19
|
+
minQualityLow: 0,
|
|
20
|
+
semanticEnabled: true,
|
|
21
|
+
semanticThreshold: 0.91,
|
|
22
|
+
semanticThresholdHigh: 1,
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -8,6 +8,9 @@ export const DEFAULT_SUPERVISOR_NO_PROGRESS_LIMIT = 2
|
|
|
8
8
|
export const DEFAULT_SUPERVISOR_REPEATED_TOOL_LIMIT = 3
|
|
9
9
|
export const DEFAULT_REFLECTION_ENABLED = true
|
|
10
10
|
export const DEFAULT_REFLECTION_AUTO_WRITE_MEMORY = true
|
|
11
|
+
export const DEFAULT_REFLECTION_MIN_QUALITY = 0
|
|
12
|
+
export const DEFAULT_REFLECTION_SEMANTIC_DEDUP_ENABLED = false
|
|
13
|
+
export const DEFAULT_REFLECTION_SEMANTIC_DEDUP_THRESHOLD = 0.88
|
|
11
14
|
|
|
12
15
|
export const SUPERVISOR_NO_PROGRESS_LIMIT_MIN = 1
|
|
13
16
|
export const SUPERVISOR_NO_PROGRESS_LIMIT_MAX = 8
|
|
@@ -24,6 +27,16 @@ function parseIntSetting(value: unknown, fallback: number, min: number, max: num
|
|
|
24
27
|
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
function parseNumberSetting(value: unknown, fallback: number, min: number, max: number): number {
|
|
31
|
+
const parsed = typeof value === 'number'
|
|
32
|
+
? value
|
|
33
|
+
: typeof value === 'string'
|
|
34
|
+
? Number.parseFloat(value)
|
|
35
|
+
: Number.NaN
|
|
36
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
37
|
+
return Math.max(min, Math.min(max, parsed))
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
function parseBoolSetting(value: unknown, fallback: boolean): boolean {
|
|
28
41
|
if (typeof value === 'boolean') return value
|
|
29
42
|
if (typeof value === 'string') {
|
|
@@ -41,6 +54,9 @@ export interface NormalizedSupervisorSettings {
|
|
|
41
54
|
supervisorRepeatedToolLimit: number
|
|
42
55
|
reflectionEnabled: boolean
|
|
43
56
|
reflectionAutoWriteMemory: boolean
|
|
57
|
+
reflectionMinQuality: number
|
|
58
|
+
reflectionSemanticDedupEnabled: boolean
|
|
59
|
+
reflectionSemanticDedupThreshold: number
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
export function normalizeSupervisorSettings(
|
|
@@ -69,6 +85,17 @@ export function normalizeSupervisorSettings(
|
|
|
69
85
|
),
|
|
70
86
|
reflectionEnabled: parseBoolSetting(current.reflectionEnabled, DEFAULT_REFLECTION_ENABLED),
|
|
71
87
|
reflectionAutoWriteMemory: parseBoolSetting(current.reflectionAutoWriteMemory, DEFAULT_REFLECTION_AUTO_WRITE_MEMORY),
|
|
88
|
+
reflectionMinQuality: parseNumberSetting(current.reflectionMinQuality, DEFAULT_REFLECTION_MIN_QUALITY, 0, 1),
|
|
89
|
+
reflectionSemanticDedupEnabled: parseBoolSetting(
|
|
90
|
+
current.reflectionSemanticDedupEnabled,
|
|
91
|
+
DEFAULT_REFLECTION_SEMANTIC_DEDUP_ENABLED,
|
|
92
|
+
),
|
|
93
|
+
reflectionSemanticDedupThreshold: parseNumberSetting(
|
|
94
|
+
current.reflectionSemanticDedupThreshold,
|
|
95
|
+
DEFAULT_REFLECTION_SEMANTIC_DEDUP_THRESHOLD,
|
|
96
|
+
0,
|
|
97
|
+
1,
|
|
98
|
+
),
|
|
72
99
|
}
|
|
73
100
|
}
|
|
74
101
|
|
|
@@ -202,6 +202,212 @@ describe('supervisor-reflection', () => {
|
|
|
202
202
|
])
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
+
it('persists low-quality reflections while skipping auto-written memory', () => {
|
|
206
|
+
const output = runWithTempDataDir(`
|
|
207
|
+
const storageMod = await import('@/lib/server/storage')
|
|
208
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
209
|
+
const reflectionMod = await import('@/lib/server/autonomy/supervisor-reflection')
|
|
210
|
+
const mod = reflectionMod.default || reflectionMod['module.exports'] || reflectionMod
|
|
211
|
+
const memoryDbMod = await import('@/lib/server/memory/memory-db')
|
|
212
|
+
const memoryMod = memoryDbMod.default || memoryDbMod['module.exports'] || memoryDbMod
|
|
213
|
+
|
|
214
|
+
storage.saveAgents({
|
|
215
|
+
'agent-a': {
|
|
216
|
+
id: 'agent-a',
|
|
217
|
+
name: 'Agent A',
|
|
218
|
+
provider: 'openai',
|
|
219
|
+
model: 'gpt-test',
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
storage.saveSessions({
|
|
224
|
+
s1: {
|
|
225
|
+
id: 's1',
|
|
226
|
+
name: 'Autonomy Session',
|
|
227
|
+
cwd: process.cwd(),
|
|
228
|
+
user: 'tester',
|
|
229
|
+
provider: 'openai',
|
|
230
|
+
model: 'gpt-test',
|
|
231
|
+
claudeSessionId: null,
|
|
232
|
+
messages: [
|
|
233
|
+
{ role: 'user', text: 'Repair the deployment workflow and keep notes for later.', time: 1 },
|
|
234
|
+
{ role: 'assistant', text: 'I retried the same shell path and nothing changed.', time: 2 },
|
|
235
|
+
],
|
|
236
|
+
createdAt: 1,
|
|
237
|
+
lastActiveAt: 2,
|
|
238
|
+
sessionType: 'human',
|
|
239
|
+
agentId: 'agent-a',
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
storage.saveSettings({
|
|
244
|
+
supervisorEnabled: true,
|
|
245
|
+
supervisorRuntimeScope: 'both',
|
|
246
|
+
supervisorNoProgressLimit: 2,
|
|
247
|
+
supervisorRepeatedToolLimit: 3,
|
|
248
|
+
reflectionEnabled: true,
|
|
249
|
+
reflectionAutoWriteMemory: true,
|
|
250
|
+
reflectionMinQuality: 0.8,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const result = await mod.observeAutonomyRunOutcome({
|
|
254
|
+
runId: 'run-low-quality',
|
|
255
|
+
sessionId: 's1',
|
|
256
|
+
agentId: 'agent-a',
|
|
257
|
+
source: 'chat',
|
|
258
|
+
status: 'completed',
|
|
259
|
+
resultText: 'I retried the same shell path and nothing changed.',
|
|
260
|
+
toolEvents: [
|
|
261
|
+
{ name: 'shell', input: '{"cmd":"npm test"}' },
|
|
262
|
+
{ name: 'shell', input: '{"cmd":"npm test"}' },
|
|
263
|
+
{ name: 'shell', input: '{"cmd":"npm test"}' },
|
|
264
|
+
],
|
|
265
|
+
mainLoopState: {
|
|
266
|
+
followupChainCount: 2,
|
|
267
|
+
summary: 'I retried the same shell path and nothing changed.',
|
|
268
|
+
},
|
|
269
|
+
sourceMessage: 'Repair the deployment workflow and keep notes for later.',
|
|
270
|
+
}, {
|
|
271
|
+
generateText: async () => JSON.stringify({
|
|
272
|
+
summary: 'Low quality reflection',
|
|
273
|
+
lessons: ['This weak note should not be written to memory.'],
|
|
274
|
+
quality_score: 0.25,
|
|
275
|
+
quality_reasoning: 'Too generic to keep as durable memory.',
|
|
276
|
+
}),
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const memories = memoryMod.getMemoryDb().list(undefined, 50)
|
|
280
|
+
.filter((entry) => entry.metadata && entry.metadata.origin === 'autonomy-reflection')
|
|
281
|
+
|
|
282
|
+
console.log(JSON.stringify({
|
|
283
|
+
reflectionSummary: result.reflection?.summary ?? null,
|
|
284
|
+
qualityScore: result.reflection?.qualityScore ?? null,
|
|
285
|
+
autoMemoryCount: result.reflection?.autoMemoryIds?.length ?? 0,
|
|
286
|
+
storedReflectionMemoryCount: memories.length,
|
|
287
|
+
}))
|
|
288
|
+
`)
|
|
289
|
+
|
|
290
|
+
assert.equal(output.reflectionSummary, 'Low quality reflection')
|
|
291
|
+
assert.equal(output.qualityScore, 0.25)
|
|
292
|
+
assert.equal(output.autoMemoryCount, 0)
|
|
293
|
+
assert.equal(output.storedReflectionMemoryCount, 0)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('skips semantically duplicate reflection memory when embedding dedup is enabled', () => {
|
|
297
|
+
const output = runWithTempDataDir(`
|
|
298
|
+
const http = await import('node:http')
|
|
299
|
+
const path = await import('node:path')
|
|
300
|
+
const Database = (await import('better-sqlite3')).default
|
|
301
|
+
const storageMod = await import('@/lib/server/storage')
|
|
302
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
303
|
+
const reflectionMod = await import('@/lib/server/autonomy/supervisor-reflection')
|
|
304
|
+
const mod = reflectionMod.default || reflectionMod['module.exports'] || reflectionMod
|
|
305
|
+
const memoryDbMod = await import('@/lib/server/memory/memory-db')
|
|
306
|
+
const memoryMod = memoryDbMod.default || memoryDbMod['module.exports'] || memoryDbMod
|
|
307
|
+
const settingsRepositoryMod = await import('@/lib/server/settings/settings-repository')
|
|
308
|
+
const settingsRepository = settingsRepositoryMod.default || settingsRepositoryMod['module.exports'] || settingsRepositoryMod
|
|
309
|
+
|
|
310
|
+
const server = http.createServer((req, res) => {
|
|
311
|
+
res.setHeader('content-type', 'application/json')
|
|
312
|
+
res.end(JSON.stringify({ embedding: [1, 0] }))
|
|
313
|
+
})
|
|
314
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
315
|
+
const endpoint = 'http://127.0.0.1:' + server.address().port
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
storage.saveAgents({
|
|
319
|
+
'agent-a': {
|
|
320
|
+
id: 'agent-a',
|
|
321
|
+
name: 'Agent A',
|
|
322
|
+
provider: 'openai',
|
|
323
|
+
model: 'gpt-test',
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
storage.saveSessions({
|
|
328
|
+
s1: {
|
|
329
|
+
id: 's1',
|
|
330
|
+
name: 'Semantic Dedup Session',
|
|
331
|
+
cwd: process.cwd(),
|
|
332
|
+
user: 'tester',
|
|
333
|
+
provider: 'openai',
|
|
334
|
+
model: 'gpt-test',
|
|
335
|
+
claudeSessionId: null,
|
|
336
|
+
messages: [
|
|
337
|
+
{ role: 'user', text: 'Release carefully and keep the durable lesson.', time: 1 },
|
|
338
|
+
{ role: 'assistant', text: 'I checked the release gates before shipping.', time: 2 },
|
|
339
|
+
],
|
|
340
|
+
createdAt: 1,
|
|
341
|
+
lastActiveAt: 2,
|
|
342
|
+
sessionType: 'human',
|
|
343
|
+
agentId: 'agent-a',
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const memDb = memoryMod.getMemoryDb()
|
|
348
|
+
const existing = memDb.add({
|
|
349
|
+
agentId: 'agent-a',
|
|
350
|
+
sessionId: 's1',
|
|
351
|
+
category: 'reflection/lesson',
|
|
352
|
+
title: 'Reflection Lesson',
|
|
353
|
+
content: 'Always verify release gates before shipping.',
|
|
354
|
+
metadata: { origin: 'autonomy-reflection' },
|
|
355
|
+
})
|
|
356
|
+
const rawDb = new Database(path.join(process.env.DATA_DIR, 'memory.db'))
|
|
357
|
+
rawDb.prepare('UPDATE memories SET embedding = ? WHERE id = ?').run(Buffer.from(new Float32Array([1, 0]).buffer), existing.id)
|
|
358
|
+
rawDb.close()
|
|
359
|
+
|
|
360
|
+
settingsRepository.saveSettings({
|
|
361
|
+
supervisorEnabled: true,
|
|
362
|
+
supervisorRuntimeScope: 'both',
|
|
363
|
+
supervisorNoProgressLimit: 2,
|
|
364
|
+
supervisorRepeatedToolLimit: 3,
|
|
365
|
+
reflectionEnabled: true,
|
|
366
|
+
reflectionAutoWriteMemory: true,
|
|
367
|
+
reflectionSemanticDedupEnabled: true,
|
|
368
|
+
reflectionSemanticDedupThreshold: 0.9,
|
|
369
|
+
embeddingProvider: 'ollama',
|
|
370
|
+
embeddingModel: 'test-embedding',
|
|
371
|
+
embeddingEndpoint: endpoint,
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const result = await mod.observeAutonomyRunOutcome({
|
|
375
|
+
runId: 'run-semantic-dedup',
|
|
376
|
+
sessionId: 's1',
|
|
377
|
+
agentId: 'agent-a',
|
|
378
|
+
source: 'chat',
|
|
379
|
+
status: 'completed',
|
|
380
|
+
resultText: 'I checked the release gates before shipping.',
|
|
381
|
+
toolEvents: [
|
|
382
|
+
{ name: 'shell', input: '{"cmd":"npm test"}' },
|
|
383
|
+
],
|
|
384
|
+
sourceMessage: 'Release carefully and keep the durable lesson.',
|
|
385
|
+
}, {
|
|
386
|
+
generateText: async () => JSON.stringify({
|
|
387
|
+
summary: 'Release gate reflection',
|
|
388
|
+
lessons: ['Confirm release gates before shipping.'],
|
|
389
|
+
quality_score: 0.95,
|
|
390
|
+
}),
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const reflectionMemories = memDb.list('agent-a', 50)
|
|
394
|
+
.filter((entry) => entry.metadata && entry.metadata.origin === 'autonomy-reflection')
|
|
395
|
+
|
|
396
|
+
console.log(JSON.stringify({
|
|
397
|
+
reflectionSummary: result.reflection?.summary ?? null,
|
|
398
|
+
autoMemoryCount: result.reflection?.autoMemoryIds?.length ?? 0,
|
|
399
|
+
reflectionMemoryContents: reflectionMemories.map((entry) => entry.content).sort(),
|
|
400
|
+
}))
|
|
401
|
+
} finally {
|
|
402
|
+
await new Promise((resolve) => server.close(resolve))
|
|
403
|
+
}
|
|
404
|
+
`)
|
|
405
|
+
|
|
406
|
+
assert.equal(output.reflectionSummary, 'Release gate reflection')
|
|
407
|
+
assert.equal(output.autoMemoryCount, 0)
|
|
408
|
+
assert.deepEqual(output.reflectionMemoryContents, ['Always verify release gates before shipping.'])
|
|
409
|
+
})
|
|
410
|
+
|
|
205
411
|
it('reflects short human chats when they contain durable personal context', () => {
|
|
206
412
|
const output = runWithTempDataDir(`
|
|
207
413
|
const storageMod = await import('@/lib/server/storage')
|
|
@@ -744,7 +744,7 @@ function inferFollowUpAt(note: string, createdAt: number): number {
|
|
|
744
744
|
return createdAt + 7 * 24 * 3600_000
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
-
function writeReflectionMemories(params: {
|
|
747
|
+
async function writeReflectionMemories(params: {
|
|
748
748
|
reflectionId: string
|
|
749
749
|
runId: string
|
|
750
750
|
sessionId: string
|
|
@@ -761,7 +761,7 @@ function writeReflectionMemories(params: {
|
|
|
761
761
|
profile: string[]
|
|
762
762
|
boundaries: string[]
|
|
763
763
|
openLoops: string[]
|
|
764
|
-
}): string[] {
|
|
764
|
+
}): Promise<string[]> {
|
|
765
765
|
const memoryDb = getMemoryDb()
|
|
766
766
|
const memoryIds: string[] = []
|
|
767
767
|
const incidentIds = params.incidents.map((incident) => incident.id)
|
|
@@ -809,11 +809,51 @@ function writeReflectionMemories(params: {
|
|
|
809
809
|
// dedup only rather than blocking the reflection write.
|
|
810
810
|
}
|
|
811
811
|
|
|
812
|
+
// Semantic dedup (opt-in): on top of the text-equality cross-run dedup
|
|
813
|
+
// above, compare each candidate note's embedding against recent reflection
|
|
814
|
+
// memories' embeddings. Catches near-duplicates the LLM re-derives in
|
|
815
|
+
// different words ("Always verify before acting" / "Confirm state first").
|
|
816
|
+
// Falls back gracefully when embeddings aren't configured.
|
|
817
|
+
let reflectionSettings: NormalizedSupervisorSettings | null = null
|
|
818
|
+
try { reflectionSettings = normalizeSupervisorSettings(loadSettings()) } catch { reflectionSettings = null }
|
|
819
|
+
const semanticDedupEnabled = reflectionSettings?.reflectionSemanticDedupEnabled === true
|
|
820
|
+
const semanticDedupThreshold = reflectionSettings?.reflectionSemanticDedupThreshold ?? 0.88
|
|
821
|
+
const semanticSkip = new Set<string>()
|
|
822
|
+
if (semanticDedupEnabled && params.agentId) {
|
|
823
|
+
try {
|
|
824
|
+
const recentEmb = memoryDb.recentReflectionEmbeddings(params.agentId, crossRunDedupCutoff, 500)
|
|
825
|
+
.filter((r) => Array.isArray(r.embedding) && r.embedding.length > 0) as Array<{ id: string; content: string; embedding: number[] }>
|
|
826
|
+
if (recentEmb.length > 0) {
|
|
827
|
+
const { getEmbedding, cosineSimilarity } = await import('@/lib/server/embeddings')
|
|
828
|
+
for (const group of groups) {
|
|
829
|
+
for (const note of group.notes) {
|
|
830
|
+
const trimmed = (note || '').trim()
|
|
831
|
+
if (!trimmed) continue
|
|
832
|
+
const norm = normalizeNote(trimmed)
|
|
833
|
+
if (!norm || seenNormalized.has(norm) || semanticSkip.has(norm)) continue
|
|
834
|
+
const emb = await getEmbedding(trimmed)
|
|
835
|
+
if (!emb) continue
|
|
836
|
+
for (const r of recentEmb) {
|
|
837
|
+
if (cosineSimilarity(emb, r.embedding) >= semanticDedupThreshold) {
|
|
838
|
+
semanticSkip.add(norm)
|
|
839
|
+
break
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
} catch {
|
|
846
|
+
// Best-effort: any failure (embedder offline, DB blip) falls through to
|
|
847
|
+
// the existing text-equality dedup. Never block the write.
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
812
851
|
for (const group of groups) {
|
|
813
852
|
for (const note of group.notes) {
|
|
814
853
|
const norm = normalizeNote(note)
|
|
815
854
|
if (!norm) continue
|
|
816
855
|
if (seenNormalized.has(norm)) continue
|
|
856
|
+
if (semanticSkip.has(norm)) continue
|
|
817
857
|
seenNormalized.add(norm)
|
|
818
858
|
const metadata: Record<string, unknown> = {
|
|
819
859
|
origin: 'autonomy-reflection',
|
|
@@ -1085,8 +1125,18 @@ export async function observeAutonomyRunOutcome(
|
|
|
1085
1125
|
if (parsed.skip) return { incidents, reflection: null }
|
|
1086
1126
|
|
|
1087
1127
|
const reflectionId = genId()
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1128
|
+
const minQuality = typeof settings.reflectionMinQuality === 'number' ? settings.reflectionMinQuality : 0
|
|
1129
|
+
const qualityScore = parsed.qualityScore
|
|
1130
|
+
const qualityGateOpen = minQuality <= 0
|
|
1131
|
+
|| qualityScore == null
|
|
1132
|
+
|| qualityScore >= minQuality
|
|
1133
|
+
if (!qualityGateOpen) {
|
|
1134
|
+
log.info(TAG,
|
|
1135
|
+
`Reflection ${reflectionId} below quality gate (score=${qualityScore?.toFixed(2) ?? 'null'}, threshold=${minQuality.toFixed(2)}); skipping memory writes`,
|
|
1136
|
+
)
|
|
1137
|
+
}
|
|
1138
|
+
const autoMemoryIds = settings.reflectionAutoWriteMemory && qualityGateOpen
|
|
1139
|
+
? await writeReflectionMemories({
|
|
1090
1140
|
reflectionId,
|
|
1091
1141
|
runId: input.runId,
|
|
1092
1142
|
sessionId: input.sessionId,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { resolveCompactionGenerationPreference } from './compaction-generation-preference'
|
|
4
|
+
|
|
5
|
+
describe('resolveCompactionGenerationPreference', () => {
|
|
6
|
+
it('returns no preference when no compaction provider is configured', () => {
|
|
7
|
+
assert.equal(resolveCompactionGenerationPreference({}), undefined)
|
|
8
|
+
assert.equal(resolveCompactionGenerationPreference({ compactionProvider: ' ' }), undefined)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('builds a trimmed compaction model preference from app settings', () => {
|
|
12
|
+
assert.deepEqual(resolveCompactionGenerationPreference({
|
|
13
|
+
compactionProvider: ' ollama ',
|
|
14
|
+
compactionModel: ' llama3.2:3b ',
|
|
15
|
+
compactionCredentialId: ' cred-1 ',
|
|
16
|
+
compactionEndpoint: ' http://localhost:11434 ',
|
|
17
|
+
}), {
|
|
18
|
+
provider: 'ollama',
|
|
19
|
+
model: 'llama3.2:3b',
|
|
20
|
+
credentialId: 'cred-1',
|
|
21
|
+
apiEndpoint: 'http://localhost:11434',
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GenerationModelPreference } from '@/lib/server/build-llm'
|
|
2
|
+
import type { AppSettings } from '@/types'
|
|
3
|
+
|
|
4
|
+
type CompactionGenerationSettings = Pick<AppSettings, 'compactionProvider' | 'compactionModel' | 'compactionCredentialId' | 'compactionEndpoint'> | Record<string, unknown> | null | undefined
|
|
5
|
+
|
|
6
|
+
function optionalSettingString(value: unknown): string | undefined {
|
|
7
|
+
const normalized = typeof value === 'string' ? value.trim() : ''
|
|
8
|
+
return normalized || undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Mirrors resolveDreamGenerationPreference — returns a model preference for
|
|
12
|
+
* the auto-compaction summarizer if app settings opt into a routing override,
|
|
13
|
+
* otherwise undefined (caller falls back to the session's primary model). */
|
|
14
|
+
export function resolveCompactionGenerationPreference(settings: CompactionGenerationSettings): GenerationModelPreference | undefined {
|
|
15
|
+
const record = (settings || {}) as Record<string, unknown>
|
|
16
|
+
const provider = optionalSettingString(record.compactionProvider)
|
|
17
|
+
if (!provider) return undefined
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
provider,
|
|
21
|
+
model: optionalSettingString(record.compactionModel),
|
|
22
|
+
credentialId: optionalSettingString(record.compactionCredentialId),
|
|
23
|
+
apiEndpoint: optionalSettingString(record.compactionEndpoint),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -682,8 +682,29 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
682
682
|
reserveTokens,
|
|
683
683
|
includeToolEvents: false,
|
|
684
684
|
})) {
|
|
685
|
+
// Resolve compaction model: if app settings opt into an override, build
|
|
686
|
+
// a separate LLM for the summarizer (cheap/local model); otherwise reuse
|
|
687
|
+
// the session's primary llm. Mirrors the dream-model override path.
|
|
688
|
+
const { resolveCompactionGenerationPreference } = await import('@/lib/server/chat-execution/compaction-generation-preference')
|
|
689
|
+
const { buildLLM } = await import('@/lib/server/build-llm')
|
|
690
|
+
// loadSettings is imported at the top of this file.
|
|
691
|
+
const settings = loadSettings()
|
|
692
|
+
const compactionPref = resolveCompactionGenerationPreference(settings)
|
|
693
|
+
let summarizerLlm = llm
|
|
694
|
+
let summarizerProvider = session.provider
|
|
695
|
+
let summarizerModel = session.model
|
|
696
|
+
if (compactionPref) {
|
|
697
|
+
try {
|
|
698
|
+
const built = await buildLLM({ preferred: compactionPref, sessionId: session.id, agentId: session.agentId || null })
|
|
699
|
+
summarizerLlm = built.llm
|
|
700
|
+
summarizerProvider = built.provider
|
|
701
|
+
summarizerModel = built.model
|
|
702
|
+
} catch (overrideErr) {
|
|
703
|
+
log.warn(TAG, `Compaction override LLM build failed for ${session.id}; falling back to session model:`, overrideErr)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
685
706
|
const summarize = async (prompt: string): Promise<string> => {
|
|
686
|
-
const response = await
|
|
707
|
+
const response = await summarizerLlm.invoke([new HumanMessage(prompt)])
|
|
687
708
|
if (typeof response.content === 'string') return response.content
|
|
688
709
|
if (Array.isArray(response.content)) {
|
|
689
710
|
return response.content
|
|
@@ -694,8 +715,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
694
715
|
}
|
|
695
716
|
const result = await llmCompact({
|
|
696
717
|
messages: recentHistory,
|
|
697
|
-
provider:
|
|
698
|
-
model:
|
|
718
|
+
provider: summarizerProvider,
|
|
719
|
+
model: summarizerModel,
|
|
699
720
|
agentId: session.agentId || null,
|
|
700
721
|
sessionId: session.id,
|
|
701
722
|
summarize,
|
|
@@ -704,6 +725,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
704
725
|
log.info(TAG,
|
|
705
726
|
`Auto-compacted ${session.id}: ${recentHistory.length} → ${effectiveHistory.length} msgs` +
|
|
706
727
|
` (prompt history ${promptHistoryTokens} tokens)` +
|
|
728
|
+
(compactionPref ? ` (override ${summarizerProvider}/${summarizerModel})` : '') +
|
|
707
729
|
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
708
730
|
)
|
|
709
731
|
}
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
import type { GenerationModelPreference } from '@/lib/server/build-llm'
|
|
2
|
-
import type { AppSettings } from '@/types'
|
|
2
|
+
import type { AppSettings, DreamConfig } from '@/types'
|
|
3
3
|
|
|
4
4
|
type DreamGenerationSettings = Pick<AppSettings, 'dreamProvider' | 'dreamModel' | 'dreamCredentialId' | 'dreamEndpoint'> | Record<string, unknown> | null | undefined
|
|
5
5
|
|
|
6
|
+
type DreamConfigOverride = Pick<DreamConfig, 'provider' | 'model' | 'credentialId' | 'endpoint'> | Partial<DreamConfig> | Record<string, unknown> | null | undefined
|
|
7
|
+
|
|
6
8
|
function optionalSettingString(value: unknown): string | undefined {
|
|
7
9
|
const normalized = typeof value === 'string' ? value.trim() : ''
|
|
8
10
|
return normalized || undefined
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Resolve which model to use for memory consolidation / dream cycles.
|
|
15
|
+
*
|
|
16
|
+
* Precedence:
|
|
17
|
+
* 1. Per-agent override (`dreamConfig.provider` on the Agent record)
|
|
18
|
+
* 2. Global app settings (`dreamProvider` etc.)
|
|
19
|
+
* 3. undefined — caller falls back to the agent's primary generation model
|
|
20
|
+
*
|
|
21
|
+
* The per-agent override lets you route different agents to different dream
|
|
22
|
+
* models (e.g. cheap local for most, but a stronger model for an agent whose
|
|
23
|
+
* memory mix needs more capable structured-output generation).
|
|
24
|
+
*/
|
|
25
|
+
export function resolveDreamGenerationPreference(
|
|
26
|
+
settings: DreamGenerationSettings,
|
|
27
|
+
override?: DreamConfigOverride,
|
|
28
|
+
): GenerationModelPreference | undefined {
|
|
29
|
+
const overrideRecord = (override || {}) as Record<string, unknown>
|
|
30
|
+
const overrideProvider = optionalSettingString(overrideRecord.provider)
|
|
31
|
+
if (overrideProvider) {
|
|
32
|
+
return {
|
|
33
|
+
provider: overrideProvider,
|
|
34
|
+
model: optionalSettingString(overrideRecord.model),
|
|
35
|
+
credentialId: optionalSettingString(overrideRecord.credentialId),
|
|
36
|
+
apiEndpoint: optionalSettingString(overrideRecord.endpoint),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
const record = (settings || {}) as Record<string, unknown>
|
|
13
41
|
const provider = optionalSettingString(record.dreamProvider)
|
|
14
42
|
if (!provider) return undefined
|
|
@@ -22,6 +22,25 @@ describe('resolveDreamGenerationPreference', () => {
|
|
|
22
22
|
apiEndpoint: 'http://localhost:11434',
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('uses a per-agent dream override before global app settings', () => {
|
|
27
|
+
assert.deepEqual(resolveDreamGenerationPreference({
|
|
28
|
+
dreamProvider: 'openai',
|
|
29
|
+
dreamModel: 'gpt-5-mini',
|
|
30
|
+
dreamCredentialId: 'global-cred',
|
|
31
|
+
dreamEndpoint: 'https://global.example/v1',
|
|
32
|
+
}, {
|
|
33
|
+
provider: ' ollama ',
|
|
34
|
+
model: ' qwen3:8b ',
|
|
35
|
+
credentialId: ' agent-cred ',
|
|
36
|
+
endpoint: ' http://127.0.0.1:11434 ',
|
|
37
|
+
}), {
|
|
38
|
+
provider: 'ollama',
|
|
39
|
+
model: 'qwen3:8b',
|
|
40
|
+
credentialId: 'agent-cred',
|
|
41
|
+
apiEndpoint: 'http://127.0.0.1:11434',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
25
44
|
})
|
|
26
45
|
|
|
27
46
|
describe('parseTier2DreamResponseText', () => {
|
|
@@ -216,7 +216,9 @@ ${memoryLines.join('\n')}`
|
|
|
216
216
|
try {
|
|
217
217
|
const { buildLLM } = await import('@/lib/server/build-llm')
|
|
218
218
|
const { loadSettings } = await import('@/lib/server/settings/settings-repository')
|
|
219
|
-
|
|
219
|
+
// `config` is the resolved per-agent dream config (defaults + overrides);
|
|
220
|
+
// pass it so a per-agent provider/model takes precedence over global settings.
|
|
221
|
+
const preferred = resolveDreamGenerationPreference(loadSettings(), config)
|
|
220
222
|
const { llm } = await buildLLM({ agentId, preferred, responseFormat: 'json_object' })
|
|
221
223
|
const { HumanMessage } = await import('@langchain/core/messages')
|
|
222
224
|
|
|
@@ -124,3 +124,34 @@ test('canCreateDailyDigestForAgent allows CLI-only agents when a dream model is
|
|
|
124
124
|
true,
|
|
125
125
|
)
|
|
126
126
|
})
|
|
127
|
+
|
|
128
|
+
test('canCreateDailyDigestForAgent allows CLI-only agents with a per-agent dream model override', async () => {
|
|
129
|
+
const now = Date.now()
|
|
130
|
+
const agentId = 'agent-dream-override-cli'
|
|
131
|
+
storage.saveSettings({})
|
|
132
|
+
storage.saveAgents({
|
|
133
|
+
[agentId]: {
|
|
134
|
+
id: agentId,
|
|
135
|
+
name: 'Per-Agent Dream Routed CLI Agent',
|
|
136
|
+
description: '',
|
|
137
|
+
systemPrompt: '',
|
|
138
|
+
provider: 'claude-cli',
|
|
139
|
+
model: 'claude-sonnet-4-5',
|
|
140
|
+
credentialId: null,
|
|
141
|
+
fallbackCredentialIds: [],
|
|
142
|
+
apiEndpoint: null,
|
|
143
|
+
dreamConfig: {
|
|
144
|
+
provider: 'ollama',
|
|
145
|
+
model: 'llama3.2',
|
|
146
|
+
endpoint: 'http://127.0.0.1:11434',
|
|
147
|
+
},
|
|
148
|
+
createdAt: now,
|
|
149
|
+
updatedAt: now,
|
|
150
|
+
} as Agent,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
assert.equal(
|
|
154
|
+
consolidation.canCreateDailyDigestForAgent(agentId, storage.loadAgents({ includeTrashed: true }), storage.loadSettings()),
|
|
155
|
+
true,
|
|
156
|
+
)
|
|
157
|
+
})
|
|
@@ -48,7 +48,7 @@ export function canCreateDailyDigestForAgent(
|
|
|
48
48
|
try {
|
|
49
49
|
resolveGenerationModelConfig({
|
|
50
50
|
agentId,
|
|
51
|
-
preferred: resolveDreamGenerationPreference(settings),
|
|
51
|
+
preferred: resolveDreamGenerationPreference(settings, agent.dreamConfig),
|
|
52
52
|
})
|
|
53
53
|
return true
|
|
54
54
|
} catch (err: unknown) {
|
|
@@ -118,10 +118,11 @@ export async function runDailyConsolidation(): Promise<{
|
|
|
118
118
|
].join('\n')
|
|
119
119
|
|
|
120
120
|
// Use an optional dream-model override before the target agent's generation provider.
|
|
121
|
+
// Precedence: per-agent dreamConfig override → global dream* settings → agent's primary.
|
|
121
122
|
const { buildLLM } = await import('@/lib/server/build-llm')
|
|
122
123
|
const { llm } = await buildLLM({
|
|
123
124
|
agentId,
|
|
124
|
-
preferred: resolveDreamGenerationPreference(settings),
|
|
125
|
+
preferred: resolveDreamGenerationPreference(settings, agents[agentId]?.dreamConfig),
|
|
125
126
|
})
|
|
126
127
|
|
|
127
128
|
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
@@ -1270,6 +1270,30 @@ function initDb() {
|
|
|
1270
1270
|
return (stmts.listByAgent.all(agentId, safeLimit) as any[]).map(rowToEntry)
|
|
1271
1271
|
},
|
|
1272
1272
|
|
|
1273
|
+
/** Return recent reflection/* memories with their embeddings deserialized
|
|
1274
|
+
* for semantic dedup. Memories without an embedding (older rows, or
|
|
1275
|
+
* embedding still being computed in background) are included with a
|
|
1276
|
+
* null embedding so callers can fall back to text dedup. */
|
|
1277
|
+
recentReflectionEmbeddings(
|
|
1278
|
+
agentId: string,
|
|
1279
|
+
sinceMs: number,
|
|
1280
|
+
limit = 200,
|
|
1281
|
+
): Array<{ id: string; content: string; embedding: number[] | null }> {
|
|
1282
|
+
const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
|
|
1283
|
+
const rows = db.prepare(
|
|
1284
|
+
`SELECT id, content, embedding FROM memories
|
|
1285
|
+
WHERE (agentId = ? OR sharedWith LIKE ?)
|
|
1286
|
+
AND category LIKE 'reflection/%'
|
|
1287
|
+
AND updatedAt >= ?
|
|
1288
|
+
ORDER BY updatedAt DESC LIMIT ?`,
|
|
1289
|
+
).all(agentId, `%"${agentId}"%`, sinceMs, safeLimit) as Array<{ id: string; content: string; embedding: Buffer | null }>
|
|
1290
|
+
return rows.map((row) => ({
|
|
1291
|
+
id: row.id,
|
|
1292
|
+
content: row.content || '',
|
|
1293
|
+
embedding: row.embedding ? deserializeEmbedding(row.embedding) : null,
|
|
1294
|
+
}))
|
|
1295
|
+
},
|
|
1296
|
+
|
|
1273
1297
|
getFrequentlyAccessedByAgent(agentId: string, minAccessCount = 3, sinceDays = 7): MemoryEntry[] {
|
|
1274
1298
|
const cutoff = Date.now() - sinceDays * 86_400_000
|
|
1275
1299
|
const rows = stmts.frequentlyAccessedByAgent.all(agentId, minAccessCount, cutoff) as Record<string, unknown>[]
|
|
@@ -32,6 +32,14 @@ export interface AppSettings {
|
|
|
32
32
|
dreamModel?: string | null
|
|
33
33
|
dreamCredentialId?: string | null
|
|
34
34
|
dreamEndpoint?: string | null
|
|
35
|
+
// Optional model override for auto-compaction (live-loop conversation
|
|
36
|
+
// summarization triggered when context usage hits the auto-compact
|
|
37
|
+
// threshold). Lets the user route the summarizer to a cheaper or faster
|
|
38
|
+
// model than the agent's primary generation model.
|
|
39
|
+
compactionProvider?: string | null
|
|
40
|
+
compactionModel?: string | null
|
|
41
|
+
compactionCredentialId?: string | null
|
|
42
|
+
compactionEndpoint?: string | null
|
|
35
43
|
loopMode?: LoopMode
|
|
36
44
|
agentLoopRecursionLimit?: number
|
|
37
45
|
delegationMaxDepth?: number
|
|
@@ -103,6 +111,12 @@ export interface AppSettings {
|
|
|
103
111
|
autonomyResumeApprovalsEnabled?: boolean
|
|
104
112
|
reflectionEnabled?: boolean
|
|
105
113
|
reflectionAutoWriteMemory?: boolean
|
|
114
|
+
/** Minimum reflection quality score (0-1) required to auto-write memories. */
|
|
115
|
+
reflectionMinQuality?: number
|
|
116
|
+
/** Enable embedding-based dedup for reflection memory writes. */
|
|
117
|
+
reflectionSemanticDedupEnabled?: boolean
|
|
118
|
+
/** Cosine threshold above which a reflection note is considered duplicate. */
|
|
119
|
+
reflectionSemanticDedupThreshold?: number
|
|
106
120
|
memoryReferenceDepth?: number
|
|
107
121
|
maxMemoriesPerLookup?: number
|
|
108
122
|
maxLinkedMemoriesExpanded?: number
|
package/src/types/dream.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface DreamConfig {
|
|
|
33
33
|
pruneThresholdDays: number
|
|
34
34
|
tier2Enabled: boolean
|
|
35
35
|
tier2MaxMemories: number
|
|
36
|
+
// Optional per-agent override for the consolidation/dream LLM. When set,
|
|
37
|
+
// takes precedence over the global `dream*` app settings. When unset, the
|
|
38
|
+
// helper falls back to global settings, then to the agent's primary
|
|
39
|
+
// generation model — same precedence as before.
|
|
40
|
+
provider?: string | null
|
|
41
|
+
model?: string | null
|
|
42
|
+
credentialId?: string | null
|
|
43
|
+
endpoint?: string | null
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export const DEFAULT_DREAM_CONFIG: DreamConfig = {
|