@vellumai/assistant 0.4.52 → 0.4.53
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 +2 -2
- package/docs/architecture/keychain-broker.md +6 -20
- package/docs/architecture/memory.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +3 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/asset-materialize-tool.test.ts +0 -1
- package/src/__tests__/asset-search-tool.test.ts +0 -1
- package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
- package/src/__tests__/attachments-store.test.ts +0 -1
- package/src/__tests__/avatar-e2e.test.ts +6 -1
- package/src/__tests__/browser-fill-credential.test.ts +3 -0
- package/src/__tests__/btw-routes.test.ts +39 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +1 -0
- package/src/__tests__/call-routes-http.test.ts +1 -2
- package/src/__tests__/canonical-guardian-store.test.ts +33 -2
- package/src/__tests__/channel-readiness-service.test.ts +1 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
- package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
- package/src/__tests__/config-loader-backfill.test.ts +1 -2
- package/src/__tests__/config-schema.test.ts +6 -37
- package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
- package/src/__tests__/credential-broker-server-use.test.ts +16 -16
- package/src/__tests__/credential-security-invariants.test.ts +14 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
- package/src/__tests__/host-shell-tool.test.ts +0 -1
- package/src/__tests__/http-user-message-parity.test.ts +19 -0
- package/src/__tests__/list-messages-attachments.test.ts +0 -1
- package/src/__tests__/log-export-workspace.test.ts +233 -0
- package/src/__tests__/managed-proxy-context.test.ts +1 -1
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/media-generate-image.test.ts +7 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
- package/src/__tests__/memory-regressions.test.ts +0 -1
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
- package/src/__tests__/oauth-cli.test.ts +1 -10
- package/src/__tests__/oauth-store.test.ts +3 -5
- package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
- package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
- package/src/__tests__/onboarding-template-contract.test.ts +1 -2
- package/src/__tests__/pricing.test.ts +0 -11
- package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
- package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
- package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
- package/src/__tests__/provider-registry-ollama.test.ts +8 -2
- package/src/__tests__/recording-handler.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +0 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
- package/src/__tests__/runtime-events-sse.test.ts +0 -1
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -1
- package/src/__tests__/secret-scanner-executor.test.ts +0 -1
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/session-abort-tool-results.test.ts +3 -1
- package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
- package/src/__tests__/session-agent-loop.test.ts +2 -2
- package/src/__tests__/session-confirmation-signals.test.ts +3 -1
- package/src/__tests__/session-error.test.ts +5 -4
- package/src/__tests__/session-history-web-search.test.ts +34 -9
- package/src/__tests__/session-pre-run-repair.test.ts +3 -1
- package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
- package/src/__tests__/session-queue.test.ts +3 -1
- package/src/__tests__/session-runtime-assembly.test.ts +118 -0
- package/src/__tests__/session-slash-known.test.ts +31 -13
- package/src/__tests__/session-slash-queue.test.ts +3 -1
- package/src/__tests__/session-slash-unknown.test.ts +3 -1
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
- package/src/__tests__/session-workspace-injection.test.ts +3 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/skillssh-registry.test.ts +21 -0
- package/src/__tests__/slack-share-routes.test.ts +1 -1
- package/src/__tests__/swarm-recursion.test.ts +5 -1
- package/src/__tests__/swarm-session-integration.test.ts +25 -14
- package/src/__tests__/swarm-tool.test.ts +5 -2
- package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/trust-store.test.ts +5 -1
- package/src/__tests__/twilio-routes.test.ts +2 -2
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-quality.test.ts +2 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/agent/loop.ts +17 -1
- package/src/bundler/app-bundler.ts +40 -24
- package/src/calls/call-controller.ts +16 -0
- package/src/calls/relay-server.ts +29 -13
- package/src/calls/voice-control-protocol.ts +1 -0
- package/src/calls/voice-quality.ts +1 -1
- package/src/calls/voice-session-bridge.ts +9 -3
- package/src/channels/types.ts +16 -0
- package/src/cli/commands/bash.ts +173 -0
- package/src/cli/commands/doctor.ts +5 -23
- package/src/cli/commands/oauth/connections.ts +4 -2
- package/src/cli/commands/oauth/providers.ts +1 -13
- package/src/cli/program.ts +2 -0
- package/src/cli/reference.ts +1 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +7 -135
- package/src/config/schema.ts +0 -6
- package/src/config/schemas/channels.ts +1 -0
- package/src/config/schemas/elevenlabs.ts +2 -2
- package/src/contacts/contact-store.ts +21 -25
- package/src/contacts/contacts-write.ts +6 -6
- package/src/contacts/types.ts +2 -0
- package/src/context/token-estimator.ts +35 -2
- package/src/context/window-manager.ts +16 -2
- package/src/daemon/config-watcher.ts +24 -6
- package/src/daemon/context-overflow-reducer.ts +13 -2
- package/src/daemon/handlers/config-ingress.ts +25 -8
- package/src/daemon/handlers/config-model.ts +21 -15
- package/src/daemon/handlers/config-telegram.ts +18 -6
- package/src/daemon/handlers/dictation.ts +0 -429
- package/src/daemon/handlers/skills.ts +1 -200
- package/src/daemon/lifecycle.ts +8 -5
- package/src/daemon/message-types/contacts.ts +2 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/sessions.ts +2 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
- package/src/daemon/server.ts +23 -2
- package/src/daemon/session-agent-loop-handlers.ts +1 -1
- package/src/daemon/session-agent-loop.ts +27 -79
- package/src/daemon/session-error.ts +5 -4
- package/src/daemon/session-process.ts +17 -10
- package/src/daemon/session-runtime-assembly.ts +50 -0
- package/src/daemon/session-slash.ts +32 -20
- package/src/daemon/session.ts +1 -0
- package/src/events/domain-events.ts +1 -0
- package/src/media/app-icon-generator.ts +2 -1
- package/src/media/avatar-router.ts +3 -2
- package/src/memory/canonical-guardian-store.ts +25 -3
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.ts +25 -16
- package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
- package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
- package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/retriever.test.ts +19 -12
- package/src/memory/schema/contacts.ts +2 -2
- package/src/memory/schema/oauth.ts +0 -1
- package/src/oauth/connect-orchestrator.ts +5 -3
- package/src/oauth/connect-types.ts +9 -2
- package/src/oauth/manual-token-connection.ts +9 -7
- package/src/oauth/oauth-store.ts +2 -8
- package/src/oauth/provider-behaviors.ts +10 -0
- package/src/oauth/seed-providers.ts +13 -5
- package/src/permissions/checker.ts +20 -1
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
- package/src/prompts/system-prompt.ts +2 -11
- package/src/prompts/templates/BOOTSTRAP.md +1 -3
- package/src/providers/anthropic/client.ts +16 -8
- package/src/providers/managed-proxy/constants.ts +1 -1
- package/src/providers/registry.ts +21 -15
- package/src/providers/types.ts +1 -1
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-invite-transports/telegram.ts +12 -6
- package/src/runtime/channel-retry-sweep.ts +6 -0
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/middleware/error-handler.ts +1 -2
- package/src/runtime/routes/app-management-routes.ts +1 -0
- package/src/runtime/routes/btw-routes.ts +20 -1
- package/src/runtime/routes/conversation-routes.ts +32 -13
- package/src/runtime/routes/inbound-message-handler.ts +10 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
- package/src/runtime/routes/integrations/slack/share.ts +5 -5
- package/src/runtime/routes/log-export-routes.ts +122 -10
- package/src/runtime/routes/session-query-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +53 -0
- package/src/runtime/routes/workspace-routes.ts +3 -0
- package/src/runtime/verification-templates.ts +1 -1
- package/src/security/oauth2.ts +4 -4
- package/src/security/secure-keys.ts +4 -4
- package/src/signals/bash.ts +157 -0
- package/src/skills/skillssh-registry.ts +6 -1
- package/src/swarm/backend-claude-code.ts +6 -6
- package/src/swarm/worker-backend.ts +1 -1
- package/src/swarm/worker-runner.ts +1 -1
- package/src/telegram/bot-username.ts +11 -0
- package/src/tools/claude-code/claude-code.ts +4 -4
- package/src/tools/credentials/broker.ts +7 -5
- package/src/tools/credentials/vault.ts +3 -2
- package/src/tools/network/__tests__/web-search.test.ts +18 -86
- package/src/tools/network/web-search.ts +9 -15
- package/src/util/platform.ts +7 -1
- package/src/util/pricing.ts +0 -1
- package/src/workspace/provider-commit-message-generator.ts +10 -6
|
@@ -1099,7 +1099,7 @@ describe("Trust Store", () => {
|
|
|
1099
1099
|
|
|
1100
1100
|
// ── default allow: browser tools ────────────────────────────
|
|
1101
1101
|
|
|
1102
|
-
test("all
|
|
1102
|
+
test("all 14 browser tools have default allow rules", () => {
|
|
1103
1103
|
const templates = getDefaultRuleTemplates();
|
|
1104
1104
|
const browserTools = [
|
|
1105
1105
|
"browser_navigate",
|
|
@@ -1109,8 +1109,12 @@ describe("Trust Store", () => {
|
|
|
1109
1109
|
"browser_click",
|
|
1110
1110
|
"browser_type",
|
|
1111
1111
|
"browser_press_key",
|
|
1112
|
+
"browser_scroll",
|
|
1113
|
+
"browser_select_option",
|
|
1114
|
+
"browser_hover",
|
|
1112
1115
|
"browser_wait_for",
|
|
1113
1116
|
"browser_extract",
|
|
1117
|
+
"browser_wait_for_download",
|
|
1114
1118
|
"browser_fill_credential",
|
|
1115
1119
|
];
|
|
1116
1120
|
|
|
@@ -120,11 +120,10 @@ mock.module("../util/logger.js", () => ({
|
|
|
120
120
|
const mockConfigObj = {
|
|
121
121
|
model: "test",
|
|
122
122
|
provider: "test",
|
|
123
|
-
apiKeys: {},
|
|
124
123
|
memory: { enabled: false },
|
|
125
124
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
126
125
|
secretDetection: { enabled: false },
|
|
127
|
-
elevenlabs: { voiceId:
|
|
126
|
+
elevenlabs: { voiceId: DEFAULT_ELEVENLABS_VOICE_ID },
|
|
128
127
|
calls: {
|
|
129
128
|
voice: {
|
|
130
129
|
language: "en-US",
|
|
@@ -322,6 +321,7 @@ import {
|
|
|
322
321
|
handleStatusCallback,
|
|
323
322
|
handleVoiceWebhook,
|
|
324
323
|
} from "../calls/twilio-routes.js";
|
|
324
|
+
import { DEFAULT_ELEVENLABS_VOICE_ID } from "../config/schemas/elevenlabs.js";
|
|
325
325
|
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
326
326
|
import { conversations } from "../memory/schema.js";
|
|
327
327
|
import {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildElevenLabsVoiceSpec,
|
|
11
11
|
resolveVoiceQualityProfile,
|
|
12
12
|
} from "../calls/voice-quality.js";
|
|
13
|
+
import { DEFAULT_ELEVENLABS_VOICE_ID } from "../config/schemas/elevenlabs.js";
|
|
13
14
|
|
|
14
15
|
describe("buildElevenLabsVoiceSpec", () => {
|
|
15
16
|
test("returns bare voiceId when no model is set", () => {
|
|
@@ -63,7 +64,7 @@ describe("buildElevenLabsVoiceSpec", () => {
|
|
|
63
64
|
describe("resolveVoiceQualityProfile", () => {
|
|
64
65
|
test("always returns ElevenLabs ttsProvider", () => {
|
|
65
66
|
mockConfig = {
|
|
66
|
-
elevenlabs: { voiceId:
|
|
67
|
+
elevenlabs: { voiceId: DEFAULT_ELEVENLABS_VOICE_ID },
|
|
67
68
|
calls: {
|
|
68
69
|
voice: {
|
|
69
70
|
language: "en-US",
|
|
@@ -548,7 +548,7 @@ async function executeWebSearch(
|
|
|
548
548
|
if (!apiKey) {
|
|
549
549
|
return {
|
|
550
550
|
content:
|
|
551
|
-
"Error: No web search API key configured.
|
|
551
|
+
"Error: No web search API key configured. Set it via `keys set perplexity <key>` or `keys set brave <key>`, or configure it from the Settings page under API Keys.",
|
|
552
552
|
isError: true,
|
|
553
553
|
};
|
|
554
554
|
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/node";
|
|
2
2
|
|
|
3
|
+
import { estimateToolsTokens } from "../context/token-estimator.js";
|
|
3
4
|
import { truncateOversizedToolResults } from "../context/tool-result-truncation.js";
|
|
4
5
|
import { getHookManager } from "../hooks/manager.js";
|
|
5
6
|
import type {
|
|
@@ -78,7 +79,7 @@ export type AgentEvent =
|
|
|
78
79
|
toolUseId: string;
|
|
79
80
|
input: Record<string, unknown>;
|
|
80
81
|
}
|
|
81
|
-
| { type: "server_tool_complete"; toolUseId: string }
|
|
82
|
+
| { type: "server_tool_complete"; toolUseId: string; isError: boolean }
|
|
82
83
|
| { type: "error"; error: Error }
|
|
83
84
|
| {
|
|
84
85
|
type: "usage";
|
|
@@ -175,6 +176,20 @@ export class AgentLoop {
|
|
|
175
176
|
this.toolExecutor = toolExecutor ?? null;
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Estimate token cost of the tool definitions sent to the provider.
|
|
181
|
+
*
|
|
182
|
+
* When `history` is provided and a dynamic `resolveTools` callback
|
|
183
|
+
* exists, the budget is derived from the resolved tool list for that
|
|
184
|
+
* turn — matching what `run()` actually sends. Without `history` (or
|
|
185
|
+
* without a resolver), falls back to the static `this.tools`.
|
|
186
|
+
*/
|
|
187
|
+
getToolTokenBudget(history?: Message[]): number {
|
|
188
|
+
const tools =
|
|
189
|
+
history && this.resolveTools ? this.resolveTools(history) : this.tools;
|
|
190
|
+
return estimateToolsTokens(tools);
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
async run(
|
|
179
194
|
messages: Message[],
|
|
180
195
|
onEvent: (event: AgentEvent) => void | Promise<void>,
|
|
@@ -318,6 +333,7 @@ export class AgentLoop {
|
|
|
318
333
|
onEvent({
|
|
319
334
|
type: "server_tool_complete",
|
|
320
335
|
toolUseId: event.toolUseId,
|
|
336
|
+
isError: event.isError,
|
|
321
337
|
});
|
|
322
338
|
}
|
|
323
339
|
},
|
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import archiver from "archiver";
|
|
15
15
|
import JSZip from "jszip";
|
|
16
16
|
|
|
17
|
-
import { getApp, getAppsDir } from "../memory/app-store.js";
|
|
17
|
+
import { getApp, getAppsDir, isMultifileApp } from "../memory/app-store.js";
|
|
18
18
|
import { computeContentId } from "../util/content-id.js";
|
|
19
19
|
import { getLogger } from "../util/logger.js";
|
|
20
20
|
import { compileApp } from "./app-compiler.js";
|
|
@@ -60,8 +60,10 @@ export async function packageApp(
|
|
|
60
60
|
const version = app.version ?? "1.0.0";
|
|
61
61
|
const contentId = computeContentId(app.name);
|
|
62
62
|
|
|
63
|
+
const multifile = isMultifileApp(app);
|
|
64
|
+
|
|
63
65
|
const manifest: AppManifest = {
|
|
64
|
-
format_version: 2,
|
|
66
|
+
format_version: multifile ? 2 : 1,
|
|
65
67
|
name: app.name,
|
|
66
68
|
...(app.description ? { description: app.description } : {}),
|
|
67
69
|
...(app.icon ? { icon: app.icon } : {}),
|
|
@@ -74,34 +76,48 @@ export async function packageApp(
|
|
|
74
76
|
content_id: contentId,
|
|
75
77
|
};
|
|
76
78
|
|
|
77
|
-
// Compile the app and bundle the
|
|
79
|
+
// Compile the app and bundle the output.
|
|
78
80
|
const compiledFiles: { name: string; data: Buffer }[] = [];
|
|
79
81
|
|
|
80
82
|
const appDir = join(getAppsDir(), appId);
|
|
81
|
-
const compileResult = await compileApp(appDir);
|
|
82
|
-
if (!compileResult.ok) {
|
|
83
|
-
const messages = compileResult.errors
|
|
84
|
-
.map((e) => {
|
|
85
|
-
const loc = e.location
|
|
86
|
-
? ` (${e.location.file}:${e.location.line}:${e.location.column})`
|
|
87
|
-
: "";
|
|
88
|
-
return `${e.text}${loc}`;
|
|
89
|
-
})
|
|
90
|
-
.join("\n");
|
|
91
|
-
throw new Error(`Compilation failed for app "${app.name}":\n${messages}`);
|
|
92
|
-
}
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
if (multifile) {
|
|
85
|
+
// Multi-file TSX app: compile src/ -> dist/
|
|
86
|
+
const compileResult = await compileApp(appDir);
|
|
87
|
+
if (!compileResult.ok) {
|
|
88
|
+
const messages = compileResult.errors
|
|
89
|
+
.map((e) => {
|
|
90
|
+
const loc = e.location
|
|
91
|
+
? ` (${e.location.file}:${e.location.line}:${e.location.column})`
|
|
92
|
+
: "";
|
|
93
|
+
return `${e.text}${loc}`;
|
|
94
|
+
})
|
|
95
|
+
.join("\n");
|
|
96
|
+
throw new Error(`Compilation failed for app "${app.name}":\n${messages}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const distDir = join(appDir, "dist");
|
|
100
|
+
const indexHtml = await readFile(join(distDir, "index.html"), "utf-8");
|
|
101
|
+
const mainJs = await readFile(join(distDir, "main.js"));
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
compiledFiles.push({ name: "index.html", data: Buffer.from(indexHtml) });
|
|
104
|
+
compiledFiles.push({ name: "main.js", data: mainJs });
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
// main.css is optional — only produced when the app imports CSS
|
|
107
|
+
const cssPath = join(distDir, "main.css");
|
|
108
|
+
if (existsSync(cssPath)) {
|
|
109
|
+
compiledFiles.push({ name: "main.css", data: await readFile(cssPath) });
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Single-file HTML app: bundle index.html directly
|
|
113
|
+
const indexHtmlPath = join(appDir, "index.html");
|
|
114
|
+
if (!existsSync(indexHtmlPath)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`App "${app.name}" has no src/ directory and no index.html`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const indexHtml = await readFile(indexHtmlPath, "utf-8");
|
|
120
|
+
compiledFiles.push({ name: "index.html", data: Buffer.from(indexHtml) });
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
// Create the zip archive
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
ASK_GUARDIAN_CAPTURE_REGEX,
|
|
51
51
|
CALL_OPENING_ACK_MARKER,
|
|
52
52
|
CALL_OPENING_MARKER,
|
|
53
|
+
CALL_VERIFICATION_COMPLETE_MARKER,
|
|
53
54
|
couldBeControlMarker,
|
|
54
55
|
END_CALL_MARKER,
|
|
55
56
|
extractBalancedJson,
|
|
@@ -209,6 +210,21 @@ export class CallController {
|
|
|
209
210
|
await this.runTurn(CALL_OPENING_MARKER);
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Kick off the first utterance after the caller has completed outbound
|
|
215
|
+
* phone verification. Sends a verification-aware marker so the LLM can
|
|
216
|
+
* greet naturally with context that verification just happened.
|
|
217
|
+
*/
|
|
218
|
+
async startPostVerificationGreeting(): Promise<void> {
|
|
219
|
+
if (this.initialGreetingStarted) return;
|
|
220
|
+
if (this.state !== "idle") return;
|
|
221
|
+
|
|
222
|
+
this.initialGreetingStarted = true;
|
|
223
|
+
this.resetSilenceTimer();
|
|
224
|
+
this.lastSentWasOpener = true;
|
|
225
|
+
await this.runTurn(CALL_VERIFICATION_COMPLETE_MARKER);
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
/**
|
|
213
229
|
* Handle a final caller utterance from the ConversationRelay.
|
|
214
230
|
* Caller utterances always trigger normal turns, even when a guardian
|
|
@@ -595,7 +595,7 @@ export class RelayConnection {
|
|
|
595
595
|
(resolved.actorTrust.trustClass === "guardian" ||
|
|
596
596
|
resolved.actorTrust.trustClass === "trusted_contact")
|
|
597
597
|
) {
|
|
598
|
-
touchContactInteraction(resolved.actorTrust.memberRecord.
|
|
598
|
+
touchContactInteraction(resolved.actorTrust.memberRecord.channel.id);
|
|
599
599
|
}
|
|
600
600
|
if (this.controller && resolved.actorTrust.trustClass !== "unknown") {
|
|
601
601
|
this.controller.setTrustContext(
|
|
@@ -612,7 +612,7 @@ export class RelayConnection {
|
|
|
612
612
|
resolved.actorTrust.trustClass === "trusted_contact")
|
|
613
613
|
) {
|
|
614
614
|
touchContactInteraction(
|
|
615
|
-
resolved.actorTrust.memberRecord.
|
|
615
|
+
resolved.actorTrust.memberRecord.channel.id,
|
|
616
616
|
);
|
|
617
617
|
}
|
|
618
618
|
if (this.controller && resolved.actorTrust.trustClass !== "unknown") {
|
|
@@ -992,14 +992,7 @@ export class RelayConnection {
|
|
|
992
992
|
}
|
|
993
993
|
|
|
994
994
|
if (isOutbound) {
|
|
995
|
-
|
|
996
|
-
this.sendTextToken(result.ttsMessage!, true);
|
|
997
|
-
|
|
998
|
-
updateCallSession(this.callSessionId, {
|
|
999
|
-
status: "completed",
|
|
1000
|
-
endedAt: Date.now(),
|
|
1001
|
-
});
|
|
1002
|
-
|
|
995
|
+
// Keep the pointer message back to the initiating conversation
|
|
1003
996
|
const successSession = getCallSession(this.callSessionId);
|
|
1004
997
|
if (successSession?.initiatedFromConversationId) {
|
|
1005
998
|
addPointerMessage(
|
|
@@ -1018,9 +1011,32 @@ export class RelayConnection {
|
|
|
1018
1011
|
});
|
|
1019
1012
|
}
|
|
1020
1013
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1014
|
+
// Update trust context on the controller so the LLM knows this is the guardian
|
|
1015
|
+
if (this.controller) {
|
|
1016
|
+
const verifiedActorTrust = resolveActorTrust({
|
|
1017
|
+
assistantId,
|
|
1018
|
+
sourceChannel: "phone",
|
|
1019
|
+
conversationExternalId: fromNumber,
|
|
1020
|
+
actorExternalId: fromNumber,
|
|
1021
|
+
});
|
|
1022
|
+
this.controller.setTrustContext(
|
|
1023
|
+
toTrustContext(verifiedActorTrust, fromNumber),
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Mark session as in-progress and transition to guardian conversation
|
|
1028
|
+
// with verification context so the LLM greets naturally.
|
|
1029
|
+
updateCallSession(this.callSessionId, { status: "in_progress" });
|
|
1030
|
+
if (this.controller) {
|
|
1031
|
+
this.controller
|
|
1032
|
+
.startPostVerificationGreeting()
|
|
1033
|
+
.catch((err) =>
|
|
1034
|
+
log.error(
|
|
1035
|
+
{ err, callSessionId: this.callSessionId },
|
|
1036
|
+
"Failed to start post-verification greeting",
|
|
1037
|
+
),
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1024
1040
|
} else if (result.verificationType === "trusted_contact") {
|
|
1025
1041
|
this.continueCallAfterTrustedContactActivation({
|
|
1026
1042
|
assistantId,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
export const CALL_OPENING_MARKER = "[CALL_OPENING]";
|
|
13
13
|
export const CALL_OPENING_ACK_MARKER = "[CALL_OPENING_ACK]";
|
|
14
|
+
export const CALL_VERIFICATION_COMPLETE_MARKER = "[CALL_VERIFICATION_COMPLETE]";
|
|
14
15
|
export const END_CALL_MARKER = "[END_CALL]";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +44,7 @@ export function buildElevenLabsVoiceSpec(config: {
|
|
|
44
44
|
*
|
|
45
45
|
* Always uses ElevenLabs TTS via Twilio ConversationRelay.
|
|
46
46
|
* The voice ID comes from the shared `elevenlabs.voiceId` config
|
|
47
|
-
* (defaults to
|
|
47
|
+
* (defaults to Amelia — ZF6FPAbjXT4488VcRRnw).
|
|
48
48
|
*/
|
|
49
49
|
export function resolveVoiceQualityProfile(
|
|
50
50
|
config?: ReturnType<typeof loadConfig>,
|
|
@@ -24,7 +24,10 @@ import { checkIngressForSecrets } from "../security/secret-ingress.js";
|
|
|
24
24
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
25
25
|
import { IngressBlockedError } from "../util/errors.js";
|
|
26
26
|
import { getLogger } from "../util/logger.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
CALL_OPENING_MARKER,
|
|
29
|
+
CALL_VERIFICATION_COMPLETE_MARKER,
|
|
30
|
+
} from "./voice-control-protocol.js";
|
|
28
31
|
|
|
29
32
|
const log = getLogger("voice-session-bridge");
|
|
30
33
|
|
|
@@ -197,7 +200,8 @@ function buildVoiceCallControlPrompt(opts: {
|
|
|
197
200
|
? " However, the disclosure text from rule 0 is separate from self-introduction and must always be included in your opening greeting, even if the Task does not mention introducing yourself."
|
|
198
201
|
: "";
|
|
199
202
|
lines.push(
|
|
200
|
-
|
|
203
|
+
'7. If the latest user turn is "(verification completed — transitioning into conversation)", the caller just completed a phone verification code challenge on this call. Greet them naturally and ask if there is anything you can help with. Keep it casual and brief.',
|
|
204
|
+
`If the latest user turn is "(call connected — deliver opening greeting)", deliver your opening greeting based solely on the Task context above. The Task already describes how to open the call — follow it directly without adding any extra introduction on top. If the Task says to introduce yourself, do so once. If the Task does not mention introducing yourself, skip the introduction.${disclosureReminder} Vary the wording naturally; do not use a fixed template.`,
|
|
201
205
|
"8. If the latest user turn includes [CALL_OPENING_ACK], treat it as the callee acknowledging your opener and continue the conversation naturally without re-introducing yourself or repeating the initial check-in question.",
|
|
202
206
|
);
|
|
203
207
|
}
|
|
@@ -269,7 +273,9 @@ export async function startVoiceTurn(
|
|
|
269
273
|
const persistedContent =
|
|
270
274
|
opts.content === CALL_OPENING_MARKER
|
|
271
275
|
? "(call connected — deliver opening greeting)"
|
|
272
|
-
: opts.content
|
|
276
|
+
: opts.content === CALL_VERIFICATION_COMPLETE_MARKER
|
|
277
|
+
? "(verification completed — transitioning into conversation)"
|
|
278
|
+
: opts.content;
|
|
273
279
|
|
|
274
280
|
// Build the call-control protocol prompt so the model knows how to emit
|
|
275
281
|
// control markers (ASK_GUARDIAN, END_CALL, etc.) and recognize opener turns.
|
package/src/channels/types.ts
CHANGED
|
@@ -74,6 +74,22 @@ export function assertInterfaceId(value: unknown, field: string): InterfaceId {
|
|
|
74
74
|
return value;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Interfaces that have an SSE client capable of displaying interactive
|
|
79
|
+
* permission prompts. Channel interfaces (telegram, slack, etc.) route
|
|
80
|
+
* approvals through the guardian system and have no interactive prompter UI.
|
|
81
|
+
*/
|
|
82
|
+
export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
|
|
83
|
+
"macos",
|
|
84
|
+
"ios",
|
|
85
|
+
"cli",
|
|
86
|
+
"vellum",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export function isInteractiveInterface(id: InterfaceId): boolean {
|
|
90
|
+
return INTERACTIVE_INTERFACES.has(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
export interface TurnInterfaceContext {
|
|
78
94
|
userMessageInterface: InterfaceId;
|
|
79
95
|
assistantMessageInterface: InterfaceId;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import type { Command } from "commander";
|
|
12
|
+
|
|
13
|
+
import { getWorkspaceDir } from "../../util/platform.js";
|
|
14
|
+
import { log } from "../logger.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
17
|
+
const POLL_INTERVAL_MS = 100;
|
|
18
|
+
|
|
19
|
+
interface BashSignalResult {
|
|
20
|
+
requestId: string;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
exitCode: number | null;
|
|
24
|
+
timedOut: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerBashCommand(program: Command): void {
|
|
29
|
+
program
|
|
30
|
+
.command("bash <command>")
|
|
31
|
+
.description(
|
|
32
|
+
"Execute a shell command through the assistant process for debugging",
|
|
33
|
+
)
|
|
34
|
+
.option(
|
|
35
|
+
"-t, --timeout <ms>",
|
|
36
|
+
"Timeout in milliseconds for command execution",
|
|
37
|
+
String(DEFAULT_TIMEOUT_MS),
|
|
38
|
+
)
|
|
39
|
+
.addHelpText(
|
|
40
|
+
"after",
|
|
41
|
+
`
|
|
42
|
+
Sends a shell command to the running assistant for execution via the
|
|
43
|
+
signals directory. The assistant spawns the command in its own process environment
|
|
44
|
+
and returns stdout, stderr, and the exit code.
|
|
45
|
+
|
|
46
|
+
This is a developer debugging tool for inspecting how the assistant invokes and
|
|
47
|
+
observes shell commands. The command runs with the assistant's environment, working
|
|
48
|
+
directory, and process context — not the caller's shell.
|
|
49
|
+
|
|
50
|
+
The CLI writes the command to signals/bash.<requestId> and polls
|
|
51
|
+
signals/bash.<requestId>.result for the output. The assistant must be running
|
|
52
|
+
for this to work.
|
|
53
|
+
|
|
54
|
+
Arguments:
|
|
55
|
+
command The shell command string to execute (e.g. "echo hello", "ls -la").
|
|
56
|
+
Runs in bash via \`bash -c\` in the assistant's process environment.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
$ assistant bash "echo hello"
|
|
60
|
+
$ assistant bash "which node"
|
|
61
|
+
$ assistant bash "env | grep PATH" --timeout 10000
|
|
62
|
+
$ assistant bash "ls -la"`,
|
|
63
|
+
)
|
|
64
|
+
.action((command: string, opts: { timeout: string }) => {
|
|
65
|
+
const timeoutMs = parseInt(opts.timeout, 10);
|
|
66
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
|
|
67
|
+
log.error("Invalid timeout value. Must be a positive integer.");
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const requestId = randomUUID();
|
|
73
|
+
const signalsDir = join(getWorkspaceDir(), "signals");
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(signalsDir, { recursive: true });
|
|
77
|
+
} catch {
|
|
78
|
+
log.error("Failed to create signals directory.");
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write the command signal for the assistant to pick up.
|
|
84
|
+
const signalPath = join(signalsDir, `bash.${requestId}`);
|
|
85
|
+
const resultPath = join(signalsDir, `bash.${requestId}.result`);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(
|
|
89
|
+
signalPath,
|
|
90
|
+
JSON.stringify({ requestId, command, timeoutMs }),
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
log.error("Failed to write bash signal file.");
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log.info(`Sent command to assistant (requestId: ${requestId})`);
|
|
99
|
+
log.info("Waiting for result...");
|
|
100
|
+
|
|
101
|
+
// Poll for the result file until timeout.
|
|
102
|
+
const deadline = Date.now() + timeoutMs + 5_000; // extra buffer for assistant overhead
|
|
103
|
+
|
|
104
|
+
const poll = setInterval(() => {
|
|
105
|
+
if (Date.now() > deadline) {
|
|
106
|
+
clearInterval(poll);
|
|
107
|
+
cleanupSignalFiles();
|
|
108
|
+
log.error(
|
|
109
|
+
"Timed out waiting for response. Is the assistant running?",
|
|
110
|
+
);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!existsSync(resultPath)) return;
|
|
116
|
+
|
|
117
|
+
let result: BashSignalResult;
|
|
118
|
+
try {
|
|
119
|
+
const content = readFileSync(resultPath, "utf-8");
|
|
120
|
+
result = JSON.parse(content) as BashSignalResult;
|
|
121
|
+
} catch {
|
|
122
|
+
// File may be partially written; retry on next poll.
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Ignore stale results from a previous invocation.
|
|
127
|
+
if (result.requestId !== requestId) return;
|
|
128
|
+
|
|
129
|
+
clearInterval(poll);
|
|
130
|
+
cleanupSignalFiles();
|
|
131
|
+
|
|
132
|
+
if (result.error) {
|
|
133
|
+
log.error(`Spawn error: ${result.error}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (result.stdout) {
|
|
139
|
+
process.stdout.write(result.stdout);
|
|
140
|
+
if (!result.stdout.endsWith("\n")) {
|
|
141
|
+
process.stdout.write("\n");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (result.stderr) {
|
|
146
|
+
process.stderr.write(result.stderr);
|
|
147
|
+
if (!result.stderr.endsWith("\n")) {
|
|
148
|
+
process.stderr.write("\n");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (result.timedOut) {
|
|
153
|
+
log.info(`Command timed out in assistant.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.exitCode != null && result.exitCode !== 0) {
|
|
157
|
+
log.info(`Exit code: ${result.exitCode}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.exitCode = result.exitCode ?? 1;
|
|
161
|
+
}, POLL_INTERVAL_MS);
|
|
162
|
+
|
|
163
|
+
function cleanupSignalFiles(): void {
|
|
164
|
+
for (const p of [signalPath, resultPath]) {
|
|
165
|
+
try {
|
|
166
|
+
unlinkSync(p);
|
|
167
|
+
} catch {
|
|
168
|
+
// Best-effort cleanup; the file may already be gone.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -7,7 +7,7 @@ import { getRuntimeHttpPort } from "../../config/env.js";
|
|
|
7
7
|
import { loadRawConfig } from "../../config/loader.js";
|
|
8
8
|
import { shouldAutoStartDaemon } from "../../daemon/connection-policy.js";
|
|
9
9
|
import { isHttpHealthy } from "../../daemon/daemon-control.js";
|
|
10
|
-
import {
|
|
10
|
+
import { getSecureKeyAsync } from "../../security/secure-keys.js";
|
|
11
11
|
import {
|
|
12
12
|
getDbPath,
|
|
13
13
|
getLogPath,
|
|
@@ -35,7 +35,7 @@ Output symbols:
|
|
|
35
35
|
|
|
36
36
|
Diagnostic checks performed:
|
|
37
37
|
1. Bun is installed Verifies bun is available in PATH
|
|
38
|
-
2. API key configured Checks for a valid provider API key in
|
|
38
|
+
2. API key configured Checks for a valid provider API key in secure storage
|
|
39
39
|
3. Assistant reachable HTTP health check against the assistant server
|
|
40
40
|
4. Database exists/readable Opens the SQLite database and runs a test query
|
|
41
41
|
5. Directory structure Verifies required ~/.vellum/ directories exist
|
|
@@ -76,32 +76,14 @@ Examples:
|
|
|
76
76
|
const raw = loadRawConfig();
|
|
77
77
|
const provider =
|
|
78
78
|
typeof raw.provider === "string" ? raw.provider : "anthropic";
|
|
79
|
-
const
|
|
80
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
81
|
-
openai: "OPENAI_API_KEY",
|
|
82
|
-
gemini: "GEMINI_API_KEY",
|
|
83
|
-
ollama: "OLLAMA_API_KEY",
|
|
84
|
-
fireworks: "FIREWORKS_API_KEY",
|
|
85
|
-
openrouter: "OPENROUTER_API_KEY",
|
|
86
|
-
};
|
|
87
|
-
const configKey = getSecureKey(provider);
|
|
88
|
-
const envVar = providerEnvVar[provider];
|
|
89
|
-
const envKey = envVar ? process.env[envVar] : undefined;
|
|
90
|
-
const plaintextKey = (
|
|
91
|
-
raw.apiKeys as Record<string, string> | undefined
|
|
92
|
-
)?.[provider];
|
|
79
|
+
const configKey = await getSecureKeyAsync(provider);
|
|
93
80
|
|
|
94
81
|
if (provider === "ollama") {
|
|
95
82
|
pass("Provider configured (Ollama; API key optional)");
|
|
96
|
-
} else if (
|
|
83
|
+
} else if (configKey) {
|
|
97
84
|
pass("API key configured");
|
|
98
85
|
} else {
|
|
99
|
-
fail(
|
|
100
|
-
"API key configured",
|
|
101
|
-
envVar
|
|
102
|
-
? `set ${envVar} or run: assistant keys set ${provider} <key>`
|
|
103
|
-
: `set API key for provider "${provider}"`,
|
|
104
|
-
);
|
|
86
|
+
fail("API key configured", `run: assistant keys set ${provider} <key>`);
|
|
105
87
|
}
|
|
106
88
|
|
|
107
89
|
// 3. Daemon reachable (HTTP health check)
|