@vellumai/assistant 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -50,6 +50,10 @@ let lastCreateOptions: Record<string, unknown> | null = null;
|
|
|
50
50
|
let lastConstructorOptions: Record<string, unknown> | null = null;
|
|
51
51
|
let shouldThrow: Error | null = null;
|
|
52
52
|
|
|
53
|
+
function userMsg(text: string): Message {
|
|
54
|
+
return { role: "user", content: [{ type: "text", text }] };
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
// Simulate OpenAI.APIError
|
|
54
58
|
class FakeAPIError extends Error {
|
|
55
59
|
status: number;
|
|
@@ -1294,10 +1298,6 @@ describe("effort config passthrough", () => {
|
|
|
1294
1298
|
// ---------------------------------------------------------------------------
|
|
1295
1299
|
|
|
1296
1300
|
describe("OpenRouterProvider reasoning", () => {
|
|
1297
|
-
function userMsg(text: string): Message {
|
|
1298
|
-
return { role: "user", content: [{ type: "text", text }] };
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
1301
|
beforeEach(() => {
|
|
1302
1302
|
fakeChunks = [textChunk("OK"), usageChunk(10, 2)];
|
|
1303
1303
|
lastCreateParams = null;
|
|
@@ -1367,6 +1367,47 @@ describe("OpenRouterProvider reasoning", () => {
|
|
|
1367
1367
|
});
|
|
1368
1368
|
});
|
|
1369
1369
|
|
|
1370
|
+
describe("OpenRouterProvider Anthropic-compatible errors", () => {
|
|
1371
|
+
test("retags Anthropic ProviderError instances as OpenRouter errors", async () => {
|
|
1372
|
+
const provider = new OpenRouterProvider("or-key", "anthropic/claude-4.5");
|
|
1373
|
+
const abortReason = createAbortReason(
|
|
1374
|
+
"user_cancel",
|
|
1375
|
+
"openrouter-provider-test",
|
|
1376
|
+
);
|
|
1377
|
+
const cause = new Error("upstream cause");
|
|
1378
|
+
const innerError = new ProviderError(
|
|
1379
|
+
"Anthropic API error (402): Payment Required",
|
|
1380
|
+
"anthropic",
|
|
1381
|
+
402,
|
|
1382
|
+
{ cause, retryAfterMs: 1250, abortReason },
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
(
|
|
1386
|
+
provider as unknown as {
|
|
1387
|
+
anthropicInner: {
|
|
1388
|
+
sendMessage: OpenRouterProvider["sendMessage"];
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
).anthropicInner = {
|
|
1392
|
+
sendMessage: async () => {
|
|
1393
|
+
throw innerError;
|
|
1394
|
+
},
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
await provider.sendMessage([userMsg("hi")]);
|
|
1399
|
+
throw new Error("expected sendMessage to throw");
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
expect(error).toBeInstanceOf(ProviderError);
|
|
1402
|
+
expect((error as ProviderError).provider).toBe("openrouter");
|
|
1403
|
+
expect((error as ProviderError).statusCode).toBe(402);
|
|
1404
|
+
expect((error as ProviderError).retryAfterMs).toBe(1250);
|
|
1405
|
+
expect((error as ProviderError).abortReason).toBe(abortReason);
|
|
1406
|
+
expect((error as Error).cause).toBe(cause);
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1370
1411
|
// ---------------------------------------------------------------------------
|
|
1371
1412
|
// Reasoning effort → OpenAI reasoning_effort mapping
|
|
1372
1413
|
// ---------------------------------------------------------------------------
|
|
@@ -1374,10 +1415,6 @@ describe("OpenRouterProvider reasoning", () => {
|
|
|
1374
1415
|
describe("OpenAIProvider reasoning_effort", () => {
|
|
1375
1416
|
let provider: OpenAIProvider;
|
|
1376
1417
|
|
|
1377
|
-
function userMsg(text: string): Message {
|
|
1378
|
-
return { role: "user", content: [{ type: "text", text }] };
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
1418
|
beforeEach(() => {
|
|
1382
1419
|
fakeChunks = [textChunk("OK"), usageChunk(10, 2)];
|
|
1383
1420
|
lastCreateParams = null;
|
|
@@ -27,6 +27,31 @@ mock.module("../util/logger.js", () => ({
|
|
|
27
27
|
|
|
28
28
|
let writeRelationshipStateCalled = false;
|
|
29
29
|
let sidecarPayload: unknown = null;
|
|
30
|
+
let writeOnboardingSectionPayload: unknown = null;
|
|
31
|
+
|
|
32
|
+
mock.module("../prompts/normalize-onboarding.js", () => ({
|
|
33
|
+
normalizeOnboardingContext: (ctx: unknown) => ctx,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
mock.module("../prompts/persona-resolver.js", () => ({
|
|
37
|
+
writeOnboardingSection: (payload: unknown) => {
|
|
38
|
+
writeOnboardingSectionPayload = payload;
|
|
39
|
+
},
|
|
40
|
+
resolveGuardianPersonaPath: () => join(TEST_DIR, "users", "guardian.md"),
|
|
41
|
+
resolveGuardianPersona: () => null,
|
|
42
|
+
resolveGuardianPersonaStrict: () => null,
|
|
43
|
+
resolveUserPersona: () => null,
|
|
44
|
+
resolveChannelPersona: () => null,
|
|
45
|
+
resolvePersonaContext: () => ({
|
|
46
|
+
userPersona: null,
|
|
47
|
+
userSlug: null,
|
|
48
|
+
channelPersona: null,
|
|
49
|
+
}),
|
|
50
|
+
resolveUserSlug: () => null,
|
|
51
|
+
ensureGuardianPersonaFile: () => {},
|
|
52
|
+
isGuardianPersonaCustomized: () => false,
|
|
53
|
+
GUARDIAN_PERSONA_TEMPLATE: "",
|
|
54
|
+
}));
|
|
30
55
|
|
|
31
56
|
mock.module("../home/relationship-state-writer.js", () => ({
|
|
32
57
|
RELATIONSHIP_STATE_FILENAME: "relationship-state.json",
|
|
@@ -59,13 +84,12 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
59
84
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
60
85
|
writeRelationshipStateCalled = false;
|
|
61
86
|
sidecarPayload = null;
|
|
87
|
+
writeOnboardingSectionPayload = null;
|
|
62
88
|
});
|
|
63
89
|
|
|
64
90
|
afterEach(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (existsSync(p)) rmSync(p, { force: true });
|
|
68
|
-
}
|
|
91
|
+
const p = workspacePath("IDENTITY.md");
|
|
92
|
+
if (existsSync(p)) rmSync(p, { force: true });
|
|
69
93
|
});
|
|
70
94
|
|
|
71
95
|
test("seeds IDENTITY.md with assistant name when file does not exist", () => {
|
|
@@ -80,19 +104,7 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
80
104
|
expect(content).toBe("# Identity\n\n- **Name:** Nova\n");
|
|
81
105
|
});
|
|
82
106
|
|
|
83
|
-
test("seeds
|
|
84
|
-
persistOnboardingArtifacts({
|
|
85
|
-
tools: ["slack"],
|
|
86
|
-
tasks: ["email"],
|
|
87
|
-
tone: "balanced",
|
|
88
|
-
userName: "Alex",
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
92
|
-
expect(content).toBe("# User\n\n- **Name:** Alex\n");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("seeds both IDENTITY.md and USER.md when both names are provided", () => {
|
|
107
|
+
test("seeds IDENTITY.md when both names are provided", () => {
|
|
96
108
|
persistOnboardingArtifacts({
|
|
97
109
|
tools: [],
|
|
98
110
|
tasks: [],
|
|
@@ -104,9 +116,6 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
104
116
|
expect(readFileSync(workspacePath("IDENTITY.md"), "utf-8")).toBe(
|
|
105
117
|
"# Identity\n\n- **Name:** Pax\n",
|
|
106
118
|
);
|
|
107
|
-
expect(readFileSync(workspacePath("USER.md"), "utf-8")).toBe(
|
|
108
|
-
"# User\n\n- **Name:** Alex\n",
|
|
109
|
-
);
|
|
110
119
|
});
|
|
111
120
|
|
|
112
121
|
test("updates Name field in existing IDENTITY.md template", () => {
|
|
@@ -128,23 +137,6 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
128
137
|
);
|
|
129
138
|
});
|
|
130
139
|
|
|
131
|
-
test("updates Name field in existing USER.md template", () => {
|
|
132
|
-
writeFileSync(
|
|
133
|
-
workspacePath("USER.md"),
|
|
134
|
-
"# User\n\n- **Name:** _(not yet chosen)_\n",
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
persistOnboardingArtifacts({
|
|
138
|
-
tools: [],
|
|
139
|
-
tasks: [],
|
|
140
|
-
tone: "casual",
|
|
141
|
-
userName: "NewUser",
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
145
|
-
expect(content).toBe("# User\n\n- **Name:** NewUser\n");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
140
|
test("updates old-format Name field in existing IDENTITY.md", () => {
|
|
149
141
|
writeFileSync(
|
|
150
142
|
workspacePath("IDENTITY.md"),
|
|
@@ -190,17 +182,6 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
190
182
|
expect(existsSync(workspacePath("IDENTITY.md"))).toBe(false);
|
|
191
183
|
});
|
|
192
184
|
|
|
193
|
-
test("skips USER.md when userName is missing", () => {
|
|
194
|
-
persistOnboardingArtifacts({
|
|
195
|
-
tools: ["notion"],
|
|
196
|
-
tasks: ["project-management"],
|
|
197
|
-
tone: "balanced",
|
|
198
|
-
assistantName: "Nova",
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
expect(existsSync(workspacePath("USER.md"))).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
185
|
test("skips IDENTITY.md when assistantName is whitespace-only", () => {
|
|
205
186
|
persistOnboardingArtifacts({
|
|
206
187
|
tools: [],
|
|
@@ -212,32 +193,17 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
212
193
|
expect(existsSync(workspacePath("IDENTITY.md"))).toBe(false);
|
|
213
194
|
});
|
|
214
195
|
|
|
215
|
-
test("
|
|
216
|
-
persistOnboardingArtifacts({
|
|
217
|
-
tools: [],
|
|
218
|
-
tasks: [],
|
|
219
|
-
tone: "balanced",
|
|
220
|
-
userName: " ",
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
expect(existsSync(workspacePath("USER.md"))).toBe(false);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("trims whitespace from names before writing", () => {
|
|
196
|
+
test("trims whitespace from assistantName before writing", () => {
|
|
227
197
|
persistOnboardingArtifacts({
|
|
228
198
|
tools: [],
|
|
229
199
|
tasks: [],
|
|
230
200
|
tone: "balanced",
|
|
231
|
-
userName: " Alex ",
|
|
232
201
|
assistantName: " Nova ",
|
|
233
202
|
});
|
|
234
203
|
|
|
235
204
|
expect(readFileSync(workspacePath("IDENTITY.md"), "utf-8")).toBe(
|
|
236
205
|
"# Identity\n\n- **Name:** Nova\n",
|
|
237
206
|
);
|
|
238
|
-
expect(readFileSync(workspacePath("USER.md"), "utf-8")).toBe(
|
|
239
|
-
"# User\n\n- **Name:** Alex\n",
|
|
240
|
-
);
|
|
241
207
|
});
|
|
242
208
|
|
|
243
209
|
test("passes onboarding payload to writeOnboardingSidecar", () => {
|
|
@@ -263,4 +229,18 @@ describe("persistOnboardingArtifacts", () => {
|
|
|
263
229
|
|
|
264
230
|
expect(writeRelationshipStateCalled).toBe(true);
|
|
265
231
|
});
|
|
232
|
+
|
|
233
|
+
test("calls writeOnboardingSection with normalized data", () => {
|
|
234
|
+
const payload = {
|
|
235
|
+
tools: ["slack", "linear"],
|
|
236
|
+
tasks: ["code-building", "writing"],
|
|
237
|
+
tone: "professional",
|
|
238
|
+
userName: "Alex",
|
|
239
|
+
assistantName: "Nova",
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
persistOnboardingArtifacts(payload);
|
|
243
|
+
|
|
244
|
+
expect(writeOnboardingSectionPayload).toEqual(payload);
|
|
245
|
+
});
|
|
266
246
|
});
|
|
@@ -5,7 +5,6 @@ import { credentialKey } from "../security/credential-key.js";
|
|
|
5
5
|
let mockIsPlatform = true;
|
|
6
6
|
let mockPlatformBaseUrl = "";
|
|
7
7
|
let mockPlatformAssistantId = "";
|
|
8
|
-
let mockPlatformInternalApiKey = "";
|
|
9
8
|
let mockSecureKeys: Record<string, string> = {};
|
|
10
9
|
|
|
11
10
|
mock.module("../config/env-registry.js", () => ({
|
|
@@ -15,7 +14,6 @@ mock.module("../config/env-registry.js", () => ({
|
|
|
15
14
|
mock.module("../config/env.js", () => ({
|
|
16
15
|
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
17
16
|
getPlatformAssistantId: () => mockPlatformAssistantId,
|
|
18
|
-
getPlatformInternalApiKey: () => mockPlatformInternalApiKey,
|
|
19
17
|
}));
|
|
20
18
|
|
|
21
19
|
mock.module("../security/secure-keys.js", () => ({
|
|
@@ -32,6 +30,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
32
30
|
}));
|
|
33
31
|
|
|
34
32
|
const originalFetch = globalThis.fetch;
|
|
33
|
+
const originalEnvCredential = process.env.ASSISTANT_API_KEY;
|
|
35
34
|
|
|
36
35
|
const { registerCallbackRoute, resolvePlatformCallbackRegistrationContext } =
|
|
37
36
|
await import("../inbound/platform-callback-registration.js");
|
|
@@ -41,13 +40,18 @@ describe("platform callback registration", () => {
|
|
|
41
40
|
mockIsPlatform = true;
|
|
42
41
|
mockPlatformBaseUrl = "";
|
|
43
42
|
mockPlatformAssistantId = "";
|
|
44
|
-
mockPlatformInternalApiKey = "";
|
|
45
43
|
mockSecureKeys = {};
|
|
44
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
46
45
|
globalThis.fetch = originalFetch;
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
afterEach(() => {
|
|
50
49
|
globalThis.fetch = originalFetch;
|
|
50
|
+
if (originalEnvCredential === undefined) {
|
|
51
|
+
delete process.env.ASSISTANT_API_KEY;
|
|
52
|
+
} else {
|
|
53
|
+
process.env.ASSISTANT_API_KEY = originalEnvCredential;
|
|
54
|
+
}
|
|
51
55
|
});
|
|
52
56
|
|
|
53
57
|
test("resolves managed callback context from stored credentials", async () => {
|
|
@@ -64,7 +68,6 @@ describe("platform callback registration", () => {
|
|
|
64
68
|
expect(context.isPlatform).toBe(true);
|
|
65
69
|
expect(context.platformBaseUrl).toBe("https://platform.example.com");
|
|
66
70
|
expect(context.assistantId).toBe("11111111-2222-4333-8444-555555555555");
|
|
67
|
-
expect(context.hasInternalApiKey).toBe(false);
|
|
68
71
|
expect(context.hasAssistantApiKey).toBe(true);
|
|
69
72
|
expect(context.authHeader).toBe("Api-Key ast-managed-key");
|
|
70
73
|
});
|
|
@@ -88,6 +91,20 @@ describe("platform callback registration", () => {
|
|
|
88
91
|
expect(context.authHeader).toBe("Api-Key ast-self-hosted-key");
|
|
89
92
|
});
|
|
90
93
|
|
|
94
|
+
test("uses ASSISTANT_API_KEY env fallback when stored credential is missing", async () => {
|
|
95
|
+
process.env.ASSISTANT_API_KEY = "env-key";
|
|
96
|
+
mockPlatformBaseUrl = "https://platform.example.com";
|
|
97
|
+
mockPlatformAssistantId = "33333333-4444-4555-8666-777777777777";
|
|
98
|
+
|
|
99
|
+
const context = await resolvePlatformCallbackRegistrationContext();
|
|
100
|
+
|
|
101
|
+
expect(context.enabled).toBe(true);
|
|
102
|
+
expect(context.platformBaseUrl).toBe("https://platform.example.com");
|
|
103
|
+
expect(context.assistantId).toBe("33333333-4444-4555-8666-777777777777");
|
|
104
|
+
expect(context.hasAssistantApiKey).toBe(true);
|
|
105
|
+
expect(context.authHeader).toBe("Api-Key env-key");
|
|
106
|
+
});
|
|
107
|
+
|
|
91
108
|
test("registerCallbackRoute falls back to assistant API key auth", async () => {
|
|
92
109
|
mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
|
|
93
110
|
"https://platform.example.com";
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getWorkspacePromptPath,
|
|
20
20
|
getWorkspaceSkillsDir,
|
|
21
21
|
getXdgVellumConfigDirName,
|
|
22
|
+
vellumRoot,
|
|
22
23
|
} from "../util/platform.js";
|
|
23
24
|
|
|
24
25
|
const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
@@ -99,7 +100,7 @@ describe("path characterization", () => {
|
|
|
99
100
|
ensureDataDir();
|
|
100
101
|
|
|
101
102
|
// Root-level dirs (ensureDataDir always creates these)
|
|
102
|
-
const root =
|
|
103
|
+
const root = vellumRoot();
|
|
103
104
|
expect(existsSync(root)).toBe(true);
|
|
104
105
|
|
|
105
106
|
// Workspace dirs (in our temp location)
|
|
@@ -411,28 +411,6 @@ describe("compilePlaybooks", () => {
|
|
|
411
411
|
describe("playbook tool edge cases", () => {
|
|
412
412
|
beforeEach(clearPlaybooks);
|
|
413
413
|
|
|
414
|
-
test("create uses memoryScopeId from context when present", async () => {
|
|
415
|
-
const scopedCtx: ToolContext = { ...ctx, memoryScopeId: "custom-scope" };
|
|
416
|
-
const result = await executePlaybookCreate(
|
|
417
|
-
{
|
|
418
|
-
trigger: "scoped trigger",
|
|
419
|
-
action: "scoped action",
|
|
420
|
-
},
|
|
421
|
-
scopedCtx,
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
expect(result.isError).toBe(false);
|
|
425
|
-
expect(result.content).toContain("Playbook created successfully");
|
|
426
|
-
|
|
427
|
-
// Verify it's invisible to default scope
|
|
428
|
-
const defaultList = await executePlaybookList({}, ctx);
|
|
429
|
-
expect(defaultList.content).toContain("No playbooks found");
|
|
430
|
-
|
|
431
|
-
// Visible to custom scope
|
|
432
|
-
const scopedList = await executePlaybookList({}, scopedCtx);
|
|
433
|
-
expect(scopedList.content).toContain("scoped trigger");
|
|
434
|
-
});
|
|
435
|
-
|
|
436
414
|
test("update detects collision with another playbook", async () => {
|
|
437
415
|
const _r1 = await executePlaybookCreate(
|
|
438
416
|
{ trigger: "trigger A", action: "action A" },
|
|
@@ -587,25 +565,4 @@ describe("playbook tool edge cases", () => {
|
|
|
587
565
|
expect(result.content).toContain("Found 1 playbook");
|
|
588
566
|
expect(result.content).toContain("**a**");
|
|
589
567
|
});
|
|
590
|
-
|
|
591
|
-
test("delete is scoped and does not affect other scopes", async () => {
|
|
592
|
-
const scopedCtx: ToolContext = { ...ctx, memoryScopeId: "scope-A" };
|
|
593
|
-
const createResult = await executePlaybookCreate(
|
|
594
|
-
{ trigger: "x", action: "y" },
|
|
595
|
-
scopedCtx,
|
|
596
|
-
);
|
|
597
|
-
const id = createResult.content.match(/ID: (\S+)/)![1];
|
|
598
|
-
|
|
599
|
-
// Try to delete from default scope
|
|
600
|
-
const deleteResult = await executePlaybookDelete({ playbook_id: id }, ctx);
|
|
601
|
-
expect(deleteResult.isError).toBe(true);
|
|
602
|
-
expect(deleteResult.content).toContain("not found");
|
|
603
|
-
|
|
604
|
-
// Delete from correct scope succeeds
|
|
605
|
-
const correctDelete = await executePlaybookDelete(
|
|
606
|
-
{ playbook_id: id },
|
|
607
|
-
scopedCtx,
|
|
608
|
-
);
|
|
609
|
-
expect(correctDelete.isError).toBe(false);
|
|
610
|
-
});
|
|
611
568
|
});
|
|
@@ -236,6 +236,53 @@ describe("registerPluginTools / unregisterPluginTools helpers", () => {
|
|
|
236
236
|
expect(retrieved?.ownerPluginId).toBe("my-plugin");
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
test("registerPluginTools exposes provider-safe aliases for unsafe plugin tool names", async () => {
|
|
240
|
+
const execute = mock(
|
|
241
|
+
async (
|
|
242
|
+
_input: Record<string, unknown>,
|
|
243
|
+
_context: ToolContext,
|
|
244
|
+
): Promise<ToolExecutionResult> => ({ content: "ok", isError: false }),
|
|
245
|
+
);
|
|
246
|
+
const accepted = registerPluginTools("stripe-plugin", [
|
|
247
|
+
makeFakeTool("Stripe Link CLI", { execute }),
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
expect(accepted).toHaveLength(1);
|
|
251
|
+
const alias = accepted[0]!.name;
|
|
252
|
+
expect(alias).toMatch(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
253
|
+
expect(alias.startsWith("Stripe_Link_CLI__")).toBe(true);
|
|
254
|
+
expect(getTool(alias)).toBeDefined();
|
|
255
|
+
expect(accepted[0]!.getDefinition().name).toBe(alias);
|
|
256
|
+
|
|
257
|
+
await accepted[0]!.execute(
|
|
258
|
+
{},
|
|
259
|
+
{
|
|
260
|
+
workingDir: "/tmp",
|
|
261
|
+
conversationId: "conv-1",
|
|
262
|
+
trustClass: "guardian",
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("registerPluginTools keeps edge-whitespace tool names distinct", () => {
|
|
270
|
+
const accepted = registerPluginTools("deploy-plugin", [
|
|
271
|
+
makeFakeTool("deploy"),
|
|
272
|
+
makeFakeTool(" deploy "),
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
expect(accepted).toHaveLength(2);
|
|
276
|
+
const aliases = accepted.map((tool) => tool.name);
|
|
277
|
+
expect(new Set(aliases).size).toBe(2);
|
|
278
|
+
expect(aliases).toContain("deploy");
|
|
279
|
+
|
|
280
|
+
const paddedAlias = aliases.find((name) => name !== "deploy");
|
|
281
|
+
expect(paddedAlias).toMatch(/^deploy__[a-f0-9]{12}$/);
|
|
282
|
+
expect(getTool("deploy")).toBeDefined();
|
|
283
|
+
expect(getTool(paddedAlias!)).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
239
286
|
test("registerPluginTools overwrites any pre-existing ownership metadata", () => {
|
|
240
287
|
// A plugin author could (maliciously or mistakenly) hand in a tool
|
|
241
288
|
// pre-tagged with another skill's or plugin's ID. The helper must
|