@vellumai/assistant 0.11.2-dev.202608052357.8f326c2 → 0.11.2-dev.202608060040.fd8f444
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 +77 -0
- package/package.json +1 -1
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +113 -66
- package/src/__tests__/anthropic-provider.test.ts +29 -11
- package/src/__tests__/conversation-agent-loop.test.ts +146 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +5 -2
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +593 -0
- package/src/__tests__/queued-message-steer-actor-scoping.test.ts +164 -0
- package/src/__tests__/schedule-routes.test.ts +439 -0
- package/src/__tests__/schedule-store.test.ts +31 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +3 -2
- package/src/agent/loop.ts +13 -10
- package/src/cli/commands/__tests__/conversations-wake.test.ts +51 -6
- package/src/cli/commands/conversations.ts +7 -3
- package/src/config/bundled-skills/schedule/SKILL.md +2 -0
- package/src/config/feature-flag-registry.json +0 -8
- package/src/context/strip-injections.ts +42 -0
- package/src/daemon/conversation-agent-loop.ts +53 -9
- package/src/daemon/handlers/conversations.ts +38 -11
- package/src/providers/anthropic/client.ts +37 -37
- package/src/providers/openai/responses-provider.ts +9 -22
- package/src/providers/types.ts +26 -6
- package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +60 -2
- package/src/runtime/routes/conversation-query-routes.ts +24 -3
- package/src/runtime/routes/schedule-routes.ts +139 -3
- package/src/runtime/routes/wake-conversation-routes.ts +23 -0
- package/src/schedule/schedule-store.ts +6 -0
package/openapi.yaml
CHANGED
|
@@ -9623,6 +9623,9 @@ paths:
|
|
|
9623
9623
|
cronRunId:
|
|
9624
9624
|
type: string
|
|
9625
9625
|
minLength: 1
|
|
9626
|
+
scheduleId:
|
|
9627
|
+
type: string
|
|
9628
|
+
minLength: 1
|
|
9626
9629
|
persist:
|
|
9627
9630
|
type: boolean
|
|
9628
9631
|
externalContent:
|
|
@@ -20880,6 +20883,10 @@ paths:
|
|
|
20880
20883
|
responses:
|
|
20881
20884
|
"200":
|
|
20882
20885
|
description: Successful response
|
|
20886
|
+
"403":
|
|
20887
|
+
description: The queued message was enqueued by a different actor principal.
|
|
20888
|
+
"404":
|
|
20889
|
+
description: Conversation or queued message not found.
|
|
20883
20890
|
/v1/migrations/export:
|
|
20884
20891
|
post:
|
|
20885
20892
|
operationId: migrations_export_post
|
|
@@ -25470,6 +25477,12 @@ paths:
|
|
|
25470
25477
|
schema:
|
|
25471
25478
|
type: string
|
|
25472
25479
|
description: When 'true', include deferred schedules that are normally hidden.
|
|
25480
|
+
- name: inference_profile
|
|
25481
|
+
in: query
|
|
25482
|
+
required: false
|
|
25483
|
+
schema:
|
|
25484
|
+
type: string
|
|
25485
|
+
description: Return only schedules pinned to this inference profile (llm.profiles key).
|
|
25473
25486
|
responses:
|
|
25474
25487
|
"200":
|
|
25475
25488
|
description: Successful response
|
|
@@ -25587,6 +25600,8 @@ paths:
|
|
|
25587
25600
|
- type: "null"
|
|
25588
25601
|
isOneShot:
|
|
25589
25602
|
type: boolean
|
|
25603
|
+
isDeferred:
|
|
25604
|
+
type: boolean
|
|
25590
25605
|
required:
|
|
25591
25606
|
- id
|
|
25592
25607
|
- name
|
|
@@ -25618,6 +25633,7 @@ paths:
|
|
|
25618
25633
|
- wakeConversationId
|
|
25619
25634
|
- workflowName
|
|
25620
25635
|
- isOneShot
|
|
25636
|
+
- isDeferred
|
|
25621
25637
|
additionalProperties: false
|
|
25622
25638
|
description: Schedule objects
|
|
25623
25639
|
required:
|
|
@@ -25801,6 +25817,8 @@ paths:
|
|
|
25801
25817
|
- type: "null"
|
|
25802
25818
|
isOneShot:
|
|
25803
25819
|
type: boolean
|
|
25820
|
+
isDeferred:
|
|
25821
|
+
type: boolean
|
|
25804
25822
|
required:
|
|
25805
25823
|
- id
|
|
25806
25824
|
- name
|
|
@@ -25832,6 +25850,7 @@ paths:
|
|
|
25832
25850
|
- wakeConversationId
|
|
25833
25851
|
- workflowName
|
|
25834
25852
|
- isOneShot
|
|
25853
|
+
- isDeferred
|
|
25835
25854
|
additionalProperties: false
|
|
25836
25855
|
description: The created schedule
|
|
25837
25856
|
required:
|
|
@@ -25967,6 +25986,8 @@ paths:
|
|
|
25967
25986
|
- type: "null"
|
|
25968
25987
|
isOneShot:
|
|
25969
25988
|
type: boolean
|
|
25989
|
+
isDeferred:
|
|
25990
|
+
type: boolean
|
|
25970
25991
|
required:
|
|
25971
25992
|
- id
|
|
25972
25993
|
- name
|
|
@@ -25998,6 +26019,7 @@ paths:
|
|
|
25998
26019
|
- wakeConversationId
|
|
25999
26020
|
- workflowName
|
|
26000
26021
|
- isOneShot
|
|
26022
|
+
- isDeferred
|
|
26001
26023
|
additionalProperties: false
|
|
26002
26024
|
description: Updated schedule list
|
|
26003
26025
|
required:
|
|
@@ -26130,6 +26152,8 @@ paths:
|
|
|
26130
26152
|
- type: "null"
|
|
26131
26153
|
isOneShot:
|
|
26132
26154
|
type: boolean
|
|
26155
|
+
isDeferred:
|
|
26156
|
+
type: boolean
|
|
26133
26157
|
required:
|
|
26134
26158
|
- id
|
|
26135
26159
|
- name
|
|
@@ -26161,6 +26185,7 @@ paths:
|
|
|
26161
26185
|
- wakeConversationId
|
|
26162
26186
|
- workflowName
|
|
26163
26187
|
- isOneShot
|
|
26188
|
+
- isDeferred
|
|
26164
26189
|
additionalProperties: false
|
|
26165
26190
|
description: Schedule object
|
|
26166
26191
|
required:
|
|
@@ -26353,6 +26378,8 @@ paths:
|
|
|
26353
26378
|
- type: "null"
|
|
26354
26379
|
isOneShot:
|
|
26355
26380
|
type: boolean
|
|
26381
|
+
isDeferred:
|
|
26382
|
+
type: boolean
|
|
26356
26383
|
required:
|
|
26357
26384
|
- id
|
|
26358
26385
|
- name
|
|
@@ -26384,6 +26411,7 @@ paths:
|
|
|
26384
26411
|
- wakeConversationId
|
|
26385
26412
|
- workflowName
|
|
26386
26413
|
- isOneShot
|
|
26414
|
+
- isDeferred
|
|
26387
26415
|
additionalProperties: false
|
|
26388
26416
|
description: Updated schedule list
|
|
26389
26417
|
required:
|
|
@@ -26519,6 +26547,8 @@ paths:
|
|
|
26519
26547
|
- type: "null"
|
|
26520
26548
|
isOneShot:
|
|
26521
26549
|
type: boolean
|
|
26550
|
+
isDeferred:
|
|
26551
|
+
type: boolean
|
|
26522
26552
|
required:
|
|
26523
26553
|
- id
|
|
26524
26554
|
- name
|
|
@@ -26550,6 +26580,7 @@ paths:
|
|
|
26550
26580
|
- wakeConversationId
|
|
26551
26581
|
- workflowName
|
|
26552
26582
|
- isOneShot
|
|
26583
|
+
- isDeferred
|
|
26553
26584
|
additionalProperties: false
|
|
26554
26585
|
description: Updated schedule list
|
|
26555
26586
|
required:
|
|
@@ -26685,6 +26716,8 @@ paths:
|
|
|
26685
26716
|
- type: "null"
|
|
26686
26717
|
isOneShot:
|
|
26687
26718
|
type: boolean
|
|
26719
|
+
isDeferred:
|
|
26720
|
+
type: boolean
|
|
26688
26721
|
required:
|
|
26689
26722
|
- id
|
|
26690
26723
|
- name
|
|
@@ -26716,6 +26749,7 @@ paths:
|
|
|
26716
26749
|
- wakeConversationId
|
|
26717
26750
|
- workflowName
|
|
26718
26751
|
- isOneShot
|
|
26752
|
+
- isDeferred
|
|
26719
26753
|
additionalProperties: false
|
|
26720
26754
|
description: Updated schedule list
|
|
26721
26755
|
required:
|
|
@@ -26988,6 +27022,8 @@ paths:
|
|
|
26988
27022
|
- type: "null"
|
|
26989
27023
|
isOneShot:
|
|
26990
27024
|
type: boolean
|
|
27025
|
+
isDeferred:
|
|
27026
|
+
type: boolean
|
|
26991
27027
|
required:
|
|
26992
27028
|
- id
|
|
26993
27029
|
- name
|
|
@@ -27019,11 +27055,52 @@ paths:
|
|
|
27019
27055
|
- wakeConversationId
|
|
27020
27056
|
- workflowName
|
|
27021
27057
|
- isOneShot
|
|
27058
|
+
- isDeferred
|
|
27022
27059
|
additionalProperties: false
|
|
27023
27060
|
description: Updated schedule list
|
|
27024
27061
|
required:
|
|
27025
27062
|
- schedules
|
|
27026
27063
|
additionalProperties: false
|
|
27064
|
+
/v1/schedules/reassign-profile:
|
|
27065
|
+
post:
|
|
27066
|
+
operationId: schedules_reassignprofile_post
|
|
27067
|
+
summary: Reassign schedules to another inference profile
|
|
27068
|
+
description:
|
|
27069
|
+
Move schedules onto one inference profile. Pass 'from' to move only the schedules pinned to that profile,
|
|
27070
|
+
so deleting a profile does not leave its schedules pointing at a name that no longer exists. Omit 'from' to move
|
|
27071
|
+
every schedule, which re-pins the whole set onto one profile. Schedules already pinned to the target are
|
|
27072
|
+
skipped.
|
|
27073
|
+
tags:
|
|
27074
|
+
- schedules
|
|
27075
|
+
requestBody:
|
|
27076
|
+
required: true
|
|
27077
|
+
content:
|
|
27078
|
+
application/json:
|
|
27079
|
+
schema:
|
|
27080
|
+
type: object
|
|
27081
|
+
properties:
|
|
27082
|
+
from:
|
|
27083
|
+
description: Inference profile key the schedules are pinned to now; omit to select every schedule
|
|
27084
|
+
type: string
|
|
27085
|
+
to:
|
|
27086
|
+
type: string
|
|
27087
|
+
description: Inference profile key to move them to; must be configured
|
|
27088
|
+
required:
|
|
27089
|
+
- to
|
|
27090
|
+
responses:
|
|
27091
|
+
"200":
|
|
27092
|
+
description: Successful response
|
|
27093
|
+
content:
|
|
27094
|
+
application/json:
|
|
27095
|
+
schema:
|
|
27096
|
+
type: object
|
|
27097
|
+
properties:
|
|
27098
|
+
reassigned:
|
|
27099
|
+
type: number
|
|
27100
|
+
description: Number of schedules moved
|
|
27101
|
+
required:
|
|
27102
|
+
- reassigned
|
|
27103
|
+
additionalProperties: false
|
|
27027
27104
|
/v1/schedules/usage-summary:
|
|
27028
27105
|
get:
|
|
27029
27106
|
operationId: schedules_usagesummary_get
|
package/package.json
CHANGED
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Verifies
|
|
3
|
-
* `SendMessageOptions.config
|
|
4
|
-
* user message carries a volatile `<memory>` block, so the loop sets
|
|
5
|
-
* `providerConfig.mutableLatestUserMessage` to tell the provider to anchor its
|
|
6
|
-
* long-TTL cache breakpoint on the most recent STABLE message instead.
|
|
2
|
+
* Verifies how the loop derives the `mutableLatestUserMessage` cache-anchor
|
|
3
|
+
* signal on `SendMessageOptions.config`.
|
|
7
4
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* The signal means "the latest user message's bytes do not recur next turn", so
|
|
6
|
+
* it is set from the history actually being sent: only when the latest user
|
|
7
|
+
* message carries a memory-v3 `<memory_spotlight>` block, the one injected
|
|
8
|
+
* block that is strip-and-replaced on the tail every turn. Every other injected
|
|
9
|
+
* block is frozen into history and re-renders byte-identically, so turns
|
|
10
|
+
* without a spotlight keep a normal cache anchor even when memory-v3 is live.
|
|
11
|
+
*
|
|
12
|
+
* When no spotlight is present the field is omitted entirely (not `false`) so
|
|
13
|
+
* the wire stays byte-identical for conversations that never see one.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
// AgentLoop reads the v3-live gate (`config.memory.v3.live`) via
|
|
17
|
-
// `isMemoryV3Live` to decide the cache-anchor signal; drive it per-test.
|
|
18
|
-
let memoryV3LiveSlot = false;
|
|
19
|
-
mock.module("../config/memory-v3-gate.js", () => ({
|
|
20
|
-
isMemoryV3Live: () => memoryV3LiveSlot,
|
|
21
|
-
isV3TierActive: () => memoryV3LiveSlot,
|
|
22
|
-
isMemoryEnabled: (config?: { memory?: { enabled?: boolean } }) =>
|
|
23
|
-
config?.memory?.enabled !== false,
|
|
24
|
-
usesConceptPageMemory: (memory?: {
|
|
25
|
-
enabled?: boolean;
|
|
26
|
-
v2?: { enabled?: boolean };
|
|
27
|
-
v3?: { live?: boolean };
|
|
28
|
-
}) =>
|
|
29
|
-
memory?.enabled !== false &&
|
|
30
|
-
(memory?.v3?.live === true || memory?.v2?.enabled === true),
|
|
31
|
-
}));
|
|
16
|
+
import { describe, expect, test } from "bun:test";
|
|
32
17
|
|
|
33
18
|
import { AgentLoop } from "../agent/loop.js";
|
|
19
|
+
import { wrapMemorySpotlightBlock } from "../plugins/defaults/memory/memory-marker.js";
|
|
34
20
|
import type {
|
|
35
21
|
Message,
|
|
36
22
|
Provider,
|
|
@@ -44,6 +30,15 @@ const userMessage: Message = {
|
|
|
44
30
|
content: [{ type: "text", text: "hi" }],
|
|
45
31
|
};
|
|
46
32
|
|
|
33
|
+
/** A user turn whose tail block is the ephemeral memory-v3 spotlight. */
|
|
34
|
+
const spotlightUserMessage: Message = {
|
|
35
|
+
role: "user",
|
|
36
|
+
content: [
|
|
37
|
+
{ type: "text", text: "hi" },
|
|
38
|
+
{ type: "text", text: wrapMemorySpotlightBlock("recalled: Alice's plan") },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
47
42
|
function textResponse(text: string): ProviderResponse {
|
|
48
43
|
return {
|
|
49
44
|
content: [{ type: "text", text }],
|
|
@@ -87,40 +82,52 @@ function makeRecordingProvider(responses: ProviderResponse[]): {
|
|
|
87
82
|
return { provider, configs: () => configs };
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
const echoTool: ToolDefinition = {
|
|
86
|
+
name: "echo",
|
|
87
|
+
description: "Echo back the input",
|
|
88
|
+
input_schema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: { value: { type: "string" } },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
describe("AgentLoop.run: mutableLatestUserMessage from spotlight presence", () => {
|
|
95
|
+
test("sets mutableLatestUserMessage when the latest user message carries a spotlight block", async () => {
|
|
96
|
+
// GIVEN a provider that records the config of each LLM call
|
|
97
|
+
const { provider, configs } = makeRecordingProvider([textResponse("done")]);
|
|
98
|
+
const loop = new AgentLoop({
|
|
99
|
+
provider,
|
|
100
|
+
systemPrompt: "system",
|
|
101
|
+
conversationId: "test-conversation",
|
|
102
|
+
config: { maxTokens: 1024 },
|
|
103
|
+
});
|
|
98
104
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
// WHEN the loop runs over a history whose tail carries the spotlight
|
|
106
|
+
await loop.run({
|
|
107
|
+
requestId: "test-request",
|
|
108
|
+
messages: [spotlightUserMessage],
|
|
109
|
+
onEvent: () => {},
|
|
110
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
111
|
+
callSite: "mainAgent",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// THEN the send carries the cache-anchor signal
|
|
115
|
+
expect(configs()).toHaveLength(1);
|
|
116
|
+
expect(configs()[0]?.mutableLatestUserMessage).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("omits mutableLatestUserMessage when the latest user message carries no spotlight", async () => {
|
|
120
|
+
// GIVEN a provider that records the config of each LLM call
|
|
121
|
+
const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
|
|
114
122
|
const loop = new AgentLoop({
|
|
115
|
-
provider
|
|
123
|
+
provider,
|
|
116
124
|
systemPrompt: "system",
|
|
117
125
|
conversationId: "test-conversation",
|
|
118
126
|
config: { maxTokens: 1024 },
|
|
119
|
-
tools: dummyTools,
|
|
120
|
-
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
121
127
|
});
|
|
122
128
|
|
|
123
|
-
// WHEN the loop runs
|
|
129
|
+
// WHEN the loop runs over a spotlight-free history: the shape memory-v3
|
|
130
|
+
// produces whenever it selects nothing to spotlight
|
|
124
131
|
await loop.run({
|
|
125
132
|
requestId: "test-request",
|
|
126
133
|
messages: [userMessage],
|
|
@@ -129,21 +136,16 @@ describe("AgentLoop.run — mutableLatestUserMessage from memory-v3-live", () =>
|
|
|
129
136
|
callSite: "mainAgent",
|
|
130
137
|
});
|
|
131
138
|
|
|
132
|
-
// THEN
|
|
133
|
-
expect(configs()).toHaveLength(
|
|
134
|
-
|
|
135
|
-
expect(cfg?.mutableLatestUserMessage).toBe(true);
|
|
136
|
-
}
|
|
139
|
+
// THEN the field is omitted entirely, not carried as false/undefined
|
|
140
|
+
expect(configs()).toHaveLength(1);
|
|
141
|
+
expect("mutableLatestUserMessage" in (configs()[0] ?? {})).toBe(false);
|
|
137
142
|
});
|
|
138
143
|
|
|
139
|
-
test("
|
|
140
|
-
// GIVEN
|
|
141
|
-
memoryV3LiveSlot = false;
|
|
142
|
-
|
|
143
|
-
// AND a provider that records the config of each LLM call
|
|
144
|
+
test("a text block merely opening with the spotlight tag is not treated as volatile", async () => {
|
|
145
|
+
// GIVEN a user message discussing the marker rather than carrying one
|
|
144
146
|
const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
|
|
145
147
|
const loop = new AgentLoop({
|
|
146
|
-
provider
|
|
148
|
+
provider,
|
|
147
149
|
systemPrompt: "system",
|
|
148
150
|
conversationId: "test-conversation",
|
|
149
151
|
config: { maxTokens: 1024 },
|
|
@@ -152,14 +154,59 @@ describe("AgentLoop.run — mutableLatestUserMessage from memory-v3-live", () =>
|
|
|
152
154
|
// WHEN the loop runs
|
|
153
155
|
await loop.run({
|
|
154
156
|
requestId: "test-request",
|
|
155
|
-
messages: [
|
|
157
|
+
messages: [
|
|
158
|
+
{
|
|
159
|
+
role: "user",
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: "<memory_spotlight>\nwhat does this tag do?",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
156
168
|
onEvent: () => {},
|
|
157
169
|
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
158
170
|
callSite: "mainAgent",
|
|
159
171
|
});
|
|
160
172
|
|
|
161
|
-
// THEN the
|
|
173
|
+
// THEN the unterminated wrapper does not count: matching the full-wrapper
|
|
174
|
+
// requirement the per-turn strip applies
|
|
162
175
|
expect(configs()).toHaveLength(1);
|
|
163
176
|
expect("mutableLatestUserMessage" in (configs()[0] ?? {})).toBe(false);
|
|
164
177
|
});
|
|
178
|
+
|
|
179
|
+
test("the signal holds for every request in a tool loop", async () => {
|
|
180
|
+
// GIVEN a spotlight-bearing opening message and a tool round-trip
|
|
181
|
+
const { provider, configs } = makeRecordingProvider([
|
|
182
|
+
toolUseResponse("t1", "echo", { value: "first" }),
|
|
183
|
+
textResponse("done"),
|
|
184
|
+
]);
|
|
185
|
+
const loop = new AgentLoop({
|
|
186
|
+
provider,
|
|
187
|
+
systemPrompt: "system",
|
|
188
|
+
conversationId: "test-conversation",
|
|
189
|
+
config: { maxTokens: 1024 },
|
|
190
|
+
tools: [echoTool],
|
|
191
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// WHEN the loop runs
|
|
195
|
+
await loop.run({
|
|
196
|
+
requestId: "test-request",
|
|
197
|
+
messages: [spotlightUserMessage],
|
|
198
|
+
onEvent: () => {},
|
|
199
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
200
|
+
callSite: "mainAgent",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// THEN both calls report the turn as volatile. The signal describes the
|
|
204
|
+
// turn-starting message, not whichever user message happens to be last, so
|
|
205
|
+
// the trailing tool-result turn does not clear it. Holding it steady is
|
|
206
|
+
// what keeps the provider marking that one block at a single TTL for the
|
|
207
|
+
// whole turn instead of writing it twice.
|
|
208
|
+
expect(configs()).toHaveLength(2);
|
|
209
|
+
expect(configs()[0]?.mutableLatestUserMessage).toBe(true);
|
|
210
|
+
expect(configs()[1]?.mutableLatestUserMessage).toBe(true);
|
|
211
|
+
});
|
|
165
212
|
});
|
|
@@ -781,10 +781,11 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
781
781
|
expect(prevLast.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
782
782
|
});
|
|
783
783
|
|
|
784
|
-
test("mutableLatestUserMessage:
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
784
|
+
test("mutableLatestUserMessage: a volatile turn start keeps the 5m TTL through its tool-use loop", async () => {
|
|
785
|
+
// The turn start is fixed for the rest of the turn, so it stays markable;
|
|
786
|
+
// but it is volatile ACROSS turns, so the long TTL would buy a write that
|
|
787
|
+
// can never be read back. Upgrading it here would also mark the same block
|
|
788
|
+
// at a second TTL, billing two writes for one reusable prefix.
|
|
788
789
|
const messages: Message[] = [
|
|
789
790
|
userMsg("Turn 1"),
|
|
790
791
|
assistantMsg("Response 1"),
|
|
@@ -804,14 +805,12 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
804
805
|
}>;
|
|
805
806
|
}>;
|
|
806
807
|
|
|
807
|
-
// Turn-start (Turn 2, index 2) still gets the 1h anchor — within a tool-use
|
|
808
|
-
// loop the block is fixed, so the volatile-latest flag must not move it.
|
|
809
808
|
const turn2 = sent[2];
|
|
810
809
|
const turn2Last = turn2.content[turn2.content.length - 1];
|
|
811
|
-
expect(turn2Last.cache_control).toEqual({ type: "ephemeral", ttl: "
|
|
810
|
+
expect(turn2Last.cache_control).toEqual({ type: "ephemeral", ttl: "5m" });
|
|
812
811
|
});
|
|
813
812
|
|
|
814
|
-
test("mutableLatestUserMessage: first turn
|
|
813
|
+
test("mutableLatestUserMessage: volatile first turn carries the 5m tail but no long-TTL anchor", async () => {
|
|
815
814
|
await provider.sendMessage([userMsg("First ever turn (volatile)")], {
|
|
816
815
|
config: { mutableLatestUserMessage: true },
|
|
817
816
|
});
|
|
@@ -823,9 +822,28 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
823
822
|
cache_control?: { type: string; ttl?: string };
|
|
824
823
|
}>;
|
|
825
824
|
}>;
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
825
|
+
// The only user message is volatile, so it gets no 1h anchor: those bytes
|
|
826
|
+
// do not recur next turn. It does get the 5m tail; the message is fixed
|
|
827
|
+
// within its own turn, so the tool-loop calls that follow read the system
|
|
828
|
+
// prompt, tools, and opening message back instead of re-writing them.
|
|
829
|
+
const lastBlock = sent[0].content[sent[0].content.length - 1];
|
|
830
|
+
expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "5m" });
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("mutableLatestUserMessage: volatile first turn with disableTurnStartCache places no message breakpoint", async () => {
|
|
834
|
+
// One-shot call sites opt out of paying for a turn-start write; the 5m
|
|
835
|
+
// bridge lands on that same block, so it honors the same opt-out.
|
|
836
|
+
await provider.sendMessage([userMsg("One-shot (volatile)")], {
|
|
837
|
+
config: { mutableLatestUserMessage: true, disableTurnStartCache: true },
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const sent = lastStreamParams!.messages as Array<{
|
|
841
|
+
role: string;
|
|
842
|
+
content: Array<{
|
|
843
|
+
type: string;
|
|
844
|
+
cache_control?: { type: string; ttl?: string };
|
|
845
|
+
}>;
|
|
846
|
+
}>;
|
|
829
847
|
const lastBlock = sent[0].content[sent[0].content.length - 1];
|
|
830
848
|
expect(lastBlock.cache_control).toBeUndefined();
|
|
831
849
|
});
|