@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getApp,
|
|
11
|
+
getAppDirPath,
|
|
12
|
+
isMultifileApp,
|
|
13
|
+
resolveEffectiveAppHtml,
|
|
14
|
+
} from "../memory/app-store.js";
|
|
8
15
|
import {
|
|
9
16
|
getActivePublishedPageByAppId,
|
|
10
17
|
updatePublishedPage,
|
|
@@ -23,17 +30,32 @@ export async function updatePublishedAppDeployment(
|
|
|
23
30
|
const publishedPage = getActivePublishedPageByAppId(appId);
|
|
24
31
|
if (!publishedPage) return;
|
|
25
32
|
|
|
26
|
-
// 2. Load the app
|
|
33
|
+
// 2. Load the app and resolve its deployable HTML. For multifile apps the
|
|
34
|
+
// real content lives in dist/index.html (compiled from src/), not in
|
|
35
|
+
// htmlDefinition (which is "" for them) — using htmlDefinition directly
|
|
36
|
+
// would deploy a blank page.
|
|
27
37
|
const app = getApp(appId);
|
|
28
|
-
if (!app
|
|
29
|
-
log.warn({ appId }, "Published app not found
|
|
38
|
+
if (!app) {
|
|
39
|
+
log.warn({ appId }, "Published app not found");
|
|
30
40
|
return;
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
// Skip rather than deploy the compile-failure fallback when a multifile
|
|
44
|
+
// app has no compiled output (e.g. a concurrent compile failed); a later
|
|
45
|
+
// successful compile re-triggers this path.
|
|
46
|
+
if (
|
|
47
|
+
isMultifileApp(app) &&
|
|
48
|
+
!existsSync(join(getAppDirPath(app.id), "dist", "index.html"))
|
|
49
|
+
) {
|
|
50
|
+
log.warn({ appId }, "Skipping auto-redeploy: compiled output missing");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const html = resolveEffectiveAppHtml(app);
|
|
55
|
+
if (!html) return;
|
|
56
|
+
|
|
33
57
|
// 3. Hash the current HTML and check if it changed
|
|
34
|
-
const newHash = createHash("sha256")
|
|
35
|
-
.update(app.htmlDefinition)
|
|
36
|
-
.digest("hex");
|
|
58
|
+
const newHash = createHash("sha256").update(html).digest("hex");
|
|
37
59
|
if (newHash === publishedPage.htmlHash) return; // No change
|
|
38
60
|
|
|
39
61
|
// 4. Get Vercel token — don't prompt, just skip if unavailable
|
|
@@ -46,7 +68,7 @@ export async function updatePublishedAppDeployment(
|
|
|
46
68
|
execute: async (token) => {
|
|
47
69
|
// 5. Deploy updated HTML using the same project slug
|
|
48
70
|
const result = await deployHtmlToVercel({
|
|
49
|
-
html
|
|
71
|
+
html,
|
|
50
72
|
name: slug,
|
|
51
73
|
token,
|
|
52
74
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACTIVATION_MOMENT_PARAMS,
|
|
5
|
+
ACTIVATION_STEPS,
|
|
6
|
+
activationStepIndex,
|
|
7
|
+
activationStepNameForMomentParam,
|
|
8
|
+
buildActivationDaemonEventId,
|
|
9
|
+
isActivationMomentParam,
|
|
10
|
+
isActivationStepName,
|
|
11
|
+
} from "../activation-funnel.js";
|
|
12
|
+
|
|
13
|
+
const ALL_STEP_NAMES = Object.values(ACTIVATION_STEPS).map((s) => s.stepName);
|
|
14
|
+
|
|
15
|
+
describe("activation funnel vocabulary", () => {
|
|
16
|
+
test("step indices are 1-5 and unique", () => {
|
|
17
|
+
const indices = Object.values(ACTIVATION_STEPS).map((s) => s.stepIndex);
|
|
18
|
+
expect([...indices].sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]);
|
|
19
|
+
expect(new Set(indices).size).toBe(indices.length);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("activationStepIndex resolves index from name", () => {
|
|
23
|
+
for (const { stepName, stepIndex } of Object.values(ACTIVATION_STEPS)) {
|
|
24
|
+
expect(activationStepIndex(stepName)).toBe(stepIndex);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("isActivationStepName accepts all five step names", () => {
|
|
29
|
+
expect(ALL_STEP_NAMES).toHaveLength(5);
|
|
30
|
+
for (const stepName of ALL_STEP_NAMES) {
|
|
31
|
+
expect(isActivationStepName(stepName)).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("isActivationStepName rejects unknown names", () => {
|
|
36
|
+
expect(isActivationStepName("bogus")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("buildActivationDaemonEventId is deterministic", () => {
|
|
40
|
+
expect(
|
|
41
|
+
buildActivationDaemonEventId("conv-abc", "activation_moment_1_complete"),
|
|
42
|
+
).toBe("activation_v1_2026_06:conv-abc:activation_moment_1_complete");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("activation moment param (model-facing tag)", () => {
|
|
47
|
+
test("ACTIVATION_MOMENT_PARAMS lists the five tokens in step order", () => {
|
|
48
|
+
expect(ACTIVATION_MOMENT_PARAMS).toEqual([
|
|
49
|
+
"moment_1",
|
|
50
|
+
"moment_2",
|
|
51
|
+
"moment_3",
|
|
52
|
+
"first_wow_executed",
|
|
53
|
+
"first_wow_interacted",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("isActivationMomentParam accepts all five tokens", () => {
|
|
58
|
+
for (const param of ACTIVATION_MOMENT_PARAMS) {
|
|
59
|
+
expect(isActivationMomentParam(param)).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("isActivationMomentParam rejects unknown tokens (incl. wire step names)", () => {
|
|
64
|
+
expect(isActivationMomentParam("bogus")).toBe(false);
|
|
65
|
+
// The wire step name is NOT a valid model-facing token.
|
|
66
|
+
expect(isActivationMomentParam("activation_moment_1_complete")).toBe(false);
|
|
67
|
+
expect(isActivationMomentParam("moment_4")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("activationStepNameForMomentParam maps each token to its step name", () => {
|
|
71
|
+
expect(activationStepNameForMomentParam("moment_1")).toBe(
|
|
72
|
+
ACTIVATION_STEPS.moment1.stepName,
|
|
73
|
+
);
|
|
74
|
+
expect(activationStepNameForMomentParam("moment_2")).toBe(
|
|
75
|
+
ACTIVATION_STEPS.moment2.stepName,
|
|
76
|
+
);
|
|
77
|
+
expect(activationStepNameForMomentParam("moment_3")).toBe(
|
|
78
|
+
ACTIVATION_STEPS.moment3.stepName,
|
|
79
|
+
);
|
|
80
|
+
expect(activationStepNameForMomentParam("first_wow_executed")).toBe(
|
|
81
|
+
ACTIVATION_STEPS.firstWowExecuted.stepName,
|
|
82
|
+
);
|
|
83
|
+
expect(activationStepNameForMomentParam("first_wow_interacted")).toBe(
|
|
84
|
+
ACTIVATION_STEPS.firstWowInteracted.stepName,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("every moment param maps to a valid wire step name", () => {
|
|
89
|
+
for (const param of ACTIVATION_MOMENT_PARAMS) {
|
|
90
|
+
expect(
|
|
91
|
+
isActivationStepName(activationStepNameForMomentParam(param)),
|
|
92
|
+
).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation funnel vocabulary — the single source of truth for the
|
|
3
|
+
* activation-rail telemetry funnel.
|
|
4
|
+
*
|
|
5
|
+
* The activation rail emits milestone ("funnel") events into the existing
|
|
6
|
+
* onboarding telemetry substrate (`type: "onboarding"`). This module defines
|
|
7
|
+
* the step vocabulary (step_name → step_index), the funnel version, the
|
|
8
|
+
* cohort arm tag, and the deterministic `daemon_event_id` used for dbt-side
|
|
9
|
+
* dedup. Downstream PRs (store, reporter, emit handler, turn hook) import
|
|
10
|
+
* from here so the vocabulary is never duplicated.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Funnel version stamped on every activation event (per JARVIS-1033). */
|
|
14
|
+
export const ACTIVATION_FUNNEL_VERSION = "activation_v1_2026_06";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Filename of the bootstrap template that drives the activation rail. The web
|
|
18
|
+
* prechat-context sets this as `onboardingContext.bootstrapTemplate` (mirrored
|
|
19
|
+
* there as `ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE`); the daemon recognizes it to
|
|
20
|
+
* mark the conversation as an activation session. Single source of truth on the
|
|
21
|
+
* daemon side so the literal isn't duplicated across modules.
|
|
22
|
+
*/
|
|
23
|
+
export const ACTIVATION_RAIL_BOOTSTRAP_TEMPLATE =
|
|
24
|
+
"BOOTSTRAP-ACTIVATION-RAIL.md";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cohort arm tag for Stream-A emission. The daemon only runs the rail for
|
|
28
|
+
* treatment users, so activation events are tagged with the treatment arm
|
|
29
|
+
* name ("variant-a"), mirroring `pre-chat-onboarding-experiment-2026-06-06`.
|
|
30
|
+
* Stream B later threads the assigned arm in place of this constant.
|
|
31
|
+
*/
|
|
32
|
+
export const ACTIVATION_AB_VARIANT = "variant-a";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Activation funnel steps keyed by short id. `stepName` is the wire value
|
|
36
|
+
* sent to telemetry; `stepIndex` is the 1-based ordinal position. This is
|
|
37
|
+
* the single source of truth for the step vocabulary.
|
|
38
|
+
*/
|
|
39
|
+
export const ACTIVATION_STEPS = {
|
|
40
|
+
moment1: { stepName: "activation_moment_1_complete", stepIndex: 1 },
|
|
41
|
+
moment2: { stepName: "activation_moment_2_complete", stepIndex: 2 },
|
|
42
|
+
moment3: { stepName: "activation_moment_3_complete", stepIndex: 3 },
|
|
43
|
+
firstWowExecuted: { stepName: "activation_first_wow_executed", stepIndex: 4 },
|
|
44
|
+
firstWowInteracted: {
|
|
45
|
+
stepName: "activation_first_wow_interacted",
|
|
46
|
+
stepIndex: 5,
|
|
47
|
+
},
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/** Union of valid activation step names. */
|
|
51
|
+
export type ActivationStepName =
|
|
52
|
+
(typeof ACTIVATION_STEPS)[keyof typeof ACTIVATION_STEPS]["stepName"];
|
|
53
|
+
|
|
54
|
+
/** step_name → step_index, derived once from the vocabulary. */
|
|
55
|
+
const STEP_INDEX_BY_NAME = new Map<ActivationStepName, number>(
|
|
56
|
+
Object.values(ACTIVATION_STEPS).map((step) => [
|
|
57
|
+
step.stepName,
|
|
58
|
+
step.stepIndex,
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/** Type guard: is `value` one of the known activation step names? */
|
|
63
|
+
export function isActivationStepName(
|
|
64
|
+
value: string,
|
|
65
|
+
): value is ActivationStepName {
|
|
66
|
+
return STEP_INDEX_BY_NAME.has(value as ActivationStepName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Model-facing activation-moment tokens. These are the values the model passes
|
|
71
|
+
* as the optional `ui_show` `activation_moment` tag; the daemon maps each token
|
|
72
|
+
* to its `ACTIVATION_STEPS[...].stepName` and records the milestone when the
|
|
73
|
+
* tagged surface is committed. Kept short/stable so the schema enum is friendly
|
|
74
|
+
* for the model — the canonical wire vocabulary stays `ActivationStepName`.
|
|
75
|
+
*/
|
|
76
|
+
export type ActivationMomentParam =
|
|
77
|
+
| "moment_1"
|
|
78
|
+
| "moment_2"
|
|
79
|
+
| "moment_3"
|
|
80
|
+
| "first_wow_executed"
|
|
81
|
+
| "first_wow_interacted";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `ActivationMomentParam` token → canonical `ActivationStepName`, derived from
|
|
85
|
+
* `ACTIVATION_STEPS` so the vocabulary stays single-sourced. The token order
|
|
86
|
+
* mirrors `ACTIVATION_STEPS` (indices 1–5).
|
|
87
|
+
*/
|
|
88
|
+
const STEP_NAME_BY_MOMENT_PARAM = {
|
|
89
|
+
moment_1: ACTIVATION_STEPS.moment1.stepName,
|
|
90
|
+
moment_2: ACTIVATION_STEPS.moment2.stepName,
|
|
91
|
+
moment_3: ACTIVATION_STEPS.moment3.stepName,
|
|
92
|
+
first_wow_executed: ACTIVATION_STEPS.firstWowExecuted.stepName,
|
|
93
|
+
first_wow_interacted: ACTIVATION_STEPS.firstWowInteracted.stepName,
|
|
94
|
+
} as const satisfies Record<ActivationMomentParam, ActivationStepName>;
|
|
95
|
+
|
|
96
|
+
/** The five valid model-facing moment tokens (e.g. for a schema enum). */
|
|
97
|
+
export const ACTIVATION_MOMENT_PARAMS = Object.keys(
|
|
98
|
+
STEP_NAME_BY_MOMENT_PARAM,
|
|
99
|
+
) as ActivationMomentParam[];
|
|
100
|
+
|
|
101
|
+
/** Type guard: is `value` one of the model-facing activation-moment tokens? */
|
|
102
|
+
export function isActivationMomentParam(
|
|
103
|
+
value: string,
|
|
104
|
+
): value is ActivationMomentParam {
|
|
105
|
+
return Object.prototype.hasOwnProperty.call(STEP_NAME_BY_MOMENT_PARAM, value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Map a model-facing moment token to its canonical activation step name. */
|
|
109
|
+
export function activationStepNameForMomentParam(
|
|
110
|
+
param: ActivationMomentParam,
|
|
111
|
+
): ActivationStepName {
|
|
112
|
+
return STEP_NAME_BY_MOMENT_PARAM[param];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* When each tagged moment is emitted relative to its `ui_show` surface:
|
|
117
|
+
* - `"show"` — at surface render time. `first_wow_executed` is an *execution*
|
|
118
|
+
* signal: the rail's result/`work_result` surface is display-only and may
|
|
119
|
+
* receive no user commit, so a commit-time emit would never fire (and if the
|
|
120
|
+
* card has an action, deferring to the click would conflate "executed" with
|
|
121
|
+
* "interacted" and corrupt funnel timing).
|
|
122
|
+
* - `"commit"` — on user commit/interaction. The other four moments represent
|
|
123
|
+
* resolving an intake, picking an offer/task, or clicking a result action —
|
|
124
|
+
* all genuine commit events.
|
|
125
|
+
*/
|
|
126
|
+
export const ACTIVATION_MOMENT_EMIT_AT = {
|
|
127
|
+
moment_1: "commit",
|
|
128
|
+
moment_2: "commit",
|
|
129
|
+
moment_3: "commit",
|
|
130
|
+
first_wow_executed: "show",
|
|
131
|
+
first_wow_interacted: "commit",
|
|
132
|
+
} as const satisfies Record<ActivationMomentParam, "show" | "commit">;
|
|
133
|
+
|
|
134
|
+
/** True when the moment records at surface-render time rather than on commit. */
|
|
135
|
+
export function activationMomentEmitsAtShow(
|
|
136
|
+
param: ActivationMomentParam,
|
|
137
|
+
): boolean {
|
|
138
|
+
return ACTIVATION_MOMENT_EMIT_AT[param] === "show";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Look up the 1-based funnel index for a step name. */
|
|
142
|
+
export function activationStepIndex(stepName: ActivationStepName): number {
|
|
143
|
+
// Non-null: ActivationStepName is derived from ACTIVATION_STEPS, so the map
|
|
144
|
+
// always contains the key.
|
|
145
|
+
return STEP_INDEX_BY_NAME.get(stepName)!;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Deterministic `daemon_event_id` for an activation event. Keying on
|
|
150
|
+
* `${funnel_version}:${sessionId}:${stepName}` lets the existing dbt
|
|
151
|
+
* dedup (keyed on `daemon_event_id`, earliest-wins) collapse a moment that
|
|
152
|
+
* fires more than once into a single row.
|
|
153
|
+
*
|
|
154
|
+
* `funnelVersion` defaults to the current `ACTIVATION_FUNNEL_VERSION` but
|
|
155
|
+
* callers that have a per-row stored version (e.g. the telemetry reporter
|
|
156
|
+
* flushing rows recorded under an older version) MUST pass the row's own
|
|
157
|
+
* `funnel_version` so the id stays stable across a version bump — otherwise
|
|
158
|
+
* queued/offline v1 rows would be keyed with the new binary's version and
|
|
159
|
+
* stop collapsing with already-ingested v1 rows from the same session.
|
|
160
|
+
*/
|
|
161
|
+
export function buildActivationDaemonEventId(
|
|
162
|
+
sessionId: string,
|
|
163
|
+
stepName: ActivationStepName,
|
|
164
|
+
funnelVersion: string = ACTIVATION_FUNNEL_VERSION,
|
|
165
|
+
): string {
|
|
166
|
+
return `${funnelVersion}:${sessionId}:${stepName}`;
|
|
167
|
+
}
|
package/src/telemetry/types.ts
CHANGED
|
@@ -183,6 +183,19 @@ export interface OnboardingTelemetryEvent extends TelemetryEventBase {
|
|
|
183
183
|
google_connected?: boolean;
|
|
184
184
|
google_scopes?: string[];
|
|
185
185
|
ab_variant?: string;
|
|
186
|
+
/**
|
|
187
|
+
* Activation-funnel fields (mirror the web funnel shape and the platform
|
|
188
|
+
* serializer). The platform accepts an onboarding event via either the
|
|
189
|
+
* legacy `screen` path or the all-funnel-fields path (`session_id` +
|
|
190
|
+
* `step_name` + `step_index` + `completed_at` + `funnel_version` +
|
|
191
|
+
* `ab_variant`).
|
|
192
|
+
*/
|
|
193
|
+
session_id?: string;
|
|
194
|
+
step_name?: string;
|
|
195
|
+
step_index?: number;
|
|
196
|
+
completed_at?: string;
|
|
197
|
+
funnel_version?: string;
|
|
198
|
+
user_id?: string;
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
/**
|
|
@@ -131,6 +131,11 @@ const mockQueryUnreportedOnboardingEvents = mock(
|
|
|
131
131
|
googleScopesJson: string | null;
|
|
132
132
|
priorAssistantsJson: string | null;
|
|
133
133
|
abVariant: string | null;
|
|
134
|
+
sessionId: string | null;
|
|
135
|
+
stepName: string | null;
|
|
136
|
+
stepIndex: number | null;
|
|
137
|
+
completedAt: string | null;
|
|
138
|
+
funnelVersion: string | null;
|
|
134
139
|
}[],
|
|
135
140
|
);
|
|
136
141
|
|
|
@@ -152,6 +157,10 @@ import { getDb } from "../memory/db-connection.js";
|
|
|
152
157
|
import { initializeDb } from "../memory/db-init.js";
|
|
153
158
|
import { authFallbackEvents } from "../memory/schema.js";
|
|
154
159
|
import type { UsageEvent } from "../usage/types.js";
|
|
160
|
+
import {
|
|
161
|
+
ACTIVATION_FUNNEL_VERSION,
|
|
162
|
+
buildActivationDaemonEventId,
|
|
163
|
+
} from "./activation-funnel.js";
|
|
155
164
|
import { UsageTelemetryReporter } from "./usage-telemetry-reporter.js";
|
|
156
165
|
|
|
157
166
|
initializeDb();
|
|
@@ -201,6 +210,34 @@ function makeUsageEvent(
|
|
|
201
210
|
};
|
|
202
211
|
}
|
|
203
212
|
|
|
213
|
+
type OnboardingEventFixture = ReturnType<
|
|
214
|
+
typeof mockQueryUnreportedOnboardingEvents
|
|
215
|
+
>[number];
|
|
216
|
+
|
|
217
|
+
function makeOnboardingEvent(
|
|
218
|
+
overrides: Partial<OnboardingEventFixture> = {},
|
|
219
|
+
): OnboardingEventFixture {
|
|
220
|
+
eventIdCounter += 1;
|
|
221
|
+
return {
|
|
222
|
+
id: `onb-${eventIdCounter}`,
|
|
223
|
+
createdAt: 1700000000000 + eventIdCounter * 1000,
|
|
224
|
+
screen: "tools",
|
|
225
|
+
toolsJson: null,
|
|
226
|
+
tasksJson: null,
|
|
227
|
+
tone: null,
|
|
228
|
+
googleConnected: null,
|
|
229
|
+
googleScopesJson: null,
|
|
230
|
+
priorAssistantsJson: null,
|
|
231
|
+
abVariant: null,
|
|
232
|
+
sessionId: null,
|
|
233
|
+
stepName: null,
|
|
234
|
+
stepIndex: null,
|
|
235
|
+
completedAt: null,
|
|
236
|
+
funnelVersion: null,
|
|
237
|
+
...overrides,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
204
241
|
const originalFetch = globalThis.fetch;
|
|
205
242
|
let mockFetch: ReturnType<typeof mock>;
|
|
206
243
|
|
|
@@ -1184,4 +1221,121 @@ describe("UsageTelemetryReporter", () => {
|
|
|
1184
1221
|
expect(idCalls.length).toBeGreaterThanOrEqual(1);
|
|
1185
1222
|
expect(idCalls[idCalls.length - 1][1]).toBe(lastRow.id);
|
|
1186
1223
|
});
|
|
1224
|
+
|
|
1225
|
+
// -------------------------------------------------------------------------
|
|
1226
|
+
// Onboarding / activation funnel events
|
|
1227
|
+
// -------------------------------------------------------------------------
|
|
1228
|
+
|
|
1229
|
+
test("activation onboarding row serializes funnel fields + deterministic daemon_event_id", async () => {
|
|
1230
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1231
|
+
const sessionId = "sess-activation-1";
|
|
1232
|
+
const stepName = "activation_moment_1_complete";
|
|
1233
|
+
mockQueryUnreportedOnboardingEvents.mockReturnValue([
|
|
1234
|
+
makeOnboardingEvent({
|
|
1235
|
+
id: "onb-activation-1",
|
|
1236
|
+
screen: stepName,
|
|
1237
|
+
abVariant: "variant-a",
|
|
1238
|
+
sessionId,
|
|
1239
|
+
stepName,
|
|
1240
|
+
stepIndex: 1,
|
|
1241
|
+
completedAt: "2026-06-06T00:00:00.000Z",
|
|
1242
|
+
funnelVersion: ACTIVATION_FUNNEL_VERSION,
|
|
1243
|
+
}),
|
|
1244
|
+
]);
|
|
1245
|
+
mockFetch.mockImplementation(() =>
|
|
1246
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
const reporter = new UsageTelemetryReporter();
|
|
1250
|
+
await reporter.flush();
|
|
1251
|
+
|
|
1252
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1253
|
+
const body = JSON.parse(
|
|
1254
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1255
|
+
);
|
|
1256
|
+
expect(body.events.length).toBe(1);
|
|
1257
|
+
expect(body.events[0]).toMatchObject({
|
|
1258
|
+
type: "onboarding",
|
|
1259
|
+
session_id: sessionId,
|
|
1260
|
+
step_name: stepName,
|
|
1261
|
+
step_index: 1,
|
|
1262
|
+
completed_at: "2026-06-06T00:00:00.000Z",
|
|
1263
|
+
funnel_version: "activation_v1_2026_06",
|
|
1264
|
+
daemon_event_id: buildActivationDaemonEventId(sessionId, stepName),
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
test("activation daemon_event_id is keyed on the row's stored funnel_version, not the binary constant", async () => {
|
|
1269
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1270
|
+
const sessionId = "sess-activation-old";
|
|
1271
|
+
const stepName = "activation_moment_1_complete";
|
|
1272
|
+
// A row recorded under an OLDER funnel version, queued across an upgrade.
|
|
1273
|
+
const oldVersion = "activation_v0_2026_05";
|
|
1274
|
+
expect(oldVersion).not.toBe(ACTIVATION_FUNNEL_VERSION);
|
|
1275
|
+
mockQueryUnreportedOnboardingEvents.mockReturnValue([
|
|
1276
|
+
makeOnboardingEvent({
|
|
1277
|
+
id: "onb-activation-old",
|
|
1278
|
+
screen: stepName,
|
|
1279
|
+
abVariant: "variant-a",
|
|
1280
|
+
sessionId,
|
|
1281
|
+
stepName,
|
|
1282
|
+
stepIndex: 1,
|
|
1283
|
+
completedAt: "2026-05-01T00:00:00.000Z",
|
|
1284
|
+
funnelVersion: oldVersion,
|
|
1285
|
+
}),
|
|
1286
|
+
]);
|
|
1287
|
+
mockFetch.mockImplementation(() =>
|
|
1288
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
const reporter = new UsageTelemetryReporter();
|
|
1292
|
+
await reporter.flush();
|
|
1293
|
+
|
|
1294
|
+
const body = JSON.parse(
|
|
1295
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1296
|
+
);
|
|
1297
|
+
expect(body.events[0]).toMatchObject({
|
|
1298
|
+
funnel_version: oldVersion,
|
|
1299
|
+
daemon_event_id: buildActivationDaemonEventId(
|
|
1300
|
+
sessionId,
|
|
1301
|
+
stepName,
|
|
1302
|
+
oldVersion,
|
|
1303
|
+
),
|
|
1304
|
+
});
|
|
1305
|
+
// Guard: the id must carry the row's version, not the binary constant.
|
|
1306
|
+
expect(body.events[0].daemon_event_id).toBe(
|
|
1307
|
+
`${oldVersion}:${sessionId}:${stepName}`,
|
|
1308
|
+
);
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
test("legacy onboarding row keeps daemon_event_id: e.id and omits funnel fields", async () => {
|
|
1312
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
1313
|
+
mockQueryUnreportedOnboardingEvents.mockReturnValue([
|
|
1314
|
+
makeOnboardingEvent({
|
|
1315
|
+
id: "onb-legacy-1",
|
|
1316
|
+
screen: "tools",
|
|
1317
|
+
toolsJson: JSON.stringify(["calendar"]),
|
|
1318
|
+
}),
|
|
1319
|
+
]);
|
|
1320
|
+
mockFetch.mockImplementation(() =>
|
|
1321
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
const reporter = new UsageTelemetryReporter();
|
|
1325
|
+
await reporter.flush();
|
|
1326
|
+
|
|
1327
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
1328
|
+
const body = JSON.parse(
|
|
1329
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
1330
|
+
);
|
|
1331
|
+
expect(body.events.length).toBe(1);
|
|
1332
|
+
const e = body.events[0];
|
|
1333
|
+
expect(e.type).toBe("onboarding");
|
|
1334
|
+
expect(e.daemon_event_id).toBe("onb-legacy-1");
|
|
1335
|
+
expect(e.session_id).toBeUndefined();
|
|
1336
|
+
expect(e.step_name).toBeUndefined();
|
|
1337
|
+
expect(e.step_index).toBeUndefined();
|
|
1338
|
+
expect(e.completed_at).toBeUndefined();
|
|
1339
|
+
expect(e.funnel_version).toBeUndefined();
|
|
1340
|
+
});
|
|
1187
1341
|
});
|
|
@@ -28,6 +28,10 @@ import { VellumPlatformClient } from "../platform/client.js";
|
|
|
28
28
|
import { getDeviceId } from "../util/device-id.js";
|
|
29
29
|
import { getLogger } from "../util/logger.js";
|
|
30
30
|
import { APP_VERSION } from "../version.js";
|
|
31
|
+
import {
|
|
32
|
+
type ActivationStepName,
|
|
33
|
+
buildActivationDaemonEventId,
|
|
34
|
+
} from "./activation-funnel.js";
|
|
31
35
|
import type { TelemetryEvent, TurnTelemetryClientInfo } from "./types.js";
|
|
32
36
|
|
|
33
37
|
const log = getLogger("usage-telemetry");
|
|
@@ -337,7 +341,22 @@ export class UsageTelemetryReporter {
|
|
|
337
341
|
...onboardingEvents.map(
|
|
338
342
|
(e): TelemetryEvent => ({
|
|
339
343
|
type: "onboarding",
|
|
340
|
-
|
|
344
|
+
// Wire-only override for activation rows: a deterministic id keyed
|
|
345
|
+
// on funnel_version/session/step lets dbt collapse a moment that
|
|
346
|
+
// fires more than once. Key on the ROW's stored `funnelVersion`
|
|
347
|
+
// (not the binary's current constant) so rows recorded under an
|
|
348
|
+
// older version — flushed offline or after an upgrade — keep a
|
|
349
|
+
// stable id and still collapse with already-ingested rows. The
|
|
350
|
+
// SQLite watermark cursor still uses `e.id`/`e.createdAt`, so this
|
|
351
|
+
// override is checkpoint-safe.
|
|
352
|
+
daemon_event_id:
|
|
353
|
+
e.sessionId && e.stepName && e.funnelVersion
|
|
354
|
+
? buildActivationDaemonEventId(
|
|
355
|
+
e.sessionId,
|
|
356
|
+
e.stepName as ActivationStepName,
|
|
357
|
+
e.funnelVersion,
|
|
358
|
+
)
|
|
359
|
+
: e.id,
|
|
341
360
|
recorded_at: e.createdAt,
|
|
342
361
|
screen: e.screen,
|
|
343
362
|
...(e.toolsJson ? { tools: JSON.parse(e.toolsJson) } : {}),
|
|
@@ -350,6 +369,12 @@ export class UsageTelemetryReporter {
|
|
|
350
369
|
? { google_scopes: JSON.parse(e.googleScopesJson) }
|
|
351
370
|
: {}),
|
|
352
371
|
...(e.abVariant ? { ab_variant: e.abVariant } : {}),
|
|
372
|
+
// Activation funnel fields — only present on activation rows.
|
|
373
|
+
...(e.sessionId ? { session_id: e.sessionId } : {}),
|
|
374
|
+
...(e.stepName ? { step_name: e.stepName } : {}),
|
|
375
|
+
...(e.stepIndex != null ? { step_index: e.stepIndex } : {}),
|
|
376
|
+
...(e.completedAt ? { completed_at: e.completedAt } : {}),
|
|
377
|
+
...(e.funnelVersion ? { funnel_version: e.funnelVersion } : {}),
|
|
353
378
|
// Onboarding events fall back to the envelope `assistant_version`
|
|
354
379
|
// — same upload-time attribution risk as before this PR. Adding
|
|
355
380
|
// the record-time column to `onboarding_events` (#30733) is a
|
|
@@ -33,28 +33,13 @@ beforeEach(() => {
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
35
|
describe("executeAcpListAgents", () => {
|
|
36
|
-
test("
|
|
37
|
-
config.setConfig({ enabled: false });
|
|
38
|
-
|
|
39
|
-
const result = await executeAcpListAgents({}, makeContext());
|
|
40
|
-
|
|
41
|
-
expect(result.isError).toBe(false);
|
|
42
|
-
const parsed = JSON.parse(result.content as string);
|
|
43
|
-
expect(parsed.enabled).toBe(false);
|
|
44
|
-
// Pulls from the shared ACP_DISABLED_HINT constant exported by
|
|
45
|
-
// resolve-agent.ts. The exact wording is checked in resolve-agent.test.ts.
|
|
46
|
-
expect(parsed.hint).toContain("acp.enabled");
|
|
47
|
-
expect(parsed.hint).toContain("config.json");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("enabled, no user config: all defaults present with source 'default' and available based on Bun.which", async () => {
|
|
36
|
+
test("no user config: all defaults present with source 'default' and available based on Bun.which", async () => {
|
|
51
37
|
config.setConfig({ agents: {} });
|
|
52
38
|
|
|
53
39
|
const result = await executeAcpListAgents({}, makeContext());
|
|
54
40
|
|
|
55
41
|
expect(result.isError).toBe(false);
|
|
56
42
|
const parsed = JSON.parse(result.content as string);
|
|
57
|
-
expect(parsed.enabled).toBe(true);
|
|
58
43
|
expect(parsed.agents.map((a: { id: string }) => a.id)).toEqual([
|
|
59
44
|
"claude",
|
|
60
45
|
"codex",
|
|
@@ -68,7 +53,7 @@ describe("executeAcpListAgents", () => {
|
|
|
68
53
|
}
|
|
69
54
|
});
|
|
70
55
|
|
|
71
|
-
test("
|
|
56
|
+
test("user overrides claude: claude has source 'config' and the user's command", async () => {
|
|
72
57
|
config.setConfig({
|
|
73
58
|
agents: {
|
|
74
59
|
claude: {
|
|
@@ -83,7 +68,6 @@ describe("executeAcpListAgents", () => {
|
|
|
83
68
|
|
|
84
69
|
expect(result.isError).toBe(false);
|
|
85
70
|
const parsed = JSON.parse(result.content as string);
|
|
86
|
-
expect(parsed.enabled).toBe(true);
|
|
87
71
|
|
|
88
72
|
const claude = parsed.agents.find((a: { id: string }) => a.id === "claude");
|
|
89
73
|
expect(claude.source).toBe("config");
|
|
@@ -1,31 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listAcpAgents } from "../../acp/resolve-agent.js";
|
|
2
2
|
import type { ToolContext, ToolExecutionResult } from "../types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Lists ACP coding agents available to spawn (configured + bundled defaults),
|
|
6
6
|
* marking each with its source (`config` vs `default`), whether the agent's
|
|
7
7
|
* binary is on PATH, and an install hint when missing.
|
|
8
|
-
*
|
|
9
|
-
* When `acp.enabled: false`, returns a single hint instructing the user to
|
|
10
|
-
* enable ACP — no agent list is surfaced because none can run.
|
|
11
8
|
*/
|
|
12
9
|
export async function executeAcpListAgents(
|
|
13
10
|
_input: Record<string, unknown>,
|
|
14
11
|
_context: ToolContext,
|
|
15
12
|
): Promise<ToolExecutionResult> {
|
|
16
|
-
const {
|
|
17
|
-
if (!enabled) {
|
|
18
|
-
return {
|
|
19
|
-
content: JSON.stringify({
|
|
20
|
-
enabled: false,
|
|
21
|
-
hint: ACP_DISABLED_HINT,
|
|
22
|
-
}),
|
|
23
|
-
isError: false,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
13
|
+
const { agents } = listAcpAgents();
|
|
26
14
|
|
|
27
15
|
return {
|
|
28
|
-
content: JSON.stringify({
|
|
16
|
+
content: JSON.stringify({ agents }),
|
|
29
17
|
isError: false,
|
|
30
18
|
};
|
|
31
19
|
}
|
|
@@ -250,16 +250,6 @@ describe("executeAcpSpawn — input validation", () => {
|
|
|
250
250
|
expect(result.content).toContain("Available:");
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
-
test("acp disabled returns error with config hint", async () => {
|
|
254
|
-
config.setConfig({ enabled: false, agents: DEFAULT_TEST_AGENTS });
|
|
255
|
-
const result = await executeAcpSpawn(
|
|
256
|
-
{ agent: "claude", task: "do something" },
|
|
257
|
-
makeContext(),
|
|
258
|
-
);
|
|
259
|
-
expect(result.isError).toBe(true);
|
|
260
|
-
expect(result.content).toContain("acp.enabled");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
253
|
test("missing binary + bun absent returns install hint, no install attempted", async () => {
|
|
264
254
|
which.setWhich({}); // neither bun nor the adapter on PATH
|
|
265
255
|
const result = await executeAcpSpawn(
|