@vellumai/assistant 0.8.9-staging.2 → 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/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- 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/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- 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/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -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/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- 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/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- 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/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- 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,192 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
-
|
|
7
|
-
import { leavesOf, loadLeafTree, membersOf, resolveDataDir } from "../tree.js";
|
|
8
|
-
|
|
9
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const BUNDLED_DATA_DIR = join(HERE, "..", "data");
|
|
11
|
-
|
|
12
|
-
describe("loadLeafTree", () => {
|
|
13
|
-
test("loads leaves with frontmatter, description, and domain", async () => {
|
|
14
|
-
const tree = await loadLeafTree(BUNDLED_DATA_DIR);
|
|
15
|
-
|
|
16
|
-
expect([...tree.leaves.keys()].sort()).toEqual([
|
|
17
|
-
"domain-a/topic-x",
|
|
18
|
-
"domain-a/topic-y",
|
|
19
|
-
"domain-b/topic-z",
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
const topicX = tree.leaves.get("domain-a/topic-x");
|
|
23
|
-
expect(topicX).toBeDefined();
|
|
24
|
-
expect(topicX?.path).toBe("domain-a/topic-x");
|
|
25
|
-
expect(topicX?.frontmatter).toEqual({
|
|
26
|
-
path: "domain-a/topic-x",
|
|
27
|
-
in_core: true,
|
|
28
|
-
});
|
|
29
|
-
expect(topicX?.domain).toBe("domain-a");
|
|
30
|
-
expect(topicX?.description.length).toBeGreaterThan(0);
|
|
31
|
-
expect(topicX?.description.startsWith("---")).toBe(false);
|
|
32
|
-
|
|
33
|
-
const topicZ = tree.leaves.get("domain-b/topic-z");
|
|
34
|
-
expect(topicZ?.frontmatter.in_core).toBe(false);
|
|
35
|
-
expect(topicZ?.domain).toBe("domain-b");
|
|
36
|
-
|
|
37
|
-
// Bundled leaves predate the optional `id` field.
|
|
38
|
-
expect(topicX?.frontmatter.id).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("parses the optional stable id when present, omits it otherwise", async () => {
|
|
42
|
-
const dataDir = await mkdtemp(join(tmpdir(), "v3-leaf-id-"));
|
|
43
|
-
try {
|
|
44
|
-
await mkdir(join(dataDir, "leaves", "domain-a"), { recursive: true });
|
|
45
|
-
await writeFile(
|
|
46
|
-
join(dataDir, "leaves", "domain-a", "topic-x.md"),
|
|
47
|
-
"---\npath: domain-a/topic-x\nin_core: true\nid: leaf-123\n---\n\nWith id.\n",
|
|
48
|
-
);
|
|
49
|
-
await writeFile(
|
|
50
|
-
join(dataDir, "leaves", "domain-a", "topic-y.md"),
|
|
51
|
-
"---\npath: domain-a/topic-y\nin_core: false\n---\n\nNo id.\n",
|
|
52
|
-
);
|
|
53
|
-
await writeFile(
|
|
54
|
-
join(dataDir, "assignments.json"),
|
|
55
|
-
JSON.stringify({ "page-a": ["domain-a/topic-x"] }),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const tree = await loadLeafTree(dataDir);
|
|
59
|
-
expect(tree.leaves.get("domain-a/topic-x")?.frontmatter.id).toBe(
|
|
60
|
-
"leaf-123",
|
|
61
|
-
);
|
|
62
|
-
expect(
|
|
63
|
-
tree.leaves.get("domain-a/topic-y")?.frontmatter.id,
|
|
64
|
-
).toBeUndefined();
|
|
65
|
-
} finally {
|
|
66
|
-
await rm(dataDir, { recursive: true, force: true });
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("builds members and byPage from assignments", async () => {
|
|
71
|
-
const tree = await loadLeafTree(BUNDLED_DATA_DIR);
|
|
72
|
-
|
|
73
|
-
// byPage is the inverted assignment map.
|
|
74
|
-
expect(tree.byPage.get("page-a")).toEqual(["domain-a/topic-x"]);
|
|
75
|
-
expect(tree.byPage.get("page-b")).toEqual([
|
|
76
|
-
"domain-a/topic-x",
|
|
77
|
-
"domain-a/topic-y",
|
|
78
|
-
]);
|
|
79
|
-
expect(tree.byPage.get("page-c")).toEqual(["domain-b/topic-z"]);
|
|
80
|
-
|
|
81
|
-
// members are the slugs assigned to each leaf.
|
|
82
|
-
expect(membersOf(tree, "domain-a/topic-x").sort()).toEqual([
|
|
83
|
-
"page-a",
|
|
84
|
-
"page-b",
|
|
85
|
-
]);
|
|
86
|
-
expect(membersOf(tree, "domain-a/topic-y")).toEqual(["page-b"]);
|
|
87
|
-
expect(membersOf(tree, "domain-b/topic-z")).toEqual(["page-c"]);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("membersOf and leavesOf return empty arrays for unknown keys", async () => {
|
|
91
|
-
const tree = await loadLeafTree(BUNDLED_DATA_DIR);
|
|
92
|
-
expect(membersOf(tree, "nope/missing")).toEqual([]);
|
|
93
|
-
expect(leavesOf(tree, "missing-slug")).toEqual([]);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("leavesOf mirrors byPage", async () => {
|
|
97
|
-
const tree = await loadLeafTree(BUNDLED_DATA_DIR);
|
|
98
|
-
expect(leavesOf(tree, "page-b")).toEqual([
|
|
99
|
-
"domain-a/topic-x",
|
|
100
|
-
"domain-a/topic-y",
|
|
101
|
-
]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("frontmatter pageLeaves win over assignments.json per page", async () => {
|
|
105
|
-
const tree = await loadLeafTree(
|
|
106
|
-
BUNDLED_DATA_DIR,
|
|
107
|
-
new Map([["page-a", ["domain-a/topic-y"]]]),
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// page-a is overridden by frontmatter; assignments.json said topic-x.
|
|
111
|
-
expect(leavesOf(tree, "page-a")).toEqual(["domain-a/topic-y"]);
|
|
112
|
-
expect(membersOf(tree, "domain-a/topic-y").sort()).toEqual([
|
|
113
|
-
"page-a",
|
|
114
|
-
"page-b",
|
|
115
|
-
]);
|
|
116
|
-
expect(membersOf(tree, "domain-a/topic-x")).toEqual(["page-b"]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("pages absent from pageLeaves fall back to assignments.json", async () => {
|
|
120
|
-
// page-a override + a page-with-empty-frontmatter that must fall back.
|
|
121
|
-
const tree = await loadLeafTree(
|
|
122
|
-
BUNDLED_DATA_DIR,
|
|
123
|
-
new Map([
|
|
124
|
-
["page-a", ["domain-a/topic-y"]],
|
|
125
|
-
["page-c", []],
|
|
126
|
-
]),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// page-b never appeared in pageLeaves → assignments.json.
|
|
130
|
-
expect(leavesOf(tree, "page-b")).toEqual([
|
|
131
|
-
"domain-a/topic-x",
|
|
132
|
-
"domain-a/topic-y",
|
|
133
|
-
]);
|
|
134
|
-
// page-c had an empty frontmatter array → falls back to assignments.json.
|
|
135
|
-
expect(leavesOf(tree, "page-c")).toEqual(["domain-b/topic-z"]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("omitting pageLeaves preserves assignments.json behavior", async () => {
|
|
139
|
-
const withMap = await loadLeafTree(BUNDLED_DATA_DIR, new Map());
|
|
140
|
-
const without = await loadLeafTree(BUNDLED_DATA_DIR);
|
|
141
|
-
|
|
142
|
-
// An empty map is equivalent to omitting it: pure assignments.json.
|
|
143
|
-
for (const slug of ["page-a", "page-b", "page-c"]) {
|
|
144
|
-
expect(leavesOf(withMap, slug)).toEqual(leavesOf(without, slug));
|
|
145
|
-
}
|
|
146
|
-
expect(leavesOf(without, "page-a")).toEqual(["domain-a/topic-x"]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("resolveDataDir", () => {
|
|
151
|
-
let tmpRoot: string;
|
|
152
|
-
const prevWorkspace = process.env.VELLUM_WORKSPACE_DIR;
|
|
153
|
-
|
|
154
|
-
beforeEach(async () => {
|
|
155
|
-
tmpRoot = await mkdtemp(join(tmpdir(), "v3-tree-"));
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
afterEach(async () => {
|
|
159
|
-
if (prevWorkspace === undefined) delete process.env.VELLUM_WORKSPACE_DIR;
|
|
160
|
-
else process.env.VELLUM_WORKSPACE_DIR = prevWorkspace;
|
|
161
|
-
await rm(tmpRoot, { recursive: true, force: true });
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("falls back to bundled stub when no workspace override exists", () => {
|
|
165
|
-
process.env.VELLUM_WORKSPACE_DIR = join(tmpRoot, "workspace");
|
|
166
|
-
expect(resolveDataDir()).toBe(BUNDLED_DATA_DIR);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("prefers workspace override when <workspace>/memory/v3/data exists", async () => {
|
|
170
|
-
const workspace = join(tmpRoot, "workspace");
|
|
171
|
-
const workspaceData = join(workspace, "memory", "v3", "data");
|
|
172
|
-
const leafDir = join(workspaceData, "leaves", "domain-w");
|
|
173
|
-
await mkdir(leafDir, { recursive: true });
|
|
174
|
-
await writeFile(
|
|
175
|
-
join(leafDir, "topic-real.md"),
|
|
176
|
-
"---\npath: domain-w/topic-real\nin_core: false\n---\n\nReal workspace leaf.\n",
|
|
177
|
-
);
|
|
178
|
-
await writeFile(
|
|
179
|
-
join(workspaceData, "assignments.json"),
|
|
180
|
-
JSON.stringify({ "real-page": ["domain-w/topic-real"] }),
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
process.env.VELLUM_WORKSPACE_DIR = workspace;
|
|
184
|
-
|
|
185
|
-
expect(resolveDataDir()).toBe(workspaceData);
|
|
186
|
-
|
|
187
|
-
const tree = await loadLeafTree(resolveDataDir());
|
|
188
|
-
expect([...tree.leaves.keys()]).toEqual(["domain-w/topic-real"]);
|
|
189
|
-
expect(membersOf(tree, "domain-w/topic-real")).toEqual(["real-page"]);
|
|
190
|
-
expect(leavesOf(tree, "real-page")).toEqual(["domain-w/topic-real"]);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v3 page→leaf classifier: assigns pages to leaf topics and UNIONS the result
|
|
3
|
-
* into each page's `leaves:` frontmatter.
|
|
4
|
-
*
|
|
5
|
-
* This ports an offline classifier into a daemon module. For each target page
|
|
6
|
-
* it runs a single tool-forced LLM call against the cache-stable numbered leaf
|
|
7
|
-
* index (system prompt) with the page content in the user message, maps the
|
|
8
|
-
* returned 1-based ids back to leaf paths, and writes back the UNION of the
|
|
9
|
-
* page's existing picks and the freshly classified picks.
|
|
10
|
-
*
|
|
11
|
-
* Recall-safe by construction:
|
|
12
|
-
* - UNION semantics never drop a page's existing leaf assignments.
|
|
13
|
-
* - A classification that fails (provider unavailable/throws, no tool use, or
|
|
14
|
-
* empty result) leaves the page's `leaves:` frontmatter UNCHANGED.
|
|
15
|
-
*
|
|
16
|
-
* Provider-injectable: pass `opts.provider` to bypass the configured-provider
|
|
17
|
-
* lookup (used by tests and callers that force a specific provider). When
|
|
18
|
-
* omitted, resolves the `memoryV3RouteL1` callsite provider.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
listPages,
|
|
23
|
-
readPage,
|
|
24
|
-
writePage,
|
|
25
|
-
} from "../../../memory/v2/page-store.js";
|
|
26
|
-
import type { ConceptPage } from "../../../memory/v2/types.js";
|
|
27
|
-
import {
|
|
28
|
-
extractToolUse,
|
|
29
|
-
getConfiguredProvider,
|
|
30
|
-
userMessage,
|
|
31
|
-
} from "../../../providers/provider-send-message.js";
|
|
32
|
-
import type { Provider, ToolDefinition } from "../../../providers/types.js";
|
|
33
|
-
import { getLogger } from "../../../util/logger.js";
|
|
34
|
-
import type { LeafPath, LeafTree, Slug } from "./types.js";
|
|
35
|
-
|
|
36
|
-
const log = getLogger("memory-v3-assign");
|
|
37
|
-
|
|
38
|
-
const ASSIGN_TOOL_NAME = "assign_leaves";
|
|
39
|
-
|
|
40
|
-
/** Default delay between classification calls to avoid hammering the provider. */
|
|
41
|
-
const DEFAULT_THROTTLE_MS = 200;
|
|
42
|
-
|
|
43
|
-
/** Description length cap in the cache-stable leaf index lines. */
|
|
44
|
-
const DESCRIPTION_CAP = 450;
|
|
45
|
-
|
|
46
|
-
/** Bounded output budget — the tool only emits a small id array. */
|
|
47
|
-
const MAX_TOKENS = 512;
|
|
48
|
-
|
|
49
|
-
export interface AssignPagesOptions {
|
|
50
|
-
/** The leaf tree the page is classified against. */
|
|
51
|
-
tree: LeafTree;
|
|
52
|
-
/** Workspace root; pages live under `<workspaceDir>/memory/concepts/`. */
|
|
53
|
-
workspaceDir: string;
|
|
54
|
-
/**
|
|
55
|
-
* Slugs to classify. When omitted, defaults to every page whose `leaves:`
|
|
56
|
-
* frontmatter is empty or missing (the unclassified set).
|
|
57
|
-
*/
|
|
58
|
-
slugs?: Slug[];
|
|
59
|
-
/**
|
|
60
|
-
* Provider override. When omitted, resolves the `memoryV3RouteL1` callsite
|
|
61
|
-
* provider once and reuses it across all pages.
|
|
62
|
-
*/
|
|
63
|
-
provider?: Provider;
|
|
64
|
-
/** Delay between calls in ms. Defaults to {@link DEFAULT_THROTTLE_MS}. */
|
|
65
|
-
throttleMs?: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Per-page classification outcome, for the job + health report to consume. */
|
|
69
|
-
export interface AssignPageResult {
|
|
70
|
-
slug: Slug;
|
|
71
|
-
/** The page's leaf assignments before this run. */
|
|
72
|
-
before: LeafPath[];
|
|
73
|
-
/** The page's leaf assignments after this run (== before when failed). */
|
|
74
|
-
after: LeafPath[];
|
|
75
|
-
/** True when classification failed and the page was left unchanged. */
|
|
76
|
-
failed: boolean;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Build the cache-stable system prompt: a numbered list of every leaf path with
|
|
81
|
-
* its (trimmed) description. Stable ordering (sorted by path) keeps the prompt
|
|
82
|
-
* prefix identical across calls so the provider can cache it.
|
|
83
|
-
*/
|
|
84
|
-
function buildLeafIndexSystemPrompt(
|
|
85
|
-
sortedLeafPaths: LeafPath[],
|
|
86
|
-
tree: LeafTree,
|
|
87
|
-
): string {
|
|
88
|
-
const lines = sortedLeafPaths.map((path, i) => {
|
|
89
|
-
const description = tree.leaves.get(path)?.description.trim() ?? "";
|
|
90
|
-
const summary = description.slice(0, DESCRIPTION_CAP);
|
|
91
|
-
return `[${i + 1}] ${path} — ${summary}`;
|
|
92
|
-
});
|
|
93
|
-
return [
|
|
94
|
-
"You are a memory classifier. Given a page's content, pick the leaf topics",
|
|
95
|
-
"the page belongs to. A page may belong to several leaves; pick every leaf",
|
|
96
|
-
"whose topic the page meaningfully covers. Return the leaf numbers via the",
|
|
97
|
-
"assign_leaves tool.",
|
|
98
|
-
"",
|
|
99
|
-
"Leaf index:",
|
|
100
|
-
...lines,
|
|
101
|
-
].join("\n");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const ASSIGN_TOOL: ToolDefinition = {
|
|
105
|
-
name: ASSIGN_TOOL_NAME,
|
|
106
|
-
description: "Return the leaf numbers this page belongs to.",
|
|
107
|
-
input_schema: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {
|
|
110
|
-
ids: {
|
|
111
|
-
type: "array",
|
|
112
|
-
items: { type: "integer" },
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/** Map 1-based ids to sorted leaf paths, dropping out-of-range/0/non-integer ids. */
|
|
119
|
-
function idsToLeafPaths(ids: unknown, sortedLeafPaths: LeafPath[]): LeafPath[] {
|
|
120
|
-
if (!Array.isArray(ids)) return [];
|
|
121
|
-
const out: LeafPath[] = [];
|
|
122
|
-
for (const id of ids) {
|
|
123
|
-
if (typeof id !== "number" || !Number.isInteger(id)) continue;
|
|
124
|
-
const idx = id - 1;
|
|
125
|
-
if (idx < 0 || idx >= sortedLeafPaths.length) continue;
|
|
126
|
-
const leafPath = sortedLeafPaths[idx];
|
|
127
|
-
if (!out.includes(leafPath)) out.push(leafPath);
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Stable union of two leaf-path lists (existing order first, then new). */
|
|
133
|
-
function unionLeafPaths(existing: LeafPath[], added: LeafPath[]): LeafPath[] {
|
|
134
|
-
const out = [...existing];
|
|
135
|
-
for (const leaf of added) {
|
|
136
|
-
if (!out.includes(leaf)) out.push(leaf);
|
|
137
|
-
}
|
|
138
|
-
return out;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** A page's existing `leaves:` frontmatter, or `[]` when empty/missing. */
|
|
142
|
-
function pageLeaves(page: ConceptPage): LeafPath[] {
|
|
143
|
-
return page.frontmatter.leaves ?? [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** The flat text a page contributes to the classifier user message. */
|
|
147
|
-
function pageClassifierInput(page: ConceptPage): string {
|
|
148
|
-
const { summary } = page.frontmatter;
|
|
149
|
-
return [summary ?? "", page.body]
|
|
150
|
-
.filter((s) => s.trim().length > 0)
|
|
151
|
-
.join("\n\n");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const sleep = (ms: number): Promise<void> =>
|
|
155
|
-
new Promise((resolve) => setTimeout(resolve, ms));
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Classify pages into leaf topics and UNION the result into each page's
|
|
159
|
-
* `leaves:` frontmatter. See the module docstring for the recall-safety
|
|
160
|
-
* guarantees.
|
|
161
|
-
*/
|
|
162
|
-
export async function assignPages(
|
|
163
|
-
opts: AssignPagesOptions,
|
|
164
|
-
): Promise<AssignPageResult[]> {
|
|
165
|
-
const { tree, workspaceDir } = opts;
|
|
166
|
-
const throttleMs = opts.throttleMs ?? DEFAULT_THROTTLE_MS;
|
|
167
|
-
|
|
168
|
-
const sortedLeafPaths = [...tree.leaves.keys()].sort((a, b) =>
|
|
169
|
-
a.localeCompare(b),
|
|
170
|
-
);
|
|
171
|
-
const systemPrompt = buildLeafIndexSystemPrompt(sortedLeafPaths, tree);
|
|
172
|
-
|
|
173
|
-
const targetSlugs = opts.slugs ?? (await unclassifiedSlugs(workspaceDir));
|
|
174
|
-
|
|
175
|
-
// Resolve the provider once and reuse it across pages.
|
|
176
|
-
const provider =
|
|
177
|
-
opts.provider ?? (await getConfiguredProvider("memoryV3RouteL1"));
|
|
178
|
-
|
|
179
|
-
const results: AssignPageResult[] = [];
|
|
180
|
-
for (let i = 0; i < targetSlugs.length; i++) {
|
|
181
|
-
const slug = targetSlugs[i];
|
|
182
|
-
const page = await readPage(workspaceDir, slug);
|
|
183
|
-
if (!page) {
|
|
184
|
-
log.warn({ slug }, "assign: page not found, skipping");
|
|
185
|
-
results.push({ slug, before: [], after: [], failed: true });
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const before = pageLeaves(page);
|
|
190
|
-
|
|
191
|
-
if (!provider) {
|
|
192
|
-
log.warn(
|
|
193
|
-
{ slug },
|
|
194
|
-
"assign: no provider available, leaving page unchanged",
|
|
195
|
-
);
|
|
196
|
-
results.push({ slug, before, after: before, failed: true });
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const classified = await classifyPage(
|
|
201
|
-
provider,
|
|
202
|
-
systemPrompt,
|
|
203
|
-
pageClassifierInput(page),
|
|
204
|
-
sortedLeafPaths,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
if (!classified) {
|
|
208
|
-
// Non-destructive: leave the page's existing picks untouched.
|
|
209
|
-
results.push({ slug, before, after: before, failed: true });
|
|
210
|
-
} else {
|
|
211
|
-
const after = unionLeafPaths(before, classified);
|
|
212
|
-
if (after.length !== before.length) {
|
|
213
|
-
await writePage(workspaceDir, {
|
|
214
|
-
...page,
|
|
215
|
-
frontmatter: { ...page.frontmatter, leaves: after },
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
results.push({ slug, before, after, failed: false });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (throttleMs > 0 && i < targetSlugs.length - 1) {
|
|
222
|
-
await sleep(throttleMs);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return results;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Pages whose `leaves:` frontmatter is empty or missing — the default targets. */
|
|
230
|
-
async function unclassifiedSlugs(workspaceDir: string): Promise<Slug[]> {
|
|
231
|
-
const slugs = await listPages(workspaceDir);
|
|
232
|
-
const unclassified: Slug[] = [];
|
|
233
|
-
for (const slug of slugs) {
|
|
234
|
-
const page = await readPage(workspaceDir, slug);
|
|
235
|
-
if (page && pageLeaves(page).length === 0) unclassified.push(slug);
|
|
236
|
-
}
|
|
237
|
-
return unclassified;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Run a single tool-forced classification call. Returns the classified leaf
|
|
242
|
-
* paths, or `null` when the call fails (throws, no tool use, or empty ids) so
|
|
243
|
-
* the caller can treat it as non-destructive.
|
|
244
|
-
*/
|
|
245
|
-
async function classifyPage(
|
|
246
|
-
provider: Provider,
|
|
247
|
-
systemPrompt: string,
|
|
248
|
-
pageInput: string,
|
|
249
|
-
sortedLeafPaths: LeafPath[],
|
|
250
|
-
): Promise<LeafPath[] | null> {
|
|
251
|
-
let response;
|
|
252
|
-
try {
|
|
253
|
-
response = await provider.sendMessage([userMessage(pageInput)], {
|
|
254
|
-
systemPrompt,
|
|
255
|
-
tools: [ASSIGN_TOOL],
|
|
256
|
-
config: {
|
|
257
|
-
callSite: "memoryV3RouteL1",
|
|
258
|
-
maxTokens: MAX_TOKENS,
|
|
259
|
-
tool_choice: { type: "tool", name: ASSIGN_TOOL_NAME },
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
} catch (err) {
|
|
263
|
-
log.warn({ err }, "assign: classification provider call failed");
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const toolUse = extractToolUse(response);
|
|
268
|
-
if (!toolUse || toolUse.name !== ASSIGN_TOOL_NAME) return null;
|
|
269
|
-
|
|
270
|
-
const leafPaths = idsToLeafPaths(toolUse.input.ids, sortedLeafPaths);
|
|
271
|
-
return leafPaths.length > 0 ? leafPaths : null;
|
|
272
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { LeafPath } from "./types.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Loads the always-on core leaf set from `<dataDir>/core.json`.
|
|
8
|
-
*
|
|
9
|
-
* The caller is responsible for resolving `dataDir` (including any
|
|
10
|
-
* workspace-local override — see `resolveDataDir` in `tree.ts`). A missing
|
|
11
|
-
* `core.json` yields an empty set (new workspaces may have no curated core).
|
|
12
|
-
*/
|
|
13
|
-
export async function loadCore(dataDir: string): Promise<Set<LeafPath>> {
|
|
14
|
-
const path = join(dataDir, "core.json");
|
|
15
|
-
let raw: string;
|
|
16
|
-
try {
|
|
17
|
-
raw = await readFile(path, "utf8");
|
|
18
|
-
} catch (err) {
|
|
19
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
20
|
-
return new Set();
|
|
21
|
-
}
|
|
22
|
-
throw err;
|
|
23
|
-
}
|
|
24
|
-
const parsed = JSON.parse(raw) as { alwaysOn?: LeafPath[] };
|
|
25
|
-
return new Set(parsed.alwaysOn ?? []);
|
|
26
|
-
}
|
|
Binary file
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import type { LeafTree, Slug } from "./types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sparse "needle" arm for memory-v3 routing: a lexical BM25 search over page
|
|
5
|
-
* titles, slug tokens, summaries, and the labels of the leaves each page
|
|
6
|
-
* belongs to. It augments topical (tree) routing by surfacing pages that share
|
|
7
|
-
* a literal term with the query even when topical routing would miss them.
|
|
8
|
-
*
|
|
9
|
-
* Implementation notes:
|
|
10
|
-
* - Hand-rolled Okapi BM25 (no dependency). The corpus is bounded (one doc per
|
|
11
|
-
* page), so a plain inverted index is plenty.
|
|
12
|
-
* - The index is built once and held in memory. The orchestrator/plugin
|
|
13
|
-
* lazy-inits it. Rebuilding on consolidation is a documented fast-follow and
|
|
14
|
-
* intentionally out of scope here.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** Okapi BM25 term-frequency saturation parameter. */
|
|
18
|
-
export const k1 = 1.5;
|
|
19
|
-
/** Okapi BM25 length-normalization parameter. */
|
|
20
|
-
export const b = 0.75;
|
|
21
|
-
|
|
22
|
-
export interface NeedleIndex {
|
|
23
|
-
/** Returns up to `k` slugs ranked by BM25 score (descending, ties by slug). */
|
|
24
|
-
query(text: string, k: number): Slug[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Lowercase, split on non-alphanumeric, drop empties. */
|
|
28
|
-
function tokenize(text: string): string[] {
|
|
29
|
-
return text
|
|
30
|
-
.toLowerCase()
|
|
31
|
-
.split(/[^a-z0-9]+/)
|
|
32
|
-
.filter((token) => token.length > 0);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface Posting {
|
|
36
|
-
doc: number;
|
|
37
|
-
tf: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function buildNeedleIndex(
|
|
41
|
-
tree: LeafTree,
|
|
42
|
-
pageSummary: (slug: Slug) => Promise<string>,
|
|
43
|
-
): Promise<NeedleIndex> {
|
|
44
|
-
const slugs: Slug[] = [...tree.byPage.keys()];
|
|
45
|
-
|
|
46
|
-
// term -> postings (doc index + term frequency in that doc)
|
|
47
|
-
const postings = new Map<string, Posting[]>();
|
|
48
|
-
const docLengths: number[] = new Array(slugs.length).fill(0);
|
|
49
|
-
let totalLength = 0;
|
|
50
|
-
|
|
51
|
-
for (let doc = 0; doc < slugs.length; doc++) {
|
|
52
|
-
const slug = slugs[doc]!;
|
|
53
|
-
|
|
54
|
-
// Title = last path segment of the slug; also index the slug tokens.
|
|
55
|
-
const segments = slug.split(/[/.]/);
|
|
56
|
-
const title = segments[segments.length - 1] ?? "";
|
|
57
|
-
|
|
58
|
-
const parts: string[] = [title, slug, await pageSummary(slug)];
|
|
59
|
-
|
|
60
|
-
for (const leafPath of tree.byPage.get(slug) ?? []) {
|
|
61
|
-
const leaf = tree.leaves.get(leafPath);
|
|
62
|
-
if (leaf) parts.push(leaf.description);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const tokens = tokenize(parts.join(" "));
|
|
66
|
-
docLengths[doc] = tokens.length;
|
|
67
|
-
totalLength += tokens.length;
|
|
68
|
-
|
|
69
|
-
const termFreqs = new Map<string, number>();
|
|
70
|
-
for (const token of tokens) {
|
|
71
|
-
termFreqs.set(token, (termFreqs.get(token) ?? 0) + 1);
|
|
72
|
-
}
|
|
73
|
-
for (const [term, tf] of termFreqs) {
|
|
74
|
-
let list = postings.get(term);
|
|
75
|
-
if (!list) {
|
|
76
|
-
list = [];
|
|
77
|
-
postings.set(term, list);
|
|
78
|
-
}
|
|
79
|
-
list.push({ doc, tf });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const docCount = slugs.length;
|
|
84
|
-
const avgDocLength = docCount > 0 ? totalLength / docCount : 0;
|
|
85
|
-
|
|
86
|
-
function query(text: string, k: number): Slug[] {
|
|
87
|
-
if (k <= 0 || docCount === 0) return [];
|
|
88
|
-
|
|
89
|
-
const queryTerms = new Set(tokenize(text));
|
|
90
|
-
const scores = new Map<number, number>();
|
|
91
|
-
|
|
92
|
-
// O(query-terms x matching-docs): walk only the postings of query terms.
|
|
93
|
-
for (const term of queryTerms) {
|
|
94
|
-
const list = postings.get(term);
|
|
95
|
-
if (!list) continue;
|
|
96
|
-
|
|
97
|
-
const idf = Math.log(
|
|
98
|
-
1 + (docCount - list.length + 0.5) / (list.length + 0.5),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
for (const { doc, tf } of list) {
|
|
102
|
-
const norm = tf * (k1 + 1);
|
|
103
|
-
const denom = tf + k1 * (1 - b + b * (docLengths[doc]! / avgDocLength));
|
|
104
|
-
scores.set(doc, (scores.get(doc) ?? 0) + idf * (norm / denom));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return [...scores.entries()]
|
|
109
|
-
.sort((a, c) => c[1] - a[1] || slugs[a[0]]!.localeCompare(slugs[c[0]]!))
|
|
110
|
-
.slice(0, k)
|
|
111
|
-
.map(([doc]) => slugs[doc]!);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { query };
|
|
115
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { ContentBlock } from "../../../providers/types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Text content block carrying a `cache_control` breakpoint with a 1-hour TTL.
|
|
5
|
-
* Shared by the v3 router (the static leaf-tree block) and selector (each
|
|
6
|
-
* leaf's static `<pages>` block): these prefixes are stable across turns — the
|
|
7
|
-
* leaf tree is byte-identical every turn, and a leaf's pages block changes only
|
|
8
|
-
* when its pages/summaries do — while v3 turns are frequently more than the
|
|
9
|
-
* default 5-minute cache window apart. A 1h TTL keeps the prefix warm across
|
|
10
|
-
* those gaps so it is read from cache rather than re-created every turn; the
|
|
11
|
-
* volatile current-message block is rendered after this one and left un-cached.
|
|
12
|
-
* Haiku does not support the extended-cache-ttl beta, so the Anthropic provider
|
|
13
|
-
* strips this `ttl` for Haiku models.
|
|
14
|
-
*
|
|
15
|
-
* Our internal `TextContent` type omits `cache_control` (only the Anthropic
|
|
16
|
-
* provider transforms it onto the wire), so we reach through a `Record` cast to
|
|
17
|
-
* keep the core types provider-agnostic.
|
|
18
|
-
*/
|
|
19
|
-
export function cachedTextBlock(text: string): ContentBlock {
|
|
20
|
-
const block: ContentBlock = { type: "text", text };
|
|
21
|
-
(block as unknown as Record<string, unknown>).cache_control = {
|
|
22
|
-
type: "ephemeral",
|
|
23
|
-
ttl: "1h",
|
|
24
|
-
};
|
|
25
|
-
return block;
|
|
26
|
-
}
|