@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae
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 +6 -6
- package/bun.lock +2 -2
- package/docs/activation-funnel-telemetry.md +310 -0
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +263 -121
- package/package.json +2 -2
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +6 -5
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +146 -0
- package/src/__tests__/compaction-events.test.ts +7 -7
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +265 -125
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
- package/src/__tests__/conversation-agent-loop.test.ts +342 -623
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +6 -1
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
- package/src/__tests__/conversation-process-callsite.test.ts +15 -15
- package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
- package/src/__tests__/conversation-queue.test.ts +10 -10
- package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +7 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
- package/src/__tests__/conversation-speed-override.test.ts +11 -10
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
- 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 +24 -21
- package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +17 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +5 -1
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- 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-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +91 -81
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +5 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +188 -28
- package/src/acp/resolve-agent.ts +119 -42
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +403 -251
- package/src/api/responses/conversation-message.ts +22 -2
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/approvals/guardian-request-resolvers.ts +27 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
- package/src/cli/lib/install-from-github.ts +797 -60
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +416 -0
- package/src/cli/lib/plugin-marketplace.ts +215 -0
- package/src/cli/lib/search-plugins.ts +201 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +9 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +80 -41
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -3
- package/src/config/feature-flag-registry.json +10 -16
- 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 -21
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +17 -3
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/compactor.ts +22 -4
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/context/strip-injections.ts +8 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
- package/src/daemon/conversation-agent-loop.ts +308 -1297
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +22 -141
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +361 -393
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +165 -11
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +444 -62
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +36 -46
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/server.ts +2 -0
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +6 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -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 +3 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/acp.ts +4 -0
- 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 -63
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +9 -2
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +40 -11
- package/src/plugin-api/types.ts +108 -0
- package/src/plugins/defaults/compaction/compact.ts +106 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +156 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
- 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/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
- 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 +91 -0
- 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/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
- 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 +592 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
- 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/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
- 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 +156 -0
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
- package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/external-api.ts +12 -2
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
- 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/model-catalog.ts +34 -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/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +185 -32
- package/src/runtime/routes/acp-routes.ts +328 -30
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +30 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +65 -305
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/services/published-app-updater.ts +30 -8
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- 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/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +9 -19
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +173 -202
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +32 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/memory/v3/__tests__/assign.test.ts +0 -242
- package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
- package/src/memory/v3/__tests__/core.test.ts +0 -39
- package/src/memory/v3/__tests__/health.test.ts +0 -219
- package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
- package/src/memory/v3/__tests__/needle.test.ts +0 -107
- package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
- package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
- package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
- package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
- package/src/memory/v3/__tests__/router.test.ts +0 -332
- package/src/memory/v3/__tests__/selector.test.ts +0 -470
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
- package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
- package/src/memory/v3/__tests__/tree.test.ts +0 -192
- package/src/memory/v3/assign.ts +0 -268
- package/src/memory/v3/capabilities.ts +0 -124
- package/src/memory/v3/core.ts +0 -26
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/maintain-job.ts +0 -314
- package/src/memory/v3/needle.ts +0 -115
- package/src/memory/v3/orchestrate.ts +0 -126
- package/src/memory/v3/page-content.ts +0 -34
- package/src/memory/v3/provider-blocks.ts +0 -26
- package/src/memory/v3/reconcile.ts +0 -523
- package/src/memory/v3/router.ts +0 -190
- package/src/memory/v3/selection-log-store.ts +0 -84
- package/src/memory/v3/selector.ts +0 -226
- package/src/memory/v3/shadow-plugin.ts +0 -411
- package/src/memory/v3/snapshot.ts +0 -209
- package/src/memory/v3/tree.ts +0 -174
- package/src/memory/v3/types.ts +0 -59
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- package/src/util/map-limit.ts +0 -27
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Memory v3 — section-grain dense Qdrant collection
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Owns a dedicated Qdrant collection holding one dense embedding per page
|
|
6
|
+
// *section* (the lead block plus each `## `-delimited heading section, chunked
|
|
7
|
+
// to fit the embedding window — see `sections.ts`). This is the dense lane for
|
|
8
|
+
// the section-grain retrieval design: where the v2 `memory_v2_concept_pages`
|
|
9
|
+
// collection embeds whole pages, this one embeds sub-page sections so a query
|
|
10
|
+
// can match the single relevant block of a long article.
|
|
11
|
+
//
|
|
12
|
+
// Reuses the v2 Qdrant URL/config (`resolveQdrantUrl` + `config.memory.qdrant.*`)
|
|
13
|
+
// and the shared embedding backend (`embedWithBackend`) rather than standing up
|
|
14
|
+
// new infrastructure. The collection carries a single dense named vector sized
|
|
15
|
+
// to the configured embedding backend (`config.memory.qdrant.vectorSize`) — no
|
|
16
|
+
// sparse channel, since the section dense lane is dense-only.
|
|
17
|
+
//
|
|
18
|
+
// This module owns the write side (collection lifecycle + upserts) and the
|
|
19
|
+
// shared Qdrant client; the read side (`dense.ts`) reuses that client to query
|
|
20
|
+
// this collection.
|
|
21
|
+
|
|
22
|
+
import { QdrantClient as QdrantRestClient } from "@qdrant/js-client-rest";
|
|
23
|
+
import { v5 as uuidv5 } from "uuid";
|
|
24
|
+
|
|
25
|
+
import type { AssistantConfig } from "../../../config/types.js";
|
|
26
|
+
import { embedWithBackend } from "../../../memory/embedding-backend.js";
|
|
27
|
+
import { resolveQdrantUrl } from "../../../memory/qdrant-client.js";
|
|
28
|
+
import { getLogger } from "../../../util/logger.js";
|
|
29
|
+
import type { Section } from "./types.js";
|
|
30
|
+
|
|
31
|
+
const log = getLogger("memory-v3-section-dense-store");
|
|
32
|
+
|
|
33
|
+
/** Name of the dedicated Qdrant collection holding section-grain embeddings. */
|
|
34
|
+
export const SECTION_COLLECTION = "memory_v3_sections";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stable UUIDv5 namespace used to derive a deterministic Qdrant point ID from a
|
|
38
|
+
* section's `(article, ordinal)` pair. The namespace itself is an arbitrary
|
|
39
|
+
* fixed UUID; what matters is that the same section always maps to the same
|
|
40
|
+
* point ID so re-upserts replace the prior point in place instead of
|
|
41
|
+
* accumulating duplicates.
|
|
42
|
+
*/
|
|
43
|
+
const SECTION_NAMESPACE = "1d2c3b4a-5e6f-4a7b-8c9d-0e1f2a3b4c5d";
|
|
44
|
+
|
|
45
|
+
let _client: QdrantRestClient | null = null;
|
|
46
|
+
let _collectionReady = false;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Lazily create the shared Qdrant REST client bound to the resolved URL.
|
|
50
|
+
* Shared by both section-dense lanes (this store and the dense read lane in
|
|
51
|
+
* `dense.ts`) so they reuse one client instance and one reset hook.
|
|
52
|
+
*/
|
|
53
|
+
export function getSectionDenseClient(
|
|
54
|
+
config: AssistantConfig,
|
|
55
|
+
): QdrantRestClient {
|
|
56
|
+
if (_client) return _client;
|
|
57
|
+
_client = new QdrantRestClient({
|
|
58
|
+
url: resolveQdrantUrl(config),
|
|
59
|
+
checkCompatibility: false,
|
|
60
|
+
});
|
|
61
|
+
return _client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Derive the deterministic Qdrant point ID for a section. Qdrant requires
|
|
66
|
+
* UUID/integer IDs; UUIDv5 over `${article}#${ordinal}` keeps the mapping
|
|
67
|
+
* stable across processes so upserts replace in place.
|
|
68
|
+
*/
|
|
69
|
+
function pointIdForSection(article: string, ordinal: number): string {
|
|
70
|
+
return uuidv5(`${article}#${ordinal}`, SECTION_NAMESPACE);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create the section-grain collection if it does not already exist, with a
|
|
75
|
+
* single dense vector sized to the configured embedding backend. Idempotent:
|
|
76
|
+
* an already-ready collection is a no-op, and a concurrent 409-on-create is
|
|
77
|
+
* treated as success.
|
|
78
|
+
*/
|
|
79
|
+
export async function ensureSectionCollection(
|
|
80
|
+
config: AssistantConfig,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (_collectionReady) return;
|
|
83
|
+
|
|
84
|
+
const client = getSectionDenseClient(config);
|
|
85
|
+
const vectorSize = config.memory.qdrant.vectorSize;
|
|
86
|
+
const onDisk = config.memory.qdrant.onDisk;
|
|
87
|
+
|
|
88
|
+
const exists = await client.collectionExists(SECTION_COLLECTION);
|
|
89
|
+
if (!exists.exists) {
|
|
90
|
+
log.info(
|
|
91
|
+
{ collection: SECTION_COLLECTION, vectorSize },
|
|
92
|
+
"Creating Qdrant collection for memory v3 sections",
|
|
93
|
+
);
|
|
94
|
+
try {
|
|
95
|
+
await client.createCollection(SECTION_COLLECTION, {
|
|
96
|
+
vectors: {
|
|
97
|
+
size: vectorSize,
|
|
98
|
+
distance: "Cosine",
|
|
99
|
+
on_disk: onDisk,
|
|
100
|
+
},
|
|
101
|
+
hnsw_config: {
|
|
102
|
+
on_disk: onDisk,
|
|
103
|
+
m: 16,
|
|
104
|
+
ef_construct: 100,
|
|
105
|
+
},
|
|
106
|
+
optimizers_config: {
|
|
107
|
+
default_segment_number: 2,
|
|
108
|
+
},
|
|
109
|
+
on_disk_payload: onDisk,
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// 409 = a concurrent caller created the collection — fall through to
|
|
113
|
+
// ensure the payload index below rather than returning early.
|
|
114
|
+
const status =
|
|
115
|
+
err instanceof Error && "status" in err
|
|
116
|
+
? (err as { status: number }).status
|
|
117
|
+
: undefined;
|
|
118
|
+
if (status !== 409) throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Always ensure the `article` payload index — on EVERY path (fresh create,
|
|
123
|
+
// concurrent 409, and a pre-existing collection whose index creation was
|
|
124
|
+
// interrupted by an earlier crash). `deleteSectionsForArticle`/pruning filter
|
|
125
|
+
// on `article`, and strict-mode Qdrant rejects filters on unindexed fields, so
|
|
126
|
+
// a missing index would make them fail until manual repair. createPayloadIndex
|
|
127
|
+
// is idempotent, so re-running it against an existing index is a no-op.
|
|
128
|
+
await client.createPayloadIndex(SECTION_COLLECTION, {
|
|
129
|
+
field_name: "article",
|
|
130
|
+
field_schema: "keyword",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
_collectionReady = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Embed each section's `text` and upsert one point per section, keyed by a
|
|
138
|
+
* deterministic `(article, ordinal)`-derived ID. Stable IDs mean re-upserting
|
|
139
|
+
* the same sections overwrites in place rather than accumulating duplicates,
|
|
140
|
+
* so the operation is idempotent. Payload carries `{ article, ordinal, title }`
|
|
141
|
+
* for downstream filtering and rendering.
|
|
142
|
+
*
|
|
143
|
+
* An empty `sections` array is a no-op (no embedding round-trip).
|
|
144
|
+
*/
|
|
145
|
+
export async function upsertSections(
|
|
146
|
+
config: AssistantConfig,
|
|
147
|
+
sections: Section[],
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
if (sections.length === 0) return;
|
|
150
|
+
|
|
151
|
+
await ensureSectionCollection(config);
|
|
152
|
+
|
|
153
|
+
const { vectors } = await embedWithBackend(
|
|
154
|
+
config,
|
|
155
|
+
sections.map((s) => s.text),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const points = sections.map((section, i) => ({
|
|
159
|
+
id: pointIdForSection(section.article, section.ordinal),
|
|
160
|
+
vector: vectors[i]!,
|
|
161
|
+
payload: {
|
|
162
|
+
article: section.article,
|
|
163
|
+
ordinal: section.ordinal,
|
|
164
|
+
title: section.title,
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
await getSectionDenseClient(config).upsert(SECTION_COLLECTION, {
|
|
169
|
+
wait: true,
|
|
170
|
+
points,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Delete every section point belonging to an article. Used by incremental
|
|
176
|
+
* rebuilds (in a later PR) to clear an article's stale sections before
|
|
177
|
+
* re-upserting its current ones. Idempotent: deleting an absent article's
|
|
178
|
+
* sections is a no-op server-side.
|
|
179
|
+
*/
|
|
180
|
+
export async function deleteSectionsForArticle(
|
|
181
|
+
config: AssistantConfig,
|
|
182
|
+
article: string,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
await ensureSectionCollection(config);
|
|
185
|
+
|
|
186
|
+
await getSectionDenseClient(config).delete(SECTION_COLLECTION, {
|
|
187
|
+
wait: true,
|
|
188
|
+
filter: { must: [{ key: "article", match: { value: article } }] },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Return every distinct `article` slug that currently has at least one section
|
|
194
|
+
* point in the collection. Scrolls the whole collection (payload only, no
|
|
195
|
+
* vectors) in bounded batches and collects the distinct `article` values.
|
|
196
|
+
*
|
|
197
|
+
* Used by the maintain job's prune stage to find articles whose points linger
|
|
198
|
+
* after the page was deleted from the index — the change-delta selector never
|
|
199
|
+
* names a slug that is gone, so a deleted page's stale points are only
|
|
200
|
+
* observable by reading the collection back. An empty/absent collection yields
|
|
201
|
+
* an empty list.
|
|
202
|
+
*/
|
|
203
|
+
export async function listSectionArticles(
|
|
204
|
+
config: AssistantConfig,
|
|
205
|
+
): Promise<string[]> {
|
|
206
|
+
await ensureSectionCollection(config);
|
|
207
|
+
|
|
208
|
+
const client = getSectionDenseClient(config);
|
|
209
|
+
const articles = new Set<string>();
|
|
210
|
+
let offset: string | number | undefined = undefined;
|
|
211
|
+
const maxIterations = 10_000;
|
|
212
|
+
const batchSize = 256;
|
|
213
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
214
|
+
const result = await client.scroll(SECTION_COLLECTION, {
|
|
215
|
+
limit: batchSize,
|
|
216
|
+
with_payload: true,
|
|
217
|
+
with_vector: false,
|
|
218
|
+
...(offset !== undefined ? { offset } : {}),
|
|
219
|
+
});
|
|
220
|
+
for (const point of result.points) {
|
|
221
|
+
const article = (point.payload as { article?: unknown } | null)?.article;
|
|
222
|
+
if (typeof article === "string") articles.add(article);
|
|
223
|
+
}
|
|
224
|
+
const next = result.next_page_offset;
|
|
225
|
+
if (next == null) break;
|
|
226
|
+
offset = typeof next === "string" ? next : (next as number);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return [...articles];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @internal Test-only: reset module-level singletons. */
|
|
233
|
+
export function _resetSectionDenseStoreForTests(): void {
|
|
234
|
+
_client = null;
|
|
235
|
+
_collectionReady = false;
|
|
236
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { SectionIndex, Slug } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section-grain "needle" lane for memory-v3 retrieval: a lexical BM25F search
|
|
5
|
+
* over a {@link SectionIndex}. Each section is a tiny two-field document — a
|
|
6
|
+
* weighted `head` line (`${lastSlugSegment} — ${title}`) and the remaining
|
|
7
|
+
* `body` text — so a literal term in a heading outranks the same term buried in
|
|
8
|
+
* prose. Scoring happens at section grain; results are deduped to distinct
|
|
9
|
+
* articles, each tagged with its best-scoring section.
|
|
10
|
+
*
|
|
11
|
+
* Implementation notes:
|
|
12
|
+
* - Hand-rolled Okapi BM25F (no dependency). The corpus is bounded (one doc per
|
|
13
|
+
* section), so a plain inverted index is plenty.
|
|
14
|
+
* - To match the validated harness the term stream includes adjacent-token
|
|
15
|
+
* bigrams (`token[i] + "_" + token[i+1]`) in addition to unigrams.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Okapi BM25 term-frequency saturation parameter. */
|
|
19
|
+
const k1 = 1.5;
|
|
20
|
+
/** Okapi BM25 length-normalization parameter. */
|
|
21
|
+
const b = 0.75;
|
|
22
|
+
|
|
23
|
+
/** `head`-field weight in the BM25F term-frequency blend. */
|
|
24
|
+
const HEAD_WEIGHT = 2.5;
|
|
25
|
+
/** `body`-field weight in the BM25F term-frequency blend. */
|
|
26
|
+
const BODY_WEIGHT = 1;
|
|
27
|
+
|
|
28
|
+
export interface SectionNeedle {
|
|
29
|
+
/**
|
|
30
|
+
* Returns up to `k` distinct articles ranked by BM25F score (descending),
|
|
31
|
+
* each tagged with its best-scoring section index (into the underlying
|
|
32
|
+
* `SectionIndex.sections`). Ties break by `(article, ordinal)`.
|
|
33
|
+
*/
|
|
34
|
+
query(text: string, k: number): { article: Slug; section: number }[];
|
|
35
|
+
/**
|
|
36
|
+
* Highest-scoring section index (into `SectionIndex.sections`) for `article`
|
|
37
|
+
* against `queryText`. Returns the article's first section index when no term
|
|
38
|
+
* matches (or `-1` if the article has no sections). Used by other lanes to
|
|
39
|
+
* attach a matched-section descriptor to an article they surface.
|
|
40
|
+
*/
|
|
41
|
+
bestSection(article: Slug, queryText: string): number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Lowercase, split on non-alphanumeric, drop empties. */
|
|
45
|
+
function tokenizeUnigrams(text: string): string[] {
|
|
46
|
+
return text
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.split(/[^a-z0-9]+/)
|
|
49
|
+
.filter((token) => token.length > 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Unigrams plus adjacent-token bigrams (`a_b`), matching the harness. */
|
|
53
|
+
function tokenize(text: string): string[] {
|
|
54
|
+
const unigrams = tokenizeUnigrams(text);
|
|
55
|
+
const terms = [...unigrams];
|
|
56
|
+
for (let i = 0; i + 1 < unigrams.length; i++) {
|
|
57
|
+
terms.push(`${unigrams[i]}_${unigrams[i + 1]}`);
|
|
58
|
+
}
|
|
59
|
+
return terms;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Split a section's text into its head line and the remaining body. */
|
|
63
|
+
function splitHeadBody(text: string): { head: string; body: string } {
|
|
64
|
+
const newline = text.indexOf("\n");
|
|
65
|
+
if (newline === -1) return { head: text, body: "" };
|
|
66
|
+
return { head: text.slice(0, newline), body: text.slice(newline + 1) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Posting {
|
|
70
|
+
doc: number;
|
|
71
|
+
/** Field-weighted term frequency: HEAD_WEIGHT*tf_head + BODY_WEIGHT*tf_body. */
|
|
72
|
+
weightedTf: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildSectionNeedle(index: SectionIndex): SectionNeedle {
|
|
76
|
+
const { sections } = index;
|
|
77
|
+
const docCount = sections.length;
|
|
78
|
+
|
|
79
|
+
// term -> postings (section index + weighted term frequency).
|
|
80
|
+
const postings = new Map<string, Posting[]>();
|
|
81
|
+
// BM25F effective document length = weighted token count per section.
|
|
82
|
+
const docLengths: number[] = new Array(docCount).fill(0);
|
|
83
|
+
let totalLength = 0;
|
|
84
|
+
|
|
85
|
+
for (let doc = 0; doc < docCount; doc++) {
|
|
86
|
+
const { head, body } = splitHeadBody(sections[doc]!.text);
|
|
87
|
+
const headTerms = tokenize(head);
|
|
88
|
+
const bodyTerms = tokenize(body);
|
|
89
|
+
|
|
90
|
+
const length =
|
|
91
|
+
HEAD_WEIGHT * headTerms.length + BODY_WEIGHT * bodyTerms.length;
|
|
92
|
+
docLengths[doc] = length;
|
|
93
|
+
totalLength += length;
|
|
94
|
+
|
|
95
|
+
const weightedTf = new Map<string, number>();
|
|
96
|
+
for (const term of headTerms) {
|
|
97
|
+
weightedTf.set(term, (weightedTf.get(term) ?? 0) + HEAD_WEIGHT);
|
|
98
|
+
}
|
|
99
|
+
for (const term of bodyTerms) {
|
|
100
|
+
weightedTf.set(term, (weightedTf.get(term) ?? 0) + BODY_WEIGHT);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const [term, tf] of weightedTf) {
|
|
104
|
+
let list = postings.get(term);
|
|
105
|
+
if (!list) {
|
|
106
|
+
list = [];
|
|
107
|
+
postings.set(term, list);
|
|
108
|
+
}
|
|
109
|
+
list.push({ doc, weightedTf: tf });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const avgDocLength = docCount > 0 ? totalLength / docCount : 0;
|
|
114
|
+
|
|
115
|
+
/** BM25F score per section for the given query terms. */
|
|
116
|
+
function scoreSections(queryTerms: Set<string>): Map<number, number> {
|
|
117
|
+
const scores = new Map<number, number>();
|
|
118
|
+
if (docCount === 0) return scores;
|
|
119
|
+
|
|
120
|
+
for (const term of queryTerms) {
|
|
121
|
+
const list = postings.get(term);
|
|
122
|
+
if (!list) continue;
|
|
123
|
+
|
|
124
|
+
const idf = Math.log(
|
|
125
|
+
1 + (docCount - list.length + 0.5) / (list.length + 0.5),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (const { doc, weightedTf } of list) {
|
|
129
|
+
const norm = weightedTf * (k1 + 1);
|
|
130
|
+
const denom =
|
|
131
|
+
weightedTf + k1 * (1 - b + b * (docLengths[doc]! / avgDocLength));
|
|
132
|
+
scores.set(doc, (scores.get(doc) ?? 0) + idf * (norm / denom));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return scores;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Deterministic order: score desc, then (article, ordinal) asc. */
|
|
140
|
+
function rankSection(
|
|
141
|
+
a: number,
|
|
142
|
+
c: number,
|
|
143
|
+
scores: Map<number, number>,
|
|
144
|
+
): number {
|
|
145
|
+
const byScore = (scores.get(c) ?? 0) - (scores.get(a) ?? 0);
|
|
146
|
+
if (byScore !== 0) return byScore;
|
|
147
|
+
const sa = sections[a]!;
|
|
148
|
+
const sc = sections[c]!;
|
|
149
|
+
return sa.article.localeCompare(sc.article) || sa.ordinal - sc.ordinal;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function query(
|
|
153
|
+
text: string,
|
|
154
|
+
k: number,
|
|
155
|
+
): { article: Slug; section: number }[] {
|
|
156
|
+
if (k <= 0 || docCount === 0) return [];
|
|
157
|
+
|
|
158
|
+
const queryTerms = new Set(tokenize(text));
|
|
159
|
+
const scores = scoreSections(queryTerms);
|
|
160
|
+
if (scores.size === 0) return [];
|
|
161
|
+
|
|
162
|
+
const ranked = [...scores.keys()].sort((a, c) => rankSection(a, c, scores));
|
|
163
|
+
|
|
164
|
+
// Dedupe to distinct articles, keeping each article's best (first-ranked)
|
|
165
|
+
// section, until we have `k` articles.
|
|
166
|
+
const seen = new Set<Slug>();
|
|
167
|
+
const result: { article: Slug; section: number }[] = [];
|
|
168
|
+
for (const doc of ranked) {
|
|
169
|
+
const article = sections[doc]!.article;
|
|
170
|
+
if (seen.has(article)) continue;
|
|
171
|
+
seen.add(article);
|
|
172
|
+
result.push({ article, section: doc });
|
|
173
|
+
if (result.length >= k) break;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function bestSection(article: Slug, queryText: string): number {
|
|
179
|
+
const docs = index.byArticle.get(article);
|
|
180
|
+
if (!docs || docs.length === 0) return -1;
|
|
181
|
+
|
|
182
|
+
const queryTerms = new Set(tokenize(queryText));
|
|
183
|
+
const scores = scoreSections(queryTerms);
|
|
184
|
+
|
|
185
|
+
let best = docs[0]!;
|
|
186
|
+
let bestScore = scores.get(best) ?? 0;
|
|
187
|
+
for (const doc of docs) {
|
|
188
|
+
const score = scores.get(doc) ?? 0;
|
|
189
|
+
// Strictly-greater keeps the earliest (lowest ordinal) section on ties,
|
|
190
|
+
// since `docs` is in ascending section order.
|
|
191
|
+
if (score > bestScore) {
|
|
192
|
+
best = doc;
|
|
193
|
+
bestScore = score;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return best;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { query, bestSection };
|
|
200
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Section, SectionIndex, Slug } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section-grain index builder for memory-v3 lane retrieval.
|
|
5
|
+
*
|
|
6
|
+
* Splits each page body into sections at `## ` headings — text before the
|
|
7
|
+
* first heading is the page lead (ordinal 0) — and emits a flat, deterministic
|
|
8
|
+
* `SectionIndex`. Over-long sections are chunked so each `Section.text` fits a
|
|
9
|
+
* typical embedding window. The function is pure: it does no I/O of its own
|
|
10
|
+
* (the caller supplies `pageBody`), no LLM calls, and no Qdrant/daemon wiring.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Max `Section.text` length in characters. Sections longer than this are split
|
|
15
|
+
* into multiple ordered `Section`s sharing the same `(article, ordinal)` so the
|
|
16
|
+
* embedding backend never receives an over-window input.
|
|
17
|
+
*/
|
|
18
|
+
export const SECTION_CHUNK_CHARS = 6000;
|
|
19
|
+
|
|
20
|
+
/** Last `/`- or `.`-delimited segment of a slug (used for the head line). */
|
|
21
|
+
function lastSlugSegment(slug: Slug): string {
|
|
22
|
+
const segments = slug.split(/[/.]/);
|
|
23
|
+
return segments[segments.length - 1] ?? slug;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RawSection {
|
|
27
|
+
title: string;
|
|
28
|
+
body: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Split a frontmatter-stripped markdown body into raw sections. The text before
|
|
33
|
+
* the first `## ` heading is the lead (title `""`); each subsequent `## `
|
|
34
|
+
* heading starts a new section whose title is the heading text.
|
|
35
|
+
*/
|
|
36
|
+
function splitIntoRawSections(body: string): RawSection[] {
|
|
37
|
+
// Always seed a lead section (title `""`). The lead may stay empty, so a
|
|
38
|
+
// headingless or empty page still yields a single ordinal-0 section.
|
|
39
|
+
const sections: { title: string; lines: string[] }[] = [
|
|
40
|
+
{ title: "", lines: [] },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
for (const line of body.split("\n")) {
|
|
44
|
+
const heading = /^## (.*)$/.exec(line);
|
|
45
|
+
if (heading) {
|
|
46
|
+
sections.push({ title: heading[1]!.trim(), lines: [] });
|
|
47
|
+
} else {
|
|
48
|
+
sections[sections.length - 1]!.lines.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return sections.map((s) => ({ title: s.title, body: s.lines.join("\n") }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Split `text` into chunks no longer than `SECTION_CHUNK_CHARS`, preferring to
|
|
57
|
+
* break on newlines so chunks stay readable. Order is preserved.
|
|
58
|
+
*/
|
|
59
|
+
function chunkText(text: string): string[] {
|
|
60
|
+
if (text.length <= SECTION_CHUNK_CHARS) return [text];
|
|
61
|
+
|
|
62
|
+
const chunks: string[] = [];
|
|
63
|
+
let remaining = text;
|
|
64
|
+
while (remaining.length > SECTION_CHUNK_CHARS) {
|
|
65
|
+
const window = remaining.slice(0, SECTION_CHUNK_CHARS);
|
|
66
|
+
const newlineBreak = window.lastIndexOf("\n");
|
|
67
|
+
// Only break on a newline if it leaves a non-trivial chunk; otherwise hard
|
|
68
|
+
// split at the window boundary to guarantee forward progress.
|
|
69
|
+
const breakAt = newlineBreak > 0 ? newlineBreak : SECTION_CHUNK_CHARS;
|
|
70
|
+
chunks.push(remaining.slice(0, breakAt));
|
|
71
|
+
remaining = remaining.slice(breakAt).replace(/^\n/, "");
|
|
72
|
+
}
|
|
73
|
+
if (remaining.length > 0) chunks.push(remaining);
|
|
74
|
+
return chunks;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function buildSectionIndex(
|
|
78
|
+
slugs: Slug[],
|
|
79
|
+
pageBody: (slug: Slug) => Promise<string>,
|
|
80
|
+
): Promise<SectionIndex> {
|
|
81
|
+
const sections: Section[] = [];
|
|
82
|
+
|
|
83
|
+
// Sort slugs so the flat `sections` array is deterministic across runs.
|
|
84
|
+
for (const article of [...slugs].sort((a, b) => a.localeCompare(b))) {
|
|
85
|
+
const body = await pageBody(article);
|
|
86
|
+
const segment = lastSlugSegment(article);
|
|
87
|
+
|
|
88
|
+
let ordinal = 0;
|
|
89
|
+
for (const raw of splitIntoRawSections(body)) {
|
|
90
|
+
const head = `${segment} — ${raw.title}`;
|
|
91
|
+
const fullText = `${head}\n${raw.body}`;
|
|
92
|
+
for (const chunk of chunkText(fullText)) {
|
|
93
|
+
sections.push({
|
|
94
|
+
article,
|
|
95
|
+
title: raw.title,
|
|
96
|
+
text: chunk,
|
|
97
|
+
ordinal: ordinal++,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const byArticle = new Map<Slug, number[]>();
|
|
104
|
+
for (let i = 0; i < sections.length; i++) {
|
|
105
|
+
const article = sections[i]!.article;
|
|
106
|
+
let indices = byArticle.get(article);
|
|
107
|
+
if (!indices) {
|
|
108
|
+
indices = [];
|
|
109
|
+
byArticle.set(article, indices);
|
|
110
|
+
}
|
|
111
|
+
indices.push(i);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { sections, byArticle };
|
|
115
|
+
}
|