@vellumai/assistant 0.8.9 → 0.8.10-dev.202606092334.09948c8
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/AGENTS.md +1 -1
- package/examples/plugins/echo/README.md +3 -3
- package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +2 -0
- package/openapi.yaml +38 -12
- package/package.json +1 -1
- package/scripts/sync-llm-catalog.ts +2 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +21 -15
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +11 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +90 -49
- package/src/__tests__/conversation-agent-loop.test.ts +113 -64
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +110 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +48 -48
- package/src/__tests__/conversation-queue.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +91 -142
- package/src/__tests__/conversation-tool-setup-skill-attribution.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-test-helpers.ts +98 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/external-plugin-loader.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +29 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skills.test.ts +123 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-audit-listener.test.ts +51 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +32 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/agent/loop.ts +26 -39
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/responses/conversation-message.ts +91 -14
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/lib/__tests__/install-from-github.test.ts +174 -18
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -1
- package/src/cli/lib/__tests__/plugin-details.test.ts +1 -1
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +1 -1
- package/src/cli/lib/__tests__/search-plugins.test.ts +11 -11
- package/src/cli/lib/install-from-github.ts +130 -19
- package/src/cli/lib/plugin-catalog-cache.ts +1 -1
- package/src/cli/lib/plugin-details.ts +4 -4
- package/src/cli/lib/plugin-marketplace.ts +2 -2
- package/src/cli/lib/search-plugins.ts +6 -6
- package/src/config/bundled-skills/skill-management/SKILL.md +89 -1
- package/src/config/env-registry.ts +12 -0
- package/src/config/feature-flag-registry.json +0 -80
- package/src/config/loader.ts +5 -22
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +101 -47
- package/src/context/strip-injections.ts +7 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +102 -6
- package/src/daemon/conversation-agent-loop.ts +126 -270
- package/src/daemon/conversation-runtime-assembly.ts +44 -8
- package/src/daemon/conversation-tool-setup.ts +9 -1
- package/src/daemon/conversation.ts +13 -0
- package/src/daemon/external-plugins-bootstrap.ts +33 -87
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +6 -2
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/skills.ts +4 -0
- package/src/events/tool-audit-listener.ts +7 -1
- package/src/memory/__tests__/tool-invocation-test-helpers.ts +69 -0
- package/src/memory/conversation-crud.ts +44 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +5 -22
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/tool-execution-events-store.test.ts +162 -0
- package/src/memory/tool-execution-events-store.ts +73 -0
- package/src/memory/tool-usage-store.ts +4 -3
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +51 -0
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/injected-block-slugs.ts +30 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +37 -6
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +39 -1
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/plugins/defaults/compaction/window-manager.ts +195 -1
- package/src/plugins/defaults/index.ts +24 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +36 -67
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/external-api.ts +1 -1
- package/src/plugins/types.ts +5 -51
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +57 -1
- package/src/providers/registry.ts +0 -8
- package/src/providers/retry.ts +35 -7
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +4 -4
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +3 -3
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/telemetry/types.ts +25 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +141 -7
- package/src/telemetry/usage-telemetry-reporter.ts +63 -7
- package/src/tools/executor.ts +4 -1
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/types.ts +10 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage for the turn-scoped overflow-recovery state machine that lives on
|
|
3
|
+
* `ContextWindowManager.reduceOverflowOneRung`. The manager holds the reducer
|
|
4
|
+
* state across the successive rung calls of a single turn, derives the
|
|
5
|
+
* compaction target from its own preflight budget corrected by the provider's
|
|
6
|
+
* actual token count, and starts a fresh ladder once the turn boundary resets
|
|
7
|
+
* it. `reduceContextOverflow` is mocked so these tests pin the manager's own
|
|
8
|
+
* orchestration without exercising the full reduction ladder.
|
|
9
|
+
*/
|
|
10
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
function makeLoggerStub(): Record<string, unknown> {
|
|
13
|
+
const stub: Record<string, unknown> = {};
|
|
14
|
+
for (const m of [
|
|
15
|
+
"info",
|
|
16
|
+
"warn",
|
|
17
|
+
"error",
|
|
18
|
+
"debug",
|
|
19
|
+
"trace",
|
|
20
|
+
"fatal",
|
|
21
|
+
"silent",
|
|
22
|
+
"child",
|
|
23
|
+
]) {
|
|
24
|
+
stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
|
|
25
|
+
}
|
|
26
|
+
return stub;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
mock.module("../util/logger.js", () => ({
|
|
30
|
+
getLogger: () => makeLoggerStub(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// ── Module-level mock state ───────────────────────────────────────────
|
|
34
|
+
// The reducer call is captured so tests can assert which state and config the
|
|
35
|
+
// manager fed in; the returned step is seeded per call so tests can drive the
|
|
36
|
+
// ladder forward across rungs.
|
|
37
|
+
|
|
38
|
+
interface ReducerState {
|
|
39
|
+
appliedTiers: string[];
|
|
40
|
+
injectionMode: string;
|
|
41
|
+
exhausted: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ReducerStepResult {
|
|
45
|
+
messages: unknown;
|
|
46
|
+
tier: string;
|
|
47
|
+
state: ReducerState;
|
|
48
|
+
estimatedTokens: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let reduceCalls: Array<{
|
|
52
|
+
messages: unknown;
|
|
53
|
+
config: Record<string, unknown>;
|
|
54
|
+
state: ReducerState | undefined;
|
|
55
|
+
}> = [];
|
|
56
|
+
let reduceSteps: ReducerStepResult[] = [];
|
|
57
|
+
|
|
58
|
+
const estimateReturns: number[] = [];
|
|
59
|
+
|
|
60
|
+
mock.module("../context/token-estimator.js", () => ({
|
|
61
|
+
estimatePromptTokens: (): number => {
|
|
62
|
+
const next = estimateReturns.shift();
|
|
63
|
+
if (next === undefined) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"estimatePromptTokens called more times than estimateReturns seeded",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return next;
|
|
69
|
+
},
|
|
70
|
+
// Deterministic per-tool budget so tests can assert the manager fed the
|
|
71
|
+
// turn's resolved tool set (not the constructor-time snapshot) into the
|
|
72
|
+
// overflow estimate.
|
|
73
|
+
estimateToolsTokens: (tools: unknown[]): number => tools.length * 1_000,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
mock.module(
|
|
77
|
+
"../plugins/defaults/compaction/context-overflow-reducer.js",
|
|
78
|
+
() => ({
|
|
79
|
+
createInitialReducerState: (): ReducerState => ({
|
|
80
|
+
appliedTiers: [],
|
|
81
|
+
injectionMode: "full",
|
|
82
|
+
exhausted: false,
|
|
83
|
+
}),
|
|
84
|
+
reduceContextOverflow: async (
|
|
85
|
+
messages: unknown,
|
|
86
|
+
config: Record<string, unknown>,
|
|
87
|
+
state: ReducerState | undefined,
|
|
88
|
+
): Promise<ReducerStepResult> => {
|
|
89
|
+
reduceCalls.push({ messages, config, state });
|
|
90
|
+
const idx = reduceCalls.length - 1;
|
|
91
|
+
const step = reduceSteps[idx];
|
|
92
|
+
if (!step) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Mock reducer called ${reduceCalls.length} time(s) but only ${reduceSteps.length} step(s) seeded`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return step;
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
|
|
103
|
+
import type { Message, Provider, ToolDefinition } from "../providers/types.js";
|
|
104
|
+
|
|
105
|
+
function makeProvider(): Provider {
|
|
106
|
+
return {
|
|
107
|
+
name: "mock-provider",
|
|
108
|
+
sendMessage: async () => ({
|
|
109
|
+
content: [],
|
|
110
|
+
model: "mock-model",
|
|
111
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
112
|
+
stopReason: "end_turn",
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function makeMessages(count: number): Message[] {
|
|
118
|
+
const messages: Message[] = [];
|
|
119
|
+
for (let i = 0; i < count; i++) {
|
|
120
|
+
messages.push({
|
|
121
|
+
role: i % 2 === 0 ? "user" : "assistant",
|
|
122
|
+
content: [{ type: "text", text: `msg ${i}` }],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return messages;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function makeConfig(maxAttempts: number = 3) {
|
|
129
|
+
return {
|
|
130
|
+
enabled: true,
|
|
131
|
+
maxInputTokens: 200_000,
|
|
132
|
+
targetBudgetRatio: 0.3,
|
|
133
|
+
compactThreshold: 0.8,
|
|
134
|
+
summaryBudgetRatio: 0.05,
|
|
135
|
+
overflowRecovery: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
safetyMarginRatio: 0.05,
|
|
138
|
+
maxAttempts,
|
|
139
|
+
interactiveLatestTurnCompression: "summarize" as const,
|
|
140
|
+
nonInteractiveLatestTurnCompression: "summarize" as const,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildManager(maxAttempts: number = 3): ContextWindowManager {
|
|
146
|
+
return new ContextWindowManager({
|
|
147
|
+
provider: makeProvider(),
|
|
148
|
+
systemPrompt: "you are a test assistant",
|
|
149
|
+
config: makeConfig(maxAttempts),
|
|
150
|
+
conversationId: "conv-test",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function makeStep(
|
|
155
|
+
appliedTiers: string[],
|
|
156
|
+
exhausted: boolean = false,
|
|
157
|
+
): ReducerStepResult {
|
|
158
|
+
return {
|
|
159
|
+
messages: makeMessages(2),
|
|
160
|
+
tier: appliedTiers[appliedTiers.length - 1] ?? "emergency_compaction",
|
|
161
|
+
state: { appliedTiers, injectionMode: "full", exhausted },
|
|
162
|
+
estimatedTokens: 100_000,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
describe("ContextWindowManager.reduceOverflowOneRung", () => {
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
reduceCalls = [];
|
|
169
|
+
reduceSteps = [];
|
|
170
|
+
estimateReturns.length = 0;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("holds reducer state across rungs within a turn", async () => {
|
|
174
|
+
// GIVEN a manager and a reducer that advances the ladder one rung per call
|
|
175
|
+
estimateReturns.push(240_000, 240_000);
|
|
176
|
+
reduceSteps = [
|
|
177
|
+
makeStep(["emergency_compaction"]),
|
|
178
|
+
makeStep(["emergency_compaction", "forced_compaction"]),
|
|
179
|
+
];
|
|
180
|
+
const manager = buildManager();
|
|
181
|
+
|
|
182
|
+
// WHEN two rungs run within the same turn
|
|
183
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
184
|
+
actualTokens: null,
|
|
185
|
+
allowAutoCompressLatestTurn: false,
|
|
186
|
+
});
|
|
187
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
188
|
+
actualTokens: null,
|
|
189
|
+
allowAutoCompressLatestTurn: false,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// THEN the first call seeds an empty ladder
|
|
193
|
+
expect(reduceCalls).toHaveLength(2);
|
|
194
|
+
expect(reduceCalls[0]?.state?.appliedTiers).toEqual([]);
|
|
195
|
+
// AND the second call carries the first rung's state forward
|
|
196
|
+
expect(reduceCalls[1]?.state?.appliedTiers).toEqual([
|
|
197
|
+
"emergency_compaction",
|
|
198
|
+
]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("reuses the first rung's corrected target across the turn's later rungs", async () => {
|
|
202
|
+
// GIVEN the provider rejected at 480k while the estimator counted 240k, and
|
|
203
|
+
// only one estimate is seeded so a second estimate call would throw
|
|
204
|
+
estimateReturns.push(240_000);
|
|
205
|
+
reduceSteps = [
|
|
206
|
+
makeStep(["emergency_compaction"]),
|
|
207
|
+
makeStep(["emergency_compaction", "forced_compaction"]),
|
|
208
|
+
];
|
|
209
|
+
const manager = buildManager();
|
|
210
|
+
|
|
211
|
+
// WHEN two rungs run within the same turn with the provider's actual count
|
|
212
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
213
|
+
actualTokens: 480_000,
|
|
214
|
+
allowAutoCompressLatestTurn: false,
|
|
215
|
+
});
|
|
216
|
+
await manager.reduceOverflowOneRung(makeMessages(4), {
|
|
217
|
+
actualTokens: 480_000,
|
|
218
|
+
allowAutoCompressLatestTurn: false,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// THEN the corrected target (190k preflight / 2x error ratio) is computed
|
|
222
|
+
// once and reused, rather than re-derived against the shrunk prompt
|
|
223
|
+
expect(reduceCalls[0]?.config.targetTokens).toBe(95_000);
|
|
224
|
+
expect(reduceCalls[1]?.config.targetTokens).toBe(95_000);
|
|
225
|
+
expect(reduceCalls[0]?.config.previousEstimatedInputTokens).toBe(240_000);
|
|
226
|
+
expect(reduceCalls[1]?.config.previousEstimatedInputTokens).toBe(240_000);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("resetOverflowRecovery starts a fresh ladder", async () => {
|
|
230
|
+
// GIVEN a manager whose ladder has already advanced one rung
|
|
231
|
+
estimateReturns.push(240_000, 240_000);
|
|
232
|
+
reduceSteps = [
|
|
233
|
+
makeStep(["emergency_compaction"]),
|
|
234
|
+
makeStep(["emergency_compaction"]),
|
|
235
|
+
];
|
|
236
|
+
const manager = buildManager();
|
|
237
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
238
|
+
actualTokens: null,
|
|
239
|
+
allowAutoCompressLatestTurn: false,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// WHEN the turn boundary resets recovery and a new rung runs
|
|
243
|
+
manager.resetOverflowRecovery();
|
|
244
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
245
|
+
actualTokens: null,
|
|
246
|
+
allowAutoCompressLatestTurn: false,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// THEN the post-reset call seeds an empty ladder again
|
|
250
|
+
expect(reduceCalls[1]?.state?.appliedTiers).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("corrected target lowers below preflight when the provider under-counted", async () => {
|
|
254
|
+
// GIVEN the provider rejected at 480k while the estimator counted 240k
|
|
255
|
+
estimateReturns.push(240_000);
|
|
256
|
+
reduceSteps = [makeStep(["emergency_compaction"])];
|
|
257
|
+
const manager = buildManager();
|
|
258
|
+
|
|
259
|
+
// WHEN a rung runs with the provider's actual token count
|
|
260
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
261
|
+
actualTokens: 480_000,
|
|
262
|
+
allowAutoCompressLatestTurn: false,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// THEN the target is the preflight budget (200k * 0.95 = 190k) divided by
|
|
266
|
+
// the 2x estimation-error ratio
|
|
267
|
+
expect(reduceCalls[0]?.config.targetTokens).toBe(95_000);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("estimates against the turn's resolved tool set, not the constructor snapshot", async () => {
|
|
271
|
+
// GIVEN a manager whose live tool set (3 tools) differs from the stale
|
|
272
|
+
// constructor-time budget snapshot
|
|
273
|
+
estimateReturns.push(240_000);
|
|
274
|
+
reduceSteps = [makeStep(["emergency_compaction"])];
|
|
275
|
+
const resolvedTools = [
|
|
276
|
+
{ name: "a" },
|
|
277
|
+
{ name: "b" },
|
|
278
|
+
{ name: "c" },
|
|
279
|
+
] as unknown as ToolDefinition[];
|
|
280
|
+
const manager = new ContextWindowManager({
|
|
281
|
+
provider: makeProvider(),
|
|
282
|
+
systemPrompt: "you are a test assistant",
|
|
283
|
+
config: makeConfig(),
|
|
284
|
+
conversationId: "conv-test",
|
|
285
|
+
toolTokenBudget: 50,
|
|
286
|
+
resolveTools: () => resolvedTools,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// WHEN a rung runs
|
|
290
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
291
|
+
actualTokens: null,
|
|
292
|
+
allowAutoCompressLatestTurn: false,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// THEN the reducer is fed the resolved tool budget (3 tools * 1k) rather
|
|
296
|
+
// than the constructor snapshot (50)
|
|
297
|
+
expect(reduceCalls[0]?.config.toolTokenBudget).toBe(3_000);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("target is the full preflight budget when the actual count is unknown", async () => {
|
|
301
|
+
// GIVEN no provider-reported actual token count
|
|
302
|
+
estimateReturns.push(240_000);
|
|
303
|
+
reduceSteps = [makeStep(["emergency_compaction"])];
|
|
304
|
+
const manager = buildManager();
|
|
305
|
+
|
|
306
|
+
// WHEN a rung runs
|
|
307
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
308
|
+
actualTokens: null,
|
|
309
|
+
allowAutoCompressLatestTurn: false,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// THEN the target is the uncorrected preflight budget (200k * 0.95)
|
|
313
|
+
expect(reduceCalls[0]?.config.targetTokens).toBe(190_000);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("long histories widen the safety margin", async () => {
|
|
317
|
+
// GIVEN a history longer than the 50-message threshold
|
|
318
|
+
estimateReturns.push(240_000);
|
|
319
|
+
reduceSteps = [makeStep(["emergency_compaction"])];
|
|
320
|
+
const manager = buildManager();
|
|
321
|
+
|
|
322
|
+
// WHEN a rung runs over 60 messages with no actual-token correction
|
|
323
|
+
await manager.reduceOverflowOneRung(makeMessages(60), {
|
|
324
|
+
actualTokens: null,
|
|
325
|
+
allowAutoCompressLatestTurn: false,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// THEN the safety margin bumps to 0.15 → preflight budget 200k * 0.85
|
|
329
|
+
expect(reduceCalls[0]?.config.targetTokens).toBe(170_000);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("forwards the policy auto-compress permission to the ladder", async () => {
|
|
333
|
+
// GIVEN the overflow policy permits the terminal auto-compress rung
|
|
334
|
+
estimateReturns.push(240_000);
|
|
335
|
+
reduceSteps = [makeStep(["emergency_compaction"])];
|
|
336
|
+
const manager = buildManager();
|
|
337
|
+
|
|
338
|
+
// WHEN a rung runs
|
|
339
|
+
await manager.reduceOverflowOneRung(makeMessages(10), {
|
|
340
|
+
actualTokens: null,
|
|
341
|
+
allowAutoCompressLatestTurn: true,
|
|
342
|
+
overrideProfile: "fast",
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// THEN the manager passes the policy decision and profile through to the
|
|
346
|
+
// reducer config alongside the budget it owns
|
|
347
|
+
expect(reduceCalls[0]?.config.allowAutoCompressLatestTurn).toBe(true);
|
|
348
|
+
expect(reduceCalls[0]?.config.overrideProfile).toBe("fast");
|
|
349
|
+
expect(reduceCalls[0]?.config.maxMiddleTierAttempts).toBe(3);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -435,6 +435,17 @@ function makeCtx(
|
|
|
435
435
|
updateConfig: () => {},
|
|
436
436
|
shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
|
|
437
437
|
maybeCompact: async () => ({ compacted: false }),
|
|
438
|
+
resetOverflowRecovery: () => {},
|
|
439
|
+
reduceOverflowOneRung: async (msgs: Message[]) => ({
|
|
440
|
+
messages: msgs,
|
|
441
|
+
tier: "forced_compaction",
|
|
442
|
+
state: {
|
|
443
|
+
appliedTiers: ["forced_compaction"],
|
|
444
|
+
injectionMode: "full",
|
|
445
|
+
exhausted: true,
|
|
446
|
+
},
|
|
447
|
+
estimatedTokens: 1000,
|
|
448
|
+
}),
|
|
438
449
|
} as unknown as Conversation["contextWindowManager"],
|
|
439
450
|
contextCompactedMessageCount: 0,
|
|
440
451
|
contextCompactedAt: null,
|
|
@@ -151,39 +151,70 @@ mock.module("../context/token-estimator.js", () => ({
|
|
|
151
151
|
let mockReducerStepFn:
|
|
152
152
|
| ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
|
|
153
153
|
| null = null;
|
|
154
|
+
const makeInitialReducerState = () => ({
|
|
155
|
+
appliedTiers: [] as string[],
|
|
156
|
+
injectionMode: "full" as const,
|
|
157
|
+
exhausted: false,
|
|
158
|
+
});
|
|
159
|
+
const runMockReducer = async (
|
|
160
|
+
msgs: Message[],
|
|
161
|
+
cfg: unknown,
|
|
162
|
+
state: unknown,
|
|
163
|
+
) => {
|
|
164
|
+
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
165
|
+
return {
|
|
166
|
+
messages: msgs,
|
|
167
|
+
tier: "forced_compaction",
|
|
168
|
+
state: {
|
|
169
|
+
appliedTiers: [
|
|
170
|
+
"forced_compaction",
|
|
171
|
+
"tool_result_truncation",
|
|
172
|
+
"media_stubbing",
|
|
173
|
+
"injection_downgrade",
|
|
174
|
+
],
|
|
175
|
+
injectionMode: "full",
|
|
176
|
+
exhausted: true,
|
|
177
|
+
},
|
|
178
|
+
estimatedTokens: 1000,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
154
181
|
mock.module(
|
|
155
182
|
"../plugins/defaults/compaction/context-overflow-reducer.js",
|
|
156
183
|
() => ({
|
|
157
|
-
createInitialReducerState:
|
|
158
|
-
|
|
159
|
-
injectionMode: "full" as const,
|
|
160
|
-
exhausted: false,
|
|
161
|
-
}),
|
|
162
|
-
reduceContextOverflow: async (
|
|
163
|
-
msgs: Message[],
|
|
164
|
-
cfg: unknown,
|
|
165
|
-
state: unknown,
|
|
166
|
-
) => {
|
|
167
|
-
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
168
|
-
return {
|
|
169
|
-
messages: msgs,
|
|
170
|
-
tier: "forced_compaction",
|
|
171
|
-
state: {
|
|
172
|
-
appliedTiers: [
|
|
173
|
-
"forced_compaction",
|
|
174
|
-
"tool_result_truncation",
|
|
175
|
-
"media_stubbing",
|
|
176
|
-
"injection_downgrade",
|
|
177
|
-
],
|
|
178
|
-
injectionMode: "full",
|
|
179
|
-
exhausted: true,
|
|
180
|
-
},
|
|
181
|
-
estimatedTokens: 1000,
|
|
182
|
-
};
|
|
183
|
-
},
|
|
184
|
+
createInitialReducerState: makeInitialReducerState,
|
|
185
|
+
reduceContextOverflow: runMockReducer,
|
|
184
186
|
}),
|
|
185
187
|
);
|
|
186
188
|
|
|
189
|
+
// Stand-in for `ContextWindowManager`'s turn-scoped overflow ladder. Threads
|
|
190
|
+
// reducer state across a turn's rungs and delegates each rung to the mocked
|
|
191
|
+
// reducer, mirroring `reduceOverflowOneRung` / `resetOverflowRecovery` so the
|
|
192
|
+
// convergence driver exercises the same per-rung escalation the real manager
|
|
193
|
+
// drives.
|
|
194
|
+
function makeOverflowLadderStub(): {
|
|
195
|
+
resetOverflowRecovery: () => void;
|
|
196
|
+
reduceOverflowOneRung: (
|
|
197
|
+
msgs: Message[],
|
|
198
|
+
opts: unknown,
|
|
199
|
+
signal?: AbortSignal,
|
|
200
|
+
) => Promise<unknown>;
|
|
201
|
+
} {
|
|
202
|
+
let state: unknown;
|
|
203
|
+
return {
|
|
204
|
+
resetOverflowRecovery: () => {
|
|
205
|
+
state = undefined;
|
|
206
|
+
},
|
|
207
|
+
reduceOverflowOneRung: async (msgs: Message[], opts: unknown) => {
|
|
208
|
+
if (!state) state = makeInitialReducerState();
|
|
209
|
+
const step = (await runMockReducer(msgs, opts, state)) as {
|
|
210
|
+
state: unknown;
|
|
211
|
+
};
|
|
212
|
+
state = step.state;
|
|
213
|
+
return step;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
187
218
|
// Policy: default to fail_gracefully
|
|
188
219
|
let mockOverflowAction: string = "fail_gracefully";
|
|
189
220
|
mock.module("../daemon/context-overflow-policy.js", () => ({
|
|
@@ -599,6 +630,16 @@ function makeCtx(
|
|
|
599
630
|
|
|
600
631
|
...ctxOverrides,
|
|
601
632
|
} as unknown as Conversation;
|
|
633
|
+
// The convergence driver resolves the turn-scoped overflow ladder off the
|
|
634
|
+
// manager; give every fake manager the ladder methods unless a test supplied
|
|
635
|
+
// its own.
|
|
636
|
+
const manager = ctx.contextWindowManager as unknown as Record<
|
|
637
|
+
string,
|
|
638
|
+
unknown
|
|
639
|
+
>;
|
|
640
|
+
if (typeof manager.reduceOverflowOneRung !== "function") {
|
|
641
|
+
Object.assign(manager, makeOverflowLadderStub());
|
|
642
|
+
}
|
|
602
643
|
fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
|
|
603
644
|
return ctx;
|
|
604
645
|
}
|
|
@@ -1721,28 +1762,30 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
|
|
|
1721
1762
|
return 170_000;
|
|
1722
1763
|
};
|
|
1723
1764
|
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1765
|
+
// The reduction ladder applies a middle tier first, then escalates to its
|
|
1766
|
+
// terminal auto-compress-latest-turn rung and reports exhaustion. The
|
|
1767
|
+
// terminal rung is what produces a `budget_yield_unrecovered` outcome when
|
|
1768
|
+
// its rerun still yields at the mid-loop checkpoint.
|
|
1726
1769
|
let reducerCallCount = 0;
|
|
1727
1770
|
mockReducerStepFn = (msgs: Message[]) => {
|
|
1728
1771
|
reducerCallCount++;
|
|
1729
|
-
const
|
|
1772
|
+
const terminal = reducerCallCount >= 2;
|
|
1730
1773
|
return {
|
|
1731
1774
|
messages: msgs,
|
|
1732
|
-
tier:
|
|
1775
|
+
tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
|
|
1733
1776
|
state: {
|
|
1734
|
-
appliedTiers:
|
|
1735
|
-
? ["forced_compaction", "
|
|
1777
|
+
appliedTiers: terminal
|
|
1778
|
+
? ["forced_compaction", "auto_compress_latest_turn"]
|
|
1736
1779
|
: ["forced_compaction"],
|
|
1737
1780
|
injectionMode: "full" as const,
|
|
1738
|
-
exhausted,
|
|
1781
|
+
exhausted: terminal,
|
|
1739
1782
|
},
|
|
1740
|
-
estimatedTokens:
|
|
1783
|
+
estimatedTokens: terminal ? 60_000 : 80_000,
|
|
1741
1784
|
};
|
|
1742
1785
|
};
|
|
1743
1786
|
|
|
1744
|
-
// The overflow policy
|
|
1745
|
-
//
|
|
1787
|
+
// The overflow policy permits the terminal auto-compress-latest-turn rung,
|
|
1788
|
+
// so the ladder runs it as its final step.
|
|
1746
1789
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1747
1790
|
|
|
1748
1791
|
// Every provider call returns a tool_use, so each loop run does a tool
|
|
@@ -1752,19 +1795,16 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
|
|
|
1752
1795
|
toolUseResponse("tu-1", "bash", { command: "ls" }),
|
|
1753
1796
|
]);
|
|
1754
1797
|
|
|
1755
|
-
//
|
|
1756
|
-
//
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1759
|
-
//
|
|
1760
|
-
// mid-loop
|
|
1798
|
+
// Forced `maybeCompact` is invoked through the loop's own pre-call budget
|
|
1799
|
+
// gate at two call sites (the reduction-ladder rungs are mocked out, so
|
|
1800
|
+
// their summary calls don't reach this manager):
|
|
1801
|
+
// 1. First-call (turn-start) gate (`force: true`) — the loop owns the
|
|
1802
|
+
// turn-start compaction. It succeeds, but the mocked estimate stays
|
|
1803
|
+
// above the mid-loop threshold, so the turn proceeds and the mid-loop
|
|
1804
|
+
// gate still trips on the next iteration.
|
|
1761
1805
|
// 2. Mid-loop after the first tool turn (`force: true`) — must signal
|
|
1762
1806
|
// `exhausted: true` so the daemon escalates to the convergence
|
|
1763
1807
|
// reducer instead of looping forever.
|
|
1764
|
-
// 3. auto_compress_latest_turn emergency compaction (`force: true`,
|
|
1765
|
-
// `minKeepRecentUserTurns: 0`) — succeeds and drops tokens below
|
|
1766
|
-
// threshold; the subsequent rerun yields again and is classified
|
|
1767
|
-
// as BUDGET_YIELD_UNRECOVERED.
|
|
1768
1808
|
let forcedMaybeCompactCallCount = 0;
|
|
1769
1809
|
const ctx = makeCtx({
|
|
1770
1810
|
loopProvider: provider,
|
|
@@ -1843,7 +1883,8 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
|
|
|
1843
1883
|
exhausted: true,
|
|
1844
1884
|
};
|
|
1845
1885
|
}
|
|
1846
|
-
//
|
|
1886
|
+
// Defensive fallback for any additional forced probe; the two gate
|
|
1887
|
+
// call sites above are the only ones this test exercises.
|
|
1847
1888
|
return {
|
|
1848
1889
|
compacted: true,
|
|
1849
1890
|
messages: [
|
|
@@ -1853,7 +1894,7 @@ describe("session-agent-loop overflow recovery (JARVIS-110)", () => {
|
|
|
1853
1894
|
},
|
|
1854
1895
|
] as Message[],
|
|
1855
1896
|
compactedPersistedMessages: 5,
|
|
1856
|
-
summaryText: "
|
|
1897
|
+
summaryText: "Fallback summary",
|
|
1857
1898
|
previousEstimatedInputTokens: 170_000,
|
|
1858
1899
|
estimatedInputTokens: 90_000,
|
|
1859
1900
|
maxInputTokens: 200_000,
|