@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
|
@@ -114,39 +114,70 @@ mock.module("../context/token-estimator.js", () => ({
|
|
|
114
114
|
let mockReducerStepFn:
|
|
115
115
|
| ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
|
|
116
116
|
| null = null;
|
|
117
|
+
const makeInitialReducerState = () => ({
|
|
118
|
+
appliedTiers: [] as string[],
|
|
119
|
+
injectionMode: "full" as const,
|
|
120
|
+
exhausted: false,
|
|
121
|
+
});
|
|
122
|
+
const runMockReducer = async (
|
|
123
|
+
msgs: Message[],
|
|
124
|
+
cfg: unknown,
|
|
125
|
+
state: unknown,
|
|
126
|
+
) => {
|
|
127
|
+
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
128
|
+
return {
|
|
129
|
+
messages: msgs,
|
|
130
|
+
tier: "forced_compaction",
|
|
131
|
+
state: {
|
|
132
|
+
appliedTiers: [
|
|
133
|
+
"forced_compaction",
|
|
134
|
+
"tool_result_truncation",
|
|
135
|
+
"media_stubbing",
|
|
136
|
+
"injection_downgrade",
|
|
137
|
+
],
|
|
138
|
+
injectionMode: "full",
|
|
139
|
+
exhausted: true,
|
|
140
|
+
},
|
|
141
|
+
estimatedTokens: 1000,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
117
144
|
mock.module(
|
|
118
145
|
"../plugins/defaults/compaction/context-overflow-reducer.js",
|
|
119
146
|
() => ({
|
|
120
|
-
createInitialReducerState:
|
|
121
|
-
|
|
122
|
-
injectionMode: "full" as const,
|
|
123
|
-
exhausted: false,
|
|
124
|
-
}),
|
|
125
|
-
reduceContextOverflow: async (
|
|
126
|
-
msgs: Message[],
|
|
127
|
-
cfg: unknown,
|
|
128
|
-
state: unknown,
|
|
129
|
-
) => {
|
|
130
|
-
if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
|
|
131
|
-
return {
|
|
132
|
-
messages: msgs,
|
|
133
|
-
tier: "forced_compaction",
|
|
134
|
-
state: {
|
|
135
|
-
appliedTiers: [
|
|
136
|
-
"forced_compaction",
|
|
137
|
-
"tool_result_truncation",
|
|
138
|
-
"media_stubbing",
|
|
139
|
-
"injection_downgrade",
|
|
140
|
-
],
|
|
141
|
-
injectionMode: "full",
|
|
142
|
-
exhausted: true,
|
|
143
|
-
},
|
|
144
|
-
estimatedTokens: 1000,
|
|
145
|
-
};
|
|
146
|
-
},
|
|
147
|
+
createInitialReducerState: makeInitialReducerState,
|
|
148
|
+
reduceContextOverflow: runMockReducer,
|
|
147
149
|
}),
|
|
148
150
|
);
|
|
149
151
|
|
|
152
|
+
// Stand-in for `ContextWindowManager`'s turn-scoped overflow ladder. Threads
|
|
153
|
+
// reducer state across a turn's rungs and delegates each rung to the mocked
|
|
154
|
+
// reducer, mirroring `reduceOverflowOneRung` / `resetOverflowRecovery` so the
|
|
155
|
+
// convergence driver exercises the same per-rung escalation the real manager
|
|
156
|
+
// drives.
|
|
157
|
+
function makeOverflowLadderStub(): {
|
|
158
|
+
resetOverflowRecovery: () => void;
|
|
159
|
+
reduceOverflowOneRung: (
|
|
160
|
+
msgs: Message[],
|
|
161
|
+
opts: unknown,
|
|
162
|
+
signal?: AbortSignal,
|
|
163
|
+
) => Promise<unknown>;
|
|
164
|
+
} {
|
|
165
|
+
let state: unknown;
|
|
166
|
+
return {
|
|
167
|
+
resetOverflowRecovery: () => {
|
|
168
|
+
state = undefined;
|
|
169
|
+
},
|
|
170
|
+
reduceOverflowOneRung: async (msgs: Message[], opts: unknown) => {
|
|
171
|
+
if (!state) state = makeInitialReducerState();
|
|
172
|
+
const step = (await runMockReducer(msgs, opts, state)) as {
|
|
173
|
+
state: unknown;
|
|
174
|
+
};
|
|
175
|
+
state = step.state;
|
|
176
|
+
return step;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
150
181
|
// Policy: default to fail_gracefully
|
|
151
182
|
let mockOverflowAction: string = "fail_gracefully";
|
|
152
183
|
mock.module("../daemon/context-overflow-policy.js", () => ({
|
|
@@ -716,6 +747,16 @@ function makeCtx(
|
|
|
716
747
|
|
|
717
748
|
...ctxOverrides,
|
|
718
749
|
} as unknown as Conversation;
|
|
750
|
+
// The convergence driver resolves the turn-scoped overflow ladder off the
|
|
751
|
+
// manager; give every fake manager the ladder methods unless a test supplied
|
|
752
|
+
// its own.
|
|
753
|
+
const manager = ctx.contextWindowManager as unknown as Record<
|
|
754
|
+
string,
|
|
755
|
+
unknown
|
|
756
|
+
>;
|
|
757
|
+
if (typeof manager.reduceOverflowOneRung !== "function") {
|
|
758
|
+
Object.assign(manager, makeOverflowLadderStub());
|
|
759
|
+
}
|
|
719
760
|
fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
|
|
720
761
|
return ctx;
|
|
721
762
|
}
|
|
@@ -1710,27 +1751,31 @@ describe("session-agent-loop", () => {
|
|
|
1710
1751
|
test("non-interactive auto-compress continues without approval prompt", async () => {
|
|
1711
1752
|
const events: ServerMessage[] = [];
|
|
1712
1753
|
|
|
1713
|
-
//
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1754
|
+
// The ladder applies a middle tier first, then escalates to its terminal
|
|
1755
|
+
// auto-compress-latest-turn rung (permitted by the overflow policy) and
|
|
1756
|
+
// reports exhaustion — mirroring the reducer owning every rung.
|
|
1757
|
+
let reducerCalls = 0;
|
|
1758
|
+
mockReducerStepFn = (msgs: Message[]) => {
|
|
1759
|
+
reducerCalls++;
|
|
1760
|
+
const terminal = reducerCalls >= 2;
|
|
1761
|
+
return {
|
|
1762
|
+
messages: msgs,
|
|
1763
|
+
tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
|
|
1764
|
+
state: {
|
|
1765
|
+
appliedTiers: terminal
|
|
1766
|
+
? ["forced_compaction", "auto_compress_latest_turn"]
|
|
1767
|
+
: ["forced_compaction"],
|
|
1768
|
+
injectionMode: "full",
|
|
1769
|
+
exhausted: terminal,
|
|
1770
|
+
},
|
|
1771
|
+
estimatedTokens: 120000,
|
|
1772
|
+
};
|
|
1773
|
+
};
|
|
1729
1774
|
|
|
1730
1775
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1731
1776
|
|
|
1732
1777
|
// The provider rejects the first two calls with context-too-large errors,
|
|
1733
|
-
// then succeeds after the
|
|
1778
|
+
// then succeeds after the terminal auto-compress rung runs.
|
|
1734
1779
|
const ctx = makeCtx({
|
|
1735
1780
|
providerResponses: [
|
|
1736
1781
|
new Error("context_length_exceeded"),
|
|
@@ -1785,24 +1830,26 @@ describe("session-agent-loop", () => {
|
|
|
1785
1830
|
// attribute the silent stall.
|
|
1786
1831
|
const events: ServerMessage[] = [];
|
|
1787
1832
|
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
1790
|
-
// the
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1833
|
+
// The ladder applies a middle tier, then runs its terminal
|
|
1834
|
+
// auto-compress-latest-turn rung and reports exhaustion. Its rerun is the
|
|
1835
|
+
// one that still yields at the mid-loop budget checkpoint.
|
|
1836
|
+
let reducerCalls = 0;
|
|
1837
|
+
mockReducerStepFn = (msgs: Message[]) => {
|
|
1838
|
+
reducerCalls++;
|
|
1839
|
+
const terminal = reducerCalls >= 2;
|
|
1840
|
+
return {
|
|
1841
|
+
messages: msgs,
|
|
1842
|
+
tier: terminal ? "auto_compress_latest_turn" : "forced_compaction",
|
|
1843
|
+
state: {
|
|
1844
|
+
appliedTiers: terminal
|
|
1845
|
+
? ["forced_compaction", "auto_compress_latest_turn"]
|
|
1846
|
+
: ["forced_compaction"],
|
|
1847
|
+
injectionMode: "full",
|
|
1848
|
+
exhausted: terminal,
|
|
1849
|
+
},
|
|
1850
|
+
estimatedTokens: 120_000,
|
|
1851
|
+
};
|
|
1852
|
+
};
|
|
1806
1853
|
|
|
1807
1854
|
mockOverflowAction = "auto_compress_latest_turn";
|
|
1808
1855
|
|
|
@@ -1934,8 +1981,10 @@ describe("session-agent-loop", () => {
|
|
|
1934
1981
|
|
|
1935
1982
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1936
1983
|
|
|
1937
|
-
//
|
|
1938
|
-
|
|
1984
|
+
// A misbehaving reducer that never reports exhaustion is bounded by the
|
|
1985
|
+
// driver's safety backstop: at most the middle-tier budget plus the
|
|
1986
|
+
// emergency and terminal rungs (maxAttempts + 2 = 5).
|
|
1987
|
+
expect(reducerCalls).toBeLessThanOrEqual(5);
|
|
1939
1988
|
});
|
|
1940
1989
|
});
|
|
1941
1990
|
|
|
@@ -922,6 +922,116 @@ describe("forkConversation", () => {
|
|
|
922
922
|
expect(loadGraphMemoryState(fork.id)).toBeNull();
|
|
923
923
|
});
|
|
924
924
|
|
|
925
|
+
test("truncated fork seeds everInjected from inherited memory attachments", async () => {
|
|
926
|
+
const source = createConversation("Truncated seed thread");
|
|
927
|
+
await addMessage(source.id, "user", "first turn", {
|
|
928
|
+
metadata: {
|
|
929
|
+
memoryInjectedBlock:
|
|
930
|
+
"# memory/concepts/topics/page-a.md\nSummary A\n\n# memory/concepts/topics/page-b.md\nSummary B",
|
|
931
|
+
},
|
|
932
|
+
skipIndexing: true,
|
|
933
|
+
});
|
|
934
|
+
await addMessage(source.id, "assistant", "first reply", {
|
|
935
|
+
skipIndexing: true,
|
|
936
|
+
});
|
|
937
|
+
const boundaryMessage = await addMessage(source.id, "user", "second turn", {
|
|
938
|
+
metadata: {
|
|
939
|
+
memoryInjectedBlock: "# memory/concepts/topics/page-c.md\nSummary C",
|
|
940
|
+
},
|
|
941
|
+
skipIndexing: true,
|
|
942
|
+
});
|
|
943
|
+
await addMessage(source.id, "assistant", "second reply", {
|
|
944
|
+
skipIndexing: true,
|
|
945
|
+
});
|
|
946
|
+
// Past the fork boundary — its attachment must NOT be claimed.
|
|
947
|
+
await addMessage(source.id, "user", "third turn", {
|
|
948
|
+
metadata: {
|
|
949
|
+
memoryInjectedBlock: "# memory/concepts/topics/page-d.md\nSummary D",
|
|
950
|
+
},
|
|
951
|
+
skipIndexing: true,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const db = getDb();
|
|
955
|
+
db.insert(activationState)
|
|
956
|
+
.values({
|
|
957
|
+
conversationId: source.id,
|
|
958
|
+
messageId: "parent-msg",
|
|
959
|
+
stateJson: JSON.stringify({ "topics/page-d": 0.9 }),
|
|
960
|
+
everInjectedJson: JSON.stringify([
|
|
961
|
+
{ slug: "topics/page-a", turn: 1 },
|
|
962
|
+
{ slug: "topics/page-b", turn: 1 },
|
|
963
|
+
{ slug: "topics/page-c", turn: 2 },
|
|
964
|
+
{ slug: "topics/page-d", turn: 3 },
|
|
965
|
+
]),
|
|
966
|
+
currentTurn: 3,
|
|
967
|
+
updatedAt: 1_700_000_000_000,
|
|
968
|
+
})
|
|
969
|
+
.run();
|
|
970
|
+
|
|
971
|
+
const fork = forkConversation({
|
|
972
|
+
conversationId: source.id,
|
|
973
|
+
throughMessageId: boundaryMessage.id,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const childState = await hydrateActivationState(db, fork.id);
|
|
977
|
+
expect(childState).not.toBeNull();
|
|
978
|
+
// Exactly the slugs whose attachments live in the copied history —
|
|
979
|
+
// page-d (injected past the boundary) stays re-injectable.
|
|
980
|
+
expect(childState?.everInjected.map((e) => e.slug).sort()).toEqual([
|
|
981
|
+
"topics/page-a",
|
|
982
|
+
"topics/page-b",
|
|
983
|
+
"topics/page-c",
|
|
984
|
+
]);
|
|
985
|
+
expect(childState?.everInjected.every((e) => e.turn === 0)).toBe(true);
|
|
986
|
+
// Activation scores and the turn counter start fresh, matching the
|
|
987
|
+
// graph tracker (also not copied for truncated forks).
|
|
988
|
+
expect(childState?.state).toEqual({});
|
|
989
|
+
expect(childState?.currentTurn).toBe(0);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test("truncated fork ignores attachments behind an inherited compaction boundary", async () => {
|
|
993
|
+
const source = createConversation("Compacted truncated thread");
|
|
994
|
+
await addMessage(source.id, "user", "compacted turn", {
|
|
995
|
+
metadata: {
|
|
996
|
+
memoryInjectedBlock:
|
|
997
|
+
"# memory/concepts/topics/page-compacted.md\nOld summary",
|
|
998
|
+
},
|
|
999
|
+
skipIndexing: true,
|
|
1000
|
+
});
|
|
1001
|
+
await addMessage(source.id, "assistant", "compacted reply", {
|
|
1002
|
+
skipIndexing: true,
|
|
1003
|
+
});
|
|
1004
|
+
const boundaryMessage = await addMessage(source.id, "user", "live turn", {
|
|
1005
|
+
metadata: {
|
|
1006
|
+
memoryInjectedBlock: "# memory/concepts/topics/page-live.md\nSummary",
|
|
1007
|
+
},
|
|
1008
|
+
skipIndexing: true,
|
|
1009
|
+
});
|
|
1010
|
+
await addMessage(source.id, "assistant", "live reply", {
|
|
1011
|
+
skipIndexing: true,
|
|
1012
|
+
});
|
|
1013
|
+
await addMessage(source.id, "user", "past boundary", {
|
|
1014
|
+
skipIndexing: true,
|
|
1015
|
+
});
|
|
1016
|
+
// First two messages sit behind the compaction boundary: their
|
|
1017
|
+
// attachments are not rendered, so the fork must not claim them.
|
|
1018
|
+
getDb()
|
|
1019
|
+
.update(conversations)
|
|
1020
|
+
.set({ contextCompactedMessageCount: 2 })
|
|
1021
|
+
.where(eq(conversations.id, source.id))
|
|
1022
|
+
.run();
|
|
1023
|
+
|
|
1024
|
+
const fork = forkConversation({
|
|
1025
|
+
conversationId: source.id,
|
|
1026
|
+
throughMessageId: boundaryMessage.id,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const childState = await hydrateActivationState(getDb(), fork.id);
|
|
1030
|
+
expect(childState?.everInjected.map((e) => e.slug)).toEqual([
|
|
1031
|
+
"topics/page-live",
|
|
1032
|
+
]);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
925
1035
|
test("defaults conversationType to standard and inherits the parent's group", async () => {
|
|
926
1036
|
const source = createConversation("Default inheritance thread");
|
|
927
1037
|
await addMessage(source.id, "user", "first message", {
|
|
@@ -96,10 +96,9 @@ mock.module("../context/token-estimator.js", () => ({
|
|
|
96
96
|
estimateToolsTokens: () => 0,
|
|
97
97
|
}));
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// tier, matching the real reducer's behavior for Tier 1.
|
|
99
|
+
// The wrapper's convergence loop seeds its gate-loop reducer state from this
|
|
100
|
+
// factory; the per-rung reduction itself runs through the manager's
|
|
101
|
+
// `reduceOverflowOneRung` (see the ContextWindowManager mock below).
|
|
103
102
|
mock.module(
|
|
104
103
|
"../plugins/defaults/compaction/context-overflow-reducer.js",
|
|
105
104
|
() => ({
|
|
@@ -108,49 +107,6 @@ mock.module(
|
|
|
108
107
|
injectionMode: "full" as const,
|
|
109
108
|
exhausted: false,
|
|
110
109
|
}),
|
|
111
|
-
reduceContextOverflow: async (
|
|
112
|
-
msgs: Message[],
|
|
113
|
-
_cfg: unknown,
|
|
114
|
-
_state: unknown,
|
|
115
|
-
compactFn?: (
|
|
116
|
-
m: Message[],
|
|
117
|
-
s: AbortSignal | undefined,
|
|
118
|
-
o: Record<string, unknown>,
|
|
119
|
-
) => Promise<{
|
|
120
|
-
compacted: boolean;
|
|
121
|
-
messages: Message[];
|
|
122
|
-
compactedPersistedMessages?: number;
|
|
123
|
-
summaryText?: string;
|
|
124
|
-
[k: string]: unknown;
|
|
125
|
-
}>,
|
|
126
|
-
signal?: AbortSignal,
|
|
127
|
-
) => {
|
|
128
|
-
let resultMessages = msgs;
|
|
129
|
-
let compactionResult;
|
|
130
|
-
if (compactFn) {
|
|
131
|
-
const cr = await compactFn(msgs, signal, { force: true });
|
|
132
|
-
if (cr.compacted) {
|
|
133
|
-
resultMessages = cr.messages;
|
|
134
|
-
compactionResult = cr;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
messages: resultMessages,
|
|
139
|
-
tier: "forced_compaction",
|
|
140
|
-
state: {
|
|
141
|
-
appliedTiers: [
|
|
142
|
-
"forced_compaction",
|
|
143
|
-
"tool_result_truncation",
|
|
144
|
-
"media_stubbing",
|
|
145
|
-
"injection_downgrade",
|
|
146
|
-
],
|
|
147
|
-
injectionMode: "full",
|
|
148
|
-
exhausted: true,
|
|
149
|
-
},
|
|
150
|
-
estimatedTokens: 1000,
|
|
151
|
-
compactionResult,
|
|
152
|
-
};
|
|
153
|
-
},
|
|
154
110
|
}),
|
|
155
111
|
);
|
|
156
112
|
|
|
@@ -223,7 +179,10 @@ let forceCompactionEnabled = false;
|
|
|
223
179
|
|
|
224
180
|
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
225
181
|
ContextWindowManager: class {
|
|
226
|
-
|
|
182
|
+
conversationId: string | undefined;
|
|
183
|
+
constructor(options?: { conversationId?: string }) {
|
|
184
|
+
this.conversationId = options?.conversationId;
|
|
185
|
+
}
|
|
227
186
|
updateConfig() {}
|
|
228
187
|
shouldCompact() {
|
|
229
188
|
return { needed: false, estimatedTokens: 0 };
|
|
@@ -253,6 +212,47 @@ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
|
253
212
|
}
|
|
254
213
|
return { compacted: false };
|
|
255
214
|
}
|
|
215
|
+
resetOverflowRecovery() {}
|
|
216
|
+
// Mirrors the real reducer's Tier 1: run forced compaction through
|
|
217
|
+
// defaultCompact (which resolves this same manager from the store) and
|
|
218
|
+
// report the ladder exhausted in one rung.
|
|
219
|
+
async reduceOverflowOneRung(
|
|
220
|
+
msgs: Message[],
|
|
221
|
+
_options: unknown,
|
|
222
|
+
signal?: AbortSignal,
|
|
223
|
+
) {
|
|
224
|
+
const { defaultCompact } =
|
|
225
|
+
await import("../plugins/defaults/compaction/compact.js");
|
|
226
|
+
const cr = await defaultCompact({
|
|
227
|
+
conversationId: this.conversationId!,
|
|
228
|
+
messages: msgs,
|
|
229
|
+
signal,
|
|
230
|
+
force: true,
|
|
231
|
+
minKeepRecentUserTurns: 0,
|
|
232
|
+
});
|
|
233
|
+
let resultMessages = msgs;
|
|
234
|
+
let compactionResult;
|
|
235
|
+
if (cr.compacted) {
|
|
236
|
+
resultMessages = cr.messages;
|
|
237
|
+
compactionResult = cr;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
messages: resultMessages,
|
|
241
|
+
tier: "forced_compaction",
|
|
242
|
+
state: {
|
|
243
|
+
appliedTiers: [
|
|
244
|
+
"forced_compaction",
|
|
245
|
+
"tool_result_truncation",
|
|
246
|
+
"media_stubbing",
|
|
247
|
+
"injection_downgrade",
|
|
248
|
+
],
|
|
249
|
+
injectionMode: "full",
|
|
250
|
+
exhausted: true,
|
|
251
|
+
},
|
|
252
|
+
estimatedTokens: 1000,
|
|
253
|
+
compactionResult,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
256
|
},
|
|
257
257
|
createContextSummaryMessage: () => ({
|
|
258
258
|
role: "user",
|
|
@@ -2,6 +2,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
5
|
+
import type { PostCompactContext } from "@vellumai/plugin-api";
|
|
5
6
|
import { eq } from "drizzle-orm";
|
|
6
7
|
|
|
7
8
|
// This test exercises v1 PKB injection. `config.memory.v2.enabled` (default
|
|
@@ -64,6 +65,8 @@ mock.module("../daemon/date-context.js", () => ({
|
|
|
64
65
|
formatTurnTimestamp: () => FIXED_TURN_TIMESTAMP,
|
|
65
66
|
}));
|
|
66
67
|
|
|
68
|
+
import { resolveCallSiteConfig } from "../config/llm-resolver.js";
|
|
69
|
+
import { LLMSchema } from "../config/schemas/llm.js";
|
|
67
70
|
import {
|
|
68
71
|
clearConversations,
|
|
69
72
|
setConversation,
|
|
@@ -87,6 +90,7 @@ import {
|
|
|
87
90
|
loadSlackChronologicalMessages,
|
|
88
91
|
resolveChannelCapabilities,
|
|
89
92
|
resolveTurnInboundActorContext,
|
|
93
|
+
resolveTurnModelProfileLabel,
|
|
90
94
|
stripChannelCapabilityContext,
|
|
91
95
|
stripInjectionsForCompaction,
|
|
92
96
|
stripNowScratchpad,
|
|
@@ -1025,18 +1029,18 @@ describe("applyRuntimeInjections — injection mode", () => {
|
|
|
1025
1029
|
// GIVEN the seeded live conversation `injection-mode-conv` whose
|
|
1026
1030
|
// conversation-scoped blocks (`<active_workspace>`, `<channel_capabilities>`)
|
|
1027
1031
|
// only resolve when `applyRuntimeInjections` finds it by conversation id
|
|
1028
|
-
// AND the agent loop hands the post-compaction hook the
|
|
1029
|
-
// flat (`requestId`, `conversationId
|
|
1030
|
-
|
|
1032
|
+
// AND the agent loop hands the post-compaction hook the irreducible
|
|
1033
|
+
// turn-identity fields flat (`requestId`, `conversationId`), with trust and
|
|
1034
|
+
// actor identity self-resolved by the hook from the live conversation
|
|
1035
|
+
const postCompactCtx: PostCompactContext = {
|
|
1031
1036
|
history: baseMessages,
|
|
1032
1037
|
requestId: "reinject-req",
|
|
1033
1038
|
conversationId: "injection-mode-conv",
|
|
1034
|
-
trust: { sourceChannel: "vellum", trustClass: "guardian" },
|
|
1035
1039
|
isNonInteractive: false,
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
+
modelProfileKey: null,
|
|
1041
|
+
};
|
|
1042
|
+
await postCompact(postCompactCtx);
|
|
1043
|
+
const result = postCompactCtx.history;
|
|
1040
1044
|
const allText = result[0].content
|
|
1041
1045
|
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
1042
1046
|
.map((b) => b.text)
|
|
@@ -1509,6 +1513,113 @@ describe("resolveTurnInboundActorContext", () => {
|
|
|
1509
1513
|
});
|
|
1510
1514
|
});
|
|
1511
1515
|
|
|
1516
|
+
// ---------------------------------------------------------------------------
|
|
1517
|
+
// resolveTurnModelProfileLabel
|
|
1518
|
+
// ---------------------------------------------------------------------------
|
|
1519
|
+
|
|
1520
|
+
describe("resolveTurnModelProfileLabel", () => {
|
|
1521
|
+
/**
|
|
1522
|
+
* A null key means the active profile is unchanged since the last notified
|
|
1523
|
+
* one, so there is no `model_profile` line to render this turn.
|
|
1524
|
+
*/
|
|
1525
|
+
test("returns null when the profile key is null", () => {
|
|
1526
|
+
// GIVEN a turn whose profile is unchanged since the last notification
|
|
1527
|
+
const llm = LLMSchema.parse({
|
|
1528
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
// WHEN the label is resolved for a null key
|
|
1532
|
+
const label = resolveTurnModelProfileLabel(null, "mainAgent", llm);
|
|
1533
|
+
|
|
1534
|
+
// THEN there is no profile line to inject
|
|
1535
|
+
expect(label).toBeNull();
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* A known profile renders its configured display label paired with the model
|
|
1540
|
+
* the call site resolves to.
|
|
1541
|
+
*/
|
|
1542
|
+
test("renders the profile's label with the resolved model", () => {
|
|
1543
|
+
// GIVEN a workspace with a labelled "fast" profile
|
|
1544
|
+
const llm = LLMSchema.parse({
|
|
1545
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1546
|
+
profiles: {
|
|
1547
|
+
fast: { label: "Fast", provider: "anthropic", model: "claude-haiku-4" },
|
|
1548
|
+
},
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
// WHEN the label is resolved for that profile at the main-agent call site
|
|
1552
|
+
const label = resolveTurnModelProfileLabel("fast", "mainAgent", llm);
|
|
1553
|
+
|
|
1554
|
+
// THEN the rendered line pairs the display label with the resolved model
|
|
1555
|
+
expect(label).toBe("Fast (claude-haiku-4)");
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* A profile with no configured label falls back to its raw key so the model
|
|
1560
|
+
* still gets a stable identifier.
|
|
1561
|
+
*/
|
|
1562
|
+
test("falls back to the raw key when the profile has no label", () => {
|
|
1563
|
+
// GIVEN a profile that sets a model but no display label
|
|
1564
|
+
const llm = LLMSchema.parse({
|
|
1565
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1566
|
+
profiles: {
|
|
1567
|
+
deep: { provider: "anthropic", model: "claude-opus-4" },
|
|
1568
|
+
},
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// WHEN the label is resolved for that profile
|
|
1572
|
+
const label = resolveTurnModelProfileLabel("deep", "mainAgent", llm);
|
|
1573
|
+
|
|
1574
|
+
// THEN the raw key stands in for the missing label
|
|
1575
|
+
expect(label).toBe("deep (claude-opus-4)");
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* A key naming a `mix` profile must announce the arm the turn's provider
|
|
1580
|
+
* calls actually run on. The conversation id is threaded as the selection
|
|
1581
|
+
* seed so the announced model is a deterministic function of the
|
|
1582
|
+
* conversation, matching the seeded expansion the provider layer performs.
|
|
1583
|
+
*/
|
|
1584
|
+
test("announces the seeded mix arm so it matches the turn's model", () => {
|
|
1585
|
+
// GIVEN a mix profile routing between two models
|
|
1586
|
+
const llm = LLMSchema.parse({
|
|
1587
|
+
default: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1588
|
+
profiles: {
|
|
1589
|
+
a: { label: "Arm A", provider: "anthropic", model: "model-a" },
|
|
1590
|
+
b: { label: "Arm B", provider: "anthropic", model: "model-b" },
|
|
1591
|
+
ab: {
|
|
1592
|
+
label: "AB",
|
|
1593
|
+
mix: [
|
|
1594
|
+
{ profile: "a", weight: 50 },
|
|
1595
|
+
{ profile: "b", weight: 50 },
|
|
1596
|
+
],
|
|
1597
|
+
},
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
// AND the model the provider calls resolve to for this conversation's seed
|
|
1602
|
+
const seededModel = resolveCallSiteConfig("mainAgent", llm, {
|
|
1603
|
+
overrideProfile: "ab",
|
|
1604
|
+
selectionSeed: "conv-seed-1",
|
|
1605
|
+
}).model;
|
|
1606
|
+
|
|
1607
|
+
// WHEN the label is resolved with the same conversation seed
|
|
1608
|
+
const label = resolveTurnModelProfileLabel(
|
|
1609
|
+
"ab",
|
|
1610
|
+
"mainAgent",
|
|
1611
|
+
llm,
|
|
1612
|
+
"conv-seed-1",
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
// THEN the announced model is the seeded arm (deterministic, not re-rolled)
|
|
1616
|
+
expect(label).toBe(`AB (${seededModel})`);
|
|
1617
|
+
expect(
|
|
1618
|
+
resolveTurnModelProfileLabel("ab", "mainAgent", llm, "conv-seed-1"),
|
|
1619
|
+
).toBe(label);
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1512
1623
|
// ---------------------------------------------------------------------------
|
|
1513
1624
|
// buildUnifiedTurnContextBlock
|
|
1514
1625
|
// ---------------------------------------------------------------------------
|