@vellumai/assistant 0.8.9-staging.1 → 0.8.9-staging.3
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/ARCHITECTURE.md +6 -6
- package/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +6 -7
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +6 -6
- package/src/__tests__/context-window-manager-compact-retry.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
- package/src/__tests__/conversation-agent-loop.test.ts +48 -96
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +6 -1
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +53 -50
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +69 -32
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
- package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
- package/src/acp/auto-install.test.ts +130 -46
- package/src/acp/auto-install.ts +86 -31
- package/src/acp/prepare-agent-env.ts +8 -9
- package/src/acp/resolve-agent.test.ts +26 -179
- package/src/acp/resolve-agent.ts +35 -122
- package/src/acp/resume-hint.ts +1 -3
- package/src/acp/session-manager.ts +32 -20
- package/src/acp/types.ts +0 -8
- package/src/agent/loop.ts +47 -45
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +16 -29
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
- package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +4 -19
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +93 -135
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +66 -11
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +10 -10
- package/src/plugins/defaults/compaction/manager-store.ts +14 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +12 -12
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +36 -43
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +25 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +10 -9
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +4 -20
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +103 -235
- package/src/tools/acp/spawn.ts +10 -120
- package/src/tools/apps/executors.ts +153 -42
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the `pre-model-call` and `
|
|
2
|
+
* Tests for the `pre-model-call` and `post-model-call` plugin hooks: a plugin
|
|
3
3
|
* can edit the outbound request, transform the finalized assistant message
|
|
4
4
|
* (persisted + streamed), and defer the live stream so the transformed text is
|
|
5
5
|
* emitted once. All hooks here use neutral transforms (redaction / uppercasing).
|
|
@@ -9,7 +9,7 @@ import { beforeEach, describe, expect, test } from "bun:test";
|
|
|
9
9
|
import type { AgentEvent } from "../agent/loop.js";
|
|
10
10
|
import { AgentLoop } from "../agent/loop.js";
|
|
11
11
|
import type {
|
|
12
|
-
|
|
12
|
+
PostModelCallContext,
|
|
13
13
|
PreModelCallContext,
|
|
14
14
|
} from "../plugin-api/types.js";
|
|
15
15
|
import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
|
|
@@ -55,7 +55,7 @@ function lastAssistant(history: Message[]): Message {
|
|
|
55
55
|
|
|
56
56
|
function registerOutputHookPlugin(hooks: {
|
|
57
57
|
preModelCall?: (ctx: PreModelCallContext) => void;
|
|
58
|
-
|
|
58
|
+
postModelCall?: (ctx: PostModelCallContext) => void;
|
|
59
59
|
}): void {
|
|
60
60
|
registerPlugin({
|
|
61
61
|
manifest: { name: "test-output-hooks", version: "0.0.0" },
|
|
@@ -67,10 +67,10 @@ function registerOutputHookPlugin(hooks: {
|
|
|
67
67
|
},
|
|
68
68
|
}
|
|
69
69
|
: {}),
|
|
70
|
-
...(hooks.
|
|
70
|
+
...(hooks.postModelCall
|
|
71
71
|
? {
|
|
72
|
-
"
|
|
73
|
-
hooks.
|
|
72
|
+
"post-model-call": async (ctx: PostModelCallContext) => {
|
|
73
|
+
hooks.postModelCall!(ctx);
|
|
74
74
|
},
|
|
75
75
|
}
|
|
76
76
|
: {}),
|
|
@@ -83,9 +83,9 @@ describe("agent loop output hooks", () => {
|
|
|
83
83
|
resetPluginRegistryAndRegisterDefaults();
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test("
|
|
86
|
+
test("post-model-call transforms the persisted message content", async () => {
|
|
87
87
|
registerOutputHookPlugin({
|
|
88
|
-
|
|
88
|
+
postModelCall: (ctx) => {
|
|
89
89
|
ctx.content = ctx.content.map((b) =>
|
|
90
90
|
b.type === "text"
|
|
91
91
|
? { type: "text", text: b.text.replace("secret", "[redacted]") }
|
|
@@ -113,7 +113,7 @@ describe("agent loop output hooks", () => {
|
|
|
113
113
|
preModelCall: (ctx) => {
|
|
114
114
|
ctx.deferAssistantOutput = true;
|
|
115
115
|
},
|
|
116
|
-
|
|
116
|
+
postModelCall: (ctx) => {
|
|
117
117
|
ctx.content = [{ type: "text", text: "[filtered]" }];
|
|
118
118
|
},
|
|
119
119
|
});
|
|
@@ -137,7 +137,7 @@ describe("agent loop output hooks", () => {
|
|
|
137
137
|
|
|
138
138
|
test("without defer, the real text still streams while storage is transformed", async () => {
|
|
139
139
|
registerOutputHookPlugin({
|
|
140
|
-
|
|
140
|
+
postModelCall: (ctx) => {
|
|
141
141
|
ctx.content = [{ type: "text", text: "[stored]" }];
|
|
142
142
|
},
|
|
143
143
|
});
|
|
@@ -168,7 +168,7 @@ describe("agent loop output hooks", () => {
|
|
|
168
168
|
stopReason: "tool_use",
|
|
169
169
|
};
|
|
170
170
|
registerOutputHookPlugin({
|
|
171
|
-
|
|
171
|
+
postModelCall: (ctx) => {
|
|
172
172
|
ctx.content = ctx.content.map((b) =>
|
|
173
173
|
b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
|
|
174
174
|
);
|
|
@@ -237,7 +237,7 @@ describe("agent loop output hooks", () => {
|
|
|
237
237
|
// mid-mutation would survive into history. The loop must clone the
|
|
238
238
|
// content before invoking the hook.
|
|
239
239
|
registerOutputHookPlugin({
|
|
240
|
-
|
|
240
|
+
postModelCall: (ctx) => {
|
|
241
241
|
ctx.content.push({ type: "text", text: "[INJECTED]" });
|
|
242
242
|
throw new Error("boom");
|
|
243
243
|
},
|
|
@@ -269,7 +269,7 @@ describe("agent loop output hooks", () => {
|
|
|
269
269
|
preModelCall: (ctx) => {
|
|
270
270
|
ctx.deferAssistantOutput = true;
|
|
271
271
|
},
|
|
272
|
-
|
|
272
|
+
postModelCall: (ctx) => {
|
|
273
273
|
seen.calls += 1;
|
|
274
274
|
ctx.content = ctx.content.map((b) =>
|
|
275
275
|
b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
|
|
@@ -81,6 +81,7 @@ mock.module("../bundler/app-compiler.js", () => ({
|
|
|
81
81
|
import * as appCreateScript from "../config/bundled-skills/app-builder/tools/app-create.js";
|
|
82
82
|
import * as appDeleteScript from "../config/bundled-skills/app-builder/tools/app-delete.js";
|
|
83
83
|
import * as appRefreshScript from "../config/bundled-skills/app-builder/tools/app-refresh.js";
|
|
84
|
+
import * as appUpdateScript from "../config/bundled-skills/app-builder/tools/app-update.js";
|
|
84
85
|
|
|
85
86
|
// ---------------------------------------------------------------------------
|
|
86
87
|
// Tests
|
|
@@ -171,4 +172,24 @@ describe("app-builder skill tool scripts", () => {
|
|
|
171
172
|
expect(parsed.appId).toBe("app-1");
|
|
172
173
|
});
|
|
173
174
|
});
|
|
175
|
+
|
|
176
|
+
// ---- app-update --------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe("app-update", () => {
|
|
179
|
+
test("exports a run function", () => {
|
|
180
|
+
expect(typeof appUpdateScript.run).toBe("function");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("delegates to executeAppUpdate and returns result", async () => {
|
|
184
|
+
const result = await appUpdateScript.run(
|
|
185
|
+
{ app_id: "app-1", name: "Renamed" },
|
|
186
|
+
makeContext(),
|
|
187
|
+
);
|
|
188
|
+
expect(result.isError).toBe(false);
|
|
189
|
+
const parsed = JSON.parse(result.content);
|
|
190
|
+
expect(parsed.updated).toBe(true);
|
|
191
|
+
expect(parsed.appId).toBe("app-1");
|
|
192
|
+
expect(parsed.name).toBe("Renamed");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
174
195
|
});
|
|
@@ -32,6 +32,7 @@ import type { AppStore } from "../tools/apps/executors.js";
|
|
|
32
32
|
import {
|
|
33
33
|
executeAppCreate,
|
|
34
34
|
executeAppRefresh,
|
|
35
|
+
executeAppUpdate,
|
|
35
36
|
} from "../tools/apps/executors.js";
|
|
36
37
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
@@ -89,6 +90,46 @@ function mockStore(
|
|
|
89
90
|
// ---------------------------------------------------------------------------
|
|
90
91
|
|
|
91
92
|
describe("executeAppCreate", () => {
|
|
93
|
+
test("falls back to the preview title when the name is omitted or blank", async () => {
|
|
94
|
+
let createdParams: Record<string, unknown> | undefined;
|
|
95
|
+
const app = makeMultifileApp({ name: "Coffee Tracker" });
|
|
96
|
+
const store: AppStore = {
|
|
97
|
+
...mockStore(app, {}),
|
|
98
|
+
createApp: (params) => {
|
|
99
|
+
createdParams = params as unknown as Record<string, unknown>;
|
|
100
|
+
return app;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await executeAppCreate(
|
|
105
|
+
{ name: " ", preview: { title: "Coffee Tracker" } },
|
|
106
|
+
store,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(result.isError).toBe(false);
|
|
110
|
+
expect(createdParams?.name).toBe("Coffee Tracker");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("defaults the name to 'New App' when neither name nor preview title is given", async () => {
|
|
114
|
+
let createdParams: Record<string, unknown> | undefined;
|
|
115
|
+
const app = makeMultifileApp({ name: "New App" });
|
|
116
|
+
const store: AppStore = {
|
|
117
|
+
...mockStore(app, {}),
|
|
118
|
+
createApp: (params) => {
|
|
119
|
+
createdParams = params as unknown as Record<string, unknown>;
|
|
120
|
+
return app;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await executeAppCreate(
|
|
125
|
+
{} as unknown as Parameters<typeof executeAppCreate>[0],
|
|
126
|
+
store,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(result.isError).toBe(false);
|
|
130
|
+
expect(createdParams?.name).toBe("New App");
|
|
131
|
+
});
|
|
132
|
+
|
|
92
133
|
test("creates multifile app with src/ scaffold", async () => {
|
|
93
134
|
const files: Record<string, string> = {};
|
|
94
135
|
let createdParams: Record<string, unknown> | undefined;
|
|
@@ -408,3 +449,94 @@ describe("executeAppRefresh", () => {
|
|
|
408
449
|
expect(parsed.error).toContain("not found");
|
|
409
450
|
});
|
|
410
451
|
});
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// executeAppUpdate
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
describe("executeAppUpdate", () => {
|
|
458
|
+
test("updates metadata and recompiles a multifile app", async () => {
|
|
459
|
+
const app = makeMultifileApp({ name: "Old", description: "old desc" });
|
|
460
|
+
let updateArgs: unknown;
|
|
461
|
+
const store: AppStore = {
|
|
462
|
+
...mockStore(app),
|
|
463
|
+
updateApp: (_id, updates) => {
|
|
464
|
+
updateArgs = updates;
|
|
465
|
+
return { ...app, ...updates };
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const result = await executeAppUpdate(
|
|
470
|
+
{ app_id: app.id, name: "New Name", description: "new desc" },
|
|
471
|
+
store,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(result.isError).toBe(false);
|
|
475
|
+
expect(updateArgs).toEqual({ name: "New Name", description: "new desc" });
|
|
476
|
+
const parsed = JSON.parse(result.content);
|
|
477
|
+
expect(parsed.updated).toBe(true);
|
|
478
|
+
expect(parsed.name).toBe("New Name");
|
|
479
|
+
expect(parsed.description).toBe("new desc");
|
|
480
|
+
expect(parsed.compiled).toBe(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("writes source_files before recompiling", async () => {
|
|
484
|
+
const files: Record<string, string> = {};
|
|
485
|
+
const app = makeMultifileApp();
|
|
486
|
+
const store: AppStore = {
|
|
487
|
+
...mockStore(app, files),
|
|
488
|
+
updateApp: (_id, updates) => ({ ...app, ...updates }),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const result = await executeAppUpdate(
|
|
492
|
+
{ app_id: app.id, source_files: { "src/App.tsx": "// new code" } },
|
|
493
|
+
store,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(result.isError).toBe(false);
|
|
497
|
+
expect(files["src/App.tsx"]).toBe("// new code");
|
|
498
|
+
expect(JSON.parse(result.content).compiled).toBe(true);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("legacy app: updates without compiling", async () => {
|
|
502
|
+
const app = makeLegacyApp({ name: "Legacy" });
|
|
503
|
+
const store: AppStore = {
|
|
504
|
+
...mockStore(app),
|
|
505
|
+
updateApp: (_id, updates) => ({ ...app, ...updates }),
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const result = await executeAppUpdate(
|
|
509
|
+
{ app_id: app.id, name: "Renamed" },
|
|
510
|
+
store,
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(result.isError).toBe(false);
|
|
514
|
+
const parsed = JSON.parse(result.content);
|
|
515
|
+
expect(parsed.updated).toBe(true);
|
|
516
|
+
expect(parsed.name).toBe("Renamed");
|
|
517
|
+
expect(parsed.compiled).toBeUndefined();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("returns error for unknown app", async () => {
|
|
521
|
+
const store = mockStore(makeMultifileApp());
|
|
522
|
+
const result = await executeAppUpdate({ app_id: "nope" }, store);
|
|
523
|
+
|
|
524
|
+
expect(result.isError).toBe(true);
|
|
525
|
+
expect(JSON.parse(result.content).error).toContain("not found");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("rejects invalid source_files", async () => {
|
|
529
|
+
const app = makeMultifileApp();
|
|
530
|
+
const store = mockStore(app);
|
|
531
|
+
const result = await executeAppUpdate(
|
|
532
|
+
{
|
|
533
|
+
app_id: app.id,
|
|
534
|
+
source_files: { "src/App.tsx": 123 as unknown as string },
|
|
535
|
+
},
|
|
536
|
+
store,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(result.isError).toBe(true);
|
|
540
|
+
expect(JSON.parse(result.content).error).toContain("must be a string");
|
|
541
|
+
});
|
|
542
|
+
});
|
|
@@ -173,7 +173,7 @@ mock.module("../memory/retriever.js", () => ({
|
|
|
173
173
|
injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
|
|
174
174
|
}));
|
|
175
175
|
|
|
176
|
-
mock.module("../
|
|
176
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
177
177
|
ContextWindowManager: class {
|
|
178
178
|
constructor() {}
|
|
179
179
|
updateConfig() {}
|
|
@@ -3,19 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The agent loop calls {@link defaultCompact} directly with a
|
|
5
5
|
* {@link CompactionContext} rather than routing through a middleware pipeline.
|
|
6
|
-
* These tests assert that the default implementation
|
|
7
|
-
*
|
|
8
|
-
* conversational options verbatim. The orchestrator
|
|
9
|
-
* (conversation-agent-loop) is exercised by
|
|
6
|
+
* These tests assert that the default implementation resolves the
|
|
7
|
+
* conversation's {@link ContextWindowManager} from the compaction store and
|
|
8
|
+
* forwards the request's conversational options verbatim. The orchestrator
|
|
9
|
+
* integration path (conversation-agent-loop) is exercised by
|
|
10
10
|
* `conversation-agent-loop-overflow.test.ts`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe, expect, test } from "bun:test";
|
|
13
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// `defaultCompact` resolves the manager from the per-conversation compaction
|
|
16
|
+
// store. Register each test's canned stub in a map the mocked store reads from.
|
|
17
|
+
const fakeContextWindowManagers = new Map<string, unknown>();
|
|
18
|
+
mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
|
|
19
|
+
createContextWindowManager: () => undefined,
|
|
20
|
+
getContextWindowManager: (conversationId: string) =>
|
|
21
|
+
fakeContextWindowManagers.get(conversationId),
|
|
22
|
+
disposeContextWindowManager: (conversationId: string) => {
|
|
23
|
+
fakeContextWindowManagers.delete(conversationId);
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const { defaultCompact } =
|
|
28
|
+
await import("../plugins/defaults/compaction/compact.js");
|
|
19
29
|
|
|
20
30
|
type ContextWindowResultShape = {
|
|
21
31
|
compacted: boolean;
|
|
@@ -55,13 +65,17 @@ function makeResult(
|
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
/** Register a stub manager under a conversation id and return its recorder. */
|
|
69
|
+
function registerManager(
|
|
70
|
+
conversationId: string,
|
|
71
|
+
result: ContextWindowResultShape,
|
|
72
|
+
) {
|
|
59
73
|
const observed: {
|
|
60
74
|
messages: unknown;
|
|
61
75
|
signal: unknown;
|
|
62
76
|
options: unknown;
|
|
63
77
|
}[] = [];
|
|
64
|
-
|
|
78
|
+
fakeContextWindowManagers.set(conversationId, {
|
|
65
79
|
maybeCompact: async (
|
|
66
80
|
messages: unknown,
|
|
67
81
|
signal: unknown,
|
|
@@ -70,8 +84,8 @@ function makeManager(result: ContextWindowResultShape) {
|
|
|
70
84
|
observed.push({ messages, signal, options });
|
|
71
85
|
return result;
|
|
72
86
|
},
|
|
73
|
-
}
|
|
74
|
-
return
|
|
87
|
+
});
|
|
88
|
+
return observed;
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
describe("defaultCompact", () => {
|
|
@@ -81,13 +95,13 @@ describe("defaultCompact", () => {
|
|
|
81
95
|
summaryText: "manager-summary",
|
|
82
96
|
compactedMessages: 7,
|
|
83
97
|
});
|
|
84
|
-
const
|
|
98
|
+
const observed = registerManager("conv-delegates", expected);
|
|
85
99
|
const messages = [{ role: "user", content: "hi" }] as never;
|
|
86
100
|
const signal = new AbortController().signal;
|
|
87
101
|
|
|
88
102
|
// WHEN defaultCompact runs with those messages and a signal
|
|
89
103
|
const result = (await defaultCompact({
|
|
90
|
-
|
|
104
|
+
conversationId: "conv-delegates",
|
|
91
105
|
messages,
|
|
92
106
|
signal,
|
|
93
107
|
})) as unknown as ContextWindowResultShape;
|
|
@@ -105,11 +119,11 @@ describe("defaultCompact", () => {
|
|
|
105
119
|
|
|
106
120
|
test("forwards the request's compaction options to the manager", async () => {
|
|
107
121
|
// GIVEN a manager and a fully-populated compaction context
|
|
108
|
-
const
|
|
122
|
+
const observed = registerManager("conv-options", makeResult());
|
|
109
123
|
|
|
110
124
|
// WHEN defaultCompact runs with force/profile/trust options
|
|
111
125
|
await defaultCompact({
|
|
112
|
-
|
|
126
|
+
conversationId: "conv-options",
|
|
113
127
|
messages: [] as never,
|
|
114
128
|
force: true,
|
|
115
129
|
overrideProfile: "fast-profile",
|
|
@@ -118,7 +132,7 @@ describe("defaultCompact", () => {
|
|
|
118
132
|
actorTrustClass: "guardian",
|
|
119
133
|
});
|
|
120
134
|
|
|
121
|
-
// THEN the manager received exactly those options, and the
|
|
135
|
+
// THEN the manager received exactly those options, and the conversation id,
|
|
122
136
|
// messages, and signal are not leaked into the options bag
|
|
123
137
|
expect(observed).toHaveLength(1);
|
|
124
138
|
expect(observed[0]!.options).toEqual({
|
|
@@ -12,8 +12,8 @@ import { describe, expect, mock, test } from "bun:test";
|
|
|
12
12
|
|
|
13
13
|
import { CompactionCircuit } from "../agent/compaction-circuit.js";
|
|
14
14
|
import type { AgentEvent } from "../agent/loop.js";
|
|
15
|
-
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
16
15
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
16
|
+
import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
|
|
17
17
|
import type { Message, ProviderResponse } from "../providers/types.js";
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
@@ -187,7 +187,7 @@ let mockCompactResult: ContextWindowResult = {
|
|
|
187
187
|
summaryText: "",
|
|
188
188
|
};
|
|
189
189
|
|
|
190
|
-
mock.module("../
|
|
190
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
191
191
|
ContextWindowManager: class {
|
|
192
192
|
nonPersistedPrefixCount = 0;
|
|
193
193
|
constructor() {}
|
|
@@ -12,7 +12,7 @@ import { describe, expect, mock, test } from "bun:test";
|
|
|
12
12
|
|
|
13
13
|
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
14
14
|
import { estimatePromptTokens } from "../context/token-estimator.js";
|
|
15
|
-
import { ContextWindowManager } from "../
|
|
15
|
+
import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
|
|
16
16
|
import type { Message, Provider } from "../providers/types.js";
|
|
17
17
|
|
|
18
18
|
mock.module("../util/logger.js", () => ({
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
function makeLoggerStub(): Record<string, unknown> {
|
|
4
|
+
const stub: Record<string, unknown> = {};
|
|
5
|
+
for (const m of [
|
|
6
|
+
"info",
|
|
7
|
+
"warn",
|
|
8
|
+
"error",
|
|
9
|
+
"debug",
|
|
10
|
+
"trace",
|
|
11
|
+
"fatal",
|
|
12
|
+
"silent",
|
|
13
|
+
"child",
|
|
14
|
+
]) {
|
|
15
|
+
stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
|
|
16
|
+
}
|
|
17
|
+
return stub;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
mock.module("../util/logger.js", () => ({
|
|
21
|
+
getLogger: () => makeLoggerStub(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
25
|
+
getMessages: () => [],
|
|
26
|
+
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
mock.module("../memory/attachments-store.js", () => ({
|
|
30
|
+
getAttachmentMetadataForMessage: () => [],
|
|
31
|
+
getAttachmentContent: () => null,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
runAssistantDrivenCompaction,
|
|
36
|
+
runEmergencyCompaction,
|
|
37
|
+
} from "../context/compactor.js";
|
|
38
|
+
import type { Message, Provider } from "../providers/types.js";
|
|
39
|
+
|
|
40
|
+
const TAIL_TIMESTAMP =
|
|
41
|
+
"2026-05-21 (Thursday) 10:00:00 -05:00 (America/Chicago)";
|
|
42
|
+
|
|
43
|
+
const compactionResponse = `
|
|
44
|
+
<compaction_result>
|
|
45
|
+
<summary>
|
|
46
|
+
Earlier turns summarized here.
|
|
47
|
+
</summary>
|
|
48
|
+
|
|
49
|
+
<key_state>
|
|
50
|
+
- Nothing critical pending.
|
|
51
|
+
</key_state>
|
|
52
|
+
|
|
53
|
+
<tail_start timestamp="${TAIL_TIMESTAMP}" preview="tail anchor message" />
|
|
54
|
+
</compaction_result>
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const ENCRYPTED_TOKEN = "expired_encrypted_token_abc123";
|
|
58
|
+
|
|
59
|
+
const userText = (text: string): Message => ({
|
|
60
|
+
role: "user",
|
|
61
|
+
content: [{ type: "text", text }],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const userTextWithTurnContext = (text: string, timestamp: string): Message => ({
|
|
65
|
+
role: "user",
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `<turn_context>\ncurrent_time: ${timestamp}\n</turn_context>\n${text}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const assistantWithWebSearch = (): Message => ({
|
|
75
|
+
role: "assistant",
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "server_tool_use",
|
|
79
|
+
id: "stu_1",
|
|
80
|
+
name: "web_search",
|
|
81
|
+
input: { query: "cats" },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "web_search_tool_result",
|
|
85
|
+
tool_use_id: "stu_1",
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "web_search_result",
|
|
89
|
+
url: "https://cats.example",
|
|
90
|
+
title: "All About Cats",
|
|
91
|
+
encrypted_content: ENCRYPTED_TOKEN,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const assistantToolUse = (): Message => ({
|
|
99
|
+
role: "assistant",
|
|
100
|
+
content: [
|
|
101
|
+
{ type: "tool_use", id: "tu_1", name: "bash", input: { cmd: "ls" } },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const toolResult = (): Message => ({
|
|
106
|
+
role: "user",
|
|
107
|
+
content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function serializeBlocks(messages: Message[]): string {
|
|
111
|
+
return JSON.stringify(messages);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Records the exact message list the provider is asked to summarize.
|
|
115
|
+
function recordingProvider(sink: { sent: Message[] }): Provider {
|
|
116
|
+
return {
|
|
117
|
+
name: "mock-provider",
|
|
118
|
+
sendMessage: async (msgs: Message[]) => {
|
|
119
|
+
sink.sent = msgs;
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: compactionResponse }],
|
|
122
|
+
model: "mock-model",
|
|
123
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
124
|
+
stopReason: "end_turn",
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Expired Anthropic web-search tokens must never reach the summarization LLM:
|
|
131
|
+
// both compaction provider calls funnel through the same request builder, so
|
|
132
|
+
// each is guarded here.
|
|
133
|
+
describe("compaction summary calls — historical web-search sanitization", () => {
|
|
134
|
+
test("runAssistantDrivenCompaction strips expired encrypted_content from the request while leaving durable history untouched", async () => {
|
|
135
|
+
// GIVEN a history whose older turn contains a native web_search_tool_result
|
|
136
|
+
// block carrying an opaque, expired `encrypted_content` token
|
|
137
|
+
const messages: Message[] = [
|
|
138
|
+
userText("old user turn"),
|
|
139
|
+
assistantWithWebSearch(),
|
|
140
|
+
userTextWithTurnContext("tail anchor message", TAIL_TIMESTAMP),
|
|
141
|
+
assistantWithWebSearch(),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// AND a snapshot of the caller's array so we can assert it is not mutated
|
|
145
|
+
const inputSnapshot = serializeBlocks(messages);
|
|
146
|
+
const sink = { sent: [] as Message[] };
|
|
147
|
+
|
|
148
|
+
// WHEN compaction runs over that history
|
|
149
|
+
const result = await runAssistantDrivenCompaction({
|
|
150
|
+
conversationId: "conv-web-search",
|
|
151
|
+
messages,
|
|
152
|
+
provider: recordingProvider(sink),
|
|
153
|
+
systemPrompt: "system",
|
|
154
|
+
compaction: { enabled: true, autoThreshold: 0.7 },
|
|
155
|
+
maxInputTokens: 1000,
|
|
156
|
+
previousEstimatedInputTokens: 900,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// THEN the summary request the provider received carries no expired token
|
|
160
|
+
// and no native web_search blocks, but keeps the search content as text
|
|
161
|
+
const sentSerialized = serializeBlocks(sink.sent);
|
|
162
|
+
expect(sentSerialized).not.toContain(ENCRYPTED_TOKEN);
|
|
163
|
+
expect(sentSerialized).not.toContain("web_search_tool_result");
|
|
164
|
+
expect(sentSerialized).not.toContain("server_tool_use");
|
|
165
|
+
expect(sentSerialized).toContain("All About Cats");
|
|
166
|
+
expect(sentSerialized).toContain("https://cats.example");
|
|
167
|
+
|
|
168
|
+
// AND the caller's durable history array is left byte-for-byte untouched,
|
|
169
|
+
// so persisted messages keep the original rich blocks
|
|
170
|
+
expect(serializeBlocks(messages)).toBe(inputSnapshot);
|
|
171
|
+
expect(result.compacted).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("runEmergencyCompaction strips expired encrypted_content from the prefix request", async () => {
|
|
175
|
+
// GIVEN a mid-turn history whose prefix (before the last tool pair) holds a
|
|
176
|
+
// web_search_tool_result block with an expired token
|
|
177
|
+
const messages: Message[] = [
|
|
178
|
+
userText("old user turn"),
|
|
179
|
+
assistantWithWebSearch(),
|
|
180
|
+
userText("keep going"),
|
|
181
|
+
assistantToolUse(),
|
|
182
|
+
toolResult(),
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const inputSnapshot = serializeBlocks(messages);
|
|
186
|
+
const sink = { sent: [] as Message[] };
|
|
187
|
+
|
|
188
|
+
// WHEN emergency compaction splits at the last tool pair and summarizes the
|
|
189
|
+
// prefix
|
|
190
|
+
const result = await runEmergencyCompaction({
|
|
191
|
+
conversationId: "conv-web-search-emergency",
|
|
192
|
+
messages,
|
|
193
|
+
provider: recordingProvider(sink),
|
|
194
|
+
systemPrompt: "system",
|
|
195
|
+
compaction: { enabled: true, autoThreshold: 0.7 },
|
|
196
|
+
// Large budget so the prefix is not front-truncated — this test exercises
|
|
197
|
+
// the web-search sanitizer, not the truncation fallback.
|
|
198
|
+
maxInputTokens: 100000,
|
|
199
|
+
previousEstimatedInputTokens: 90000,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// THEN the prefix sent to the provider carries no expired token
|
|
203
|
+
const sentSerialized = serializeBlocks(sink.sent);
|
|
204
|
+
expect(sentSerialized).not.toContain(ENCRYPTED_TOKEN);
|
|
205
|
+
expect(sentSerialized).not.toContain("web_search_tool_result");
|
|
206
|
+
expect(sentSerialized).not.toContain("server_tool_use");
|
|
207
|
+
expect(sentSerialized).toContain("All About Cats");
|
|
208
|
+
|
|
209
|
+
// AND the caller's durable history is untouched
|
|
210
|
+
expect(serializeBlocks(messages)).toBe(inputSnapshot);
|
|
211
|
+
expect(result.compacted).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { estimatePromptTokens } from "../context/token-estimator.js";
|
|
4
|
-
import type {
|
|
5
|
-
ContextWindowCompactOptions,
|
|
6
|
-
ContextWindowResult,
|
|
7
|
-
} from "../context/window-manager.js";
|
|
8
|
-
import { createContextSummaryMessage } from "../context/window-manager.js";
|
|
9
4
|
import {
|
|
10
5
|
createInitialReducerState,
|
|
11
6
|
reduceContextOverflow,
|
|
12
7
|
type ReducerConfig,
|
|
13
8
|
type ReducerState,
|
|
14
|
-
} from "../
|
|
9
|
+
} from "../plugins/defaults/compaction/context-overflow-reducer.js";
|
|
10
|
+
import type {
|
|
11
|
+
ContextWindowCompactOptions,
|
|
12
|
+
ContextWindowResult,
|
|
13
|
+
} from "../plugins/defaults/compaction/window-manager.js";
|
|
14
|
+
import { createContextSummaryMessage } from "../plugins/defaults/compaction/window-manager.js";
|
|
15
15
|
import type { Message } from "../providers/types.js";
|
|
16
16
|
|
|
17
17
|
function msg(role: "user" | "assistant", text: string): Message {
|
|
@@ -94,7 +94,7 @@ mock.module("../context/compactor.js", () => ({
|
|
|
94
94
|
},
|
|
95
95
|
}));
|
|
96
96
|
|
|
97
|
-
import { ContextWindowManager } from "../
|
|
97
|
+
import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
|
|
98
98
|
import type { Message, Provider } from "../providers/types.js";
|
|
99
99
|
|
|
100
100
|
function makeProvider(): Provider {
|
|
@@ -167,7 +167,7 @@ mock.module("../memory/retriever.js", () => ({
|
|
|
167
167
|
injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
|
|
168
168
|
}));
|
|
169
169
|
|
|
170
|
-
mock.module("../
|
|
170
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
171
171
|
ContextWindowManager: class {
|
|
172
172
|
constructor() {}
|
|
173
173
|
updateConfig() {}
|