@vellumai/assistant 0.8.9-staging.3 → 0.8.9-staging.4
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/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/context-window-manager-compact-retry.test.ts +120 -14
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/daemon/conversation-agent-loop.ts +9 -17
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/plugins/defaults/compaction/window-manager.ts +56 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/chat-completions-provider.ts +19 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/services/published-app-updater.ts +30 -8
- package/src/tools/skills/load.ts +1 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -1,47 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `assistant memory v3` CLI subgroup.
|
|
3
3
|
*
|
|
4
|
-
* Operator-facing
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* write). The read-only `health` report and the `set-core` cost preview also
|
|
9
|
-
* route through the daemon so it stays the single source of truth for the live
|
|
10
|
-
* workspace tree.
|
|
4
|
+
* Operator-facing live-lane maintenance subcommands for the v3 memory subsystem
|
|
5
|
+
* (section-lane retrieval model). The daemon owns the live shadow lanes, so the
|
|
6
|
+
* verbs route through `cliIpcCall` to handlers that run inside the assistant
|
|
7
|
+
* process (where the in-memory lanes can be invalidated/rebuilt after a write).
|
|
11
8
|
*
|
|
12
9
|
* Subcommands:
|
|
13
10
|
*
|
|
14
|
-
* - `health` — print the structural health report (read-only).
|
|
15
|
-
* - `reconcile` — v1 convergence pass: rewrites any dangling page/core ref to
|
|
16
|
-
* the current on-disk tree and prunes stale core entries. It does NOT yet
|
|
17
|
-
* detect renames/moves/splits — that requires a captured prior leaf
|
|
18
|
-
* snapshot (a follow-up), so `renames` is always empty in v1.
|
|
19
|
-
* - `set-core` — add/remove always-on core leaves. Validates that every added
|
|
20
|
-
* leaf exists, previews the resulting always-on page count, and writes only
|
|
21
|
-
* on `--yes`.
|
|
22
11
|
* - `rebuild-index` — invalidate the v3 lanes so the next turn rebuilds.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
12
|
+
* - `backfill-sections` — one-time: embed every page's sections (including
|
|
13
|
+
* synthetic skill/CLI rows) into the dense store, then advance the maintain
|
|
14
|
+
* checkpoint. For the transition before A/B/cutover, since the collection
|
|
15
|
+
* starts empty and the incremental maintain pass only re-embeds deltas.
|
|
26
16
|
*/
|
|
27
17
|
|
|
28
18
|
import type { Command } from "commander";
|
|
29
19
|
|
|
30
20
|
import { cliIpcCall } from "../../ipc/cli-client.js";
|
|
31
21
|
import type {
|
|
32
|
-
|
|
22
|
+
MemoryV3BackfillSectionsResult,
|
|
33
23
|
MemoryV3RebuildIndexResult,
|
|
34
|
-
MemoryV3ReconcileResult,
|
|
35
|
-
MemoryV3SetCoreResult,
|
|
36
24
|
} from "../../runtime/routes/memory-v3-routes.js";
|
|
37
25
|
import { registerCommand } from "../lib/register-command.js";
|
|
38
26
|
import { log } from "../logger.js";
|
|
39
27
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
/**
|
|
29
|
+
* IPC timeout for `backfill-sections`. The one-time full-corpus section embed
|
|
30
|
+
* runs every page's chunks through the embedder sequentially, which easily
|
|
31
|
+
* outlasts `cliIpcCall`'s default 60s — so we give it a generous 30-minute
|
|
32
|
+
* ceiling rather than report a spurious "Request timed out" while the assistant
|
|
33
|
+
* keeps working.
|
|
34
|
+
*/
|
|
35
|
+
const BACKFILL_IPC_TIMEOUT_MS = 30 * 60 * 1000;
|
|
45
36
|
|
|
46
37
|
export function registerMemoryV3Command(program: Command): void {
|
|
47
38
|
// Reuse an existing `memory` parent if some other registrar attached to it
|
|
@@ -54,97 +45,80 @@ export function registerMemoryV3Command(program: Command): void {
|
|
|
54
45
|
registerCommand(memory, {
|
|
55
46
|
name: "v3",
|
|
56
47
|
transport: "ipc",
|
|
57
|
-
description: "Memory v3
|
|
48
|
+
description: "Memory v3 live-lane maintenance (section-lane model)",
|
|
58
49
|
build: (v3) => {
|
|
59
50
|
v3.addHelpText(
|
|
60
51
|
"after",
|
|
61
52
|
`
|
|
62
|
-
The v3 memory subsystem
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fail-closed validates before committing, and invalidates the live lanes so
|
|
66
|
-
the next turn rebuilds from the reconciled state.
|
|
53
|
+
The v3 memory subsystem retrieves concept pages over section-grain lanes and
|
|
54
|
+
caches them as live shadow lanes inside the assistant. These commands maintain
|
|
55
|
+
that live state safely.
|
|
67
56
|
|
|
68
57
|
Examples:
|
|
69
|
-
$ assistant memory v3
|
|
70
|
-
$ assistant memory v3
|
|
71
|
-
$ assistant memory v3 set-core --remove domain-a/topic-y --yes
|
|
72
|
-
$ assistant memory v3 reconcile
|
|
73
|
-
$ assistant memory v3 rebuild-index`,
|
|
58
|
+
$ assistant memory v3 rebuild-index
|
|
59
|
+
$ assistant memory v3 backfill-sections`,
|
|
74
60
|
);
|
|
75
61
|
|
|
76
|
-
// ──
|
|
62
|
+
// ── rebuild-index ─────────────────────────────────────────────────────
|
|
77
63
|
|
|
78
|
-
v3.command("
|
|
79
|
-
.description("
|
|
80
|
-
.option("--json", "Emit raw JSON instead of the rendered report")
|
|
64
|
+
v3.command("rebuild-index")
|
|
65
|
+
.description("Invalidate the v3 lanes so the next turn rebuilds")
|
|
81
66
|
.addHelpText(
|
|
82
67
|
"after",
|
|
83
68
|
`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"all green" when no maintenance is warranted.
|
|
69
|
+
Drops the assistant's cached v3 shadow lanes so the section index is rebuilt
|
|
70
|
+
from the current on-disk state on the next turn. Useful after editing concept
|
|
71
|
+
pages out-of-band.
|
|
88
72
|
|
|
89
73
|
Examples:
|
|
90
|
-
$ assistant memory v3
|
|
91
|
-
$ assistant memory v3 health --json | jq '.counts'`,
|
|
74
|
+
$ assistant memory v3 rebuild-index`,
|
|
92
75
|
)
|
|
93
|
-
.action(async (
|
|
94
|
-
const result = await cliIpcCall<
|
|
95
|
-
"
|
|
76
|
+
.action(async () => {
|
|
77
|
+
const result = await cliIpcCall<MemoryV3RebuildIndexResult>(
|
|
78
|
+
"memory_v3_rebuild_index",
|
|
96
79
|
{ body: {} },
|
|
97
80
|
);
|
|
98
81
|
if (!result.ok) {
|
|
99
|
-
log.error(result.error ?? "Failed to
|
|
82
|
+
log.error(result.error ?? "Failed to invalidate the v3 lanes");
|
|
100
83
|
process.exitCode = 1;
|
|
101
84
|
return;
|
|
102
85
|
}
|
|
103
|
-
|
|
104
|
-
if (opts.json === true) {
|
|
105
|
-
log.info(JSON.stringify(payload, null, 2));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
log.info(
|
|
109
|
-
payload.rendered === ""
|
|
110
|
-
? "memory-v3 health: all green"
|
|
111
|
-
: payload.rendered,
|
|
112
|
-
);
|
|
86
|
+
log.info("v3 lanes invalidated; next turn will rebuild.");
|
|
113
87
|
});
|
|
114
88
|
|
|
115
|
-
// ──
|
|
89
|
+
// ── backfill-sections ─────────────────────────────────────────────────
|
|
116
90
|
|
|
117
|
-
v3.command("
|
|
91
|
+
v3.command("backfill-sections")
|
|
118
92
|
.description(
|
|
119
|
-
"
|
|
93
|
+
"One-time: embed every page's sections into the dense store (incl skills/CLI)",
|
|
120
94
|
)
|
|
121
95
|
.option("--json", "Emit raw JSON instead of a formatted summary")
|
|
122
96
|
.addHelpText(
|
|
123
97
|
"after",
|
|
124
98
|
`
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
99
|
+
Embeds EVERY concept page's sections — including synthetic skill and CLI
|
|
100
|
+
capability rows — into the section dense store in one pass, then advances
|
|
101
|
+
the maintain checkpoint so the next incremental pass only re-embeds future
|
|
102
|
+
edits. Use this once on an existing install before the section-lane A/B and
|
|
103
|
+
cutover: the dense collection starts empty, and the periodic maintenance
|
|
104
|
+
pass only re-embeds pages edited since its last run (and never the synthetic
|
|
105
|
+
rows), so most of the corpus would otherwise never be embedded.
|
|
130
106
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
so renames always report as empty and a true rename surfaces as a
|
|
134
|
-
delete + add. Full rename/move/split detection requires persisting a prior
|
|
135
|
-
leaf snapshot and is a planned follow-up.
|
|
107
|
+
Idempotent and safe to re-run. Runs inside the assistant so it uses the live
|
|
108
|
+
configuration and advances the checkpoint the assistant reads.
|
|
136
109
|
|
|
137
110
|
Examples:
|
|
138
|
-
$ assistant memory v3
|
|
139
|
-
$ assistant memory v3
|
|
111
|
+
$ assistant memory v3 backfill-sections
|
|
112
|
+
$ assistant memory v3 backfill-sections --json | jq '.sections'`,
|
|
140
113
|
)
|
|
141
114
|
.action(async (opts: { json?: boolean }) => {
|
|
142
|
-
const result = await cliIpcCall<
|
|
143
|
-
"
|
|
115
|
+
const result = await cliIpcCall<MemoryV3BackfillSectionsResult>(
|
|
116
|
+
"memory_v3_backfill_sections",
|
|
144
117
|
{ body: {} },
|
|
118
|
+
{ timeoutMs: BACKFILL_IPC_TIMEOUT_MS },
|
|
145
119
|
);
|
|
146
120
|
if (!result.ok) {
|
|
147
|
-
log.error(result.error ?? "Failed to
|
|
121
|
+
log.error(result.error ?? "Failed to backfill section embeddings");
|
|
148
122
|
process.exitCode = 1;
|
|
149
123
|
return;
|
|
150
124
|
}
|
|
@@ -154,127 +128,11 @@ Examples:
|
|
|
154
128
|
return;
|
|
155
129
|
}
|
|
156
130
|
log.info(
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
log.info(
|
|
163
|
-
`Deleted: ${payload.deleted.length === 0 ? "none" : payload.deleted.join(", ")}`,
|
|
131
|
+
`Embedded ${payload.sections} sections across ${payload.articles} pages` +
|
|
132
|
+
(payload.failures > 0
|
|
133
|
+
? ` (${payload.failures} page(s) failed — see assistant logs).`
|
|
134
|
+
: "."),
|
|
164
135
|
);
|
|
165
|
-
log.info(
|
|
166
|
-
`Pruned core: ${payload.prunedCore.length === 0 ? "none" : payload.prunedCore.join(", ")}`,
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// ── set-core ──────────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
v3.command("set-core")
|
|
173
|
-
.description(
|
|
174
|
-
"Add/remove always-on core leaves (previews cost; writes on --yes)",
|
|
175
|
-
)
|
|
176
|
-
.option(
|
|
177
|
-
"--add <leaf>",
|
|
178
|
-
"Leaf path to add to the always-on core set (repeatable)",
|
|
179
|
-
collect,
|
|
180
|
-
[] as string[],
|
|
181
|
-
)
|
|
182
|
-
.option(
|
|
183
|
-
"--remove <leaf>",
|
|
184
|
-
"Leaf path to remove from the always-on core set (repeatable)",
|
|
185
|
-
collect,
|
|
186
|
-
[] as string[],
|
|
187
|
-
)
|
|
188
|
-
.option("--yes", "Apply the change (otherwise preview only)")
|
|
189
|
-
.option("--json", "Emit raw JSON instead of a formatted summary")
|
|
190
|
-
.addHelpText(
|
|
191
|
-
"after",
|
|
192
|
-
`
|
|
193
|
-
Edits the always-on core leaf set in core.json. Every --add entry must
|
|
194
|
-
exist in the live tree (unknown leaves are rejected). Without --yes this
|
|
195
|
-
PREVIEWS the resulting core set and the number of unique page slugs it
|
|
196
|
-
would pin always-on, WITHOUT writing. Pass --yes to persist and invalidate
|
|
197
|
-
the lanes.
|
|
198
|
-
|
|
199
|
-
Examples:
|
|
200
|
-
$ assistant memory v3 set-core --add domain-a/topic-x # preview
|
|
201
|
-
$ assistant memory v3 set-core --add domain-a/topic-x --yes # apply
|
|
202
|
-
$ assistant memory v3 set-core --remove domain-b/topic-z --yes`,
|
|
203
|
-
)
|
|
204
|
-
.action(
|
|
205
|
-
async (opts: {
|
|
206
|
-
add: string[];
|
|
207
|
-
remove: string[];
|
|
208
|
-
yes?: boolean;
|
|
209
|
-
json?: boolean;
|
|
210
|
-
}) => {
|
|
211
|
-
if (opts.add.length === 0 && opts.remove.length === 0) {
|
|
212
|
-
log.error("Pass at least one --add or --remove leaf path.");
|
|
213
|
-
process.exitCode = 1;
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const result = await cliIpcCall<MemoryV3SetCoreResult>(
|
|
218
|
-
"memory_v3_set_core",
|
|
219
|
-
{
|
|
220
|
-
body: {
|
|
221
|
-
add: opts.add,
|
|
222
|
-
remove: opts.remove,
|
|
223
|
-
write: opts.yes === true,
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
);
|
|
227
|
-
if (!result.ok) {
|
|
228
|
-
log.error(result.error ?? "Failed to update the v3 core set");
|
|
229
|
-
process.exitCode = 1;
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const payload = result.result!;
|
|
233
|
-
if (opts.json === true) {
|
|
234
|
-
log.info(JSON.stringify(payload, null, 2));
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
log.info(
|
|
238
|
-
`Core leaves (${payload.nextCore.length}): ${
|
|
239
|
-
payload.nextCore.length === 0
|
|
240
|
-
? "none"
|
|
241
|
-
: payload.nextCore.join(", ")
|
|
242
|
-
}`,
|
|
243
|
-
);
|
|
244
|
-
log.info(`Always-on page count: ${payload.alwaysOnPageCount}`);
|
|
245
|
-
if (payload.written) {
|
|
246
|
-
log.info("Written to core.json; lanes invalidated.");
|
|
247
|
-
} else {
|
|
248
|
-
log.info("Preview only — re-run with --yes to apply.");
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
// ── rebuild-index ─────────────────────────────────────────────────────
|
|
254
|
-
|
|
255
|
-
v3.command("rebuild-index")
|
|
256
|
-
.description("Invalidate the v3 lanes so the next turn rebuilds")
|
|
257
|
-
.addHelpText(
|
|
258
|
-
"after",
|
|
259
|
-
`
|
|
260
|
-
Drops the daemon's cached v3 shadow lanes so the tree + needle are rebuilt
|
|
261
|
-
from the current on-disk state on the next turn. Useful after editing leaf
|
|
262
|
-
files or assignments out-of-band.
|
|
263
|
-
|
|
264
|
-
Examples:
|
|
265
|
-
$ assistant memory v3 rebuild-index`,
|
|
266
|
-
)
|
|
267
|
-
.action(async () => {
|
|
268
|
-
const result = await cliIpcCall<MemoryV3RebuildIndexResult>(
|
|
269
|
-
"memory_v3_rebuild_index",
|
|
270
|
-
{ body: {} },
|
|
271
|
-
);
|
|
272
|
-
if (!result.ok) {
|
|
273
|
-
log.error(result.error ?? "Failed to invalidate the v3 lanes");
|
|
274
|
-
process.exitCode = 1;
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
log.info("v3 lanes invalidated; next turn will rebuild.");
|
|
278
136
|
});
|
|
279
137
|
},
|
|
280
138
|
});
|
|
@@ -25,7 +25,6 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
|
|
|
25
25
|
profile: "cost-optimized",
|
|
26
26
|
contextWindow: { maxInputTokens: 1000000 },
|
|
27
27
|
},
|
|
28
|
-
memoryV3RouteL1: { profile: "balanced", temperature: 0 },
|
|
29
28
|
memoryV3SelectL2: { profile: "balanced", temperature: 0 },
|
|
30
29
|
recall: {
|
|
31
30
|
profile: "balanced",
|
|
@@ -78,6 +78,15 @@ export function resolveCallSiteConfig(
|
|
|
78
78
|
): z.infer<typeof LLMConfigBase> {
|
|
79
79
|
const layers: Mergeable[] = [llm.default as Mergeable];
|
|
80
80
|
|
|
81
|
+
// Effective logit-bias preset, tracked outside the deep-merge so it ties to
|
|
82
|
+
// the single highest-precedence *profile* that wins resolution rather than
|
|
83
|
+
// inheriting from a lower one. Profile layers are appended low→high, and each
|
|
84
|
+
// one fully determines the preset (a profile that omits `logitBias` clears
|
|
85
|
+
// any value a lower-precedence profile set), so the last profile appended
|
|
86
|
+
// wins — matching the merge's own precedence and including the implicit
|
|
87
|
+
// call-site default selected by `effectiveDefault`.
|
|
88
|
+
const biasRef: LogitBiasRef = { preset: undefined };
|
|
89
|
+
|
|
81
90
|
const activeFragment = resolveProfileFragment(llm.activeProfile, llm, opts);
|
|
82
91
|
const overrideFragment = resolveProfileFragment(
|
|
83
92
|
opts.overrideProfile,
|
|
@@ -89,18 +98,32 @@ export function resolveCallSiteConfig(
|
|
|
89
98
|
effectiveDefault(callSite, llm, opts.overrideProfile != null);
|
|
90
99
|
|
|
91
100
|
if (callSite === "mainAgent") {
|
|
92
|
-
appendCallSiteLayers(layers, callSite, llm, site, opts);
|
|
93
|
-
appendProfileLayer(layers, activeFragment);
|
|
94
|
-
appendProfileLayer(layers, overrideFragment);
|
|
101
|
+
appendCallSiteLayers(layers, callSite, llm, site, opts, biasRef);
|
|
102
|
+
appendProfileLayer(layers, activeFragment, biasRef);
|
|
103
|
+
appendProfileLayer(layers, overrideFragment, biasRef);
|
|
95
104
|
} else {
|
|
96
|
-
appendProfileLayer(layers, activeFragment);
|
|
97
|
-
appendProfileLayer(layers, overrideFragment);
|
|
98
|
-
appendCallSiteLayers(layers, callSite, llm, site, opts);
|
|
105
|
+
appendProfileLayer(layers, activeFragment, biasRef);
|
|
106
|
+
appendProfileLayer(layers, overrideFragment, biasRef);
|
|
107
|
+
appendCallSiteLayers(layers, callSite, llm, site, opts, biasRef);
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
const resolved = finalize(
|
|
111
|
+
deepMerge(...layers.map(withImpliedProviderForKnownModel)),
|
|
112
|
+
);
|
|
113
|
+
// `logitBias` is profile-scoped: the winning profile is its only source.
|
|
114
|
+
// Overwrite — or clear — whatever the deep-merge may have copied from a
|
|
115
|
+
// non-profile layer (`llm.default` or a call-site fragment), so a preset set
|
|
116
|
+
// outside a profile can't apply to a profile that didn't opt in.
|
|
117
|
+
if (biasRef.preset !== undefined) {
|
|
118
|
+
resolved.logitBias = biasRef.preset;
|
|
119
|
+
} else {
|
|
120
|
+
delete (resolved as { logitBias?: unknown }).logitBias;
|
|
121
|
+
}
|
|
122
|
+
return resolved;
|
|
102
123
|
}
|
|
103
124
|
|
|
125
|
+
type LogitBiasRef = { preset: ProfileEntry["logitBias"] };
|
|
126
|
+
|
|
104
127
|
// ---------------------------------------------------------------------------
|
|
105
128
|
// Internal helpers
|
|
106
129
|
// ---------------------------------------------------------------------------
|
|
@@ -261,8 +284,10 @@ function withImpliedProviderForKnownModel(source: Mergeable): Mergeable {
|
|
|
261
284
|
function appendProfileLayer(
|
|
262
285
|
layers: Mergeable[],
|
|
263
286
|
profile: ProfileEntry | undefined,
|
|
287
|
+
biasRef: LogitBiasRef,
|
|
264
288
|
): void {
|
|
265
289
|
if (profile != null) {
|
|
290
|
+
biasRef.preset = profile.logitBias;
|
|
266
291
|
layers.push(profileConfigFragment(profile));
|
|
267
292
|
}
|
|
268
293
|
}
|
|
@@ -273,6 +298,7 @@ function appendCallSiteLayers(
|
|
|
273
298
|
llm: z.infer<typeof LLMSchema>,
|
|
274
299
|
site: z.infer<typeof LLMSchema>["callSites"][LLMCallSite] | undefined,
|
|
275
300
|
opts: ResolveCallSiteOpts,
|
|
301
|
+
biasRef: LogitBiasRef,
|
|
276
302
|
): void {
|
|
277
303
|
if (site != null) {
|
|
278
304
|
if (site.profile != null) {
|
|
@@ -286,6 +312,7 @@ function appendCallSiteLayers(
|
|
|
286
312
|
`LLM call site "${callSite}" references undefined profile "${site.profile}"`,
|
|
287
313
|
);
|
|
288
314
|
}
|
|
315
|
+
biasRef.preset = profileFragment.logitBias;
|
|
289
316
|
layers.push(profileConfigFragment(profileFragment));
|
|
290
317
|
}
|
|
291
318
|
// Strip the `profile` discriminator before merging — it isn't a
|
|
@@ -304,6 +331,11 @@ function profileConfigFragment(profile: ProfileEntry): Mergeable {
|
|
|
304
331
|
// profile before this point), but strip it defensively so it can never
|
|
305
332
|
// leak into the merged `LLMConfigBase`.
|
|
306
333
|
mix: _mix,
|
|
334
|
+
// `logitBias` is profile-identity metadata, not inheritable config: a
|
|
335
|
+
// preset must apply only to the profile that opted in, never bleed from a
|
|
336
|
+
// lower-precedence (e.g. active) profile into one that merely inherited it.
|
|
337
|
+
// `RetryProvider` resolves it from the applied profile, not the merge.
|
|
338
|
+
logitBias: _logitBias,
|
|
307
339
|
...config
|
|
308
340
|
} = profile;
|
|
309
341
|
return config as Mergeable;
|
|
@@ -7,19 +7,35 @@ describe("MemoryV3ConfigSchema", () => {
|
|
|
7
7
|
const parsed = MemoryV3ConfigSchema.parse({});
|
|
8
8
|
expect(parsed).toEqual({
|
|
9
9
|
workingSet: { maxPages: 150, evictWindow: 5 },
|
|
10
|
-
|
|
10
|
+
needleK: 100,
|
|
11
|
+
denseK: 100,
|
|
12
|
+
edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
|
|
11
13
|
});
|
|
12
14
|
});
|
|
13
15
|
|
|
14
|
-
test("accepts
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
16
|
+
test("accepts explicit lane-K overrides", () => {
|
|
17
|
+
const parsed = MemoryV3ConfigSchema.parse({ needleK: 50, denseK: 75 });
|
|
18
|
+
expect(parsed.needleK).toBe(50);
|
|
19
|
+
expect(parsed.denseK).toBe(75);
|
|
18
20
|
});
|
|
19
21
|
|
|
20
|
-
test("
|
|
21
|
-
|
|
22
|
-
expect(
|
|
23
|
-
|
|
22
|
+
test("accepts a partial edge override, defaulting the rest", () => {
|
|
23
|
+
const parsed = MemoryV3ConfigSchema.parse({ edge: { hubDegree: 10 } });
|
|
24
|
+
expect(parsed.edge).toEqual({
|
|
25
|
+
hubDegree: 10,
|
|
26
|
+
seedCount: 18,
|
|
27
|
+
perSeed: 6,
|
|
28
|
+
cap: 45,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("rejects non-positive or non-integer lane knobs", () => {
|
|
33
|
+
expect(() => MemoryV3ConfigSchema.parse({ needleK: 0 })).toThrow();
|
|
34
|
+
expect(() => MemoryV3ConfigSchema.parse({ denseK: -4 })).toThrow();
|
|
35
|
+
expect(() => MemoryV3ConfigSchema.parse({ needleK: 1.5 })).toThrow();
|
|
36
|
+
expect(() =>
|
|
37
|
+
MemoryV3ConfigSchema.parse({ edge: { perSeed: 0 } }),
|
|
38
|
+
).toThrow();
|
|
39
|
+
expect(() => MemoryV3ConfigSchema.parse({ edge: { cap: -1 } })).toThrow();
|
|
24
40
|
});
|
|
25
41
|
});
|
|
@@ -121,13 +121,6 @@ const CATALOG_RECORD: CatalogRecord = {
|
|
|
121
121
|
"Selects which concept pages to inject for the next agent turn by routing over a cached page index.",
|
|
122
122
|
domain: "memory",
|
|
123
123
|
},
|
|
124
|
-
memoryV3RouteL1: {
|
|
125
|
-
id: "memoryV3RouteL1",
|
|
126
|
-
displayName: "Memory V3 L1 Router",
|
|
127
|
-
description:
|
|
128
|
-
"Picks which leaves of the topic tree to open for the next agent turn by routing over a cache-warm static leaf block.",
|
|
129
|
-
domain: "memory",
|
|
130
|
-
},
|
|
131
124
|
memoryV3SelectL2: {
|
|
132
125
|
id: "memoryV3SelectL2",
|
|
133
126
|
displayName: "Memory V3 L2 Selector",
|
|
@@ -50,7 +50,6 @@ export const LLMCallSiteEnum = z.enum([
|
|
|
50
50
|
"memoryV2Migration",
|
|
51
51
|
"memoryV2Sweep",
|
|
52
52
|
"memoryRouter",
|
|
53
|
-
"memoryV3RouteL1",
|
|
54
53
|
"memoryV3SelectL2",
|
|
55
54
|
"memoryV2Consolidation",
|
|
56
55
|
"memoryRetrospective",
|
|
@@ -124,6 +123,17 @@ const VerbosityEnum = z.enum(["low", "medium", "high"]);
|
|
|
124
123
|
const ModelSchema = z.string().min(1);
|
|
125
124
|
const MaxTokensSchema = z.number().int().positive();
|
|
126
125
|
const TemperatureSchema = z.number().min(0).max(2).nullable();
|
|
126
|
+
// Named, code-resolved logit-bias preset a profile may opt into. The value is a
|
|
127
|
+
// preset *name*, not an inline token→bias map, so the workspace config stays
|
|
128
|
+
// small. This is profile-identity metadata, not inheritable config: the resolver
|
|
129
|
+
// strips it from the deep-merge and re-attaches it from the winning profile (see
|
|
130
|
+
// `profileConfigFragment` / `resolveCallSiteConfig`), and `RetryProvider`
|
|
131
|
+
// resolves it to a `logit_bias` map at request time, forwarded only on the
|
|
132
|
+
// Fireworks (OpenAI-compatible) path. Keep these literals in sync with the
|
|
133
|
+
// presets handled by `resolveLogitBiasPreset` in
|
|
134
|
+
// `providers/inference/logit-bias.ts` (kept separate to avoid a schema →
|
|
135
|
+
// provider import cycle).
|
|
136
|
+
const LogitBiasPresetSchema = z.enum(["suppress-cjk"]);
|
|
127
137
|
|
|
128
138
|
// ---------------------------------------------------------------------------
|
|
129
139
|
// Thinking & ContextWindow
|
|
@@ -316,6 +326,11 @@ export const LLMConfigBase = z.object({
|
|
|
316
326
|
thinking: ThinkingSchema.default(ThinkingSchema.parse({})),
|
|
317
327
|
contextWindow: ContextWindowSchema.default(ContextWindowSchema.parse({})),
|
|
318
328
|
openrouter: OpenRouterSchema.default(OpenRouterSchema.parse({})),
|
|
329
|
+
// Not deep-merged like the other fields: `resolveCallSiteConfig` sets this
|
|
330
|
+
// from the single highest-precedence profile that won resolution (see
|
|
331
|
+
// `profileConfigFragment`, which strips it from the merge), so a preset
|
|
332
|
+
// can't bleed from a lower-precedence profile into one that didn't opt in.
|
|
333
|
+
logitBias: LogitBiasPresetSchema.optional(),
|
|
319
334
|
});
|
|
320
335
|
export type LLMConfigBase = z.infer<typeof LLMConfigBase>;
|
|
321
336
|
|
|
@@ -336,6 +351,7 @@ const LLMConfigFragment = z.object({
|
|
|
336
351
|
thinking: ThinkingFragmentSchema.optional(),
|
|
337
352
|
contextWindow: ContextWindowDeepPartialSchema.optional(),
|
|
338
353
|
openrouter: OpenRouterDeepPartialSchema.optional(),
|
|
354
|
+
logitBias: LogitBiasPresetSchema.optional(),
|
|
339
355
|
});
|
|
340
356
|
type LLMConfigFragment = z.infer<typeof LLMConfigFragment>;
|
|
341
357
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Memory v3 (
|
|
4
|
+
* Memory v3 (section-grain lane retrieval) working-set configuration.
|
|
5
5
|
*
|
|
6
6
|
* The working set is the per-conversation set of concept pages carried
|
|
7
7
|
* forward across turns. Eviction keeps it bounded: pages unseen for longer
|
|
@@ -28,20 +28,70 @@ export const MemoryV3WorkingSetSchema = z
|
|
|
28
28
|
})
|
|
29
29
|
.describe("Memory v3 working-set retention/eviction tuning.");
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Edge-lane tuning for the link-graph expansion that folds a turn's lexical and
|
|
33
|
+
* dense seeds outward to their first-class neighbours.
|
|
34
|
+
*/
|
|
35
|
+
export const MemoryV3EdgeSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
hubDegree: z
|
|
38
|
+
.number({ error: "memory.v3.edge.hubDegree must be a number" })
|
|
39
|
+
.int("memory.v3.edge.hubDegree must be an integer")
|
|
40
|
+
.positive("memory.v3.edge.hubDegree must be a positive integer")
|
|
41
|
+
.default(30)
|
|
42
|
+
.describe(
|
|
43
|
+
"In-degree above which an article is treated as a hub and excluded from edge expansion (too generic to be a useful surface).",
|
|
44
|
+
),
|
|
45
|
+
seedCount: z
|
|
46
|
+
.number({ error: "memory.v3.edge.seedCount must be a number" })
|
|
47
|
+
.int("memory.v3.edge.seedCount must be an integer")
|
|
48
|
+
.positive("memory.v3.edge.seedCount must be a positive integer")
|
|
49
|
+
.default(18)
|
|
50
|
+
.describe(
|
|
51
|
+
"Number of top needle+dense seeds (in rank order) expanded by the edge lane.",
|
|
52
|
+
),
|
|
53
|
+
perSeed: z
|
|
54
|
+
.number({ error: "memory.v3.edge.perSeed must be a number" })
|
|
55
|
+
.int("memory.v3.edge.perSeed must be an integer")
|
|
56
|
+
.positive("memory.v3.edge.perSeed must be a positive integer")
|
|
57
|
+
.default(6)
|
|
58
|
+
.describe("Maximum neighbours surfaced per expanded seed."),
|
|
59
|
+
cap: z
|
|
60
|
+
.number({ error: "memory.v3.edge.cap must be a number" })
|
|
61
|
+
.int("memory.v3.edge.cap must be an integer")
|
|
62
|
+
.positive("memory.v3.edge.cap must be a positive integer")
|
|
63
|
+
.default(45)
|
|
64
|
+
.describe(
|
|
65
|
+
"Hard cap on the total number of distinct articles surfaced by the edge lane.",
|
|
66
|
+
),
|
|
67
|
+
})
|
|
68
|
+
.describe("Memory v3 edge-lane (link-graph expansion) tuning.");
|
|
69
|
+
|
|
31
70
|
export const MemoryV3ConfigSchema = z
|
|
32
71
|
.object({
|
|
33
72
|
workingSet: MemoryV3WorkingSetSchema.default(
|
|
34
73
|
MemoryV3WorkingSetSchema.parse({}),
|
|
35
74
|
),
|
|
36
|
-
|
|
37
|
-
.number({ error: "memory.v3.
|
|
38
|
-
.int("memory.v3.
|
|
39
|
-
.positive("memory.v3.
|
|
40
|
-
.default(
|
|
75
|
+
needleK: z
|
|
76
|
+
.number({ error: "memory.v3.needleK must be a number" })
|
|
77
|
+
.int("memory.v3.needleK must be an integer")
|
|
78
|
+
.positive("memory.v3.needleK must be a positive integer")
|
|
79
|
+
.default(100)
|
|
80
|
+
.describe(
|
|
81
|
+
"Number of section-grain BM25 needle articles folded into the candidate pool each turn.",
|
|
82
|
+
),
|
|
83
|
+
denseK: z
|
|
84
|
+
.number({ error: "memory.v3.denseK must be a number" })
|
|
85
|
+
.int("memory.v3.denseK must be an integer")
|
|
86
|
+
.positive("memory.v3.denseK must be a positive integer")
|
|
87
|
+
.default(100)
|
|
41
88
|
.describe(
|
|
42
|
-
"
|
|
89
|
+
"Number of dense-lane articles folded into the candidate pool each turn.",
|
|
43
90
|
),
|
|
91
|
+
edge: MemoryV3EdgeSchema.default(MemoryV3EdgeSchema.parse({})),
|
|
44
92
|
})
|
|
45
|
-
.describe(
|
|
93
|
+
.describe(
|
|
94
|
+
"Memory v3 — section-grain lane retrieval with a carry-forward working set",
|
|
95
|
+
);
|
|
46
96
|
|
|
47
97
|
export type MemoryV3Config = z.infer<typeof MemoryV3ConfigSchema>;
|
|
@@ -74,6 +74,24 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
|
|
|
74
74
|
thinking: { enabled: false, streamThinking: false },
|
|
75
75
|
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
76
76
|
},
|
|
77
|
+
// Open-weight economy option: Kimi K2.6 served by Fireworks via managed
|
|
78
|
+
// platform inference. Carries the `suppress-cjk` logit-bias preset to
|
|
79
|
+
// discourage the model from spontaneously emitting Chinese in English
|
|
80
|
+
// output; the preset is profile-scoped and only forwarded on the Fireworks
|
|
81
|
+
// path (see `providers/inference/logit-bias.ts`).
|
|
82
|
+
"balanced-economy": {
|
|
83
|
+
intent: "balanced",
|
|
84
|
+
provider: "fireworks",
|
|
85
|
+
connectionName: "fireworks-managed",
|
|
86
|
+
source: "managed",
|
|
87
|
+
label: "Balanced Economy",
|
|
88
|
+
description: "Strong open model (Kimi K2.6) at a lower price point",
|
|
89
|
+
maxTokens: 16000,
|
|
90
|
+
effort: "high",
|
|
91
|
+
thinking: { enabled: true, streamThinking: true },
|
|
92
|
+
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
93
|
+
logitBias: "suppress-cjk",
|
|
94
|
+
},
|
|
77
95
|
};
|
|
78
96
|
|
|
79
97
|
/**
|