@vellumai/assistant 0.11.4-staging.2 → 0.11.4-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/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/knip.json +3 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +81 -44
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +8 -6
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
clearFeatureFlagOverridesCache,
|
|
12
12
|
initFeatureFlagOverrides,
|
|
13
13
|
isAssistantFeatureFlagEnabled,
|
|
14
|
+
refreshOverridesFromGateway,
|
|
14
15
|
} from "../config/assistant-feature-flags.js";
|
|
15
16
|
import { mockGatewayIpc, resetMockGatewayIpc } from "./mock-gateway-ipc.js";
|
|
16
17
|
|
|
@@ -110,3 +111,51 @@ describe("initFeatureFlagOverrides", () => {
|
|
|
110
111
|
expect(isAssistantFeatureFlagEnabled("second-call", config)).toBe(false);
|
|
111
112
|
});
|
|
112
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("refreshOverridesFromGateway", () => {
|
|
116
|
+
it("replaces the cache with the new gateway values", async () => {
|
|
117
|
+
mockGatewayIpc({ "moving-flag": true });
|
|
118
|
+
await initFeatureFlagOverrides();
|
|
119
|
+
|
|
120
|
+
resetMockGatewayIpc();
|
|
121
|
+
mockGatewayIpc({ "moving-flag": false });
|
|
122
|
+
|
|
123
|
+
expect(await refreshOverridesFromGateway()).toBe(true);
|
|
124
|
+
|
|
125
|
+
const config = {} as any;
|
|
126
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps the last known values when the refresh fails", async () => {
|
|
130
|
+
mockGatewayIpc({ "moving-flag": true });
|
|
131
|
+
await initFeatureFlagOverrides();
|
|
132
|
+
|
|
133
|
+
resetMockGatewayIpc();
|
|
134
|
+
mockGatewayIpc(null, { error: true });
|
|
135
|
+
|
|
136
|
+
expect(await refreshOverridesFromGateway()).toBe(false);
|
|
137
|
+
|
|
138
|
+
// A failed refresh must not drop the flag to its registry default: a
|
|
139
|
+
// value the assistant selects behavior from (an experiment arm, a kill
|
|
140
|
+
// switch) would otherwise flip on one timed-out IPC call.
|
|
141
|
+
const config = {} as any;
|
|
142
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("keeps the cache readable for the whole call", async () => {
|
|
146
|
+
mockGatewayIpc({ "moving-flag": true });
|
|
147
|
+
await initFeatureFlagOverrides();
|
|
148
|
+
|
|
149
|
+
resetMockGatewayIpc();
|
|
150
|
+
mockGatewayIpc({ "moving-flag": false });
|
|
151
|
+
|
|
152
|
+
const config = {} as any;
|
|
153
|
+
const inFlight = refreshOverridesFromGateway();
|
|
154
|
+
// A sync read landing mid-refresh sees the previous values, never an
|
|
155
|
+
// empty cache.
|
|
156
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(true);
|
|
157
|
+
|
|
158
|
+
await inFlight;
|
|
159
|
+
expect(isAssistantFeatureFlagEnabled("moving-flag", config)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -47,6 +47,9 @@ import { skillLoadTool } from "../tools/skills/load.js";
|
|
|
47
47
|
import { executeScaffoldManagedSkill } from "../tools/skills/scaffold-managed.js";
|
|
48
48
|
import type { ToolContext } from "../tools/types.js";
|
|
49
49
|
|
|
50
|
+
/** scaffold_managed_skill requires activation hints; a fixed set for tests. */
|
|
51
|
+
const HINTS = ["user asks to run the lifecycle test procedure"];
|
|
52
|
+
|
|
50
53
|
function makeContext(): ToolContext {
|
|
51
54
|
return {
|
|
52
55
|
workingDir: "/tmp",
|
|
@@ -126,6 +129,7 @@ Run the custom lifecycle verification procedure.
|
|
|
126
129
|
name: "Lifecycle Test",
|
|
127
130
|
description: "Integration test skill.",
|
|
128
131
|
body_markdown: "Run the lifecycle test procedure.",
|
|
132
|
+
activation_hints: HINTS,
|
|
129
133
|
emoji: "🧪",
|
|
130
134
|
},
|
|
131
135
|
makeContext(),
|
|
@@ -185,6 +189,7 @@ Run the custom lifecycle verification procedure.
|
|
|
185
189
|
name: "V1",
|
|
186
190
|
description: "Version 1.",
|
|
187
191
|
body_markdown: "Original body.",
|
|
192
|
+
activation_hints: HINTS,
|
|
188
193
|
},
|
|
189
194
|
ctx,
|
|
190
195
|
);
|
|
@@ -196,6 +201,7 @@ Run the custom lifecycle verification procedure.
|
|
|
196
201
|
name: "V2",
|
|
197
202
|
description: "Version 2.",
|
|
198
203
|
body_markdown: "Updated body.",
|
|
204
|
+
activation_hints: HINTS,
|
|
199
205
|
overwrite: true,
|
|
200
206
|
},
|
|
201
207
|
ctx,
|
|
@@ -241,6 +247,7 @@ Run the custom lifecycle verification procedure.
|
|
|
241
247
|
description: "Created from scaffold.",
|
|
242
248
|
body_markdown:
|
|
243
249
|
"This skill was dynamically created.\n\nRun: `echo chain-test-ok`",
|
|
250
|
+
activation_hints: HINTS,
|
|
244
251
|
},
|
|
245
252
|
ctx,
|
|
246
253
|
);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { tmpdir } from "os";
|
|
4
12
|
import { dirname, join } from "path";
|
|
5
13
|
|
|
6
14
|
import { __resetRegistryForTesting, getTool } from "../tools/registry.js";
|
|
@@ -97,8 +105,23 @@ import { run } from "../config/bundled-skills/image-studio/tools/media-generate-
|
|
|
97
105
|
// Clean up after this file to prevent contamination of later test files.
|
|
98
106
|
afterAll(() => {
|
|
99
107
|
__resetRegistryForTesting();
|
|
108
|
+
for (const dir of tempWorkingDirs) {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
100
111
|
});
|
|
101
112
|
|
|
113
|
+
const tempWorkingDirs: string[] = [];
|
|
114
|
+
|
|
115
|
+
/** Build a ToolContext with a fresh temp working directory. */
|
|
116
|
+
function makeContext(): ToolContext {
|
|
117
|
+
const workingDir = mkdtempSync(join(tmpdir(), "media-gen-test-"));
|
|
118
|
+
tempWorkingDirs.push(workingDir);
|
|
119
|
+
return {
|
|
120
|
+
conversationId: "conv-123",
|
|
121
|
+
workingDir,
|
|
122
|
+
} as unknown as ToolContext;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
const CONFIG_DIR = join(
|
|
103
126
|
dirname(import.meta.dirname!),
|
|
104
127
|
"config",
|
|
@@ -128,12 +151,10 @@ beforeEach(() => {
|
|
|
128
151
|
platformBaseUrl: "",
|
|
129
152
|
assistantApiKey: "",
|
|
130
153
|
};
|
|
154
|
+
fakeContext = makeContext();
|
|
131
155
|
});
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
conversationId: "conv-123",
|
|
135
|
-
workingDir: "/tmp",
|
|
136
|
-
} as unknown as ToolContext;
|
|
157
|
+
let fakeContext: ToolContext;
|
|
137
158
|
|
|
138
159
|
describe("image-studio skill script wrapper", () => {
|
|
139
160
|
test("exports a run function without registering media_generate_image in the tool registry", async () => {
|
|
@@ -431,28 +452,117 @@ describe("image-studio skill script wrapper", () => {
|
|
|
431
452
|
});
|
|
432
453
|
|
|
433
454
|
test("reads source images from file paths on disk", async () => {
|
|
434
|
-
// Write a temp image file inside the workspace
|
|
435
|
-
const tmpPath = join(
|
|
455
|
+
// Write a temp image file inside the workspace
|
|
456
|
+
const tmpPath = join(fakeContext.workingDir, "test-source-image.png");
|
|
436
457
|
const pngBytes = Buffer.from(
|
|
437
458
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
|
438
459
|
"base64",
|
|
439
460
|
);
|
|
440
461
|
await Bun.write(tmpPath, pngBytes);
|
|
441
462
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
const result = await run(
|
|
464
|
+
{ prompt: "edit this", mode: "edit", source_paths: [tmpPath] },
|
|
465
|
+
fakeContext,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(result.isError).toBe(false);
|
|
469
|
+
expect(result.content).toContain("Generated 1 image");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("saves the image into media/generated and returns the embed instruction", async () => {
|
|
473
|
+
const result = await run({ prompt: "a sunset" }, fakeContext);
|
|
474
|
+
|
|
475
|
+
expect(result.isError).toBe(false);
|
|
476
|
+
expect(result.content).toContain("Saved to media/generated/a-sunset.png");
|
|
477
|
+
expect(result.content).toContain(
|
|
478
|
+
"",
|
|
479
|
+
);
|
|
480
|
+
const saved = Bun.file(
|
|
481
|
+
join(fakeContext.workingDir, "media/generated/a-sunset.png"),
|
|
482
|
+
);
|
|
483
|
+
expect(await saved.exists()).toBe(true);
|
|
484
|
+
const bytes = Buffer.from(await saved.arrayBuffer());
|
|
485
|
+
expect(bytes.equals(Buffer.from("generated-data", "base64"))).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("uses the image title for the filename when present", async () => {
|
|
489
|
+
mockGenerateResult = {
|
|
490
|
+
images: [
|
|
491
|
+
{
|
|
492
|
+
mimeType: "image/png",
|
|
493
|
+
dataBase64: "img1",
|
|
494
|
+
title: "Twilight Gremlin!",
|
|
495
|
+
} as never,
|
|
496
|
+
],
|
|
497
|
+
text: "",
|
|
498
|
+
resolvedModel: "gemini-3.1-flash-image-preview",
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const result = await run({ prompt: "a pond creature" }, fakeContext);
|
|
502
|
+
|
|
503
|
+
expect(result.isError).toBe(false);
|
|
504
|
+
expect(result.content).toContain(
|
|
505
|
+
"Saved to media/generated/twilight-gremlin.png",
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("gives each variant a distinct filename", async () => {
|
|
510
|
+
mockGenerateResult = {
|
|
511
|
+
images: [
|
|
512
|
+
{ mimeType: "image/png", dataBase64: "img1" },
|
|
513
|
+
{ mimeType: "image/png", dataBase64: "img2" },
|
|
514
|
+
],
|
|
515
|
+
text: undefined as unknown as string,
|
|
516
|
+
resolvedModel: "gemini-3.1-flash-image-preview",
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const result = await run({ prompt: "a robot", variants: 2 }, fakeContext);
|
|
520
|
+
|
|
521
|
+
expect(result.isError).toBe(false);
|
|
522
|
+
expect(result.content).toContain("- media/generated/a-robot.png");
|
|
523
|
+
expect(result.content).toContain("- media/generated/a-robot-2.png");
|
|
524
|
+
expect(
|
|
525
|
+
await Bun.file(
|
|
526
|
+
join(fakeContext.workingDir, "media/generated/a-robot-2.png"),
|
|
527
|
+
).exists(),
|
|
528
|
+
).toBe(true);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("suffixes the filename when a previous generation already used it", async () => {
|
|
532
|
+
const first = await run({ prompt: "a fox" }, fakeContext);
|
|
533
|
+
expect(first.content).toContain("Saved to media/generated/a-fox.png");
|
|
534
|
+
|
|
535
|
+
const second = await run({ prompt: "a fox" }, fakeContext);
|
|
536
|
+
expect(second.content).toContain("Saved to media/generated/a-fox-2.png");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("refuses to write through a symlink that escapes the workspace", async () => {
|
|
540
|
+
const outside = mkdtempSync(join(tmpdir(), "media-gen-outside-"));
|
|
541
|
+
tempWorkingDirs.push(outside);
|
|
542
|
+
symlinkSync(outside, join(fakeContext.workingDir, "media"));
|
|
543
|
+
|
|
544
|
+
const result = await run({ prompt: "a cat" }, fakeContext);
|
|
545
|
+
|
|
546
|
+
expect(result.isError).toBe(false);
|
|
547
|
+
expect(result.content).toContain("Could not save to the workspace");
|
|
548
|
+
expect(result.content).not.toContain("vellum://workspace/");
|
|
549
|
+
expect(readdirSync(outside)).toEqual([]);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("falls back to inline-only when the workspace write fails", async () => {
|
|
553
|
+
// Occupy the media path with a regular file so the directory creation
|
|
554
|
+
// under it fails.
|
|
555
|
+
writeFileSync(join(fakeContext.workingDir, "media"), "not a directory");
|
|
556
|
+
|
|
557
|
+
const result = await run({ prompt: "a cat" }, fakeContext);
|
|
558
|
+
|
|
559
|
+
expect(result.isError).toBe(false);
|
|
560
|
+
expect(result.content).toContain("Could not save to the workspace");
|
|
561
|
+
expect(result.content).toContain(
|
|
562
|
+
"will be attached to your reply automatically",
|
|
563
|
+
);
|
|
564
|
+
expect(result.content).not.toContain("vellum://workspace/");
|
|
565
|
+
expect(result.contentBlocks).toHaveLength(1);
|
|
456
566
|
});
|
|
457
567
|
|
|
458
568
|
test("returns error when all source_paths are invalid", async () => {
|
|
@@ -64,9 +64,13 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
64
64
|
broadcastMessage: broadcastMessageMock,
|
|
65
65
|
}));
|
|
66
66
|
|
|
67
|
-
import
|
|
67
|
+
import {
|
|
68
|
+
type UserPromptSubmitContext,
|
|
69
|
+
VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
70
|
+
} from "@vellumai/plugin-api";
|
|
68
71
|
|
|
69
72
|
import type { AssistantEvent } from "../api/index.js";
|
|
73
|
+
import { ESCALATION_CONTINUATION_CONTENT } from "../calls/voice-triage-escalate.js";
|
|
70
74
|
import type { AssistantConfig } from "../config/schema.js";
|
|
71
75
|
import type { Conversation } from "../daemon/conversation.js";
|
|
72
76
|
import type { QdrantSparseVector } from "../persistence/embeddings/qdrant-client.js";
|
|
@@ -74,6 +78,7 @@ import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/c
|
|
|
74
78
|
import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory/hooks/user-prompt-submit.js";
|
|
75
79
|
import type { Message } from "../providers/types.js";
|
|
76
80
|
import { asConversation } from "./helpers/mock-conversation.js";
|
|
81
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
77
82
|
|
|
78
83
|
/** Canonical metrics payload the graph retriever attaches to a real hit. */
|
|
79
84
|
function makeMetrics() {
|
|
@@ -175,13 +180,14 @@ function installConversation(
|
|
|
175
180
|
trusted?: boolean;
|
|
176
181
|
abortController?: AbortController;
|
|
177
182
|
},
|
|
178
|
-
):
|
|
183
|
+
): Conversation {
|
|
179
184
|
currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
|
|
180
185
|
currentConversation = asConversation({
|
|
181
186
|
graphMemory,
|
|
182
187
|
trustContext: undefined,
|
|
183
188
|
abortController: opts?.abortController ?? new AbortController(),
|
|
184
189
|
});
|
|
190
|
+
return currentConversation;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
beforeEach(() => {
|
|
@@ -192,9 +198,95 @@ beforeEach(() => {
|
|
|
192
198
|
broadcastMessageMock.mockReset();
|
|
193
199
|
currentConversation = undefined;
|
|
194
200
|
currentTrustClass = "guardian";
|
|
201
|
+
setConfig("memory", { enabled: true, v3: { live: false } });
|
|
195
202
|
});
|
|
196
203
|
|
|
197
204
|
describe("user-prompt-submit hook (memory retrieval)", () => {
|
|
205
|
+
test("voice front door skips serial legacy retrieval", async () => {
|
|
206
|
+
const { memory, prepareMemoryMock, recordPkbQueryVectorsMock } =
|
|
207
|
+
makeFakeGraphMemory();
|
|
208
|
+
const conversation = installConversation(memory, { trusted: true });
|
|
209
|
+
conversation.currentCallSite = "voiceFrontDoor";
|
|
210
|
+
const ctx = makeHookCtx({ conversationId: "conv-voice" });
|
|
211
|
+
|
|
212
|
+
await userPromptSubmitMemoryRetrieval(ctx);
|
|
213
|
+
|
|
214
|
+
expect(prepareMemoryMock).not.toHaveBeenCalled();
|
|
215
|
+
expect(recordPkbQueryVectorsMock).toHaveBeenCalledWith(
|
|
216
|
+
undefined,
|
|
217
|
+
undefined,
|
|
218
|
+
);
|
|
219
|
+
expect(applyRuntimeInjectionsMock).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("voice escalation continuation routes legacy retrieval on the preceding user message", async () => {
|
|
223
|
+
const { memory, prepareMemoryMock } = makeFakeGraphMemory();
|
|
224
|
+
installConversation(memory, { trusted: true });
|
|
225
|
+
const latestMessages: Message[] = [
|
|
226
|
+
{
|
|
227
|
+
role: "user",
|
|
228
|
+
content: [
|
|
229
|
+
{ type: "text", text: "<info>\ncarried context\n</info>" },
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: "<turn_context>\nturn context\n</turn_context>",
|
|
233
|
+
},
|
|
234
|
+
{ type: "text", text: "What is my preferred editor?" },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
role: "assistant",
|
|
239
|
+
content: [{ type: "text", text: "Let me check that." }],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
role: "user",
|
|
243
|
+
content: [{ type: "text", text: "Continue the answer." }],
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
const ctx = makeHookCtx({
|
|
247
|
+
latestMessages,
|
|
248
|
+
isHiddenPrompt: true,
|
|
249
|
+
prompt: ESCALATION_CONTINUATION_CONTENT,
|
|
250
|
+
messageKind: VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await userPromptSubmitMemoryRetrieval(ctx);
|
|
254
|
+
|
|
255
|
+
expect(prepareMemoryMock).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(prepareMemoryMock.mock.calls[0]?.[0]).toBe(latestMessages);
|
|
257
|
+
expect(prepareMemoryMock.mock.calls[0]?.[4]).toEqual([
|
|
258
|
+
{
|
|
259
|
+
role: "user",
|
|
260
|
+
content: [{ type: "text", text: "What is my preferred editor?" }],
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("generic hidden prompts route legacy retrieval on their own content", async () => {
|
|
266
|
+
const { memory, prepareMemoryMock } = makeFakeGraphMemory();
|
|
267
|
+
installConversation(memory, { trusted: true });
|
|
268
|
+
const latestMessages: Message[] = [
|
|
269
|
+
{
|
|
270
|
+
role: "user",
|
|
271
|
+
content: [{ type: "text", text: "Visible caller message" }],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
role: "user",
|
|
275
|
+
content: [{ type: "text", text: "Hidden prompt content" }],
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
const ctx = makeHookCtx({
|
|
279
|
+
latestMessages,
|
|
280
|
+
isHiddenPrompt: true,
|
|
281
|
+
prompt: "Hidden prompt content",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await userPromptSubmitMemoryRetrieval(ctx);
|
|
285
|
+
|
|
286
|
+
expect(prepareMemoryMock).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(prepareMemoryMock.mock.calls[0]?.[4]).toBe(latestMessages);
|
|
288
|
+
});
|
|
289
|
+
|
|
198
290
|
test("adopts the injected run messages when the actor is trusted", async () => {
|
|
199
291
|
const injected: Message[] = [
|
|
200
292
|
{ role: "user", content: [{ type: "text", text: "injected" }] },
|
|
@@ -26,7 +26,7 @@ describe("resolveWebhookUrl", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
expect(url).toBe(
|
|
29
|
-
"https://callbacks.vellum.ai/abc/webhooks/plugins/imessage/events-photon",
|
|
29
|
+
"https://callbacks.vellum.ai/abc/webhooks/plugins/imessage/events-photon/",
|
|
30
30
|
);
|
|
31
31
|
expect(spy.mock.calls[0]![1]).toBe(
|
|
32
32
|
"webhooks/plugins/imessage/events-photon",
|
|
@@ -124,10 +124,10 @@ describe("resolveWebhookUrl", () => {
|
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
test("hands the vendor a URL with
|
|
128
|
-
//
|
|
129
|
-
// given
|
|
130
|
-
//
|
|
127
|
+
test("hands the vendor a URL with a trailing slash", async () => {
|
|
128
|
+
// Django in front of managed callbacks canonicalizes onto `/`. A vendor
|
|
129
|
+
// given the slashless spelling is 301'd, and clients that follow a 301
|
|
130
|
+
// on POST typically retry as GET and drop the body.
|
|
131
131
|
const spy = spyOn(registration, "resolveCallbackUrl").mockResolvedValue(
|
|
132
132
|
"https://callbacks.vellum.ai/abc/webhooks/plugins/imessage/events-comms/",
|
|
133
133
|
);
|
|
@@ -135,13 +135,16 @@ describe("resolveWebhookUrl", () => {
|
|
|
135
135
|
await expect(
|
|
136
136
|
resolveWebhookUrl({ plugin: "imessage", path: "events-comms" }),
|
|
137
137
|
).resolves.toBe(
|
|
138
|
-
"https://callbacks.vellum.ai/abc/webhooks/plugins/imessage/events-comms",
|
|
138
|
+
"https://callbacks.vellum.ai/abc/webhooks/plugins/imessage/events-comms/",
|
|
139
|
+
);
|
|
140
|
+
expect(spy.mock.calls[0]![1]).toBe(
|
|
141
|
+
"webhooks/plugins/imessage/events-comms",
|
|
139
142
|
);
|
|
140
143
|
spy.mockRestore();
|
|
141
144
|
});
|
|
142
145
|
|
|
143
146
|
test("leaves a URL carrying a query string alone", async () => {
|
|
144
|
-
//
|
|
147
|
+
// Appending there would cut into the query rather than the path.
|
|
145
148
|
const spy = spyOn(registration, "resolveCallbackUrl").mockResolvedValue(
|
|
146
149
|
"https://example.test/webhooks/plugins/imessage/events-comms?token=abc/",
|
|
147
150
|
);
|
|
@@ -22,6 +22,7 @@ import { Glob } from "bun";
|
|
|
22
22
|
* barrel. Bundled defaults are compiled into the daemon, so the barrel is
|
|
23
23
|
* their loading mechanism: the in-process analog of loading an external
|
|
24
24
|
* plugin from its manifest.
|
|
25
|
+
* - `src/plugins/defaults/worker-entrypoints.ts`, the compiled sidecar loader.
|
|
25
26
|
* - Imports of modules directly under `defaults/` (e.g.
|
|
26
27
|
* `injector-order.js`) — those are host-level shared modules, not plugin
|
|
27
28
|
* internals.
|
|
@@ -47,8 +48,11 @@ import { Glob } from "bun";
|
|
|
47
48
|
const DEFAULTS_REL = join("src", "plugins", "defaults");
|
|
48
49
|
const DEFAULTS_ABS = join(process.cwd(), DEFAULTS_REL);
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
const
|
|
51
|
+
/** Bundled-plugin composition roots allowed to load plugin internals. */
|
|
52
|
+
const COMPOSITION_ROOTS = new Set([
|
|
53
|
+
join(DEFAULTS_REL, "index.ts"),
|
|
54
|
+
join(DEFAULTS_REL, "worker-entrypoints.ts"),
|
|
55
|
+
]);
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* Allowed host→plugin reaches: plugin name → sorted list of host files (paths
|
|
@@ -151,7 +155,7 @@ function collectHostFiles(): HostFile[] {
|
|
|
151
155
|
if (norm.split(sep).includes("__tests__")) {
|
|
152
156
|
continue;
|
|
153
157
|
}
|
|
154
|
-
if (norm
|
|
158
|
+
if (COMPOSITION_ROOTS.has(norm)) {
|
|
155
159
|
continue;
|
|
156
160
|
}
|
|
157
161
|
// A file at least two segments below defaults/ belongs to a plugin —
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
|
|
21
21
|
const testDataDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
QdrantManager,
|
|
25
|
+
resolveQdrantReleaseAsset,
|
|
26
|
+
} from "../persistence/embeddings/qdrant-manager.js";
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Short timeouts so tests complete fast but with enough headroom for CI and
|
|
@@ -96,6 +99,16 @@ describe("QdrantManager", () => {
|
|
|
96
99
|
});
|
|
97
100
|
});
|
|
98
101
|
|
|
102
|
+
test("selects the pinned Windows x64 zip release", () => {
|
|
103
|
+
expect(resolveQdrantReleaseAsset("win32", "x64")).toEqual({
|
|
104
|
+
binaryName: "qdrant.exe",
|
|
105
|
+
filename: "qdrant-x86_64-pc-windows-msvc.zip",
|
|
106
|
+
format: "zip",
|
|
107
|
+
sha256:
|
|
108
|
+
"9d3b1d1fa58566bc71709347c4c9b83a0111d23a550daa93c3ee48e3150c4470",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
99
112
|
// ── getUrl ───────────────────────────────────────────────────
|
|
100
113
|
|
|
101
114
|
describe("getUrl", () => {
|
|
@@ -57,6 +57,7 @@ mock.module("../persistence/lifecycle-quiesce.js", () => ({
|
|
|
57
57
|
|
|
58
58
|
import { getDb } from "../persistence/db-connection.js";
|
|
59
59
|
import { initializeDb } from "../persistence/db-init.js";
|
|
60
|
+
import { isPluginDirActivated } from "../plugins/mtime-cache.js";
|
|
60
61
|
import {
|
|
61
62
|
createSchedule,
|
|
62
63
|
deferClaimedSchedule,
|
|
@@ -342,6 +343,26 @@ describe("fire-time source-availability gate", () => {
|
|
|
342
343
|
expect(skipRunsFor(job.id)).toHaveLength(1);
|
|
343
344
|
});
|
|
344
345
|
|
|
346
|
+
test("executes an armed sourced row even though no plugin is activated in this process", async () => {
|
|
347
|
+
const job = await seedDueSourcedScript();
|
|
348
|
+
// The tick runs in the schedule worker, which activates no plugins, so the
|
|
349
|
+
// activation ledger is empty here. The gate must therefore read the disk
|
|
350
|
+
// alone: reading activation would skip every plugin schedule. Arming is
|
|
351
|
+
// the daemon-side reconciler's call.
|
|
352
|
+
expect(isPluginDirActivated(pluginDir)).toBe(false);
|
|
353
|
+
|
|
354
|
+
const result = await runDueSchedulesOnce();
|
|
355
|
+
|
|
356
|
+
expect(result.completed).toBe(1);
|
|
357
|
+
expect(result.skipped).toBe(0);
|
|
358
|
+
expect(existsSync(marker)).toBe(true);
|
|
359
|
+
const runs = rawDb()
|
|
360
|
+
.query("SELECT status FROM cron_runs WHERE job_id = ?")
|
|
361
|
+
.all(job.id) as Array<{ status: string }>;
|
|
362
|
+
expect(runs).toHaveLength(1);
|
|
363
|
+
expect(runs[0].status).toBe("ok");
|
|
364
|
+
});
|
|
365
|
+
|
|
345
366
|
test("does not execute a due sourced row while the feature flag is off", async () => {
|
|
346
367
|
const job = await seedDueSourcedScript();
|
|
347
368
|
setOverridesForTesting({ "plugin-schedules": false });
|