@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
package/openapi.yaml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
openapi: 3.1.0
|
|
4
4
|
info:
|
|
5
5
|
title: Vellum Assistant API
|
|
6
|
-
version: 0.8.
|
|
6
|
+
version: 0.8.9
|
|
7
7
|
description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
|
|
8
8
|
servers:
|
|
9
9
|
- url: http://127.0.0.1:7821
|
|
@@ -480,6 +480,8 @@ paths:
|
|
|
480
480
|
type: string
|
|
481
481
|
createdAt:
|
|
482
482
|
type: number
|
|
483
|
+
updatedAt:
|
|
484
|
+
type: number
|
|
483
485
|
version:
|
|
484
486
|
type: string
|
|
485
487
|
contentId:
|
|
@@ -488,6 +490,7 @@ paths:
|
|
|
488
490
|
- id
|
|
489
491
|
- name
|
|
490
492
|
- createdAt
|
|
493
|
+
- updatedAt
|
|
491
494
|
- version
|
|
492
495
|
- contentId
|
|
493
496
|
additionalProperties: false
|
|
@@ -15854,10 +15857,10 @@ paths:
|
|
|
15854
15857
|
type: object
|
|
15855
15858
|
properties: {}
|
|
15856
15859
|
additionalProperties: false
|
|
15857
|
-
/v1/memory/v3/
|
|
15860
|
+
/v1/memory/v3/backfill-sections:
|
|
15858
15861
|
post:
|
|
15859
|
-
operationId:
|
|
15860
|
-
summary:
|
|
15862
|
+
operationId: memory_v3_backfillsections_post
|
|
15863
|
+
summary: "One-time: embed every page's sections (incl synthetic skill/CLI rows) into the dense store"
|
|
15861
15864
|
tags:
|
|
15862
15865
|
- memory
|
|
15863
15866
|
responses:
|
|
@@ -15868,31 +15871,16 @@ paths:
|
|
|
15868
15871
|
schema:
|
|
15869
15872
|
type: object
|
|
15870
15873
|
properties:
|
|
15871
|
-
|
|
15872
|
-
type:
|
|
15873
|
-
|
|
15874
|
-
type:
|
|
15875
|
-
|
|
15876
|
-
|
|
15877
|
-
type: number
|
|
15878
|
-
danglingRefs:
|
|
15879
|
-
type: number
|
|
15880
|
-
novelClusters:
|
|
15881
|
-
type: number
|
|
15882
|
-
oversizedLeaves:
|
|
15883
|
-
type: number
|
|
15884
|
-
tinyLeaves:
|
|
15885
|
-
type: number
|
|
15886
|
-
required:
|
|
15887
|
-
- unassigned
|
|
15888
|
-
- danglingRefs
|
|
15889
|
-
- novelClusters
|
|
15890
|
-
- oversizedLeaves
|
|
15891
|
-
- tinyLeaves
|
|
15892
|
-
additionalProperties: false
|
|
15874
|
+
articles:
|
|
15875
|
+
type: number
|
|
15876
|
+
sections:
|
|
15877
|
+
type: number
|
|
15878
|
+
failures:
|
|
15879
|
+
type: number
|
|
15893
15880
|
required:
|
|
15894
|
-
-
|
|
15895
|
-
-
|
|
15881
|
+
- articles
|
|
15882
|
+
- sections
|
|
15883
|
+
- failures
|
|
15896
15884
|
additionalProperties: false
|
|
15897
15885
|
/v1/memory/v3/rebuild-index:
|
|
15898
15886
|
post:
|
|
@@ -15914,93 +15902,6 @@ paths:
|
|
|
15914
15902
|
required:
|
|
15915
15903
|
- ok
|
|
15916
15904
|
additionalProperties: false
|
|
15917
|
-
/v1/memory/v3/reconcile:
|
|
15918
|
-
post:
|
|
15919
|
-
operationId: memory_v3_reconcile_post
|
|
15920
|
-
summary: v1 convergence/prune pass over page+core refs (no rename detection without a prior snapshot)
|
|
15921
|
-
tags:
|
|
15922
|
-
- memory
|
|
15923
|
-
responses:
|
|
15924
|
-
"200":
|
|
15925
|
-
description: Successful response
|
|
15926
|
-
content:
|
|
15927
|
-
application/json:
|
|
15928
|
-
schema:
|
|
15929
|
-
type: object
|
|
15930
|
-
properties:
|
|
15931
|
-
renames:
|
|
15932
|
-
type: array
|
|
15933
|
-
items:
|
|
15934
|
-
type: object
|
|
15935
|
-
properties:
|
|
15936
|
-
id:
|
|
15937
|
-
type: string
|
|
15938
|
-
oldPath:
|
|
15939
|
-
type: string
|
|
15940
|
-
newPath:
|
|
15941
|
-
type: string
|
|
15942
|
-
required:
|
|
15943
|
-
- oldPath
|
|
15944
|
-
- newPath
|
|
15945
|
-
additionalProperties: false
|
|
15946
|
-
deleted:
|
|
15947
|
-
type: array
|
|
15948
|
-
items:
|
|
15949
|
-
type: string
|
|
15950
|
-
prunedCore:
|
|
15951
|
-
type: array
|
|
15952
|
-
items:
|
|
15953
|
-
type: string
|
|
15954
|
-
required:
|
|
15955
|
-
- renames
|
|
15956
|
-
- deleted
|
|
15957
|
-
- prunedCore
|
|
15958
|
-
additionalProperties: false
|
|
15959
|
-
/v1/memory/v3/set-core:
|
|
15960
|
-
post:
|
|
15961
|
-
operationId: memory_v3_setcore_post
|
|
15962
|
-
summary: Add/remove always-on core leaves (validates + previews cost)
|
|
15963
|
-
tags:
|
|
15964
|
-
- memory
|
|
15965
|
-
responses:
|
|
15966
|
-
"200":
|
|
15967
|
-
description: Successful response
|
|
15968
|
-
content:
|
|
15969
|
-
application/json:
|
|
15970
|
-
schema:
|
|
15971
|
-
type: object
|
|
15972
|
-
properties:
|
|
15973
|
-
nextCore:
|
|
15974
|
-
type: array
|
|
15975
|
-
items:
|
|
15976
|
-
type: string
|
|
15977
|
-
alwaysOnPageCount:
|
|
15978
|
-
type: number
|
|
15979
|
-
written:
|
|
15980
|
-
type: boolean
|
|
15981
|
-
required:
|
|
15982
|
-
- nextCore
|
|
15983
|
-
- alwaysOnPageCount
|
|
15984
|
-
- written
|
|
15985
|
-
additionalProperties: false
|
|
15986
|
-
requestBody:
|
|
15987
|
-
required: true
|
|
15988
|
-
content:
|
|
15989
|
-
application/json:
|
|
15990
|
-
schema:
|
|
15991
|
-
type: object
|
|
15992
|
-
properties:
|
|
15993
|
-
add:
|
|
15994
|
-
type: array
|
|
15995
|
-
items:
|
|
15996
|
-
type: string
|
|
15997
|
-
remove:
|
|
15998
|
-
type: array
|
|
15999
|
-
items:
|
|
16000
|
-
type: string
|
|
16001
|
-
write:
|
|
16002
|
-
type: boolean
|
|
16003
|
-
additionalProperties: false
|
|
16004
15905
|
/v1/messages:
|
|
16005
15906
|
get:
|
|
16006
15907
|
operationId: messages_get
|
package/package.json
CHANGED
|
@@ -2,7 +2,11 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import type { Anthropic } from "@anthropic-ai/sdk";
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ContentBlock,
|
|
7
|
+
Message,
|
|
8
|
+
ToolDefinition,
|
|
9
|
+
} from "../providers/types.js";
|
|
6
10
|
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
// Mock Anthropic SDK — must be before importing the provider
|
|
@@ -102,7 +106,6 @@ mock.module("@anthropic-ai/sdk", () => ({
|
|
|
102
106
|
}));
|
|
103
107
|
|
|
104
108
|
// Import after mocking
|
|
105
|
-
import { cachedTextBlock } from "../plugins/defaults/memory-v3-shadow/provider-blocks.js";
|
|
106
109
|
import { AnthropicProvider } from "../providers/anthropic/client.js";
|
|
107
110
|
import {
|
|
108
111
|
isPlaceholderSentinelText,
|
|
@@ -138,6 +141,18 @@ function toolResultMsg(toolUseId: string, content: string): Message {
|
|
|
138
141
|
};
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
// A stable prefix text block carrying a 1h-TTL cache_control breakpoint, as a
|
|
145
|
+
// caller may stamp before the provider sees it. The internal ContentBlock type
|
|
146
|
+
// omits cache_control (only the provider transforms it onto the wire), so reach
|
|
147
|
+
// it via a cast.
|
|
148
|
+
function cachedPrefixBlock(text: string): ContentBlock {
|
|
149
|
+
return {
|
|
150
|
+
type: "text",
|
|
151
|
+
text,
|
|
152
|
+
cache_control: { type: "ephemeral", ttl: "1h" },
|
|
153
|
+
} as unknown as ContentBlock;
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
const sampleTools: ToolDefinition[] = [
|
|
142
157
|
{
|
|
143
158
|
name: "file_read",
|
|
@@ -325,13 +340,13 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
325
340
|
});
|
|
326
341
|
|
|
327
342
|
test("preserves a caller's 1h block cache_control and sends the extended-cache beta (non-Haiku)", async () => {
|
|
328
|
-
//
|
|
329
|
-
//
|
|
343
|
+
// A caller that stamps a stable prefix block with a 1h TTL: the non-Haiku
|
|
344
|
+
// path must forward it unchanged and send the beta header.
|
|
330
345
|
await provider.sendMessage([
|
|
331
346
|
{
|
|
332
347
|
role: "user",
|
|
333
348
|
content: [
|
|
334
|
-
|
|
349
|
+
cachedPrefixBlock("stable pages block"),
|
|
335
350
|
{ type: "text", text: "volatile current message" },
|
|
336
351
|
],
|
|
337
352
|
},
|
|
@@ -349,7 +364,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
349
364
|
|
|
350
365
|
test("strips ttl from a caller's block cache_control and omits the beta for Haiku", async () => {
|
|
351
366
|
// Haiku doesn't support the extended-cache-ttl beta, so a caller-stamped
|
|
352
|
-
// 1h ttl
|
|
367
|
+
// 1h ttl on a message block must be stripped before sending.
|
|
353
368
|
const haiku = new AnthropicProvider(
|
|
354
369
|
"sk-ant-test",
|
|
355
370
|
"claude-haiku-4-5-20251001",
|
|
@@ -358,7 +373,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
358
373
|
{
|
|
359
374
|
role: "user",
|
|
360
375
|
content: [
|
|
361
|
-
|
|
376
|
+
cachedPrefixBlock("stable pages block"),
|
|
362
377
|
{ type: "text", text: "volatile current message" },
|
|
363
378
|
],
|
|
364
379
|
},
|
|
@@ -385,7 +400,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
385
400
|
{
|
|
386
401
|
role: "user",
|
|
387
402
|
content: [
|
|
388
|
-
|
|
403
|
+
cachedPrefixBlock("stable pages block"),
|
|
389
404
|
{ type: "text", text: "volatile current message" },
|
|
390
405
|
],
|
|
391
406
|
},
|
|
@@ -56,6 +56,11 @@ let runCalls: Array<{
|
|
|
56
56
|
let runResults: CompactionRunResult[] = [];
|
|
57
57
|
const estimateReturns: number[] = [];
|
|
58
58
|
|
|
59
|
+
// Captured arguments from each runEmergencyCompaction invocation, so the
|
|
60
|
+
// emergencyCompact tests can assert the manager fills in the fields it owns.
|
|
61
|
+
let emergencyCalls: Record<string, unknown>[] = [];
|
|
62
|
+
let emergencyResult: CompactionRunResult | undefined;
|
|
63
|
+
|
|
59
64
|
mock.module("../context/token-estimator.js", () => ({
|
|
60
65
|
estimatePromptTokens: (): number => {
|
|
61
66
|
const next = estimateReturns.shift();
|
|
@@ -92,6 +97,15 @@ mock.module("../context/compactor.js", () => ({
|
|
|
92
97
|
}
|
|
93
98
|
return result;
|
|
94
99
|
},
|
|
100
|
+
runEmergencyCompaction: async (
|
|
101
|
+
args: Record<string, unknown>,
|
|
102
|
+
): Promise<CompactionRunResult> => {
|
|
103
|
+
emergencyCalls.push(args);
|
|
104
|
+
if (!emergencyResult) {
|
|
105
|
+
throw new Error("emergencyResult not seeded");
|
|
106
|
+
}
|
|
107
|
+
return emergencyResult;
|
|
108
|
+
},
|
|
95
109
|
}));
|
|
96
110
|
|
|
97
111
|
import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
|
|
@@ -142,24 +156,28 @@ function compactResult(
|
|
|
142
156
|
};
|
|
143
157
|
}
|
|
144
158
|
|
|
159
|
+
function makeConfig(maxAttempts: number = 3) {
|
|
160
|
+
return {
|
|
161
|
+
enabled: true,
|
|
162
|
+
maxInputTokens: 200_000,
|
|
163
|
+
targetBudgetRatio: 0.3,
|
|
164
|
+
compactThreshold: 0.8,
|
|
165
|
+
summaryBudgetRatio: 0.05,
|
|
166
|
+
overflowRecovery: {
|
|
167
|
+
enabled: true,
|
|
168
|
+
safetyMarginRatio: 0.05,
|
|
169
|
+
maxAttempts,
|
|
170
|
+
interactiveLatestTurnCompression: "summarize" as const,
|
|
171
|
+
nonInteractiveLatestTurnCompression: "summarize" as const,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
145
176
|
function buildManager(maxAttempts: number = 3): ContextWindowManager {
|
|
146
177
|
return new ContextWindowManager({
|
|
147
178
|
provider: makeProvider(),
|
|
148
179
|
systemPrompt: "you are a test assistant",
|
|
149
|
-
config:
|
|
150
|
-
enabled: true,
|
|
151
|
-
maxInputTokens: 200_000,
|
|
152
|
-
targetBudgetRatio: 0.3,
|
|
153
|
-
compactThreshold: 0.8,
|
|
154
|
-
summaryBudgetRatio: 0.05,
|
|
155
|
-
overflowRecovery: {
|
|
156
|
-
enabled: true,
|
|
157
|
-
safetyMarginRatio: 0.05,
|
|
158
|
-
maxAttempts,
|
|
159
|
-
interactiveLatestTurnCompression: "summarize",
|
|
160
|
-
nonInteractiveLatestTurnCompression: "summarize",
|
|
161
|
-
},
|
|
162
|
-
},
|
|
180
|
+
config: makeConfig(maxAttempts),
|
|
163
181
|
conversationId: "conv-test",
|
|
164
182
|
});
|
|
165
183
|
}
|
|
@@ -289,3 +307,91 @@ describe("ContextWindowManager.maybeCompact retry budget", () => {
|
|
|
289
307
|
expect(result.estimatedInputTokens).toBe(165_000);
|
|
290
308
|
});
|
|
291
309
|
});
|
|
310
|
+
|
|
311
|
+
describe("ContextWindowManager.emergencyCompact", () => {
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
emergencyCalls = [];
|
|
314
|
+
emergencyResult = undefined;
|
|
315
|
+
estimateReturns.length = 0;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("supplies manager-owned fields and forwards only overflow inputs", async () => {
|
|
319
|
+
/**
|
|
320
|
+
* The manager owns provider, system prompt, token budget, conversation
|
|
321
|
+
* id, compaction config, and non-persisted prefix count; the caller
|
|
322
|
+
* provides only the overflow-specific inputs.
|
|
323
|
+
*/
|
|
324
|
+
// GIVEN a manager seeded with a non-persisted prefix and a canned result
|
|
325
|
+
emergencyResult = compactResult({
|
|
326
|
+
compacted: true,
|
|
327
|
+
compactedMessages: 40,
|
|
328
|
+
summaryText: "emergency summary",
|
|
329
|
+
});
|
|
330
|
+
const manager = buildManager();
|
|
331
|
+
manager.seedNonPersistedPrefix(3);
|
|
332
|
+
const messages = makeMessages(8);
|
|
333
|
+
const signal = new AbortController().signal;
|
|
334
|
+
|
|
335
|
+
// WHEN emergency compaction runs
|
|
336
|
+
const result = await manager.emergencyCompact(
|
|
337
|
+
messages,
|
|
338
|
+
{ previousEstimatedInputTokens: 242_201, overrideProfile: "fast" },
|
|
339
|
+
signal,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// THEN the manager fills its owned fields and forwards the caller inputs
|
|
343
|
+
expect(emergencyCalls.length).toBe(1);
|
|
344
|
+
const args = emergencyCalls[0]!;
|
|
345
|
+
expect(args.conversationId).toBe("conv-test");
|
|
346
|
+
expect(args.systemPrompt).toBe("you are a test assistant");
|
|
347
|
+
expect(args.maxInputTokens).toBe(200_000);
|
|
348
|
+
expect(args.nonPersistedPrefixCount).toBe(3);
|
|
349
|
+
expect(args.tools).toBeUndefined();
|
|
350
|
+
expect(args.force).toBe(true);
|
|
351
|
+
expect(args.signal).toBe(signal);
|
|
352
|
+
expect(args.messages).toBe(messages);
|
|
353
|
+
expect(args.previousEstimatedInputTokens).toBe(242_201);
|
|
354
|
+
expect(args.overrideProfile).toBe("fast");
|
|
355
|
+
expect((args.provider as { name: string }).name).toBe("mock-provider");
|
|
356
|
+
expect(result.summaryText).toBe("emergency summary");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("defaults overrideProfile to null when omitted", async () => {
|
|
360
|
+
/**
|
|
361
|
+
* Callers that don't resolve a per-conversation profile get an explicit
|
|
362
|
+
* null forwarded to the summary call.
|
|
363
|
+
*/
|
|
364
|
+
// GIVEN a manager and a canned result
|
|
365
|
+
emergencyResult = compactResult({ compacted: true });
|
|
366
|
+
const manager = buildManager();
|
|
367
|
+
|
|
368
|
+
// WHEN emergency compaction runs without an override profile
|
|
369
|
+
await manager.emergencyCompact(makeMessages(6), {
|
|
370
|
+
previousEstimatedInputTokens: 210_000,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// THEN overrideProfile is forwarded as null
|
|
374
|
+
expect(emergencyCalls[0]!.overrideProfile).toBeNull();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("throws when the manager has no conversation id", async () => {
|
|
378
|
+
/**
|
|
379
|
+
* Emergency compaction needs a conversation id for attachment and
|
|
380
|
+
* timestamp lookups; a manager without one cannot run it.
|
|
381
|
+
*/
|
|
382
|
+
// GIVEN a manager constructed without a conversation id
|
|
383
|
+
const manager = new ContextWindowManager({
|
|
384
|
+
provider: makeProvider(),
|
|
385
|
+
systemPrompt: "you are a test assistant",
|
|
386
|
+
config: makeConfig(),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// WHEN/THEN emergency compaction rejects without invoking the compactor
|
|
390
|
+
await expect(
|
|
391
|
+
manager.emergencyCompact(makeMessages(4), {
|
|
392
|
+
previousEstimatedInputTokens: 210_000,
|
|
393
|
+
}),
|
|
394
|
+
).rejects.toThrow(/conversationId/);
|
|
395
|
+
expect(emergencyCalls.length).toBe(0);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Regression tests for app surface refresh and eventing side effects in
|
|
3
3
|
* createToolExecutor (conversation-tool-setup.ts).
|
|
4
4
|
*
|
|
5
|
-
* Tests verify that app_refresh, app_create, and app_delete hooks
|
|
6
|
-
* correctly, and that
|
|
7
|
-
*
|
|
5
|
+
* Tests verify that app_refresh, app_update, app_create, and app_delete hooks
|
|
6
|
+
* fire correctly, and that non-hooked tools (app_file_edit, app_file_write) do
|
|
7
|
+
* not trigger side effects.
|
|
8
8
|
*
|
|
9
9
|
* File-change detection for file_write/file_edit is handled by
|
|
10
10
|
* AppSourceWatcher (see app-source-watcher.test.ts).
|
|
@@ -248,6 +248,76 @@ describe("session-tool-setup app refresh side effects", () => {
|
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
// ── app_update ──────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
describe("app_update", () => {
|
|
254
|
+
test("triggers refreshSurfacesForApp and broadcast on success", async () => {
|
|
255
|
+
const ctx = makeCtx();
|
|
256
|
+
const executor = makeFakeExecutor({
|
|
257
|
+
content: '{"updated":true,"appId":"app-7"}',
|
|
258
|
+
isError: false,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const toolFn = createToolExecutor(
|
|
262
|
+
executor as unknown as ToolExecutor,
|
|
263
|
+
noopPrompter,
|
|
264
|
+
noopSecretPrompter,
|
|
265
|
+
ctx,
|
|
266
|
+
noopLifecycleHandler,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await toolFn("app_update", { app_id: "app-7" });
|
|
270
|
+
|
|
271
|
+
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect((refreshSpy.mock.calls as unknown[][])[0][1]).toBe("app-7");
|
|
273
|
+
expectAppChangeBroadcast("app-7");
|
|
274
|
+
expect(updatePublishedSpy).toHaveBeenCalledTimes(1);
|
|
275
|
+
expect((updatePublishedSpy.mock.calls as unknown[][])[0][0]).toBe(
|
|
276
|
+
"app-7",
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("skips side effects when result is an error", async () => {
|
|
281
|
+
const ctx = makeCtx();
|
|
282
|
+
const executor = makeFakeExecutor({
|
|
283
|
+
content: "Error: not found",
|
|
284
|
+
isError: true,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const toolFn = createToolExecutor(
|
|
288
|
+
executor as unknown as ToolExecutor,
|
|
289
|
+
noopPrompter,
|
|
290
|
+
noopSecretPrompter,
|
|
291
|
+
ctx,
|
|
292
|
+
noopLifecycleHandler,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
await toolFn("app_update", { app_id: "app-err" });
|
|
296
|
+
|
|
297
|
+
expect(refreshSpy).not.toHaveBeenCalled();
|
|
298
|
+
expect(broadcastSpy).not.toHaveBeenCalled();
|
|
299
|
+
expect(updatePublishedSpy).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("skips side effects when app_id is missing", async () => {
|
|
303
|
+
const ctx = makeCtx();
|
|
304
|
+
const executor = makeFakeExecutor({ content: "{}", isError: false });
|
|
305
|
+
|
|
306
|
+
const toolFn = createToolExecutor(
|
|
307
|
+
executor as unknown as ToolExecutor,
|
|
308
|
+
noopPrompter,
|
|
309
|
+
noopSecretPrompter,
|
|
310
|
+
ctx,
|
|
311
|
+
noopLifecycleHandler,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await toolFn("app_update", {});
|
|
315
|
+
|
|
316
|
+
expect(refreshSpy).not.toHaveBeenCalled();
|
|
317
|
+
expect(broadcastSpy).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
251
321
|
// ── app_create side effects ─────────────────────────────────────────
|
|
252
322
|
|
|
253
323
|
describe("app_create side effects", () => {
|
|
@@ -518,7 +588,6 @@ describe("session-tool-setup app refresh side effects", () => {
|
|
|
518
588
|
"write_file",
|
|
519
589
|
"shell",
|
|
520
590
|
"app_list",
|
|
521
|
-
"app_update",
|
|
522
591
|
"app_file_edit",
|
|
523
592
|
"app_file_write",
|
|
524
593
|
]) {
|
|
@@ -230,6 +230,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
230
230
|
"runtime/routes/platform-routes.ts", // CLI platform connect/disconnect/status routes (CLI-migrated to IPC)
|
|
231
231
|
"ipc/skill-routes/providers.ts", // host.providers.secureKeys.getProviderKey IPC route (out-of-process SkillHost companion)
|
|
232
232
|
"daemon/external-plugins-bootstrap.ts", // reads credentials at plugin init (manifest.requiresCredential) via the CES-mediated getSecureKeyAsync path
|
|
233
|
+
"plugins/external-api.ts", // globalThis runtime bridge that exposes getSecureKeyAsync to dynamically-imported workspace plugins (compiled-binary plugin loading)
|
|
233
234
|
"inbound/platform-callback-registration.ts", // managed credential lookup for platform base URL, assistant ID, and API key
|
|
234
235
|
"tts/providers/elevenlabs-provider.ts", // ElevenLabs TTS API key lookup
|
|
235
236
|
"tts/providers/deepgram-provider.ts", // Deepgram TTS API key lookup
|
|
@@ -1249,3 +1249,76 @@ describe("resolveDefaultProfileKey", () => {
|
|
|
1249
1249
|
expect(resolveDefaultProfileKey("mainAgent", llm)).toBe("balanced");
|
|
1250
1250
|
});
|
|
1251
1251
|
});
|
|
1252
|
+
|
|
1253
|
+
describe("resolveCallSiteConfig logitBias provenance", () => {
|
|
1254
|
+
const kimi = "accounts/fireworks/models/kimi-k2p6";
|
|
1255
|
+
|
|
1256
|
+
test("forwards logitBias from the active profile that opted in", () => {
|
|
1257
|
+
const llm = LLMSchema.parse({
|
|
1258
|
+
default: fullDefault,
|
|
1259
|
+
profiles: {
|
|
1260
|
+
"balanced-economy": {
|
|
1261
|
+
provider: "fireworks",
|
|
1262
|
+
model: kimi,
|
|
1263
|
+
logitBias: "suppress-cjk",
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
activeProfile: "balanced-economy",
|
|
1267
|
+
});
|
|
1268
|
+
expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBe(
|
|
1269
|
+
"suppress-cjk",
|
|
1270
|
+
);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("a higher-precedence override profile that omits logitBias clears it", () => {
|
|
1274
|
+
// Active profile opts in, but a pinned (override) Kimi profile did not —
|
|
1275
|
+
// the override must not inherit suppress-cjk just because it resolves to
|
|
1276
|
+
// Fireworks.
|
|
1277
|
+
const llm = LLMSchema.parse({
|
|
1278
|
+
default: fullDefault,
|
|
1279
|
+
profiles: {
|
|
1280
|
+
"balanced-economy": {
|
|
1281
|
+
provider: "fireworks",
|
|
1282
|
+
model: kimi,
|
|
1283
|
+
logitBias: "suppress-cjk",
|
|
1284
|
+
},
|
|
1285
|
+
"my-kimi": { provider: "fireworks", model: kimi },
|
|
1286
|
+
},
|
|
1287
|
+
activeProfile: "balanced-economy",
|
|
1288
|
+
});
|
|
1289
|
+
const resolved = resolveCallSiteConfig("mainAgent", llm, {
|
|
1290
|
+
overrideProfile: "my-kimi",
|
|
1291
|
+
});
|
|
1292
|
+
expect(resolved.logitBias).toBeUndefined();
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
test("does not leak the active profile's logitBias into a call site's own profile", () => {
|
|
1296
|
+
// For non-main call sites the call-site profile wins; since it didn't opt
|
|
1297
|
+
// in, the active profile's preset must not bleed through.
|
|
1298
|
+
const llm = LLMSchema.parse({
|
|
1299
|
+
default: fullDefault,
|
|
1300
|
+
profiles: {
|
|
1301
|
+
"balanced-economy": {
|
|
1302
|
+
provider: "fireworks",
|
|
1303
|
+
model: kimi,
|
|
1304
|
+
logitBias: "suppress-cjk",
|
|
1305
|
+
},
|
|
1306
|
+
plain: { provider: "anthropic", model: "claude-opus-4-7" },
|
|
1307
|
+
},
|
|
1308
|
+
activeProfile: "balanced-economy",
|
|
1309
|
+
callSites: { memoryExtraction: { profile: "plain" } },
|
|
1310
|
+
});
|
|
1311
|
+
expect(
|
|
1312
|
+
resolveCallSiteConfig("memoryExtraction", llm).logitBias,
|
|
1313
|
+
).toBeUndefined();
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
test("a logitBias on a non-profile layer (llm.default) does not apply when the winning profile omits it", () => {
|
|
1317
|
+
const llm = LLMSchema.parse({
|
|
1318
|
+
default: { ...fullDefault, logitBias: "suppress-cjk" },
|
|
1319
|
+
profiles: { plain: { provider: "anthropic", model: "claude-opus-4-7" } },
|
|
1320
|
+
activeProfile: "plain",
|
|
1321
|
+
});
|
|
1322
|
+
expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBeUndefined();
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `globalThis.__vellumPluginRuntime` bridge.
|
|
3
|
+
*
|
|
4
|
+
* The bridge exists so workspace-local plugins (`<workspaceDir>/plugins/*`)
|
|
5
|
+
* can register with the daemon's bundled module instances even when the
|
|
6
|
+
* daemon is a `bun --compile` binary. Absolute-path imports against a
|
|
7
|
+
* compiled binary load fresh disk copies into a disjoint module graph; the
|
|
8
|
+
* bridge sidesteps that by attaching a stable handle to globalThis.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getPluginRuntime,
|
|
15
|
+
installPluginRuntime,
|
|
16
|
+
uninstallPluginRuntimeForTests,
|
|
17
|
+
} from "../plugins/external-api.js";
|
|
18
|
+
import { registerPlugin } from "../plugins/registry.js";
|
|
19
|
+
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
20
|
+
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
21
|
+
|
|
22
|
+
describe("plugin external-api bridge", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
uninstallPluginRuntimeForTests();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
uninstallPluginRuntimeForTests();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("installs a runtime handle on globalThis", () => {
|
|
32
|
+
expect(getPluginRuntime()).toBeUndefined();
|
|
33
|
+
installPluginRuntime();
|
|
34
|
+
const runtime = getPluginRuntime();
|
|
35
|
+
expect(runtime).toBeDefined();
|
|
36
|
+
expect(runtime?.version).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("exposes the canonical registerPlugin / hub / secrets references", () => {
|
|
40
|
+
installPluginRuntime();
|
|
41
|
+
const runtime = getPluginRuntime();
|
|
42
|
+
expect(runtime?.registerPlugin).toBe(registerPlugin);
|
|
43
|
+
expect(runtime?.assistantEventHub).toBe(assistantEventHub);
|
|
44
|
+
expect(runtime?.getSecureKeyAsync).toBe(getSecureKeyAsync);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("plugins can read the runtime via the documented globalThis key", () => {
|
|
48
|
+
installPluginRuntime();
|
|
49
|
+
// Mirror the access pattern documented for plugin authors.
|
|
50
|
+
const runtime = (
|
|
51
|
+
globalThis as { __vellumPluginRuntime?: { version: number } }
|
|
52
|
+
).__vellumPluginRuntime;
|
|
53
|
+
expect(runtime).toBeDefined();
|
|
54
|
+
expect(runtime?.version).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("install is idempotent — repeat calls preserve the same handle", () => {
|
|
58
|
+
installPluginRuntime();
|
|
59
|
+
const first = getPluginRuntime();
|
|
60
|
+
installPluginRuntime();
|
|
61
|
+
installPluginRuntime();
|
|
62
|
+
expect(getPluginRuntime()).toBe(first);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("getPluginRuntime returns undefined when the bridge is not installed", () => {
|
|
66
|
+
expect(getPluginRuntime()).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|