@vellumai/assistant 0.9.0 → 0.9.1-staging.1
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 +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for
|
|
2
|
+
* Tests for address-based member resolution in resolveActorTrust.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* ("I don't recognize this number") path instead of the unverified-caller
|
|
9
|
-
* guidance path.
|
|
4
|
+
* All member lookups use the canonical (type, address) path — the address
|
|
5
|
+
* column is the sole identity for all channel types. Phone channels
|
|
6
|
+
* registered by the inbound name-capture flow have `address` set (E.164)
|
|
7
|
+
* and are discovered through the same path as every other channel.
|
|
10
8
|
*
|
|
11
|
-
* This suite verifies that
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* This suite verifies that address-based lookup returns the correct
|
|
10
|
+
* `memberRecord` with the right channel/status so relay-setup-router
|
|
11
|
+
* can emit the appropriate outcome (e.g. `unverified_caller`).
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -22,18 +20,14 @@ mock.module("../util/logger.js", () => ({
|
|
|
22
20
|
}));
|
|
23
21
|
|
|
24
22
|
// ── Contact store stubs — filled in per test ─────────────────────────────────
|
|
25
|
-
let _byExternalId: ReturnType<
|
|
26
|
-
typeof import("../contacts/contact-store.js")["findContactByChannelExternalId"]
|
|
27
|
-
> = null;
|
|
28
23
|
let _byAddress: ReturnType<
|
|
29
|
-
typeof import("../contacts/contact-store.js")["findContactByAddress"]
|
|
24
|
+
(typeof import("../contacts/contact-store.js"))["findContactByAddress"]
|
|
30
25
|
> = null;
|
|
31
26
|
let _guardian: ReturnType<
|
|
32
|
-
typeof import("../contacts/contact-store.js")["findGuardianForChannel"]
|
|
27
|
+
(typeof import("../contacts/contact-store.js"))["findGuardianForChannel"]
|
|
33
28
|
> = null;
|
|
34
29
|
|
|
35
30
|
mock.module("../contacts/contact-store.js", () => ({
|
|
36
|
-
findContactByChannelExternalId: (_type: string, _id: string) => _byExternalId,
|
|
37
31
|
findContactByAddress: (_type: string, _addr: string) => _byAddress,
|
|
38
32
|
findGuardianForChannel: (_channel: string) => _guardian,
|
|
39
33
|
}));
|
|
@@ -69,7 +63,7 @@ function makeContact(
|
|
|
69
63
|
id: channelId,
|
|
70
64
|
contactId: "contact-test",
|
|
71
65
|
type: "phone",
|
|
72
|
-
address: PHONE
|
|
66
|
+
address: PHONE,
|
|
73
67
|
externalUserId,
|
|
74
68
|
externalChatId: null,
|
|
75
69
|
isPrimary: true,
|
|
@@ -94,7 +88,6 @@ function makeContact(
|
|
|
94
88
|
|
|
95
89
|
describe("resolveActorTrust — address fallback", () => {
|
|
96
90
|
beforeEach(() => {
|
|
97
|
-
_byExternalId = null;
|
|
98
91
|
_byAddress = null;
|
|
99
92
|
_guardian = null;
|
|
100
93
|
});
|
|
@@ -117,12 +110,8 @@ describe("resolveActorTrust — address fallback", () => {
|
|
|
117
110
|
expect(result.trustClass).toBe("unknown");
|
|
118
111
|
});
|
|
119
112
|
|
|
120
|
-
test("
|
|
121
|
-
|
|
122
|
-
const primaryContact = makeContact("contact", "active", PHONE);
|
|
123
|
-
const addressContact = makeContact("contact", "unverified", null);
|
|
124
|
-
_byExternalId = primaryContact;
|
|
125
|
-
_byAddress = addressContact;
|
|
113
|
+
test("address lookup is the sole member resolution path", () => {
|
|
114
|
+
_byAddress = makeContact("contact", "active", null);
|
|
126
115
|
|
|
127
116
|
const result = resolveActorTrust({
|
|
128
117
|
assistantId: "asst-1",
|
|
@@ -132,11 +121,10 @@ describe("resolveActorTrust — address fallback", () => {
|
|
|
132
121
|
});
|
|
133
122
|
|
|
134
123
|
expect(result.memberRecord?.channel.status).toBe("active");
|
|
135
|
-
expect(result.memberRecord?.channel.
|
|
124
|
+
expect(result.memberRecord?.channel.address).toBe(PHONE);
|
|
136
125
|
});
|
|
137
126
|
|
|
138
127
|
test("returns null memberRecord when neither lookup finds the number", () => {
|
|
139
|
-
_byExternalId = null;
|
|
140
128
|
_byAddress = null;
|
|
141
129
|
|
|
142
130
|
const result = resolveActorTrust({
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compaction summarizer's input routing.
|
|
3
|
+
*
|
|
4
|
+
* Budget-gate compaction summarizes the *injected* history so the summary
|
|
5
|
+
* call's prompt prefix matches the agent's warm prefix cache (a cache read
|
|
6
|
+
* rather than a fresh cache write). Overflow recovery summarizes the
|
|
7
|
+
* injection-*stripped* history because its emergency split keeps an
|
|
8
|
+
* un-stripped verbatim prefix that the post-compaction re-injection would
|
|
9
|
+
* otherwise double-stack.
|
|
10
|
+
*/
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { PostCompactContext } from "@vellumai/plugin-api";
|
|
14
|
+
|
|
15
|
+
import type { AgentEvent } from "../agent/loop.js";
|
|
16
|
+
import { AgentLoop } from "../agent/loop.js";
|
|
17
|
+
import type { ContextWindowConfig } from "../config/types.js";
|
|
18
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
19
|
+
import { HOOKS } from "../plugin-api/constants.js";
|
|
20
|
+
import {
|
|
21
|
+
createContextWindowManager,
|
|
22
|
+
disposeContextWindowManager,
|
|
23
|
+
getContextWindowManager,
|
|
24
|
+
} from "../plugins/defaults/compaction/manager-store.js";
|
|
25
|
+
import {
|
|
26
|
+
registerPlugin,
|
|
27
|
+
resetPluginRegistryForTests,
|
|
28
|
+
} from "../plugins/registry.js";
|
|
29
|
+
import type {
|
|
30
|
+
Message,
|
|
31
|
+
Provider,
|
|
32
|
+
ProviderResponse,
|
|
33
|
+
SendMessageOptions,
|
|
34
|
+
} from "../providers/types.js";
|
|
35
|
+
import { ContextOverflowError } from "../providers/types.js";
|
|
36
|
+
|
|
37
|
+
const testPostCompactPlugin = {
|
|
38
|
+
manifest: { name: "test-post-compact", version: "0.0.0" },
|
|
39
|
+
hooks: {
|
|
40
|
+
[HOOKS.POST_COMPACT]: async (input: PostCompactContext): Promise<void> => {
|
|
41
|
+
void input;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CONVERSATION_ID = "compaction-strip-conversation";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A runtime `<workspace>` injection block. `stripInjectionsForCompaction`
|
|
50
|
+
* matches its full `{ prefix: "<workspace>\n", suffix: "\n</workspace>" }`
|
|
51
|
+
* wrapper and drops it from user-message content.
|
|
52
|
+
*/
|
|
53
|
+
const WORKSPACE_INJECTION =
|
|
54
|
+
"<workspace>\nActive workspace: project-x\n</workspace>";
|
|
55
|
+
const TURN_BODY = "Hello there, this is the turn body.";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A user turn carrying a real text block plus a runtime injection block.
|
|
59
|
+
* Stripping drops the injection block and keeps the real one.
|
|
60
|
+
*/
|
|
61
|
+
const injectedUserMessage: Message = {
|
|
62
|
+
role: "user",
|
|
63
|
+
content: [
|
|
64
|
+
{ type: "text", text: TURN_BODY },
|
|
65
|
+
{ type: "text", text: WORKSPACE_INJECTION },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function textResponse(text: string): ProviderResponse {
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text }],
|
|
72
|
+
model: "mock-model",
|
|
73
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
74
|
+
stopReason: "end_turn",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createMockProvider(responses: ProviderResponse[]): Provider {
|
|
79
|
+
let callIndex = 0;
|
|
80
|
+
return {
|
|
81
|
+
name: "mock",
|
|
82
|
+
async sendMessage(
|
|
83
|
+
_messages: Message[],
|
|
84
|
+
_options?: SendMessageOptions,
|
|
85
|
+
): Promise<ProviderResponse> {
|
|
86
|
+
const response = responses[callIndex] ?? responses[responses.length - 1];
|
|
87
|
+
callIndex++;
|
|
88
|
+
return response;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface CompactionInputCapture {
|
|
94
|
+
budget: Message[] | null;
|
|
95
|
+
overflow: Message[] | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Install a per-conversation manager that records the messages handed to the
|
|
100
|
+
* budget summarizer (`maybeCompact`) and to overflow recovery
|
|
101
|
+
* (`recoverContextOverflow`).
|
|
102
|
+
*/
|
|
103
|
+
function installCapturingManager(capture: CompactionInputCapture): {
|
|
104
|
+
trust: TrustContext;
|
|
105
|
+
} {
|
|
106
|
+
createContextWindowManager({
|
|
107
|
+
provider: { name: "mock-provider" } as unknown as Provider,
|
|
108
|
+
systemPrompt: "system",
|
|
109
|
+
config: {} as unknown as ContextWindowConfig,
|
|
110
|
+
conversationId: CONVERSATION_ID,
|
|
111
|
+
});
|
|
112
|
+
const manager = getContextWindowManager(CONVERSATION_ID);
|
|
113
|
+
if (manager) {
|
|
114
|
+
manager.maybeCompact = (async (messages: Message[]) => {
|
|
115
|
+
capture.budget = messages;
|
|
116
|
+
return {
|
|
117
|
+
messages: [injectedUserMessage],
|
|
118
|
+
compacted: true,
|
|
119
|
+
exhausted: false,
|
|
120
|
+
};
|
|
121
|
+
}) as unknown as typeof manager.maybeCompact;
|
|
122
|
+
manager.recoverContextOverflow = (async (messages: Message[]) => {
|
|
123
|
+
capture.overflow = messages;
|
|
124
|
+
return {
|
|
125
|
+
messages: [injectedUserMessage],
|
|
126
|
+
compacted: true,
|
|
127
|
+
exhausted: false,
|
|
128
|
+
};
|
|
129
|
+
}) as unknown as typeof manager.recoverContextOverflow;
|
|
130
|
+
}
|
|
131
|
+
return { trust: { sourceChannel: "vellum", trustClass: "unknown" } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function serialize(messages: Message[] | null): string {
|
|
135
|
+
return JSON.stringify(messages ?? []);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
describe("AgentLoop compaction summarizer input", () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
resetPluginRegistryForTests();
|
|
141
|
+
registerPlugin(testPostCompactPlugin);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
disposeContextWindowManager(CONVERSATION_ID);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("budget-gate compaction summarizes the injected history", async () => {
|
|
149
|
+
// GIVEN a history whose user turn carries a runtime-injection block
|
|
150
|
+
const capture: CompactionInputCapture = { budget: null, overflow: null };
|
|
151
|
+
const provider = createMockProvider([
|
|
152
|
+
textResponse("done after compaction"),
|
|
153
|
+
]);
|
|
154
|
+
const loop = new AgentLoop({
|
|
155
|
+
provider,
|
|
156
|
+
systemPrompt: "system",
|
|
157
|
+
conversationId: CONVERSATION_ID,
|
|
158
|
+
tools: [],
|
|
159
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
160
|
+
});
|
|
161
|
+
const events: AgentEvent[] = [];
|
|
162
|
+
|
|
163
|
+
// WHEN the budget gate trips and in-place compaction runs
|
|
164
|
+
await loop.run({
|
|
165
|
+
requestId: "req",
|
|
166
|
+
messages: [injectedUserMessage],
|
|
167
|
+
onEvent: (event) => {
|
|
168
|
+
events.push(event);
|
|
169
|
+
},
|
|
170
|
+
resolveContextWindow: () => ({
|
|
171
|
+
maxInputTokens: 10,
|
|
172
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
173
|
+
}),
|
|
174
|
+
compactInPlace: true,
|
|
175
|
+
...installCapturingManager(capture),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// THEN the budget summarizer received the injected history, injection intact
|
|
179
|
+
expect(capture.budget).not.toBeNull();
|
|
180
|
+
expect(serialize(capture.budget)).toContain("<workspace>");
|
|
181
|
+
// AND overflow recovery was not invoked
|
|
182
|
+
expect(capture.overflow).toBeNull();
|
|
183
|
+
// AND the history-stripped marker still fires for the durable base
|
|
184
|
+
expect(events.some((e) => e.type === "history_stripped")).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("overflow-driven compaction summarizes the injection-stripped history", async () => {
|
|
188
|
+
// GIVEN a history whose user turn carries a runtime-injection block
|
|
189
|
+
const capture: CompactionInputCapture = { budget: null, overflow: null };
|
|
190
|
+
|
|
191
|
+
// AND a provider that rejects the first call as context-too-large
|
|
192
|
+
let throwOnce = true;
|
|
193
|
+
const provider: Provider = {
|
|
194
|
+
name: "mock",
|
|
195
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
196
|
+
if (throwOnce) {
|
|
197
|
+
throwOnce = false;
|
|
198
|
+
throw new ContextOverflowError("prompt too long", "mock", {
|
|
199
|
+
actualTokens: 999_999,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return textResponse("done after overflow recovery");
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const loop = new AgentLoop({
|
|
206
|
+
provider,
|
|
207
|
+
systemPrompt: "system",
|
|
208
|
+
conversationId: CONVERSATION_ID,
|
|
209
|
+
tools: [],
|
|
210
|
+
toolExecutor: async () => ({ content: "ok", isError: false }),
|
|
211
|
+
});
|
|
212
|
+
// A blocking watermark plus `compactInPlace: false` keep the budget gate
|
|
213
|
+
// from firing, so only the overflow rejection drives compaction.
|
|
214
|
+
loop.compactionCircuit.lastPostCompactionEstimate = 0;
|
|
215
|
+
const events: AgentEvent[] = [];
|
|
216
|
+
|
|
217
|
+
// WHEN the provider overflow forces overflow recovery
|
|
218
|
+
await loop.run({
|
|
219
|
+
requestId: "req",
|
|
220
|
+
messages: [injectedUserMessage],
|
|
221
|
+
onEvent: (event) => {
|
|
222
|
+
events.push(event);
|
|
223
|
+
},
|
|
224
|
+
resolveContextWindow: () => ({
|
|
225
|
+
maxInputTokens: 10,
|
|
226
|
+
overflowRecovery: { enabled: true, safetyMarginRatio: 0 },
|
|
227
|
+
}),
|
|
228
|
+
compactInPlace: false,
|
|
229
|
+
...installCapturingManager(capture),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// THEN overflow recovery received the history with the injection removed
|
|
233
|
+
expect(capture.overflow).not.toBeNull();
|
|
234
|
+
expect(serialize(capture.overflow)).not.toContain("<workspace>");
|
|
235
|
+
// AND the real turn body survives the strip
|
|
236
|
+
expect(serialize(capture.overflow)).toContain(TURN_BODY);
|
|
237
|
+
// AND the budget summarizer was not invoked
|
|
238
|
+
expect(capture.budget).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -365,6 +365,75 @@ describe("agent loop output hooks", () => {
|
|
|
365
365
|
expect(calls[0].systemPrompt).toContain("[EDITED]");
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
+
test("pre-model-call routes the call to the hook's chosen inference profile", async () => {
|
|
369
|
+
// GIVEN a hook that selects an inference profile for the user-facing call
|
|
370
|
+
registerOutputHookPlugin({
|
|
371
|
+
preModelCall: (ctx) => {
|
|
372
|
+
if (ctx.callSite !== "mainAgent") return;
|
|
373
|
+
ctx.modelProfile = "fast-profile";
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
const { provider, calls } = createMockProvider([textResponse("hi")]);
|
|
377
|
+
const loop = new AgentLoop({
|
|
378
|
+
provider: provider,
|
|
379
|
+
systemPrompt: "system",
|
|
380
|
+
conversationId: "test-conversation",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// WHEN the loop issues the user-facing provider call
|
|
384
|
+
await loop.run({
|
|
385
|
+
requestId: "test-request",
|
|
386
|
+
messages: [userMessage],
|
|
387
|
+
onEvent: collect([]),
|
|
388
|
+
callSite: "mainAgent",
|
|
389
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// THEN the provider call carries the hook's profile as the override
|
|
393
|
+
expect(calls[0].options?.config?.overrideProfile).toBe("fast-profile");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("pre-model-call seeds modelProfile from the resolved override and clearing it drops the override", async () => {
|
|
397
|
+
// GIVEN a hook that observes the seeded override and then clears it
|
|
398
|
+
let seeded: string | null | undefined;
|
|
399
|
+
registerOutputHookPlugin({
|
|
400
|
+
preModelCall: (ctx) => {
|
|
401
|
+
seeded = ctx.modelProfile;
|
|
402
|
+
ctx.modelProfile = null;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
const { provider, calls } = createMockProvider([textResponse("hi")]);
|
|
406
|
+
const loop = new AgentLoop({
|
|
407
|
+
provider: provider,
|
|
408
|
+
systemPrompt: "system",
|
|
409
|
+
conversationId: "test-conversation",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// WHEN the loop runs with an ad-hoc override profile
|
|
413
|
+
await loop.run({
|
|
414
|
+
requestId: "test-request",
|
|
415
|
+
messages: [userMessage],
|
|
416
|
+
onEvent: collect([]),
|
|
417
|
+
callSite: "mainAgent",
|
|
418
|
+
overrideProfile: "seed-profile",
|
|
419
|
+
trust: { sourceChannel: "vellum", trustClass: "unknown" },
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// THEN the hook saw the loop's resolved override
|
|
423
|
+
expect(seeded).toBe("seed-profile");
|
|
424
|
+
// AND clearing it sends no override on the provider call
|
|
425
|
+
expect(calls[0].options?.config?.overrideProfile).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Context-window sizing and overflow recovery read the profile resolved
|
|
429
|
+
// before the hook runs, so a hook routing to a smaller-window profile does
|
|
430
|
+
// not resize the budget gate for that call. Tracked for a follow-up that
|
|
431
|
+
// resolves the routed profile ahead of the context-sizing gate.
|
|
432
|
+
test.todo(
|
|
433
|
+
"pre-model-call routing resizes the context-window budget for the routed profile",
|
|
434
|
+
() => {},
|
|
435
|
+
);
|
|
436
|
+
|
|
368
437
|
test("fail-open: a hook that mutates in place AND then throws cannot corrupt the persisted content", async () => {
|
|
369
438
|
// The hook mutates the array it receives before throwing. If the loop
|
|
370
439
|
// handed the hook the real `assistantMessage.content` array, the
|
|
@@ -195,6 +195,7 @@ interface CapturedRunAgentLoopOptions {
|
|
|
195
195
|
titleText?: string;
|
|
196
196
|
callSite?: string;
|
|
197
197
|
overrideProfile?: string;
|
|
198
|
+
forceOverrideProfile?: boolean;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
const capturedRunAgentLoopOptions: CapturedRunAgentLoopOptions[] = [];
|
|
@@ -323,6 +324,29 @@ describe("SubagentManager.spawn — overrideProfile inheritance", () => {
|
|
|
323
324
|
const captured = capturedRunAgentLoopOptions[0];
|
|
324
325
|
expect(captured.callSite).toBe("subagentSpawn");
|
|
325
326
|
expect(captured.overrideProfile).toBe("fast");
|
|
327
|
+
expect("forceOverrideProfile" in captured).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("forwards forced overrideProfile from SubagentConfig into runAgentLoop", async () => {
|
|
331
|
+
capturedRunAgentLoopOptions.length = 0;
|
|
332
|
+
|
|
333
|
+
const manager = new SubagentManager();
|
|
334
|
+
await manager.spawn(
|
|
335
|
+
{
|
|
336
|
+
parentConversationId: "parent-forced",
|
|
337
|
+
label: "child",
|
|
338
|
+
objective: "do the thing",
|
|
339
|
+
overrideProfile: "fast",
|
|
340
|
+
forceOverrideProfile: true,
|
|
341
|
+
},
|
|
342
|
+
() => {},
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(capturedRunAgentLoopOptions).toHaveLength(1);
|
|
346
|
+
const captured = capturedRunAgentLoopOptions[0];
|
|
347
|
+
expect(captured.callSite).toBe("subagentSpawn");
|
|
348
|
+
expect(captured.overrideProfile).toBe("fast");
|
|
349
|
+
expect(captured.forceOverrideProfile).toBe(true);
|
|
326
350
|
});
|
|
327
351
|
|
|
328
352
|
test("omits overrideProfile when SubagentConfig does not set it", async () => {
|
|
@@ -344,6 +368,7 @@ describe("SubagentManager.spawn — overrideProfile inheritance", () => {
|
|
|
344
368
|
// Field must be absent rather than carrying `undefined`, mirroring the
|
|
345
369
|
// agent loop's "field omitted when unset" contract.
|
|
346
370
|
expect("overrideProfile" in captured).toBe(false);
|
|
371
|
+
expect("forceOverrideProfile" in captured).toBe(false);
|
|
347
372
|
});
|
|
348
373
|
});
|
|
349
374
|
|
|
@@ -28,7 +28,7 @@ afterAll(() => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe("always-loaded tool count", () => {
|
|
31
|
-
test("should be exactly
|
|
31
|
+
test("should be exactly 10 with recall occupying the existing slot", async () => {
|
|
32
32
|
await initializeTools();
|
|
33
33
|
const allDefs = getAllToolDefinitions();
|
|
34
34
|
|
|
@@ -52,7 +52,6 @@ describe("always-loaded tool count", () => {
|
|
|
52
52
|
// path would allow unchecked host command execution.
|
|
53
53
|
const expectedNames = [
|
|
54
54
|
"bash",
|
|
55
|
-
"credential_store",
|
|
56
55
|
"file_edit",
|
|
57
56
|
"file_read",
|
|
58
57
|
"file_write",
|
|
@@ -66,6 +65,6 @@ describe("always-loaded tool count", () => {
|
|
|
66
65
|
|
|
67
66
|
expect(activeNames).toEqual(expectedNames);
|
|
68
67
|
expect(activeNames.filter((name) => name === "recall")).toHaveLength(1);
|
|
69
|
-
expect(activeTools.length).toBe(
|
|
68
|
+
expect(activeTools.length).toBe(10);
|
|
70
69
|
});
|
|
71
70
|
});
|
|
@@ -20,7 +20,6 @@ const ALLOWLIST = new Set([
|
|
|
20
20
|
"assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
|
|
21
21
|
"assistant/src/daemon/app-source-watcher.ts", // uses getAppsDir for recursive fs.watch root, not per-app paths
|
|
22
22
|
"assistant/src/tools/filesystem/write.ts", // uses getAppsDir as an exemption root for the artifact-HTML guard, not per-app paths
|
|
23
|
-
"assistant/src/memory/preloaded-apps.ts", // seeds fixed-slug apps (id === dirName by construction) and writes the definition JSON, which app-store has no fixed-id helper for
|
|
24
23
|
]);
|
|
25
24
|
|
|
26
25
|
function isTestFile(filePath: string): boolean {
|
|
@@ -60,10 +60,7 @@ const CANONICAL_KEY_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
|
60
60
|
* Keep this list minimal — only files that genuinely need to reference
|
|
61
61
|
* the legacy format for backward compatibility.
|
|
62
62
|
*/
|
|
63
|
-
const LEGACY_KEY_ALLOWLIST = new Set([
|
|
64
|
-
// macOS client: fallback reads from legacy config section
|
|
65
|
-
"clients/macos/vellum-assistant/Features/Settings/SettingsAccountTab.swift",
|
|
66
|
-
]);
|
|
63
|
+
const LEGACY_KEY_ALLOWLIST = new Set<string>([]);
|
|
67
64
|
|
|
68
65
|
function isTestFile(filePath: string): boolean {
|
|
69
66
|
return (
|
|
@@ -58,8 +58,6 @@ function loadRegistry(): Registry {
|
|
|
58
58
|
const LEGACY_KEY_ALLOWLIST = new Set([
|
|
59
59
|
// Type definitions documenting the legacy format
|
|
60
60
|
"assistant/src/config/types.ts",
|
|
61
|
-
// macOS client: fallback reads from legacy config section
|
|
62
|
-
"clients/macos/vellum-assistant/Features/Settings/SettingsAccountTab.swift",
|
|
63
61
|
]);
|
|
64
62
|
|
|
65
63
|
function isTestFile(filePath: string): boolean {
|
|
@@ -4,10 +4,7 @@ import { SYNC_TAGS } from "../daemon/message-types/sync.js";
|
|
|
4
4
|
import type { AssistantEvent } from "../runtime/assistant-event.js";
|
|
5
5
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
6
6
|
import { ROUTES as AVATAR_ROUTES } from "../runtime/routes/avatar-routes.js";
|
|
7
|
-
import {
|
|
8
|
-
publishIdentityChanged,
|
|
9
|
-
publishIdentityIntroChanged,
|
|
10
|
-
} from "../runtime/sync/resource-sync-events.js";
|
|
7
|
+
import { publishIdentityChanged } from "../runtime/sync/resource-sync-events.js";
|
|
11
8
|
|
|
12
9
|
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
13
10
|
const deadline = Date.now() + 500;
|
|
@@ -81,29 +78,7 @@ describe("avatar and identity sync events", () => {
|
|
|
81
78
|
});
|
|
82
79
|
expect(received[1].message).toEqual({
|
|
83
80
|
type: "sync_changed",
|
|
84
|
-
tags: [SYNC_TAGS.assistantIdentity
|
|
85
|
-
});
|
|
86
|
-
} finally {
|
|
87
|
-
subscription.dispose();
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("identity intro changes emit only the identity intro sync tag", async () => {
|
|
92
|
-
const received: AssistantEvent[] = [];
|
|
93
|
-
const subscription = assistantEventHub.subscribe({
|
|
94
|
-
type: "process",
|
|
95
|
-
callback: (event) => {
|
|
96
|
-
received.push(event);
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
publishIdentityIntroChanged();
|
|
102
|
-
await waitFor(() => received.length === 1);
|
|
103
|
-
|
|
104
|
-
expect(received[0].message).toEqual({
|
|
105
|
-
type: "sync_changed",
|
|
106
|
-
tags: [SYNC_TAGS.assistantIdentityIntro],
|
|
81
|
+
tags: [SYNC_TAGS.assistantIdentity],
|
|
107
82
|
});
|
|
108
83
|
} finally {
|
|
109
84
|
subscription.dispose();
|
|
@@ -78,10 +78,8 @@ mock.module("../prompts/persona-resolver.js", () => ({
|
|
|
78
78
|
resolveUserSlug: () => null,
|
|
79
79
|
}));
|
|
80
80
|
|
|
81
|
-
mock.module("../runtime/routes/
|
|
82
|
-
|
|
83
|
-
readWorkspaceIdentityIntro: () => null,
|
|
84
|
-
setCachedIntro: () => {},
|
|
81
|
+
mock.module("../runtime/routes/workspace-greetings.js", () => ({
|
|
82
|
+
readWorkspaceGreetings: () => null,
|
|
85
83
|
}));
|
|
86
84
|
|
|
87
85
|
// Mock getOrCreateConversation from conversation-store so the handler
|
|
@@ -345,13 +343,13 @@ describe("POST /v1/btw", () => {
|
|
|
345
343
|
expect(options!.config!.callSite).toBe("emptyStateGreeting");
|
|
346
344
|
});
|
|
347
345
|
|
|
348
|
-
test("
|
|
346
|
+
test("generic requests pass the default callSite", async () => {
|
|
349
347
|
const provider = makeMockProvider();
|
|
350
348
|
const session = makeMockSession(provider);
|
|
351
349
|
mockGetOrCreateConversation.mockImplementationOnce(async () => session);
|
|
352
350
|
|
|
353
351
|
const { result } = await callHandler({
|
|
354
|
-
conversationKey: "
|
|
352
|
+
conversationKey: "profile-intro",
|
|
355
353
|
content: "Generate an intro",
|
|
356
354
|
});
|
|
357
355
|
await readStream(result as ReadableStream<Uint8Array>);
|
|
@@ -361,7 +359,7 @@ describe("POST /v1/btw", () => {
|
|
|
361
359
|
expect(options!.config!.callSite).toBe("identityIntro");
|
|
362
360
|
});
|
|
363
361
|
|
|
364
|
-
test("
|
|
362
|
+
test("generic requests do not synthesize a static name greeting", async () => {
|
|
365
363
|
const identityPath = join(getWorkspaceDir(), "IDENTITY.md");
|
|
366
364
|
writeFileSync(
|
|
367
365
|
identityPath,
|
|
@@ -375,7 +373,7 @@ describe("POST /v1/btw", () => {
|
|
|
375
373
|
mockGetOrCreateConversation.mockImplementationOnce(async () => session);
|
|
376
374
|
|
|
377
375
|
const { result } = await callHandler({
|
|
378
|
-
conversationKey: "
|
|
376
|
+
conversationKey: "profile-intro",
|
|
379
377
|
content: "Generate an intro",
|
|
380
378
|
});
|
|
381
379
|
const text = await readStream(result as ReadableStream<Uint8Array>);
|
|
@@ -790,9 +790,6 @@ describe("Permission Checker", () => {
|
|
|
790
790
|
const workingDir = join(homedir(), "projects", "myapp");
|
|
791
791
|
expect(generateScopeOptions(workingDir, "web_fetch")).toHaveLength(0);
|
|
792
792
|
expect(generateScopeOptions(workingDir, "skill_load")).toHaveLength(0);
|
|
793
|
-
expect(generateScopeOptions(workingDir, "credential_store")).toHaveLength(
|
|
794
|
-
0,
|
|
795
|
-
);
|
|
796
793
|
expect(
|
|
797
794
|
generateScopeOptions(workingDir, "computer_use_click"),
|
|
798
795
|
).toHaveLength(0);
|