@vellumai/assistant 0.10.3-dev.202606282129.2e2ccad → 0.10.3-dev.202606282329.3fa6b86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +0 -6
- package/package.json +1 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +423 -0
- package/src/__tests__/voice-quality.test.ts +38 -12
- package/src/api/responses/memory-v3-selection-log.ts +4 -6
- package/src/background-wake/background-wake-routes.test.ts +53 -23
- package/src/background-wake/wake-intent-hooks.test.ts +5 -4
- package/src/calls/tts-call-strategy.ts +4 -3
- package/src/calls/voice-quality.ts +47 -59
- package/src/config/feature-flag-registry.json +0 -8
- package/src/daemon/lifecycle.ts +2 -4
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/memory/db-async-query.ts +1 -329
- package/src/memory/db-connection.ts +1 -238
- package/src/memory/db-init.ts +1 -268
- package/src/memory/db-maintenance.ts +1 -158
- package/src/memory/db-singleton.ts +1 -105
- package/src/memory/jobs-worker.ts +2 -6
- package/src/memory/schema/conversations.ts +1 -236
- package/src/memory/schema/inference.ts +1 -28
- package/src/memory/schema/oauth.ts +1 -97
- package/src/memory/schema.ts +1 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +17 -20
- package/src/memory/v2/consolidation-job.ts +10 -21
- package/src/persistence/db-async-query.ts +329 -0
- package/src/persistence/db-connection.ts +238 -0
- package/src/persistence/db-init.ts +270 -0
- package/src/persistence/db-maintenance.ts +158 -0
- package/src/persistence/db-singleton.ts +105 -0
- package/src/persistence/schema/conversations.ts +236 -0
- package/src/persistence/schema/inference.ts +28 -0
- package/src/persistence/schema/oauth.ts +97 -0
- package/src/plugins/defaults/compaction/compact.ts +2 -1
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +2 -1
- package/src/plugins/defaults/compaction/window-manager.ts +8 -7
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +6 -2
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -3
- package/src/plugins/defaults/history-repair/terminal.ts +1 -1
- package/src/plugins/defaults/image-recovery/recover.ts +5 -2
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -1
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +2 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +20 -28
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +3 -10
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +14 -15
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +36 -95
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +19 -46
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +1 -6
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +6 -6
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +0 -4
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +9 -43
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +6 -2
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +6 -2
- package/src/plugins/defaults/title-generate/hooks/stop.ts +1 -2
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +5 -3
- package/src/runtime/routes/background-wake-routes.ts +19 -5
- package/src/schedule/scheduler.ts +5 -0
- package/src/background-wake/runtime-registry.ts +0 -24
- /package/src/{memory → persistence}/schema/a2a.ts +0 -0
- /package/src/{memory → persistence}/schema/acp.ts +0 -0
- /package/src/{memory → persistence}/schema/bookmarks.ts +0 -0
- /package/src/{memory → persistence}/schema/calls.ts +0 -0
- /package/src/{memory → persistence}/schema/contacts.ts +0 -0
- /package/src/{memory → persistence}/schema/guardian.ts +0 -0
- /package/src/{memory → persistence}/schema/index.ts +0 -0
- /package/src/{memory → persistence}/schema/infrastructure.ts +0 -0
- /package/src/{memory → persistence}/schema/memory-core.ts +0 -0
- /package/src/{memory → persistence}/schema/memory-graph.ts +0 -0
- /package/src/{memory → persistence}/schema/notifications.ts +0 -0
- /package/src/{memory → persistence}/schema/tasks.ts +0 -0
package/openapi.yaml
CHANGED
|
@@ -8659,8 +8659,6 @@ paths:
|
|
|
8659
8659
|
type: number
|
|
8660
8660
|
live:
|
|
8661
8661
|
type: boolean
|
|
8662
|
-
shadow:
|
|
8663
|
-
type: boolean
|
|
8664
8662
|
selections:
|
|
8665
8663
|
type: array
|
|
8666
8664
|
items:
|
|
@@ -8690,7 +8688,6 @@ paths:
|
|
|
8690
8688
|
required:
|
|
8691
8689
|
- turn
|
|
8692
8690
|
- live
|
|
8693
|
-
- shadow
|
|
8694
8691
|
- selections
|
|
8695
8692
|
- injectedText
|
|
8696
8693
|
additionalProperties: false
|
|
@@ -18266,8 +18263,6 @@ paths:
|
|
|
18266
18263
|
type: number
|
|
18267
18264
|
live:
|
|
18268
18265
|
type: boolean
|
|
18269
|
-
shadow:
|
|
18270
|
-
type: boolean
|
|
18271
18266
|
selections:
|
|
18272
18267
|
type: array
|
|
18273
18268
|
items:
|
|
@@ -18297,7 +18292,6 @@ paths:
|
|
|
18297
18292
|
required:
|
|
18298
18293
|
- turn
|
|
18299
18294
|
- live
|
|
18300
|
-
- shadow
|
|
18301
18295
|
- selections
|
|
18302
18296
|
- injectedText
|
|
18303
18297
|
additionalProperties: false
|
package/package.json
CHANGED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { Glob } from "bun";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Guard tests for the plugin import boundary. Each immediate subdirectory of
|
|
9
|
+
* `assistant/src/plugins/defaults/` is one self-contained plugin. A plugin's
|
|
10
|
+
* production code may import only:
|
|
11
|
+
* 1. from `@vellumai/plugin-api` (the public plugin API surface), or
|
|
12
|
+
* 2. from within its own plugin directory.
|
|
13
|
+
*
|
|
14
|
+
* Anything else is an "escaping import": a relative import resolving outside
|
|
15
|
+
* the plugin's own directory (reaching into assistant host code, a sibling
|
|
16
|
+
* plugin, or `plugins/types.js`), or a bare specifier other than
|
|
17
|
+
* `@vellumai/plugin-api` (node:*, bun:*, or a third-party package).
|
|
18
|
+
*
|
|
19
|
+
* Two guards run here:
|
|
20
|
+
* - A baseline RATCHET ({@link BASELINE}) that fails when a NEW escaping
|
|
21
|
+
* import appears for a plugin (and when a baseline entry goes stale), so
|
|
22
|
+
* coupling to host internals cannot silently grow.
|
|
23
|
+
* - An ANTI-BACKSLIDE check that forbids importing, from `providers/types.js`
|
|
24
|
+
* or `config/schemas/llm.js`, any symbol that `@vellumai/plugin-api`
|
|
25
|
+
* already re-exports — locking in the migration that moved those type
|
|
26
|
+
* imports onto the public API.
|
|
27
|
+
*
|
|
28
|
+
* Tests run from `assistant/`, so paths are resolved against `process.cwd()`.
|
|
29
|
+
* Test files (`*.test.ts`, `__tests__/`) are out of scope — the boundary we
|
|
30
|
+
* guard is the shipped plugin runtime, and plugin tests legitimately import
|
|
31
|
+
* test infrastructure.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** `assistant/src/plugins/defaults`, relative to the `assistant/` cwd. */
|
|
35
|
+
const DEFAULTS_REL = join("src", "plugins", "defaults");
|
|
36
|
+
const DEFAULTS_ABS = join(process.cwd(), DEFAULTS_REL);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Allowed escaping imports per plugin: plugin name → sorted list of distinct
|
|
40
|
+
* specifiers it reaches for outside itself. These are host-internal couplings
|
|
41
|
+
* with no `@vellumai/plugin-api` equivalent (memory/daemon/context/config
|
|
42
|
+
* internals, runtime builtins, third-party packages). `providers/types.js`
|
|
43
|
+
* appears only for `ToolDefinition` (a `@vellumai/skill-host-contracts` type
|
|
44
|
+
* distinct from plugin-api's), which the anti-backslide guard enforces.
|
|
45
|
+
*
|
|
46
|
+
* Regenerate after an intentional change with:
|
|
47
|
+
* UPDATE_PLUGIN_IMPORT_BASELINE=1 bun test src/__tests__/plugin-import-boundary-guard.test.ts
|
|
48
|
+
* and paste the printed object here, explaining the new coupling in your PR.
|
|
49
|
+
*/
|
|
50
|
+
const BASELINE: Record<string, readonly string[]> = {
|
|
51
|
+
compaction: [
|
|
52
|
+
"../../../config/loader.js",
|
|
53
|
+
"../../../config/schemas/compaction.js",
|
|
54
|
+
"../../../config/schemas/inference.js",
|
|
55
|
+
"../../../config/types.js",
|
|
56
|
+
"../../../context/compactor.js",
|
|
57
|
+
"../../../context/token-estimator.js",
|
|
58
|
+
"../../../context/tool-result-truncation.js",
|
|
59
|
+
"../../../daemon/conversation-media-retry.js",
|
|
60
|
+
"../../../daemon/conversation-registry.js",
|
|
61
|
+
"../../../daemon/conversation-runtime-assembly.js",
|
|
62
|
+
"../../../providers/types.js",
|
|
63
|
+
"../../../runtime/actor-trust-resolver.js",
|
|
64
|
+
"../../../util/logger.js",
|
|
65
|
+
"../../types.js",
|
|
66
|
+
],
|
|
67
|
+
"exploration-drift": ["../../../../subagent/index.js"],
|
|
68
|
+
"image-fallback": [
|
|
69
|
+
"../../../../providers/provider-send-message.js",
|
|
70
|
+
"node:crypto",
|
|
71
|
+
"node:fs",
|
|
72
|
+
"node:path",
|
|
73
|
+
],
|
|
74
|
+
"image-recovery": [
|
|
75
|
+
"../../../agent/image-optimize.js",
|
|
76
|
+
"../../../context/image-dimensions.js",
|
|
77
|
+
"../../../memory/conversation-crud.js",
|
|
78
|
+
"../../../util/logger.js",
|
|
79
|
+
],
|
|
80
|
+
"max-tokens-continue": ["../../../../agent/loop.js"],
|
|
81
|
+
"memory-retrieval": [
|
|
82
|
+
"../../../../config/loader.js",
|
|
83
|
+
"../../../../config/memory-v3-gate.js",
|
|
84
|
+
"../../../../daemon/conversation-registry.js",
|
|
85
|
+
"../../../../daemon/conversation-runtime-assembly.js",
|
|
86
|
+
"../../../../daemon/message-types/memory.js",
|
|
87
|
+
"../../../../daemon/trust-context.js",
|
|
88
|
+
"../../../../memory/conversation-crud.js",
|
|
89
|
+
"../../../../memory/graph/conversation-graph-memory.js",
|
|
90
|
+
"../../../../memory/memory-recall-log-store.js",
|
|
91
|
+
"../../../../runtime/assistant-event-hub.js",
|
|
92
|
+
"../../../config/loader.js",
|
|
93
|
+
"../../../context/strip-injections.js",
|
|
94
|
+
"../../../daemon/conversation-registry.js",
|
|
95
|
+
"../../../daemon/conversation-runtime-assembly.js",
|
|
96
|
+
"../../../daemon/conversation-workspace.js",
|
|
97
|
+
"../../../daemon/now-scratchpad.js",
|
|
98
|
+
"../../../daemon/pkb-context-tracker.js",
|
|
99
|
+
"../../../daemon/pkb-reminder-builder.js",
|
|
100
|
+
"../../../daemon/trust-context.js",
|
|
101
|
+
"../../../documents/document-comments-store.js",
|
|
102
|
+
"../../../memory/graph/conversation-graph-memory.js",
|
|
103
|
+
"../../../memory/pkb/autoinject.js",
|
|
104
|
+
"../../../memory/pkb/context.js",
|
|
105
|
+
"../../../memory/pkb/pkb-search.js",
|
|
106
|
+
"../../../memory/pkb/types.js",
|
|
107
|
+
"../../../memory/v2/static-context.js",
|
|
108
|
+
"../../../runtime/capabilities.js",
|
|
109
|
+
"../../../types.js",
|
|
110
|
+
"../../../util/logger.js",
|
|
111
|
+
"../../../util/platform.js",
|
|
112
|
+
"../../memory-v3-shadow/ever-injected-store.js",
|
|
113
|
+
"../../types.js",
|
|
114
|
+
"../memory-v3-shadow/injector.js",
|
|
115
|
+
"node:fs",
|
|
116
|
+
"node:path",
|
|
117
|
+
],
|
|
118
|
+
"memory-v3-shadow": [
|
|
119
|
+
"../../../api/responses/memory-v3-selection-log.js",
|
|
120
|
+
"../../../config/loader.js",
|
|
121
|
+
"../../../config/memory-v3-gate.js",
|
|
122
|
+
"../../../config/schema.js",
|
|
123
|
+
"../../../config/skills.js",
|
|
124
|
+
"../../../config/types.js",
|
|
125
|
+
"../../../daemon/conversation-error.js",
|
|
126
|
+
"../../../daemon/conversation-notices.js",
|
|
127
|
+
"../../../daemon/conversation-registry.js",
|
|
128
|
+
"../../../daemon/trust-context.js",
|
|
129
|
+
"../../../memory/checkpoints.js",
|
|
130
|
+
"../../../memory/conversation-crud.js",
|
|
131
|
+
"../../../memory/db-connection.js",
|
|
132
|
+
"../../../memory/embedding-backend.js",
|
|
133
|
+
"../../../memory/embedding-billing-breaker.js",
|
|
134
|
+
"../../../memory/embedding-cache.js",
|
|
135
|
+
"../../../memory/embedding-types.js",
|
|
136
|
+
"../../../memory/jobs-store.js",
|
|
137
|
+
"../../../memory/memory-marker.js",
|
|
138
|
+
"../../../memory/message-content.js",
|
|
139
|
+
"../../../memory/prompt-override.js",
|
|
140
|
+
"../../../memory/qdrant-client.js",
|
|
141
|
+
"../../../memory/v2/cli-command-store.js",
|
|
142
|
+
"../../../memory/v2/injected-block-slugs.js",
|
|
143
|
+
"../../../memory/v2/page-index.js",
|
|
144
|
+
"../../../memory/v2/page-store.js",
|
|
145
|
+
"../../../memory/v2/skill-store.js",
|
|
146
|
+
"../../../providers/cache-control.js",
|
|
147
|
+
"../../../providers/provider-send-message.js",
|
|
148
|
+
"../../../providers/types.js",
|
|
149
|
+
"../../../skills/frontmatter.js",
|
|
150
|
+
"../../../util/log-redact.js",
|
|
151
|
+
"../../../util/logger.js",
|
|
152
|
+
"../../../util/platform.js",
|
|
153
|
+
"../../../util/strip-comment-lines.js",
|
|
154
|
+
"../../../util/truncate.js",
|
|
155
|
+
"../../types.js",
|
|
156
|
+
"@qdrant/js-client-rest",
|
|
157
|
+
"node:crypto",
|
|
158
|
+
"node:fs",
|
|
159
|
+
"node:path",
|
|
160
|
+
"uuid",
|
|
161
|
+
"zod",
|
|
162
|
+
],
|
|
163
|
+
"task-progress-nudge": [
|
|
164
|
+
"../../../../daemon/conversation-registry.js",
|
|
165
|
+
"../../../../providers/weak-open-model.js",
|
|
166
|
+
"../../../../subagent/index.js",
|
|
167
|
+
],
|
|
168
|
+
"title-generate": [
|
|
169
|
+
"../../../../config/loader.js",
|
|
170
|
+
"../../../../memory/conversation-crud.js",
|
|
171
|
+
"../../../../memory/conversation-title-service.js",
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Symbols that `@vellumai/plugin-api` re-exports from `providers/types.js`. A
|
|
177
|
+
* plugin must import these from the public API, not the host module. NOT
|
|
178
|
+
* listed (and therefore allowed from `providers/types.js`): `ToolDefinition`,
|
|
179
|
+
* which `providers/types.js` re-exports from `@vellumai/skill-host-contracts`
|
|
180
|
+
* — a different type than plugin-api's own `ToolDefinition`.
|
|
181
|
+
*/
|
|
182
|
+
const PLUGIN_API_PROVIDER_TYPES: ReadonlySet<string> = new Set([
|
|
183
|
+
"ContentBlock",
|
|
184
|
+
"FileContent",
|
|
185
|
+
"ImageContent",
|
|
186
|
+
"Message",
|
|
187
|
+
"Provider",
|
|
188
|
+
"ProviderEvent",
|
|
189
|
+
"ProviderResponse",
|
|
190
|
+
"RedactedThinkingContent",
|
|
191
|
+
"SendMessageConfig",
|
|
192
|
+
"SendMessageOptions",
|
|
193
|
+
"ServerToolUseContent",
|
|
194
|
+
"TextContent",
|
|
195
|
+
"ThinkingContent",
|
|
196
|
+
"ToolResultContent",
|
|
197
|
+
"ToolUseContent",
|
|
198
|
+
"WebSearchToolResultContent",
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
/** Symbols `@vellumai/plugin-api` re-exports from `config/schemas/llm.js`. */
|
|
202
|
+
const PLUGIN_API_LLM_TYPES: ReadonlySet<string> = new Set(["LLMCallSite"]);
|
|
203
|
+
|
|
204
|
+
/** Matches `import ... from "X"`, `export ... from "X"`, `import("X")`,
|
|
205
|
+
* `require("X")`, and side-effect `import "X"` — including multi-line forms,
|
|
206
|
+
* since the between-keyword-and-`from` span never contains a quote. */
|
|
207
|
+
function importSpecifierRegex(): RegExp {
|
|
208
|
+
return /(?:import|export)\b[^'"]*?from\s*['"]([^'"]+)['"]|(?:import|require)\(\s*['"]([^'"]+)['"]\s*\)|^\s*import\s+['"]([^'"]+)['"]/gm;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface PluginFile {
|
|
212
|
+
/** Plugin directory name (first path segment under `defaults/`). */
|
|
213
|
+
plugin: string;
|
|
214
|
+
/** Path relative to the `assistant/` cwd, for messages. */
|
|
215
|
+
relPath: string;
|
|
216
|
+
/** Absolute path on disk. */
|
|
217
|
+
absPath: string;
|
|
218
|
+
source: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Production plugin files: one per plugin subdirectory, tests excluded. */
|
|
222
|
+
function collectPluginFiles(): PluginFile[] {
|
|
223
|
+
const files: PluginFile[] = [];
|
|
224
|
+
for (const ext of ["ts", "tsx"]) {
|
|
225
|
+
for (const rel of new Glob(`${DEFAULTS_REL}/**/*.${ext}`).scanSync({
|
|
226
|
+
cwd: process.cwd(),
|
|
227
|
+
})) {
|
|
228
|
+
const norm = rel.split("/").join(sep);
|
|
229
|
+
if (norm.endsWith(".test.ts") || norm.endsWith(".test.tsx")) continue;
|
|
230
|
+
if (norm.split(sep).includes("__tests__")) continue;
|
|
231
|
+
const underDefaults = relative(DEFAULTS_REL, norm);
|
|
232
|
+
const segments = underDefaults.split(sep);
|
|
233
|
+
// A file directly under defaults/ (e.g. index.ts) is the registry, not a
|
|
234
|
+
// plugin — it has no plugin directory segment.
|
|
235
|
+
if (segments.length < 2) continue;
|
|
236
|
+
const absPath = join(process.cwd(), norm);
|
|
237
|
+
files.push({
|
|
238
|
+
plugin: segments[0]!,
|
|
239
|
+
relPath: norm,
|
|
240
|
+
absPath,
|
|
241
|
+
source: readFileSync(absPath, "utf-8"),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return files;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface Escape {
|
|
249
|
+
plugin: string;
|
|
250
|
+
specifier: string;
|
|
251
|
+
relPath: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Every escaping import across the production plugin files. */
|
|
255
|
+
function collectEscapes(files: PluginFile[]): Escape[] {
|
|
256
|
+
const escapes: Escape[] = [];
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
const pluginRoot = join(DEFAULTS_ABS, file.plugin);
|
|
259
|
+
const regex = importSpecifierRegex();
|
|
260
|
+
let match: RegExpExecArray | null;
|
|
261
|
+
while ((match = regex.exec(file.source)) !== null) {
|
|
262
|
+
const specifier = match[1] ?? match[2] ?? match[3];
|
|
263
|
+
if (!specifier || specifier === "@vellumai/plugin-api") continue;
|
|
264
|
+
if (specifier.startsWith(".")) {
|
|
265
|
+
const resolved = resolve(dirname(file.absPath), specifier);
|
|
266
|
+
// Stays inside the plugin's own directory → allowed.
|
|
267
|
+
if (!relative(pluginRoot, resolved).startsWith("..")) continue;
|
|
268
|
+
}
|
|
269
|
+
escapes.push({ plugin: file.plugin, specifier, relPath: file.relPath });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return escapes;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Distinct escaping specifiers per plugin, sorted — the ratchet's view. */
|
|
276
|
+
function escapesByPlugin(escapes: Escape[]): Map<string, string[]> {
|
|
277
|
+
const sets = new Map<string, Set<string>>();
|
|
278
|
+
for (const e of escapes) {
|
|
279
|
+
let set = sets.get(e.plugin);
|
|
280
|
+
if (!set) sets.set(e.plugin, (set = new Set()));
|
|
281
|
+
set.add(e.specifier);
|
|
282
|
+
}
|
|
283
|
+
return new Map(
|
|
284
|
+
[...sets].map(([plugin, set]) => [plugin, [...set].sort()] as const),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Parse the named-import symbols out of a `{ ... }` import clause. */
|
|
289
|
+
function parseNamedSymbols(clause: string): string[] {
|
|
290
|
+
return clause
|
|
291
|
+
.split(",")
|
|
292
|
+
.map(
|
|
293
|
+
(part) =>
|
|
294
|
+
part
|
|
295
|
+
.trim()
|
|
296
|
+
.replace(/^type\s+/, "")
|
|
297
|
+
.split(/\s+as\s+/)[0]!,
|
|
298
|
+
)
|
|
299
|
+
.map((name) => name.trim())
|
|
300
|
+
.filter((name) => name.length > 0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Symbols a plugin file imports from a host module whose specifier ends with
|
|
304
|
+
* `hostPathSuffix` (e.g. `providers/types.js`), across multi-line clauses. */
|
|
305
|
+
function symbolsImportedFrom(source: string, hostPathSuffix: string): string[] {
|
|
306
|
+
const escaped = hostPathSuffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
307
|
+
const regex = new RegExp(
|
|
308
|
+
String.raw`import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]*` +
|
|
309
|
+
escaped +
|
|
310
|
+
String.raw`)['"]`,
|
|
311
|
+
"g",
|
|
312
|
+
);
|
|
313
|
+
const symbols: string[] = [];
|
|
314
|
+
let match: RegExpExecArray | null;
|
|
315
|
+
while ((match = regex.exec(source)) !== null) {
|
|
316
|
+
symbols.push(...parseNamedSymbols(match[1]!));
|
|
317
|
+
}
|
|
318
|
+
return symbols;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
describe("plugin import boundary", () => {
|
|
322
|
+
const files = collectPluginFiles();
|
|
323
|
+
const escapes = collectEscapes(files);
|
|
324
|
+
const byPlugin = escapesByPlugin(escapes);
|
|
325
|
+
|
|
326
|
+
test("no new escaping import beyond the committed baseline", () => {
|
|
327
|
+
if (process.env.UPDATE_PLUGIN_IMPORT_BASELINE === "1") {
|
|
328
|
+
const regenerated: Record<string, string[]> = {};
|
|
329
|
+
for (const plugin of [...byPlugin.keys()].sort()) {
|
|
330
|
+
regenerated[plugin] = byPlugin.get(plugin)!;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(
|
|
334
|
+
"Regenerated plugin import baseline — paste into BASELINE:\n" +
|
|
335
|
+
JSON.stringify(regenerated, null, 2),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const exampleFile = new Map<string, string>();
|
|
340
|
+
for (const e of escapes) {
|
|
341
|
+
exampleFile.set(`${e.plugin}${e.specifier}`, e.relPath);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const plugins = new Set([...byPlugin.keys(), ...Object.keys(BASELINE)]);
|
|
345
|
+
const added: string[] = [];
|
|
346
|
+
const stale: string[] = [];
|
|
347
|
+
for (const plugin of [...plugins].sort()) {
|
|
348
|
+
const found = new Set(byPlugin.get(plugin) ?? []);
|
|
349
|
+
const allowed = new Set(BASELINE[plugin] ?? []);
|
|
350
|
+
for (const spec of [...found].sort()) {
|
|
351
|
+
if (!allowed.has(spec)) {
|
|
352
|
+
const where = exampleFile.get(`${plugin}${spec}`);
|
|
353
|
+
added.push(` - ${plugin}: "${spec}" (e.g. ${where})`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const spec of [...allowed].sort()) {
|
|
357
|
+
if (!found.has(spec)) stale.push(` - ${plugin}: "${spec}"`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const problems: string[] = [];
|
|
362
|
+
if (added.length > 0) {
|
|
363
|
+
problems.push(
|
|
364
|
+
"New escaping imports (a plugin reached outside itself for something",
|
|
365
|
+
"not in its baseline):",
|
|
366
|
+
...added,
|
|
367
|
+
"",
|
|
368
|
+
"If @vellumai/plugin-api already exports this symbol, import it from",
|
|
369
|
+
"there. If this is genuinely host-internal with no plugin-api",
|
|
370
|
+
"equivalent, regenerate the baseline with",
|
|
371
|
+
"UPDATE_PLUGIN_IMPORT_BASELINE=1 and explain the new coupling in your PR.",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (stale.length > 0) {
|
|
375
|
+
if (problems.length > 0) problems.push("");
|
|
376
|
+
problems.push(
|
|
377
|
+
"Stale baseline entries (no longer imported — tighten the baseline):",
|
|
378
|
+
...stale,
|
|
379
|
+
"",
|
|
380
|
+
"Regenerate with UPDATE_PLUGIN_IMPORT_BASELINE=1 to drop them.",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
expect(problems, problems.join("\n")).toEqual([]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("plugins import plugin-api-provided types from @vellumai/plugin-api, not host modules", () => {
|
|
388
|
+
const violations: string[] = [];
|
|
389
|
+
for (const file of files) {
|
|
390
|
+
for (const symbol of symbolsImportedFrom(
|
|
391
|
+
file.source,
|
|
392
|
+
"providers/types.js",
|
|
393
|
+
)) {
|
|
394
|
+
if (PLUGIN_API_PROVIDER_TYPES.has(symbol)) {
|
|
395
|
+
violations.push(
|
|
396
|
+
` - ${file.relPath}: "${symbol}" from providers/types.js`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const symbol of symbolsImportedFrom(
|
|
401
|
+
file.source,
|
|
402
|
+
"config/schemas/llm.js",
|
|
403
|
+
)) {
|
|
404
|
+
if (PLUGIN_API_LLM_TYPES.has(symbol)) {
|
|
405
|
+
violations.push(
|
|
406
|
+
` - ${file.relPath}: "${symbol}" from config/schemas/llm.js`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const message = [
|
|
413
|
+
"Plugin files import symbols from host modules that @vellumai/plugin-api",
|
|
414
|
+
"already re-exports. Import them from @vellumai/plugin-api instead:",
|
|
415
|
+
...violations,
|
|
416
|
+
"",
|
|
417
|
+
"(ToolDefinition from providers/types.js is allowed — it is a distinct",
|
|
418
|
+
"@vellumai/skill-host-contracts type, not plugin-api's ToolDefinition.)",
|
|
419
|
+
].join("\n");
|
|
420
|
+
|
|
421
|
+
expect(violations, message).toEqual([]);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -378,8 +378,34 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
378
378
|
},
|
|
379
379
|
};
|
|
380
380
|
const profile = resolveVoiceQualityProfile();
|
|
381
|
-
expect(profile.ttsProvider).toBe("
|
|
382
|
-
expect(profile.voice).toBe(
|
|
381
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
382
|
+
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("synthesized-play TwiML carries a non-empty native fallback voice (64106 regression)", () => {
|
|
386
|
+
// Regression for the dropped-call bug: when a synthesized-play provider's
|
|
387
|
+
// mid-call synthesis fails, the controller falls back to native token TTS.
|
|
388
|
+
// If the TwiML voice is empty, Twilio rejects the turn with error 64106
|
|
389
|
+
// ("TTS provider rejected the request due to invalid parameters") and the
|
|
390
|
+
// call drops. The profile must expose a real ElevenLabs fallback voice.
|
|
391
|
+
mockConfig = {
|
|
392
|
+
calls: {
|
|
393
|
+
voice: { language: "en-US", transcriptionProvider: "Deepgram" },
|
|
394
|
+
},
|
|
395
|
+
services: {
|
|
396
|
+
tts: {
|
|
397
|
+
provider: "fish-audio",
|
|
398
|
+
providers: {
|
|
399
|
+
elevenlabs: { voiceId: "fallback-voice-xyz" },
|
|
400
|
+
"fish-audio": { referenceId: "ref-123" },
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
const profile = resolveVoiceQualityProfile();
|
|
406
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
407
|
+
expect(profile.voice).toBe("fallback-voice-xyz");
|
|
408
|
+
expect(profile.voice).not.toBe("");
|
|
383
409
|
});
|
|
384
410
|
|
|
385
411
|
test("preserves language setting for synthesized providers", () => {
|
|
@@ -429,8 +455,8 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
429
455
|
};
|
|
430
456
|
const profile = resolveVoiceQualityProfile();
|
|
431
457
|
// Should resolve to fish-audio from canonical config
|
|
432
|
-
expect(profile.ttsProvider).toBe("
|
|
433
|
-
expect(profile.voice).toBe(
|
|
458
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
459
|
+
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
434
460
|
});
|
|
435
461
|
|
|
436
462
|
// -- Strategy-based behavior (explicit call strategy) -------------------
|
|
@@ -457,8 +483,8 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
457
483
|
},
|
|
458
484
|
};
|
|
459
485
|
const profile = resolveVoiceQualityProfile();
|
|
460
|
-
expect(profile.ttsProvider).toBe("
|
|
461
|
-
expect(profile.voice).toBe(
|
|
486
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
487
|
+
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
462
488
|
});
|
|
463
489
|
|
|
464
490
|
test("native-twilio strategy delegates voice-spec to registered builder", () => {
|
|
@@ -567,10 +593,10 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
567
593
|
},
|
|
568
594
|
};
|
|
569
595
|
const profile = resolveVoiceQualityProfile();
|
|
570
|
-
// Deepgram is synthesized-play in the catalog
|
|
571
|
-
//
|
|
572
|
-
expect(profile.ttsProvider).toBe("
|
|
573
|
-
expect(profile.voice).toBe(
|
|
596
|
+
// Deepgram is synthesized-play in the catalog. The TwiML still carries the
|
|
597
|
+
// ElevenLabs fallback voice so a synthesis failure can speak natively.
|
|
598
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
599
|
+
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
574
600
|
});
|
|
575
601
|
|
|
576
602
|
test("Deepgram preserves language setting on synthesized-play path", () => {
|
|
@@ -594,7 +620,7 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
594
620
|
};
|
|
595
621
|
const profile = resolveVoiceQualityProfile();
|
|
596
622
|
expect(profile.language).toBe("fr-FR");
|
|
597
|
-
expect(profile.ttsProvider).toBe("
|
|
598
|
-
expect(profile.voice).toBe(
|
|
623
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
624
|
+
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
599
625
|
});
|
|
600
626
|
});
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Returned as part of `LlmContextResponse` — see `./llm-context-response.ts`.
|
|
11
11
|
*
|
|
12
|
-
* `live`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the block that WOULD have been injected.
|
|
12
|
+
* `live` reflects the CURRENT `memory.v3.live` config state, not the per-turn
|
|
13
|
+
* history (the selection rows don't record whether v3 was live when they were
|
|
14
|
+
* written). The rendered `injectedText` is the `<memory>` block for the logged
|
|
15
|
+
* selection.
|
|
17
16
|
*
|
|
18
17
|
* `injectedText` is inspector-only, not a verbatim record of the live cards +
|
|
19
18
|
* spotlight. It re-renders each selection's matched section — resolved from the
|
|
@@ -58,7 +57,6 @@ export type MemoryV3SelectionRow = z.infer<typeof MemoryV3SelectionRowSchema>;
|
|
|
58
57
|
export const MemoryV3SelectionLogSchema = z.object({
|
|
59
58
|
turn: z.number(),
|
|
60
59
|
live: z.boolean(),
|
|
61
|
-
shadow: z.boolean(),
|
|
62
60
|
selections: z.array(MemoryV3SelectionRowSchema),
|
|
63
61
|
injectedText: z.string(),
|
|
64
62
|
});
|