@swarmclawai/swarmclaw 1.9.28 → 1.9.30
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 +34 -7
- package/bin/swarmclaw.js +10 -5
- package/package.json +3 -3
- package/src/cli/binary.test.js +18 -3
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/lib/server/build-llm.test.ts +41 -0
- package/src/lib/server/build-llm.ts +16 -3
- package/src/lib/server/connectors/email.test.ts +4 -1
- package/src/lib/server/connectors/email.ts +10 -2
- package/src/lib/server/memory/dream-generation-preference.ts +22 -0
- package/src/lib/server/memory/dream-service.test.ts +64 -0
- package/src/lib/server/memory/dream-service.ts +74 -5
- package/src/lib/server/memory/memory-consolidation.test.ts +37 -0
- package/src/lib/server/memory/memory-consolidation.ts +16 -5
- package/src/types/app-settings.ts +5 -0
package/README.md
CHANGED
|
@@ -151,15 +151,23 @@ clawhub install swarmclaw
|
|
|
151
151
|
|
|
152
152
|
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
|
|
153
153
|
|
|
154
|
-
## v1.9.
|
|
154
|
+
## v1.9.30 Highlights
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
PR integration release for dream-model routing, email bridge TLS opt-outs, installed CLI runtime resolution, and an OpenClaw plugin workflow example.
|
|
157
157
|
|
|
158
|
-
- **
|
|
159
|
-
- **Email bridge TLS
|
|
160
|
-
- **
|
|
161
|
-
- **
|
|
162
|
-
|
|
158
|
+
- **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
|
|
159
|
+
- **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
|
|
160
|
+
- **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
|
|
161
|
+
- **OpenClaw plugin workflow.** README guidance now includes a concrete TweetClaw plugin workflow for OpenClaw operators.
|
|
162
|
+
|
|
163
|
+
## v1.9.29 Highlights
|
|
164
|
+
|
|
165
|
+
Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
|
|
166
|
+
|
|
167
|
+
- **Edit Agent tooltips.** Help tips in the Edit Agent sheet now render above modal layers instead of being hidden behind the dialog.
|
|
168
|
+
- **Installed package builds.** The npm package now ships the Dagre type declarations needed by `swarmclaw server --build`.
|
|
169
|
+
- **Local Ollama dream output.** Structured dream/reflection calls request Ollama JSON mode and validate balanced JSON before writing memories.
|
|
170
|
+
- **Regression coverage.** CLI/package, model-build, and dream-parser tests cover the reported failure modes.
|
|
163
171
|
|
|
164
172
|
## Hosted Deploys
|
|
165
173
|
|
|
@@ -220,6 +228,7 @@ SwarmClaw is built for OpenClaw operators who need more than one agent or one ga
|
|
|
220
228
|
- Deploy official-image OpenClaw runtimes locally, via VPS bundles, or over SSH.
|
|
221
229
|
- Edit OpenClaw agent files such as `SOUL.md`, `IDENTITY.md`, `USER.md`, `TOOLS.md`, and `AGENTS.md`.
|
|
222
230
|
- Import OpenClaw `SKILL.md` files and use them in SwarmClaw's runtime skill system.
|
|
231
|
+
- Use OpenClaw plugins for domain workflows. For example, `openclaw plugins install @xquik/tweetclaw` installs [TweetClaw](https://github.com/Xquik-dev/tweetclaw) via [ClawHub](https://clawhub.ai/kriptoburak/xquik-tweetclaw) for X/Twitter search, follower export, monitors, webhooks, and approval-gated post/reply actions.
|
|
223
232
|
|
|
224
233
|
## Use Cases
|
|
225
234
|
|
|
@@ -411,6 +420,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
411
420
|
|
|
412
421
|
## Releases
|
|
413
422
|
|
|
423
|
+
### v1.9.30 Highlights
|
|
424
|
+
|
|
425
|
+
PR integration release for dream-model routing, email bridge TLS opt-outs, installed CLI runtime resolution, and an OpenClaw plugin workflow example.
|
|
426
|
+
|
|
427
|
+
- **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
|
|
428
|
+
- **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
|
|
429
|
+
- **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
|
|
430
|
+
- **OpenClaw plugin workflow.** README guidance now includes a concrete TweetClaw plugin workflow for OpenClaw operators.
|
|
431
|
+
|
|
432
|
+
### v1.9.29 Highlights
|
|
433
|
+
|
|
434
|
+
Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
|
|
435
|
+
|
|
436
|
+
- **Edit Agent tooltips.** Help tips in the Edit Agent sheet now render above modal layers instead of being hidden behind the dialog.
|
|
437
|
+
- **Installed package builds.** The npm package now ships the Dagre type declarations needed by `swarmclaw server --build`.
|
|
438
|
+
- **Local Ollama dream output.** Structured dream/reflection calls request Ollama JSON mode and validate balanced JSON before writing memories.
|
|
439
|
+
- **Regression coverage.** CLI/package, model-build, and dream-parser tests cover the reported failure modes.
|
|
440
|
+
|
|
414
441
|
### v1.9.28 Highlights
|
|
415
442
|
|
|
416
443
|
Issue-fix release for installed CLI groups, email bridge TLS handling, built-in model overrides, and Windows desktop native modules.
|
package/bin/swarmclaw.js
CHANGED
|
@@ -47,11 +47,14 @@ function supportsStripTypes() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function hasTsxRuntime() {
|
|
50
|
+
return Boolean(resolveTsxRuntimeImportPath())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveTsxRuntimeImportPath() {
|
|
50
54
|
try {
|
|
51
|
-
require.resolve('tsx
|
|
52
|
-
return true
|
|
55
|
+
return require.resolve('tsx')
|
|
53
56
|
} catch {
|
|
54
|
-
return
|
|
57
|
+
return null
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -71,9 +74,10 @@ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
|
|
|
71
74
|
return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
const
|
|
77
|
+
const tsxImportPath = options.tsxImportPath ?? resolveTsxRuntimeImportPath()
|
|
78
|
+
const tsxAvailable = options.hasTsxRuntime ?? Boolean(tsxImportPath)
|
|
75
79
|
if (tsxAvailable) {
|
|
76
|
-
return ['--no-warnings', '--import', 'tsx', cliPath, ...argv]
|
|
80
|
+
return ['--no-warnings', '--import', tsxImportPath || 'tsx', cliPath, ...argv]
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
return null
|
|
@@ -374,6 +378,7 @@ module.exports = {
|
|
|
374
378
|
normalizeLegacyTsCliArgv,
|
|
375
379
|
pathIsInsideNodeModules,
|
|
376
380
|
resolveLegacyTsCliPath,
|
|
381
|
+
resolveTsxRuntimeImportPath,
|
|
377
382
|
TS_CLI_ACTIONS,
|
|
378
383
|
normalizeLegacyCliEnv,
|
|
379
384
|
printPackageVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.30",
|
|
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/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/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",
|
|
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",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"@tailwindcss/postcss": "^4",
|
|
120
120
|
"@tanstack/react-query": "^5.91.0",
|
|
121
121
|
"@types/better-sqlite3": "^7.6.13",
|
|
122
|
+
"@types/dagre": "^0.7.54",
|
|
122
123
|
"@types/mailparser": "^3.4.6",
|
|
123
124
|
"@types/node": "^20",
|
|
124
125
|
"@types/nodemailer": "^7.0.11",
|
|
@@ -179,7 +180,6 @@
|
|
|
179
180
|
},
|
|
180
181
|
"devDependencies": {
|
|
181
182
|
"@electron/rebuild": "^3.7.2",
|
|
182
|
-
"@types/dagre": "^0.7.54",
|
|
183
183
|
"electron": "^33.3.0",
|
|
184
184
|
"electron-builder": "^25.1.8",
|
|
185
185
|
"eslint": "^9",
|
package/src/cli/binary.test.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('node:fs')
|
|
|
7
7
|
const os = require('node:os')
|
|
8
8
|
const path = require('node:path')
|
|
9
9
|
const { spawnSync } = require('node:child_process')
|
|
10
|
-
const { buildLegacyTsCliArgs, resolveLegacyTsCliPath } = require('../../bin/swarmclaw.js')
|
|
10
|
+
const { buildLegacyTsCliArgs, resolveLegacyTsCliPath, resolveTsxRuntimeImportPath } = require('../../bin/swarmclaw.js')
|
|
11
11
|
|
|
12
12
|
const CLI_BIN = path.join(__dirname, '..', '..', 'bin', 'swarmclaw.js')
|
|
13
13
|
const PACKAGE_JSON = require('../../package.json')
|
|
@@ -199,23 +199,38 @@ test('binary -v alias output matches package version', () => {
|
|
|
199
199
|
assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`)
|
|
200
200
|
})
|
|
201
201
|
|
|
202
|
+
test('package ships dagre type declarations required by installed builds', () => {
|
|
203
|
+
assert.equal(PACKAGE_JSON.dependencies.dagre, '^0.8.5')
|
|
204
|
+
assert.equal(PACKAGE_JSON.dependencies['@types/dagre'], '^0.7.54')
|
|
205
|
+
assert.equal(PACKAGE_JSON.devDependencies?.['@types/dagre'], undefined)
|
|
206
|
+
})
|
|
207
|
+
|
|
202
208
|
test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
|
|
203
209
|
const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts')
|
|
210
|
+
const tsxImportPath = resolveTsxRuntimeImportPath()
|
|
204
211
|
const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], {
|
|
205
212
|
supportsStripTypes: false,
|
|
206
213
|
hasTsxRuntime: true,
|
|
207
214
|
})
|
|
208
215
|
|
|
209
|
-
assert.deepEqual(args, ['--no-warnings', '--import',
|
|
216
|
+
assert.deepEqual(args, ['--no-warnings', '--import', tsxImportPath, cliPath, 'runs', 'list'])
|
|
210
217
|
})
|
|
211
218
|
|
|
212
219
|
test('legacy TS launcher uses tsx instead of strip-types inside node_modules', () => {
|
|
213
220
|
const cliPath = path.join(os.tmpdir(), 'node_modules', '@swarmclawai', 'swarmclaw', 'src', 'cli', 'index.ts')
|
|
221
|
+
const tsxImportPath = resolveTsxRuntimeImportPath()
|
|
214
222
|
const args = buildLegacyTsCliArgs(cliPath, ['agents', 'list'], {
|
|
215
223
|
supportsStripTypes: true,
|
|
216
224
|
hasTsxRuntime: true,
|
|
217
225
|
})
|
|
218
226
|
|
|
219
|
-
assert.deepEqual(args, ['--no-warnings', '--import',
|
|
227
|
+
assert.deepEqual(args, ['--no-warnings', '--import', tsxImportPath, cliPath, 'agents', 'list'])
|
|
220
228
|
assert.equal(resolveLegacyTsCliPath(), path.join(APP_ROOT, 'src', 'cli', 'index.ts'))
|
|
221
229
|
})
|
|
230
|
+
|
|
231
|
+
test('legacy TS launcher imports the package-local tsx runtime by absolute path', () => {
|
|
232
|
+
const tsxImportPath = resolveTsxRuntimeImportPath()
|
|
233
|
+
|
|
234
|
+
assert.equal(path.isAbsolute(tsxImportPath), true)
|
|
235
|
+
assert.match(tsxImportPath, /tsx/)
|
|
236
|
+
})
|
|
@@ -42,7 +42,7 @@ function TooltipContent({
|
|
|
42
42
|
data-slot="tooltip-content"
|
|
43
43
|
sideOffset={sideOffset}
|
|
44
44
|
className={cn(
|
|
45
|
-
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-
|
|
45
|
+
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[1300] w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
46
46
|
className
|
|
47
47
|
)}
|
|
48
48
|
{...props}
|
|
@@ -250,6 +250,47 @@ test('buildChatModel disables parallel_tool_calls for Ollama local to avoid dupl
|
|
|
250
250
|
assert.equal(llm.clientConfig?.baseURL, 'http://localhost:11434/v1')
|
|
251
251
|
})
|
|
252
252
|
|
|
253
|
+
test('buildChatModel passes JSON format mode only to local Ollama structured calls', () => {
|
|
254
|
+
const local = buildChatModel({
|
|
255
|
+
provider: 'ollama',
|
|
256
|
+
model: 'gemma4:e4b',
|
|
257
|
+
ollamaMode: 'local',
|
|
258
|
+
apiKey: null,
|
|
259
|
+
responseFormat: 'json_object',
|
|
260
|
+
}) as ChatOpenAI & { modelKwargs?: Record<string, unknown> }
|
|
261
|
+
|
|
262
|
+
assert.equal(local.modelKwargs?.parallel_tool_calls, false)
|
|
263
|
+
assert.equal(local.modelKwargs?.format, 'json')
|
|
264
|
+
|
|
265
|
+
saveCredentials({
|
|
266
|
+
'cred-ollama-cloud-json': {
|
|
267
|
+
id: 'cred-ollama-cloud-json',
|
|
268
|
+
provider: 'ollama',
|
|
269
|
+
name: 'Ollama Cloud',
|
|
270
|
+
encryptedKey: encryptKey('ollama-cloud-key'),
|
|
271
|
+
createdAt: Date.now(),
|
|
272
|
+
},
|
|
273
|
+
} as Record<string, {
|
|
274
|
+
id: string
|
|
275
|
+
provider: string
|
|
276
|
+
name: string
|
|
277
|
+
encryptedKey: string
|
|
278
|
+
createdAt: number
|
|
279
|
+
}>)
|
|
280
|
+
|
|
281
|
+
const cloud = buildChatModel({
|
|
282
|
+
provider: 'ollama',
|
|
283
|
+
model: 'glm-5:cloud',
|
|
284
|
+
ollamaMode: 'cloud',
|
|
285
|
+
apiKey: null,
|
|
286
|
+
credentialId: 'cred-ollama-cloud-json',
|
|
287
|
+
responseFormat: 'json_object',
|
|
288
|
+
}) as ChatOpenAI & { modelKwargs?: Record<string, unknown> }
|
|
289
|
+
|
|
290
|
+
assert.equal(cloud.modelKwargs?.parallel_tool_calls, false)
|
|
291
|
+
assert.equal(cloud.modelKwargs?.format, undefined)
|
|
292
|
+
})
|
|
293
|
+
|
|
253
294
|
test('buildChatModel uses a reasoning_content-preserving bridge for DeepSeek', () => {
|
|
254
295
|
const llm = buildChatModel({
|
|
255
296
|
provider: 'deepseek',
|
|
@@ -18,6 +18,7 @@ const OLLAMA_CLOUD_URL = 'https://ollama.com/v1'
|
|
|
18
18
|
const OLLAMA_LOCAL_URL = 'http://localhost:11434/v1'
|
|
19
19
|
export const OPENAI_COMPAT_MODEL_TIMEOUT_MS = 180_000
|
|
20
20
|
export const OPENAI_COMPAT_MODEL_MAX_RETRIES = 2
|
|
21
|
+
export type GenerationResponseFormat = 'json_object'
|
|
21
22
|
|
|
22
23
|
export interface GenerationModelPreference {
|
|
23
24
|
provider?: string | null
|
|
@@ -27,6 +28,7 @@ export interface GenerationModelPreference {
|
|
|
27
28
|
apiEndpoint?: string | null
|
|
28
29
|
gatewayProfileId?: string | null
|
|
29
30
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high' | null
|
|
31
|
+
responseFormat?: GenerationResponseFormat | null
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface ResolvedGenerationModelConfig {
|
|
@@ -36,6 +38,7 @@ interface ResolvedGenerationModelConfig {
|
|
|
36
38
|
apiKey: string | null
|
|
37
39
|
apiEndpoint: string | null
|
|
38
40
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
|
|
41
|
+
responseFormat?: GenerationResponseFormat
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
type OpenAiReasoningEffort = 'low' | 'medium' | 'high'
|
|
@@ -43,6 +46,7 @@ type ChatOpenAiConfig = ConstructorParameters<typeof ChatOpenAI>[0] & {
|
|
|
43
46
|
modelKwargs?: {
|
|
44
47
|
reasoning_effort?: OpenAiReasoningEffort
|
|
45
48
|
parallel_tool_calls?: boolean
|
|
49
|
+
format?: 'json'
|
|
46
50
|
}
|
|
47
51
|
configuration?: {
|
|
48
52
|
baseURL?: string
|
|
@@ -68,8 +72,9 @@ export function buildChatModel(opts: {
|
|
|
68
72
|
credentialId?: string | null
|
|
69
73
|
apiEndpoint?: string | null
|
|
70
74
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
|
|
75
|
+
responseFormat?: GenerationResponseFormat
|
|
71
76
|
}) {
|
|
72
|
-
const { provider, model, ollamaMode, apiKey, credentialId, apiEndpoint, thinkingLevel } = opts
|
|
77
|
+
const { provider, model, ollamaMode, apiKey, credentialId, apiEndpoint, thinkingLevel, responseFormat } = opts
|
|
73
78
|
const resolvedCredentialId = resolveProviderCredentialId({ provider, ollamaMode: ollamaMode ?? null, credentialId })
|
|
74
79
|
const resolvedApiKey = apiKey ?? resolveApiKeyFromCredential(resolvedCredentialId)
|
|
75
80
|
const providers = getProviderList()
|
|
@@ -118,7 +123,10 @@ export function buildChatModel(opts: {
|
|
|
118
123
|
timeout: OPENAI_COMPAT_MODEL_TIMEOUT_MS,
|
|
119
124
|
maxRetries: OPENAI_COMPAT_MODEL_MAX_RETRIES,
|
|
120
125
|
configuration: { baseURL },
|
|
121
|
-
modelKwargs: {
|
|
126
|
+
modelKwargs: {
|
|
127
|
+
parallel_tool_calls: false,
|
|
128
|
+
...(responseFormat === 'json_object' && !runtime.useCloud ? { format: 'json' as const } : {}),
|
|
129
|
+
},
|
|
122
130
|
})
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -222,6 +230,7 @@ function resolvePreferredGenerationConfig(
|
|
|
222
230
|
apiKey,
|
|
223
231
|
apiEndpoint,
|
|
224
232
|
thinkingLevel: candidate.thinkingLevel || undefined,
|
|
233
|
+
responseFormat: candidate.responseFormat || undefined,
|
|
225
234
|
}
|
|
226
235
|
}
|
|
227
236
|
return null
|
|
@@ -232,6 +241,7 @@ export function resolveGenerationModelConfig(options?: {
|
|
|
232
241
|
sessionId?: string | null
|
|
233
242
|
agentId?: string | null
|
|
234
243
|
excludeProviders?: string[]
|
|
244
|
+
responseFormat?: GenerationResponseFormat
|
|
235
245
|
}): ResolvedGenerationModelConfig {
|
|
236
246
|
const providers = getProviderList()
|
|
237
247
|
const excludeProviders = new Set((options?.excludeProviders || []).map((value) => normalizePreferenceValue(value)).filter(Boolean))
|
|
@@ -252,7 +262,9 @@ export function resolveGenerationModelConfig(options?: {
|
|
|
252
262
|
...getAgentGenerationPreferences(sessionAgent),
|
|
253
263
|
...getAgentGenerationPreferences(directAgent),
|
|
254
264
|
], excludeProviders)
|
|
255
|
-
if (resolved) return
|
|
265
|
+
if (resolved) return options?.responseFormat
|
|
266
|
+
? { ...resolved, responseFormat: options.responseFormat }
|
|
267
|
+
: resolved
|
|
256
268
|
|
|
257
269
|
const sessionLabel = options?.sessionId ? `session "${options.sessionId}"` : null
|
|
258
270
|
const agentLabel = options?.agentId ? `agent "${options.agentId}"` : null
|
|
@@ -271,6 +283,7 @@ export async function buildLLM(options?: {
|
|
|
271
283
|
sessionId?: string | null
|
|
272
284
|
agentId?: string | null
|
|
273
285
|
excludeProviders?: string[]
|
|
286
|
+
responseFormat?: GenerationResponseFormat
|
|
274
287
|
}) {
|
|
275
288
|
const resolved = resolveGenerationModelConfig(options)
|
|
276
289
|
return {
|
|
@@ -81,7 +81,10 @@ describe('email TLS configuration', () => {
|
|
|
81
81
|
assert.equal(parseTlsRejectUnauthorized(false), false)
|
|
82
82
|
assert.equal(parseTlsRejectUnauthorized('false'), false)
|
|
83
83
|
assert.equal(parseTlsRejectUnauthorized('0'), false)
|
|
84
|
-
|
|
84
|
+
const tls = buildEmailTlsOptions({ tlsRejectUnauthorized: false })
|
|
85
|
+
assert.equal(tls.rejectUnauthorized, false)
|
|
86
|
+
assert.equal(typeof tls.checkServerIdentity, 'function')
|
|
87
|
+
assert.equal(tls.checkServerIdentity?.('localhost', {} as never), undefined)
|
|
85
88
|
})
|
|
86
89
|
|
|
87
90
|
it('handles IMAP socket errors without leaving the emitter unhandled', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import type { ConnectionOptions } from 'node:tls'
|
|
3
4
|
import { ImapFlow } from 'imapflow'
|
|
4
5
|
import { createTransport, type Transporter } from 'nodemailer'
|
|
5
6
|
import { simpleParser } from 'mailparser'
|
|
@@ -34,6 +35,10 @@ interface ImapErrorEmitter {
|
|
|
34
35
|
on(event: 'error', listener: (err: unknown) => void): unknown
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
type EmailTlsOptions = Pick<ConnectionOptions, 'rejectUnauthorized' | 'checkServerIdentity'> & {
|
|
39
|
+
rejectUnauthorized: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
export function buildAttachments(options?: OutboundSendOptions): MailAttachment[] {
|
|
38
43
|
const source = options?.mediaPath
|
|
39
44
|
if (!source) return []
|
|
@@ -59,8 +64,11 @@ export function parseTlsRejectUnauthorized(value: unknown): boolean {
|
|
|
59
64
|
return true
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
export function buildEmailTlsOptions(config: Pick<EmailConfig, 'tlsRejectUnauthorized'>):
|
|
63
|
-
|
|
67
|
+
export function buildEmailTlsOptions(config: Pick<EmailConfig, 'tlsRejectUnauthorized'>): EmailTlsOptions {
|
|
68
|
+
const reject = config.tlsRejectUnauthorized !== false
|
|
69
|
+
return reject
|
|
70
|
+
? { rejectUnauthorized: true }
|
|
71
|
+
: { rejectUnauthorized: false, checkServerIdentity: () => undefined }
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
export function attachImapErrorHandler(imap: ImapErrorEmitter, onDisconnected: () => void): void {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GenerationModelPreference } from '@/lib/server/build-llm'
|
|
2
|
+
import type { AppSettings } from '@/types'
|
|
3
|
+
|
|
4
|
+
type DreamGenerationSettings = Pick<AppSettings, 'dreamProvider' | 'dreamModel' | 'dreamCredentialId' | 'dreamEndpoint'> | 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
|
+
export function resolveDreamGenerationPreference(settings: DreamGenerationSettings): GenerationModelPreference | undefined {
|
|
12
|
+
const record = (settings || {}) as Record<string, unknown>
|
|
13
|
+
const provider = optionalSettingString(record.dreamProvider)
|
|
14
|
+
if (!provider) return undefined
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
provider,
|
|
18
|
+
model: optionalSettingString(record.dreamModel),
|
|
19
|
+
credentialId: optionalSettingString(record.dreamCredentialId),
|
|
20
|
+
apiEndpoint: optionalSettingString(record.dreamEndpoint),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { resolveDreamGenerationPreference } from './dream-generation-preference'
|
|
4
|
+
import { parseTier2DreamResponseText } from './dream-service'
|
|
5
|
+
|
|
6
|
+
describe('resolveDreamGenerationPreference', () => {
|
|
7
|
+
it('returns no preference when no dream provider is configured', () => {
|
|
8
|
+
assert.equal(resolveDreamGenerationPreference({}), undefined)
|
|
9
|
+
assert.equal(resolveDreamGenerationPreference({ dreamProvider: ' ' }), undefined)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('builds a trimmed dream model preference from app settings', () => {
|
|
13
|
+
assert.deepEqual(resolveDreamGenerationPreference({
|
|
14
|
+
dreamProvider: ' ollama ',
|
|
15
|
+
dreamModel: ' gemma4:e4b ',
|
|
16
|
+
dreamCredentialId: ' cred-1 ',
|
|
17
|
+
dreamEndpoint: ' http://localhost:11434 ',
|
|
18
|
+
}), {
|
|
19
|
+
provider: 'ollama',
|
|
20
|
+
model: 'gemma4:e4b',
|
|
21
|
+
credentialId: 'cred-1',
|
|
22
|
+
apiEndpoint: 'http://localhost:11434',
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('parseTier2DreamResponseText', () => {
|
|
28
|
+
it('parses a plain structured dream response', () => {
|
|
29
|
+
const parsed = parseTier2DreamResponseText(JSON.stringify({
|
|
30
|
+
consolidations: [{
|
|
31
|
+
sourceIds: ['mem-1', 'mem-2'],
|
|
32
|
+
title: 'Shared pattern',
|
|
33
|
+
content: 'Both memories describe the same workflow.',
|
|
34
|
+
}],
|
|
35
|
+
reflections: [{ title: 'Focus', content: 'The agent prefers short release loops.' }],
|
|
36
|
+
flagged: [{ memoryId: 'mem-3', reason: 'Contradicts the current release process.' }],
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(parsed?.consolidations?.[0]?.sourceIds, ['mem-1', 'mem-2'])
|
|
40
|
+
assert.equal(parsed?.reflections?.[0]?.title, 'Focus')
|
|
41
|
+
assert.equal(parsed?.flagged?.[0]?.memoryId, 'mem-3')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('extracts fenced JSON with nested braces inside strings', () => {
|
|
45
|
+
const parsed = parseTier2DreamResponseText([
|
|
46
|
+
'```json',
|
|
47
|
+
'{',
|
|
48
|
+
' "consolidations": [{',
|
|
49
|
+
' "sourceIds": ["mem-1"],',
|
|
50
|
+
' "title": "Payload shape",',
|
|
51
|
+
' "content": "The JSON example was {\\"ok\\":true} and should stay intact."',
|
|
52
|
+
' }]',
|
|
53
|
+
'}',
|
|
54
|
+
'```',
|
|
55
|
+
].join('\n'))
|
|
56
|
+
|
|
57
|
+
assert.equal(parsed?.consolidations?.[0]?.content, 'The JSON example was {"ok":true} and should stay intact.')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('rejects malformed or schema-incompatible responses', () => {
|
|
61
|
+
assert.equal(parseTier2DreamResponseText('no json here'), null)
|
|
62
|
+
assert.equal(parseTier2DreamResponseText('{"consolidations":[{"sourceIds":[123],"title":"Bad","content":"Bad"}]}'), null)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
|
+
import { z } from 'zod'
|
|
2
3
|
import type { DreamCycle, DreamCycleResult, DreamConfig, DreamTrigger, Agent } from '@/types'
|
|
3
4
|
import { DEFAULT_DREAM_CONFIG } from '@/types/dream'
|
|
4
5
|
import { getMemoryDb } from '@/lib/server/memory/memory-db'
|
|
5
6
|
import { saveDreamCycle } from '@/lib/server/memory/dream-cycles'
|
|
6
|
-
import { errorMessage
|
|
7
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
7
8
|
import { log } from '@/lib/server/logger'
|
|
9
|
+
import { resolveDreamGenerationPreference } from '@/lib/server/memory/dream-generation-preference'
|
|
8
10
|
|
|
9
11
|
const TAG = 'dream-service'
|
|
10
12
|
|
|
@@ -102,6 +104,70 @@ interface Tier2Response {
|
|
|
102
104
|
flagged?: Tier2Flagged[]
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
const Tier2ResponseSchema = z.object({
|
|
108
|
+
consolidations: z.array(z.object({
|
|
109
|
+
sourceIds: z.array(z.string()),
|
|
110
|
+
title: z.string(),
|
|
111
|
+
content: z.string(),
|
|
112
|
+
})).optional(),
|
|
113
|
+
reflections: z.array(z.object({
|
|
114
|
+
title: z.string(),
|
|
115
|
+
content: z.string(),
|
|
116
|
+
})).optional(),
|
|
117
|
+
flagged: z.array(z.object({
|
|
118
|
+
memoryId: z.string(),
|
|
119
|
+
reason: z.string(),
|
|
120
|
+
})).optional(),
|
|
121
|
+
}).passthrough()
|
|
122
|
+
|
|
123
|
+
function findBalancedJsonObjectEnd(text: string, start: number): number {
|
|
124
|
+
let depth = 0
|
|
125
|
+
let inString = false
|
|
126
|
+
let escaped = false
|
|
127
|
+
|
|
128
|
+
for (let index = start; index < text.length; index += 1) {
|
|
129
|
+
const char = text[index]
|
|
130
|
+
if (inString) {
|
|
131
|
+
if (escaped) escaped = false
|
|
132
|
+
else if (char === '\\') escaped = true
|
|
133
|
+
else if (char === '"') inString = false
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (char === '"') {
|
|
137
|
+
inString = true
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (char === '{') depth += 1
|
|
141
|
+
else if (char === '}') depth -= 1
|
|
142
|
+
if (depth === 0) return index + 1
|
|
143
|
+
}
|
|
144
|
+
return -1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function extractFirstBalancedJsonObject(text: string): string | null {
|
|
148
|
+
const source = String(text || '')
|
|
149
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
150
|
+
if (source[index] !== '{') continue
|
|
151
|
+
const end = findBalancedJsonObjectEnd(source, index)
|
|
152
|
+
if (end === -1) return null
|
|
153
|
+
return source.slice(index, end)
|
|
154
|
+
}
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function parseTier2DreamResponseText(text: string): Tier2Response | null {
|
|
159
|
+
const jsonText = extractFirstBalancedJsonObject(text)
|
|
160
|
+
if (!jsonText) return null
|
|
161
|
+
let raw: unknown
|
|
162
|
+
try {
|
|
163
|
+
raw = JSON.parse(jsonText)
|
|
164
|
+
} catch {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
const parsed = Tier2ResponseSchema.safeParse(raw)
|
|
168
|
+
return parsed.success ? parsed.data : null
|
|
169
|
+
}
|
|
170
|
+
|
|
105
171
|
export async function runTier2Dream(
|
|
106
172
|
agentId: string,
|
|
107
173
|
config: DreamConfig,
|
|
@@ -149,7 +215,9 @@ ${memoryLines.join('\n')}`
|
|
|
149
215
|
|
|
150
216
|
try {
|
|
151
217
|
const { buildLLM } = await import('@/lib/server/build-llm')
|
|
152
|
-
const {
|
|
218
|
+
const { loadSettings } = await import('@/lib/server/settings/settings-repository')
|
|
219
|
+
const preferred = resolveDreamGenerationPreference(loadSettings())
|
|
220
|
+
const { llm } = await buildLLM({ agentId, preferred, responseFormat: 'json_object' })
|
|
153
221
|
const { HumanMessage } = await import('@langchain/core/messages')
|
|
154
222
|
|
|
155
223
|
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
@@ -159,9 +227,10 @@ ${memoryLines.join('\n')}`
|
|
|
159
227
|
? response.content.map((b) => ('text' in b && typeof b.text === 'string' ? b.text : '')).join('')
|
|
160
228
|
: ''
|
|
161
229
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
230
|
+
const parsed = parseTier2DreamResponseText(text) ?? {}
|
|
231
|
+
if (!parsed.consolidations && !parsed.reflections && !parsed.flagged) {
|
|
232
|
+
errors.push('Tier 2 dream response was not valid structured JSON.')
|
|
233
|
+
}
|
|
165
234
|
|
|
166
235
|
// Process consolidations
|
|
167
236
|
if (Array.isArray(parsed.consolidations)) {
|
|
@@ -38,6 +38,7 @@ after(() => {
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
test('runDailyConsolidation skips orphaned and CLI-only agent namespaces without reporting errors', async () => {
|
|
41
|
+
storage.saveSettings({})
|
|
41
42
|
const db = memDb.getMemoryDb()
|
|
42
43
|
const now = Date.now()
|
|
43
44
|
const orphanId = 'live-orphan-agent'
|
|
@@ -87,3 +88,39 @@ test('runDailyConsolidation skips orphaned and CLI-only agent namespaces without
|
|
|
87
88
|
false,
|
|
88
89
|
)
|
|
89
90
|
})
|
|
91
|
+
|
|
92
|
+
test('canCreateDailyDigestForAgent allows CLI-only agents when a dream model is configured', async () => {
|
|
93
|
+
const now = Date.now()
|
|
94
|
+
const agentId = 'dream-routed-cli-agent'
|
|
95
|
+
storage.saveAgents({
|
|
96
|
+
[agentId]: {
|
|
97
|
+
id: agentId,
|
|
98
|
+
name: 'Dream Routed CLI Agent',
|
|
99
|
+
description: '',
|
|
100
|
+
systemPrompt: '',
|
|
101
|
+
provider: 'claude-cli',
|
|
102
|
+
model: 'claude-sonnet-4-5',
|
|
103
|
+
credentialId: null,
|
|
104
|
+
fallbackCredentialIds: [],
|
|
105
|
+
apiEndpoint: null,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
} as Agent,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
storage.saveSettings({})
|
|
112
|
+
assert.equal(
|
|
113
|
+
consolidation.canCreateDailyDigestForAgent(agentId, storage.loadAgents({ includeTrashed: true }), storage.loadSettings()),
|
|
114
|
+
false,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
storage.saveSettings({
|
|
118
|
+
dreamProvider: 'ollama',
|
|
119
|
+
dreamModel: 'llama3.2',
|
|
120
|
+
dreamEndpoint: 'http://127.0.0.1:11434',
|
|
121
|
+
})
|
|
122
|
+
assert.equal(
|
|
123
|
+
consolidation.canCreateDailyDigestForAgent(agentId, storage.loadAgents({ includeTrashed: true }), storage.loadSettings()),
|
|
124
|
+
true,
|
|
125
|
+
)
|
|
126
|
+
})
|
|
@@ -4,6 +4,9 @@ import { resolveGenerationModelConfig } from '@/lib/server/build-llm'
|
|
|
4
4
|
import { HumanMessage } from '@langchain/core/messages'
|
|
5
5
|
import { errorMessage } from '@/lib/shared-utils'
|
|
6
6
|
import { onNextIdleWindow } from '@/lib/server/runtime/idle-window'
|
|
7
|
+
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
8
|
+
import { resolveDreamGenerationPreference } from '@/lib/server/memory/dream-generation-preference'
|
|
9
|
+
import type { AppSettings } from '@/types'
|
|
7
10
|
|
|
8
11
|
let consolidationRegistered = false
|
|
9
12
|
let compactionRegistered = false
|
|
@@ -35,14 +38,18 @@ export function registerCompactionIdleCallback(): void {
|
|
|
35
38
|
})
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
function canCreateDailyDigestForAgent(
|
|
41
|
+
export function canCreateDailyDigestForAgent(
|
|
39
42
|
agentId: string,
|
|
40
43
|
agents: ReturnType<typeof loadAgents>,
|
|
44
|
+
settings: Partial<AppSettings> | Record<string, unknown> | null | undefined = loadSettings(),
|
|
41
45
|
): boolean {
|
|
42
46
|
const agent = agents[agentId]
|
|
43
47
|
if (!agent || agent.trashedAt) return false
|
|
44
48
|
try {
|
|
45
|
-
resolveGenerationModelConfig({
|
|
49
|
+
resolveGenerationModelConfig({
|
|
50
|
+
agentId,
|
|
51
|
+
preferred: resolveDreamGenerationPreference(settings),
|
|
52
|
+
})
|
|
46
53
|
return true
|
|
47
54
|
} catch (err: unknown) {
|
|
48
55
|
const message = errorMessage(err)
|
|
@@ -65,6 +72,7 @@ export async function runDailyConsolidation(): Promise<{
|
|
|
65
72
|
const memDb = getMemoryDb()
|
|
66
73
|
const counts = memDb.countsByAgent()
|
|
67
74
|
const agents = loadAgents({ includeTrashed: true })
|
|
75
|
+
const settings = loadSettings()
|
|
68
76
|
const today = new Date().toISOString().slice(0, 10) // YYYY-MM-DD
|
|
69
77
|
const digestTitle = `Daily digest: ${today}`
|
|
70
78
|
const cutoff24h = Date.now() - 24 * 3600_000
|
|
@@ -76,7 +84,7 @@ export async function runDailyConsolidation(): Promise<{
|
|
|
76
84
|
const agentId = agentKey
|
|
77
85
|
|
|
78
86
|
try {
|
|
79
|
-
if (!canCreateDailyDigestForAgent(agentId, agents)) continue
|
|
87
|
+
if (!canCreateDailyDigestForAgent(agentId, agents, settings)) continue
|
|
80
88
|
|
|
81
89
|
// Check if digest already exists for today
|
|
82
90
|
const existing = memDb.search(digestTitle, agentId)
|
|
@@ -109,9 +117,12 @@ export async function runDailyConsolidation(): Promise<{
|
|
|
109
117
|
...memoryLines,
|
|
110
118
|
].join('\n')
|
|
111
119
|
|
|
112
|
-
// Use the target agent's
|
|
120
|
+
// Use an optional dream-model override before the target agent's generation provider.
|
|
113
121
|
const { buildLLM } = await import('@/lib/server/build-llm')
|
|
114
|
-
const { llm } = await buildLLM({
|
|
122
|
+
const { llm } = await buildLLM({
|
|
123
|
+
agentId,
|
|
124
|
+
preferred: resolveDreamGenerationPreference(settings),
|
|
125
|
+
})
|
|
115
126
|
|
|
116
127
|
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
117
128
|
const digestContent = typeof response.content === 'string'
|
|
@@ -27,6 +27,11 @@ export interface AppSettings {
|
|
|
27
27
|
embeddingModel?: string | null
|
|
28
28
|
embeddingCredentialId?: string | null
|
|
29
29
|
embeddingEndpoint?: string | null
|
|
30
|
+
// Optional model override for memory consolidation and dream cycles.
|
|
31
|
+
dreamProvider?: string | null
|
|
32
|
+
dreamModel?: string | null
|
|
33
|
+
dreamCredentialId?: string | null
|
|
34
|
+
dreamEndpoint?: string | null
|
|
30
35
|
loopMode?: LoopMode
|
|
31
36
|
agentLoopRecursionLimit?: number
|
|
32
37
|
delegationMaxDepth?: number
|