@vellumai/assistant 0.11.1 → 0.11.2-staging.2
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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
// case only the dense Qdrant write is deferred so the cache (and the v3 needle
|
|
25
25
|
// lane it feeds) still reflects the current skills. An unexpected error in
|
|
26
26
|
// another step leaves the prior cache intact (skills are best-effort).
|
|
27
|
+
//
|
|
28
|
+
// Always-candidate skills are pinned into the selector pool by catalog
|
|
29
|
+
// declaration rather than by similarity, so their membership and their cards
|
|
30
|
+
// are served from the local catalog before the first seed run completes —
|
|
31
|
+
// `listAlwaysCandidateSkillSlugs` primes that fallback, and the seeded snapshot
|
|
32
|
+
// replaces it as soon as it lands.
|
|
27
33
|
|
|
28
34
|
import { listCatalogSkills, listInstalledSkills } from "@vellumai/plugin-api";
|
|
29
35
|
|
|
@@ -79,6 +85,30 @@ export function skillSlugFor(id: string): string {
|
|
|
79
85
|
* successful re-seed so callers always see a consistent snapshot.
|
|
80
86
|
*/
|
|
81
87
|
let entries: Map<string, SkillEntry> | null = null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Ids of the enabled skills that declared `always-candidate` in their
|
|
91
|
+
* frontmatter, captured from the same enumeration that builds {@link entries}
|
|
92
|
+
* and replaced with it. Reading it here rather than re-enumerating the catalog
|
|
93
|
+
* keeps the injection engines off the per-turn filesystem scan
|
|
94
|
+
* `listInstalledSkills` performs.
|
|
95
|
+
*/
|
|
96
|
+
let alwaysCandidateIds: ReadonlySet<string> = new Set<string>();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pre-seed view of the same enabled-skill cards, read straight from the local
|
|
100
|
+
* catalog by {@link ensureCatalogFallback}.
|
|
101
|
+
*
|
|
102
|
+
* Always-candidate membership is a static catalog fact, so the injection
|
|
103
|
+
* engines must not have to wait on Qdrant plus a configured embedding backend
|
|
104
|
+
* to learn it: on a freshly hatched assistant the first turn arrives before the
|
|
105
|
+
* boot seed finishes, and an empty pin set means the model never sees a pinned
|
|
106
|
+
* skill card. The fallback is built at most once and is dropped the moment a
|
|
107
|
+
* seed run installs the authoritative {@link entries} snapshot.
|
|
108
|
+
*/
|
|
109
|
+
let fallbackEntries: Map<string, SkillEntry> | null = null;
|
|
110
|
+
let fallbackAlwaysCandidateIds: ReadonlySet<string> = new Set<string>();
|
|
111
|
+
let fallbackLoad: Promise<void> | null = null;
|
|
82
112
|
let requestedSeedGeneration = 0;
|
|
83
113
|
let processedSeedGeneration = 0;
|
|
84
114
|
let activeSeedDrain: Promise<void> | null = null;
|
|
@@ -105,11 +135,93 @@ const seedWaiters: Array<{ generation: number; resolve: () => void }> = [];
|
|
|
105
135
|
*/
|
|
106
136
|
let legacyKindBackfillDone = false;
|
|
107
137
|
|
|
138
|
+
interface InstalledSkillCards {
|
|
139
|
+
/** Every locally installed skill id, whatever its resolved state. */
|
|
140
|
+
installedIds: Set<string>;
|
|
141
|
+
/** Rendered capability cards for the enabled skills, in catalog order. */
|
|
142
|
+
cards: SkillEntry[];
|
|
143
|
+
/** The subset of {@link cards} whose skill declared `always-candidate`. */
|
|
144
|
+
alwaysCandidateIds: Set<string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Enumerate the installed skill catalog and render one capability card per
|
|
149
|
+
* enabled skill. Feature-flag gating is resolved host-side: gated skills arrive
|
|
150
|
+
* as `state: "unavailable"` and are dropped by the enabled filter, as are
|
|
151
|
+
* skills the user disabled.
|
|
152
|
+
*
|
|
153
|
+
* The single definition of "which skills are enabled and what does each card
|
|
154
|
+
* say": the seed run embeds these cards and caches them in {@link entries},
|
|
155
|
+
* and {@link ensureCatalogFallback} serves the same cards from the catalog
|
|
156
|
+
* before the first seed completes.
|
|
157
|
+
*/
|
|
158
|
+
async function buildInstalledSkillCards(): Promise<InstalledSkillCards> {
|
|
159
|
+
const installed = await listInstalledSkills();
|
|
160
|
+
const installedIds = new Set<string>(installed.map((s) => s.id));
|
|
161
|
+
const cards: SkillEntry[] = [];
|
|
162
|
+
const alwaysCandidateIds = new Set<string>();
|
|
163
|
+
for (const skill of installed) {
|
|
164
|
+
if (skill.state !== "enabled") {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const augmented = augmentMcpSetupDescription(skill);
|
|
168
|
+
// Always-candidate skills are pinned into the selector pool every turn, so
|
|
169
|
+
// they get a larger budget for a fuller, multi-mode capability statement.
|
|
170
|
+
const content = buildSkillContent(
|
|
171
|
+
augmented,
|
|
172
|
+
skill.alwaysCandidate ? ALWAYS_CANDIDATE_CARD_CHARS : undefined,
|
|
173
|
+
);
|
|
174
|
+
if (skill.alwaysCandidate) {
|
|
175
|
+
alwaysCandidateIds.add(skill.id);
|
|
176
|
+
}
|
|
177
|
+
cards.push({ id: skill.id, content });
|
|
178
|
+
}
|
|
179
|
+
return { installedIds, cards, alwaysCandidateIds };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Populate {@link fallbackEntries} from the local catalog when no seed run has
|
|
184
|
+
* installed {@link entries} yet. A no-op once either cache is present, so the
|
|
185
|
+
* per-turn cost after the first call is a resolved promise rather than the
|
|
186
|
+
* filesystem scan `listInstalledSkills` performs. Concurrent callers share one
|
|
187
|
+
* in-flight build; a failed build is not latched, so the next turn retries.
|
|
188
|
+
*/
|
|
189
|
+
async function ensureCatalogFallback(): Promise<void> {
|
|
190
|
+
if (entries || fallbackEntries) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const pending = (fallbackLoad ??= buildInstalledSkillCards()
|
|
194
|
+
.then(({ cards, alwaysCandidateIds: ids }) => {
|
|
195
|
+
// A seed that landed while this build was in flight is authoritative.
|
|
196
|
+
if (entries) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
fallbackEntries = new Map(cards.map((card) => [card.id, card]));
|
|
200
|
+
fallbackAlwaysCandidateIds = ids;
|
|
201
|
+
})
|
|
202
|
+
.catch((err: unknown) => {
|
|
203
|
+
log.warn(
|
|
204
|
+
{ err },
|
|
205
|
+
"Failed to enumerate the skill catalog for the pre-seed card fallback",
|
|
206
|
+
);
|
|
207
|
+
})
|
|
208
|
+
.finally(() => {
|
|
209
|
+
fallbackLoad = null;
|
|
210
|
+
}));
|
|
211
|
+
await pending;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The authoritative seeded snapshot, or the pre-seed catalog fallback. */
|
|
215
|
+
function readableEntries(): Map<string, SkillEntry> | null {
|
|
216
|
+
return entries ?? fallbackEntries;
|
|
217
|
+
}
|
|
218
|
+
|
|
108
219
|
/**
|
|
109
220
|
* Steps (per run):
|
|
110
221
|
* 1. Enumerate the installed skill catalog with resolved states
|
|
111
|
-
* (`listInstalledSkills`). Feature-flag
|
|
112
|
-
*
|
|
222
|
+
* (`buildInstalledSkillCards`, over `listInstalledSkills`). Feature-flag
|
|
223
|
+
* gating is resolved host-side: gated skills arrive as
|
|
224
|
+
* `state: "unavailable"` and are not seeded.
|
|
113
225
|
* 2. Build a `SkillEntry` per enabled skill, applying the mcp-setup
|
|
114
226
|
* augmentation and the prose-style content render (`buildSkillContent`,
|
|
115
227
|
* capped at 500 chars).
|
|
@@ -176,29 +288,17 @@ function resolveSeedWaiters(): void {
|
|
|
176
288
|
async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
177
289
|
try {
|
|
178
290
|
const config = getConfig();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// the enabled filter.
|
|
191
|
-
const seeds: SkillEntry[] = [];
|
|
192
|
-
for (const skill of enabled) {
|
|
193
|
-
const augmented = augmentMcpSetupDescription(skill);
|
|
194
|
-
// Always-candidate skills are pinned into the selector pool every turn, so
|
|
195
|
-
// they get a larger budget for a fuller, multi-mode capability statement.
|
|
196
|
-
const content = buildSkillContent(
|
|
197
|
-
augmented,
|
|
198
|
-
skill.alwaysCandidate ? ALWAYS_CANDIDATE_CARD_CHARS : undefined,
|
|
199
|
-
);
|
|
200
|
-
seeds.push({ id: skill.id, content });
|
|
201
|
-
}
|
|
291
|
+
|
|
292
|
+
// `installedIds` tracks every locally-installed skill id (regardless of
|
|
293
|
+
// enabled/disabled/unavailable state) so the catalog-seeding loop below
|
|
294
|
+
// treats them all as "installed" and never re-seeds a disabled skill from
|
|
295
|
+
// the remote catalog as if it were uninstalled.
|
|
296
|
+
const {
|
|
297
|
+
installedIds,
|
|
298
|
+
cards,
|
|
299
|
+
alwaysCandidateIds: nextAlwaysCandidateIds,
|
|
300
|
+
} = await buildInstalledSkillCards();
|
|
301
|
+
const seeds: SkillEntry[] = [...cards];
|
|
202
302
|
|
|
203
303
|
// Seed uninstalled catalog skills so their activation hints are
|
|
204
304
|
// discoverable by intent. Track whether the catalog was available so we
|
|
@@ -366,6 +466,11 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
366
466
|
// observes the new skill set (skill entries share the unified concept-page
|
|
367
467
|
// collection and surface in the same index).
|
|
368
468
|
entries = nextEntries;
|
|
469
|
+
alwaysCandidateIds = nextAlwaysCandidateIds;
|
|
470
|
+
// The seeded snapshot supersedes the pre-seed catalog fallback, which also
|
|
471
|
+
// omits the uninstalled catalog skills this run embedded.
|
|
472
|
+
fallbackEntries = null;
|
|
473
|
+
fallbackAlwaysCandidateIds = new Set<string>();
|
|
369
474
|
invalidatePageIndex();
|
|
370
475
|
|
|
371
476
|
// Surface a dense-embed failure to `throwOnError` callers (the managed-
|
|
@@ -380,9 +485,11 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
380
485
|
}
|
|
381
486
|
|
|
382
487
|
/**
|
|
383
|
-
* Synchronous lookup of a
|
|
384
|
-
*
|
|
385
|
-
*
|
|
488
|
+
* Synchronous lookup of a rendered `SkillEntry` by skill id, served from the
|
|
489
|
+
* seeded cache or — before the first seed completes — from the pre-seed
|
|
490
|
+
* catalog fallback {@link listAlwaysCandidateSkillSlugs} primes. Returns `null`
|
|
491
|
+
* when neither is populated, when the id is unknown, or when a prior seed run
|
|
492
|
+
* dropped the id (e.g. the skill was disabled).
|
|
386
493
|
*
|
|
387
494
|
* Accepts either a bare skill id (`example-skill`) or its unified-collection
|
|
388
495
|
* slug (`skills/example-skill`) so render-side callers can pass through what
|
|
@@ -395,7 +502,7 @@ export function getSkillCapability(idOrSlug: string): SkillEntry | null {
|
|
|
395
502
|
const id = idOrSlug.startsWith(SKILL_SLUG_PREFIX)
|
|
396
503
|
? idOrSlug.slice(SKILL_SLUG_PREFIX.length)
|
|
397
504
|
: idOrSlug;
|
|
398
|
-
const entry =
|
|
505
|
+
const entry = readableEntries()?.get(id);
|
|
399
506
|
return entry ? Object.freeze({ ...entry }) : null;
|
|
400
507
|
}
|
|
401
508
|
|
|
@@ -423,9 +530,40 @@ export function listSkillEntries(): SkillEntry[] {
|
|
|
423
530
|
.map((entry) => Object.freeze({ ...entry }));
|
|
424
531
|
}
|
|
425
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Slugs of the enabled skills marked `always-candidate`, restricted to those
|
|
535
|
+
* whose card is renderable.
|
|
536
|
+
*
|
|
537
|
+
* Retrieval scores a skill by how closely the turn resembles its card, so a
|
|
538
|
+
* cross-cutting capability the user never names by hand never wins a candidate
|
|
539
|
+
* slot. These skills opt out of that: whether they apply is a judgment for the
|
|
540
|
+
* model, so the injection engines pin their cards instead of waiting for a
|
|
541
|
+
* similarity hit.
|
|
542
|
+
*
|
|
543
|
+
* Async because membership is a static catalog fact that must not depend on the
|
|
544
|
+
* embedding store: before the first seed run completes this reads the catalog
|
|
545
|
+
* directly (see {@link ensureCatalogFallback}), which also makes each pinned
|
|
546
|
+
* slug's card resolvable through {@link getSkillCapability} so the render path
|
|
547
|
+
* emits it instead of silently dropping the pin. Once a seed run lands, its
|
|
548
|
+
* snapshot is authoritative.
|
|
549
|
+
*/
|
|
550
|
+
export async function listAlwaysCandidateSkillSlugs(): Promise<string[]> {
|
|
551
|
+
await ensureCatalogFallback();
|
|
552
|
+
const available = readableEntries();
|
|
553
|
+
const ids = entries ? alwaysCandidateIds : fallbackAlwaysCandidateIds;
|
|
554
|
+
return [...ids]
|
|
555
|
+
.filter((id) => available?.has(id) === true)
|
|
556
|
+
.sort()
|
|
557
|
+
.map((id) => skillSlugFor(id));
|
|
558
|
+
}
|
|
559
|
+
|
|
426
560
|
/** @internal Test-only: clear the module-level cache. */
|
|
427
561
|
export function _resetSkillStoreForTests(): void {
|
|
428
562
|
entries = null;
|
|
563
|
+
alwaysCandidateIds = new Set<string>();
|
|
564
|
+
fallbackEntries = null;
|
|
565
|
+
fallbackAlwaysCandidateIds = new Set<string>();
|
|
566
|
+
fallbackLoad = null;
|
|
429
567
|
requestedSeedGeneration = 0;
|
|
430
568
|
processedSeedGeneration = 0;
|
|
431
569
|
activeSeedDrain = null;
|
|
@@ -129,6 +129,8 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
129
129
|
const skillState = {
|
|
130
130
|
/** id → SkillEntry consulted by `getSkillCapability`. */
|
|
131
131
|
entries: new Map<string, SkillEntry>(),
|
|
132
|
+
/** Skill ids the store reports as `always-candidate`. */
|
|
133
|
+
alwaysCandidateIds: new Set<string>(),
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
mock.module("../../substrate/skill-store.js", () => ({
|
|
@@ -146,6 +148,11 @@ mock.module("../../substrate/skill-store.js", () => ({
|
|
|
146
148
|
// time. Tests stage skill content via `skillState.entries`; expose them
|
|
147
149
|
// here so the page-index loader sees a consistent view.
|
|
148
150
|
listSkillEntries: () => Array.from(skillState.entries.values()),
|
|
151
|
+
listAlwaysCandidateSkillSlugs: () =>
|
|
152
|
+
[...skillState.alwaysCandidateIds]
|
|
153
|
+
.filter((id) => skillState.entries.has(id))
|
|
154
|
+
.sort()
|
|
155
|
+
.map((id) => `skills/${id}`),
|
|
149
156
|
}));
|
|
150
157
|
|
|
151
158
|
// ---------------------------------------------------------------------------
|
|
@@ -527,6 +534,7 @@ function resetState(): void {
|
|
|
527
534
|
state.queryResponses.dense.length = 0;
|
|
528
535
|
state.queryResponses.sparse.length = 0;
|
|
529
536
|
skillState.entries.clear();
|
|
537
|
+
skillState.alwaysCandidateIds.clear();
|
|
530
538
|
cliCommandState.entries.clear();
|
|
531
539
|
telemetryState.recordCalls.length = 0;
|
|
532
540
|
telemetryState.recordShouldThrow = false;
|
|
@@ -547,6 +555,13 @@ function stageSkills(entries: SkillEntry[]): void {
|
|
|
547
555
|
}
|
|
548
556
|
}
|
|
549
557
|
|
|
558
|
+
/** Mark staged skill ids as `always-candidate` in the mocked store. */
|
|
559
|
+
function stageAlwaysCandidates(ids: string[]): void {
|
|
560
|
+
for (const id of ids) {
|
|
561
|
+
skillState.alwaysCandidateIds.add(id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
550
565
|
/** Stage cli-command-store cache entries for the upcoming render. */
|
|
551
566
|
function stageCliCommands(entries: CliCommandEntryStub[]): void {
|
|
552
567
|
for (const entry of entries) {
|
|
@@ -980,6 +995,122 @@ describe("injectMemoryV2Block", () => {
|
|
|
980
995
|
expect(skillIdx).toBeGreaterThan(headerIdx);
|
|
981
996
|
});
|
|
982
997
|
|
|
998
|
+
// ---------------------------------------------------------------------------
|
|
999
|
+
// Always-candidate skills
|
|
1000
|
+
// ---------------------------------------------------------------------------
|
|
1001
|
+
|
|
1002
|
+
test("pins an always-candidate skill card that retrieval never surfaced", async () => {
|
|
1003
|
+
stageTurn([]);
|
|
1004
|
+
stageSkills([
|
|
1005
|
+
{
|
|
1006
|
+
id: "visualize",
|
|
1007
|
+
content:
|
|
1008
|
+
'The "Visualize" skill (visualize) is available. Render a polished visual inline.',
|
|
1009
|
+
},
|
|
1010
|
+
]);
|
|
1011
|
+
stageAlwaysCandidates(["visualize"]);
|
|
1012
|
+
|
|
1013
|
+
const result = await injectMemoryV2Block({
|
|
1014
|
+
conversationId: "conv-pin-1",
|
|
1015
|
+
currentTurn: 1,
|
|
1016
|
+
recentTurnPairs: [
|
|
1017
|
+
{ assistantMessage: "", userMessage: "How does DNS work?" },
|
|
1018
|
+
],
|
|
1019
|
+
nowText: "Now",
|
|
1020
|
+
messageId: "msg-1",
|
|
1021
|
+
config: makeConfig(),
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
expect(result.toInject).toEqual(["skills/visualize"]);
|
|
1025
|
+
expect(result.block).toContain("### Skills You Can Use");
|
|
1026
|
+
expect(result.block).toContain(
|
|
1027
|
+
'The "Visualize" skill (visualize) is available. Render a polished visual inline. → use skill_load to activate',
|
|
1028
|
+
);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test("does not double-list a pinned skill retrieval also picked", async () => {
|
|
1032
|
+
stageTurn([{ slug: "skills/visualize", denseScore: 0.9 }]);
|
|
1033
|
+
stageSkills([
|
|
1034
|
+
{
|
|
1035
|
+
id: "visualize",
|
|
1036
|
+
content:
|
|
1037
|
+
'The "Visualize" skill (visualize) is available. Render a polished visual inline.',
|
|
1038
|
+
},
|
|
1039
|
+
]);
|
|
1040
|
+
stageAlwaysCandidates(["visualize"]);
|
|
1041
|
+
|
|
1042
|
+
const result = await injectMemoryV2Block({
|
|
1043
|
+
conversationId: "conv-pin-2",
|
|
1044
|
+
currentTurn: 1,
|
|
1045
|
+
recentTurnPairs: [
|
|
1046
|
+
{ assistantMessage: "", userMessage: "Draw me a diagram" },
|
|
1047
|
+
],
|
|
1048
|
+
nowText: "Now",
|
|
1049
|
+
messageId: "msg-1",
|
|
1050
|
+
config: makeConfig(),
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
expect(result.toInject).toEqual(["skills/visualize"]);
|
|
1054
|
+
const first = result.block!.indexOf('The "Visualize" skill');
|
|
1055
|
+
expect(first).toBeGreaterThan(-1);
|
|
1056
|
+
expect(result.block!.indexOf('The "Visualize" skill', first + 1)).toBe(-1);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
test("re-attaches a pinned skill only once across turns", async () => {
|
|
1060
|
+
stageTurn([]);
|
|
1061
|
+
stageSkills([
|
|
1062
|
+
{
|
|
1063
|
+
id: "visualize",
|
|
1064
|
+
content:
|
|
1065
|
+
'The "Visualize" skill (visualize) is available. Render a polished visual inline.',
|
|
1066
|
+
},
|
|
1067
|
+
]);
|
|
1068
|
+
stageAlwaysCandidates(["visualize"]);
|
|
1069
|
+
|
|
1070
|
+
const params = {
|
|
1071
|
+
conversationId: "conv-pin-3",
|
|
1072
|
+
recentTurnPairs: [
|
|
1073
|
+
{ assistantMessage: "", userMessage: "How does DNS work?" },
|
|
1074
|
+
],
|
|
1075
|
+
nowText: "Now",
|
|
1076
|
+
config: makeConfig(),
|
|
1077
|
+
};
|
|
1078
|
+
const first = await injectMemoryV2Block({
|
|
1079
|
+
...params,
|
|
1080
|
+
currentTurn: 1,
|
|
1081
|
+
messageId: "msg-1",
|
|
1082
|
+
});
|
|
1083
|
+
expect(first.toInject).toEqual(["skills/visualize"]);
|
|
1084
|
+
|
|
1085
|
+
stageTurn([]);
|
|
1086
|
+
const second = await injectMemoryV2Block({
|
|
1087
|
+
...params,
|
|
1088
|
+
currentTurn: 2,
|
|
1089
|
+
messageId: "msg-2",
|
|
1090
|
+
});
|
|
1091
|
+
expect(second.toInject).toEqual([]);
|
|
1092
|
+
expect(second.block).toBeNull();
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
test("does not pin an always-candidate skill missing from the store cache", async () => {
|
|
1096
|
+
stageTurn([]);
|
|
1097
|
+
stageAlwaysCandidates(["visualize"]);
|
|
1098
|
+
|
|
1099
|
+
const result = await injectMemoryV2Block({
|
|
1100
|
+
conversationId: "conv-pin-4",
|
|
1101
|
+
currentTurn: 1,
|
|
1102
|
+
recentTurnPairs: [
|
|
1103
|
+
{ assistantMessage: "", userMessage: "How does DNS work?" },
|
|
1104
|
+
],
|
|
1105
|
+
nowText: "Now",
|
|
1106
|
+
messageId: "msg-1",
|
|
1107
|
+
config: makeConfig(),
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
expect(result.toInject).toEqual([]);
|
|
1111
|
+
expect(result.block).toBeNull();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
983
1114
|
test("renders concept-page sections before the skills subsection in mixed blocks", async () => {
|
|
984
1115
|
// Concept page hit AND a skill — concept-page sections come first, then
|
|
985
1116
|
// the skills subsection.
|
|
@@ -73,6 +73,7 @@ mock.module("../../../../../util/logger.js", () => ({
|
|
|
73
73
|
mock.module("../../substrate/skill-store.js", () => ({
|
|
74
74
|
SKILL_SLUG_PREFIX: "skills/",
|
|
75
75
|
listSkillEntries: () => skillState.entries,
|
|
76
|
+
listAlwaysCandidateSkillSlugs: () => [],
|
|
76
77
|
}));
|
|
77
78
|
|
|
78
79
|
// Stub `computeInjectionScores` so tier-2 tests can dictate scores
|
|
@@ -57,6 +57,9 @@ export interface MemoryV2ConceptRowRecord {
|
|
|
57
57
|
* A single-batch (no-tier carve-out) workspace produces `tier3:0`.
|
|
58
58
|
* The bucket index lets inspector queries attribute selections to
|
|
59
59
|
* specific hash-bucketed parallel calls.
|
|
60
|
+
* - `always_candidate` — a skill pinned by its `always-candidate`
|
|
61
|
+
* frontmatter rather than selected by activation or the router. Like
|
|
62
|
+
* the router-mode tags it carries zeroed activation values.
|
|
60
63
|
* - `carry_over` — router-mode row representing a slug carried over
|
|
61
64
|
* from `priorEverInjected` that the router did NOT re-pick on this
|
|
62
65
|
* turn. The cached attachment from a prior turn is still present
|
|
@@ -72,6 +75,7 @@ export interface MemoryV2ConceptRowRecord {
|
|
|
72
75
|
| "ann_top50"
|
|
73
76
|
| "both"
|
|
74
77
|
| "router"
|
|
78
|
+
| "always_candidate"
|
|
75
79
|
| "carry_over"
|
|
76
80
|
| "tier1"
|
|
77
81
|
| "tier2"
|
|
@@ -33,7 +33,11 @@ import {
|
|
|
33
33
|
import { getEdgeIndex } from "../substrate/edge-index.js";
|
|
34
34
|
import { getPageIndex } from "../substrate/page-index.js";
|
|
35
35
|
import { readPage, renderPageContent } from "../substrate/page-store.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
getSkillCapability,
|
|
38
|
+
isSkillSlug,
|
|
39
|
+
listAlwaysCandidateSkillSlugs,
|
|
40
|
+
} from "../substrate/skill-store.js";
|
|
37
41
|
import { spreadActivation } from "../substrate/spread.js";
|
|
38
42
|
import type { ActivationState, EverInjectedEntry } from "../substrate/types.js";
|
|
39
43
|
import {
|
|
@@ -316,6 +320,28 @@ export async function injectMemoryV2Block(
|
|
|
316
320
|
* which this helper overwrites. `nextStateMap` is the activation
|
|
317
321
|
* pipeline's sparse next-state; router-mode callers pass an empty map.
|
|
318
322
|
*/
|
|
323
|
+
/**
|
|
324
|
+
* Telemetry row for a pinned always-candidate skill. Activation never scored
|
|
325
|
+
* it, so every score field is zero, matching the router-mode rows.
|
|
326
|
+
*/
|
|
327
|
+
function makePinnedSkillRow(slug: string): MemoryV2ConceptRowRecord {
|
|
328
|
+
return {
|
|
329
|
+
slug,
|
|
330
|
+
finalActivation: 0,
|
|
331
|
+
ownActivation: 0,
|
|
332
|
+
priorActivation: 0,
|
|
333
|
+
simUser: 0,
|
|
334
|
+
simAssistant: 0,
|
|
335
|
+
simNow: 0,
|
|
336
|
+
simUserRerankBoost: 0,
|
|
337
|
+
simAssistantRerankBoost: 0,
|
|
338
|
+
inRerankPool: false,
|
|
339
|
+
spreadContribution: 0,
|
|
340
|
+
source: "always_candidate",
|
|
341
|
+
status: "not_injected",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
319
345
|
async function finalizeInjection(args: {
|
|
320
346
|
workspaceDir: string;
|
|
321
347
|
conversationId: string;
|
|
@@ -342,12 +368,32 @@ async function finalizeInjection(args: {
|
|
|
342
368
|
currentTurn,
|
|
343
369
|
messageId,
|
|
344
370
|
priorEverInjected,
|
|
345
|
-
slugsToRender,
|
|
371
|
+
slugsToRender: selectedSlugs,
|
|
346
372
|
telemetryRows,
|
|
347
373
|
config,
|
|
348
374
|
nextStateMap,
|
|
349
375
|
} = args;
|
|
350
376
|
|
|
377
|
+
const everInjectedSet = new Set(priorEverInjected.map((entry) => entry.slug));
|
|
378
|
+
|
|
379
|
+
// Skills marked `always-candidate` are pinned rather than retrieved: their
|
|
380
|
+
// relevance is a judgment the model makes each turn, not a similarity the
|
|
381
|
+
// embedding finds, so activation and the router both leave them out. Attach
|
|
382
|
+
// them alongside whatever this turn selected, skipping any already carried by
|
|
383
|
+
// the cached prefix — v2 is append-only, so a card attached once stays
|
|
384
|
+
// visible until compaction evicts the turn and re-opens the slug.
|
|
385
|
+
const selectedSet = new Set(selectedSlugs);
|
|
386
|
+
const pinnedSlugs = (await listAlwaysCandidateSkillSlugs()).filter(
|
|
387
|
+
(slug) => !selectedSet.has(slug) && !everInjectedSet.has(slug),
|
|
388
|
+
);
|
|
389
|
+
const slugsToRender = [...selectedSlugs, ...pinnedSlugs];
|
|
390
|
+
const telemetrySlugSet = new Set(telemetryRows.map((row) => row.slug));
|
|
391
|
+
for (const slug of pinnedSlugs) {
|
|
392
|
+
if (!telemetrySlugSet.has(slug)) {
|
|
393
|
+
telemetryRows.push(makePinnedSkillRow(slug));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
351
397
|
// `mode` is `let` because the trailing try/finally promotes it to "errored"
|
|
352
398
|
// when the render/telemetry path throws — we still want a log row written
|
|
353
399
|
// (with whatever rows we managed to build) so silent failures are
|
|
@@ -377,7 +423,6 @@ async function finalizeInjection(args: {
|
|
|
377
423
|
(isCliCommandSlug(slug) && !getCliCommandCapability(slug)),
|
|
378
424
|
),
|
|
379
425
|
);
|
|
380
|
-
const everInjectedSet = new Set(priorEverInjected.map((entry) => entry.slug));
|
|
381
426
|
const newlyInjected = slugsToRender.filter(
|
|
382
427
|
(slug) => !everInjectedSet.has(slug) && !missingSyntheticSlugs.has(slug),
|
|
383
428
|
);
|
|
@@ -3,7 +3,11 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
|
|
4
4
|
import type { RerankDtype } from "../../../../config/schemas/memory-v2.js";
|
|
5
5
|
import { EmbeddingRuntimeManager } from "../../../../persistence/embeddings/embedding-runtime-manager.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
PromiseGuard,
|
|
8
|
+
workerComputeEnv,
|
|
9
|
+
workerMemoryEnv,
|
|
10
|
+
} from "../host-utils.js";
|
|
7
11
|
import { getLogger } from "../logging.js";
|
|
8
12
|
import { getEmbeddingModelsDir } from "../paths.js";
|
|
9
13
|
|
|
@@ -160,7 +164,7 @@ export class LocalRerankBackend {
|
|
|
160
164
|
modelCacheDir,
|
|
161
165
|
this.dtype,
|
|
162
166
|
],
|
|
163
|
-
env: workerMemoryEnv(),
|
|
167
|
+
env: { ...workerMemoryEnv(), ...workerComputeEnv() },
|
|
164
168
|
stdin: "pipe",
|
|
165
169
|
stdout: "pipe",
|
|
166
170
|
stderr: "pipe",
|
|
@@ -418,7 +418,7 @@ describe("selectPool — infrastructure failures throw", () => {
|
|
|
418
418
|
source: "memory_v3",
|
|
419
419
|
code: "PROVIDER_BILLING",
|
|
420
420
|
userMessage:
|
|
421
|
-
"You'
|
|
421
|
+
"You're out of credits. Add credits in Settings → Billing to continue.",
|
|
422
422
|
errorCategory: "credits_exhausted",
|
|
423
423
|
});
|
|
424
424
|
});
|
|
@@ -182,7 +182,7 @@ export const POST = async (request: Request): Promise<Response> => {
|
|
|
182
182
|
// Drafting a short email is latency-bound, not depth-bound, so run it on
|
|
183
183
|
// the fast model rather than the balanced main-agent default. `inference`
|
|
184
184
|
// is the general-purpose call site plugins use for exactly this — it
|
|
185
|
-
// resolves to the
|
|
185
|
+
// resolves to the Cost (cost-optimized) profile, so we reuse it instead
|
|
186
186
|
// of teaching the daemon about a reengagement-specific call site.
|
|
187
187
|
callSite: "inference",
|
|
188
188
|
signal: request.signal,
|
|
@@ -35,6 +35,7 @@ const unifiedTurnContextInjector: Injector = {
|
|
|
35
35
|
timestamp,
|
|
36
36
|
interfaceName: ctx.interfaceName,
|
|
37
37
|
clientOs: ctx.clientOs,
|
|
38
|
+
visibleApp: ctx.visibleApp,
|
|
38
39
|
channelName: ctx.channelName,
|
|
39
40
|
actorContext: ctx.actorContext,
|
|
40
41
|
configuredUserTimezone: ctx.configuredUserTimezone,
|
|
@@ -19,6 +19,20 @@ export interface UnifiedTurnContextOptions {
|
|
|
19
19
|
timestamp: string;
|
|
20
20
|
interfaceName?: string;
|
|
21
21
|
clientOs?: string;
|
|
22
|
+
/**
|
|
23
|
+
* App the user currently has open on screen. Rendered as the `visible_app:`
|
|
24
|
+
* line so unqualified references to "the app" resolve without a lookup.
|
|
25
|
+
*/
|
|
26
|
+
visibleApp?: {
|
|
27
|
+
appId: string;
|
|
28
|
+
/** Opaque for workspace apps, so the readable handle travels alongside. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Directory stem: the handle a human would recognize the app by. */
|
|
31
|
+
slug: string;
|
|
32
|
+
sourceDir: string;
|
|
33
|
+
/** Set when the app is bundled by an installed plugin rather than built here. */
|
|
34
|
+
pluginName?: string;
|
|
35
|
+
} | null;
|
|
22
36
|
channelName?: string;
|
|
23
37
|
actorContext?: InboundActorContext | null;
|
|
24
38
|
configuredUserTimezone?: string | null;
|
|
@@ -104,6 +118,18 @@ export function buildUnifiedTurnContextBlock(
|
|
|
104
118
|
if (options.clientOs) {
|
|
105
119
|
lines.push(`client_os: ${options.clientOs}`);
|
|
106
120
|
}
|
|
121
|
+
if (options.visibleApp) {
|
|
122
|
+
const app = options.visibleApp;
|
|
123
|
+
// Plugin-bundled apps are owned by their plugin: their source is replaced
|
|
124
|
+
// on plugin update, so the model is told where it came from rather than
|
|
125
|
+
// treating it as an ordinary sandbox app to rewrite.
|
|
126
|
+
const provenance = app.pluginName
|
|
127
|
+
? ` It is bundled by the "${sanitizeInlineContextValue(app.pluginName)}" plugin, not built in the sandbox.`
|
|
128
|
+
: "";
|
|
129
|
+
lines.push(
|
|
130
|
+
`visible_app: "${sanitizeInlineContextValue(app.name)}" (app_id: "${sanitizeInlineContextValue(app.appId)}", slug: "${sanitizeInlineContextValue(app.slug)}", source: "${sanitizeInlineContextValue(app.sourceDir)}"). The user has this app open on screen; unqualified references to "the app" mean this one. The app tools take the app_id.${provenance}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
107
133
|
|
|
108
134
|
// Actor identity and trust fields — only for non-guardian turns.
|
|
109
135
|
if (options.actorContext) {
|
package/src/plugins/types.ts
CHANGED
|
@@ -153,6 +153,23 @@ export interface TurnContext {
|
|
|
153
153
|
* transport interface.
|
|
154
154
|
*/
|
|
155
155
|
readonly clientOs?: string;
|
|
156
|
+
/**
|
|
157
|
+
* The app the client has open on screen, resolved from the id the client
|
|
158
|
+
* reports with each message. Rendered as the `visible_app:` line so the
|
|
159
|
+
* assistant can resolve "the app" to what the user is looking at instead of
|
|
160
|
+
* guessing or asking. Absent when no app is in view.
|
|
161
|
+
*/
|
|
162
|
+
readonly visibleApp?: {
|
|
163
|
+
/** Canonical id the app tools key off (an opaque UUID for workspace apps). */
|
|
164
|
+
appId: string;
|
|
165
|
+
name: string;
|
|
166
|
+
/** Directory stem: the readable handle for an otherwise opaque id. */
|
|
167
|
+
slug: string;
|
|
168
|
+
/** Absolute path of the app's source directory. */
|
|
169
|
+
sourceDir: string;
|
|
170
|
+
/** Owning plugin, when the app is plugin-bundled rather than sandbox-built. */
|
|
171
|
+
pluginName?: string;
|
|
172
|
+
} | null;
|
|
156
173
|
/** Channel label gating response-discretion guidance in `<turn_context>`. */
|
|
157
174
|
readonly channelName?: string;
|
|
158
175
|
/**
|
|
@@ -341,9 +341,9 @@ To share a workspace file, use a markdown link with the \`vellum://\` scheme:
|
|
|
341
341
|
|
|
342
342
|
The path after \`workspace/\` is relative to your working directory. The file renders as a downloadable attachment. For host filesystem files, use \`vellum://host/absolute/path\`.
|
|
343
343
|
|
|
344
|
-
Use the same link form
|
|
344
|
+
Use the same link form to reference a file you are discussing: in the app, the link lets the user open or download it.
|
|
345
345
|
|
|
346
|
-
Embed images
|
|
346
|
+
Embed images, audio, and video inline with \`\`. Reference every other file type, including PDFs and documents, as a plain link: \`[name](vellum://workspace/path)\`.
|
|
347
347
|
`,
|
|
348
348
|
},
|
|
349
349
|
{
|