@vellumai/assistant 0.6.5 → 0.6.6
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/AGENTS.md +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
|
@@ -18,15 +18,22 @@ import {
|
|
|
18
18
|
countMemoryPrefixBlocks,
|
|
19
19
|
extractMemoryPrefixBlocks,
|
|
20
20
|
} from "../memory/graph/conversation-graph-memory.js";
|
|
21
|
-
import { searchPkbFiles } from "../memory/pkb/pkb-search.js";
|
|
22
21
|
import type { QdrantSparseVector } from "../memory/qdrant-client.js";
|
|
23
22
|
import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
|
|
24
23
|
import {
|
|
25
24
|
extractTagLineTexts,
|
|
25
|
+
isReactionTagLine,
|
|
26
26
|
type RenderableSlackMessage,
|
|
27
27
|
renderSlackTranscript,
|
|
28
28
|
} from "../messaging/providers/slack/render-transcript.js";
|
|
29
29
|
import { isPermissionControlsV2Enabled } from "../permissions/v2-consent-policy.js";
|
|
30
|
+
import { getInjectors } from "../plugins/registry.js";
|
|
31
|
+
import type {
|
|
32
|
+
InjectionBlock,
|
|
33
|
+
InjectionPlacement,
|
|
34
|
+
TurnContext,
|
|
35
|
+
TurnInjectionInputs,
|
|
36
|
+
} from "../plugins/types.js";
|
|
30
37
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
31
38
|
import {
|
|
32
39
|
type ActorTrustContext,
|
|
@@ -36,17 +43,10 @@ import {
|
|
|
36
43
|
import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
|
|
37
44
|
import type { SubagentState } from "../subagent/types.js";
|
|
38
45
|
import { TERMINAL_STATUSES } from "../subagent/types.js";
|
|
39
|
-
import { getLogger } from "../util/logger.js";
|
|
40
46
|
import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
|
|
41
47
|
import { stripCommentLines } from "../util/strip-comment-lines.js";
|
|
42
48
|
import { filterMessagesForUntrustedActor } from "./conversation-lifecycle.js";
|
|
43
|
-
import {
|
|
44
|
-
getInContextPkbPaths,
|
|
45
|
-
type PkbContextConversation,
|
|
46
|
-
} from "./pkb-context-tracker.js";
|
|
47
|
-
import { buildPkbReminder } from "./pkb-reminder-builder.js";
|
|
48
|
-
|
|
49
|
-
const pkbReminderLog = getLogger("pkb-reminder");
|
|
49
|
+
import { type PkbContextConversation } from "./pkb-context-tracker.js";
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Describes the capabilities of the channel through which the user is
|
|
@@ -522,16 +522,11 @@ export function buildSubagentStatusBlock(
|
|
|
522
522
|
return lines.join("\n");
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
...message,
|
|
532
|
-
content: [...message.content, { type: "text" as const, text: statusBlock }],
|
|
533
|
-
};
|
|
534
|
-
}
|
|
525
|
+
// The `<active_subagents>` block is emitted by the `subagent-status` default
|
|
526
|
+
// injector (`plugins/defaults/injectors.ts`) as an `append-user-tail`
|
|
527
|
+
// placement. Use {@link applyRuntimeInjections} with
|
|
528
|
+
// `options.subagentStatusBlock` set, or drive the injector chain directly
|
|
529
|
+
// via `collectInjectorBlocks`.
|
|
535
530
|
|
|
536
531
|
/**
|
|
537
532
|
* Append voice call-control protocol instructions to the last user
|
|
@@ -575,42 +570,10 @@ export function readNowScratchpad(): string | null {
|
|
|
575
570
|
}
|
|
576
571
|
|
|
577
572
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* thing the model reads.
|
|
573
|
+
* The `<NOW.md>` block is emitted by the `now-md` default injector
|
|
574
|
+
* (`plugins/defaults/injectors.ts`) as an `after-memory-prefix` placement.
|
|
575
|
+
* Use {@link applyRuntimeInjections} with `options.nowScratchpad` set.
|
|
582
576
|
*/
|
|
583
|
-
export function injectNowScratchpad(
|
|
584
|
-
message: Message,
|
|
585
|
-
content: string,
|
|
586
|
-
): Message {
|
|
587
|
-
const scratchpadBlock = {
|
|
588
|
-
type: "text" as const,
|
|
589
|
-
text: `<NOW.md Always keep this up to date; keep under 10 lines>\n${content}\n</NOW.md>`,
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
// Find insertion point: skip any leading injected-context text blocks
|
|
593
|
-
// (e.g. memory_context) so the scratchpad lands between injected context
|
|
594
|
-
// and the user's original content.
|
|
595
|
-
let insertIdx = 0;
|
|
596
|
-
for (let i = 0; i < message.content.length; i++) {
|
|
597
|
-
const block = message.content[i];
|
|
598
|
-
if (block.type === "text" && block.text.startsWith("<memory_context")) {
|
|
599
|
-
insertIdx = i + 1;
|
|
600
|
-
} else {
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return {
|
|
606
|
-
...message,
|
|
607
|
-
content: [
|
|
608
|
-
...message.content.slice(0, insertIdx),
|
|
609
|
-
scratchpadBlock,
|
|
610
|
-
...message.content.slice(insertIdx),
|
|
611
|
-
],
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
577
|
|
|
615
578
|
/** Strip `<NOW.md>` blocks injected by `injectNowScratchpad`. */
|
|
616
579
|
export function stripNowScratchpad(messages: Message[]): Message[] {
|
|
@@ -638,16 +601,6 @@ const AUTOINJECT_FILENAME = "_autoinject.md";
|
|
|
638
601
|
/** Max buffer.md lines injected into prompts — keeps context bounded even when filing is off. */
|
|
639
602
|
const MAX_BUFFER_LINES = 50;
|
|
640
603
|
|
|
641
|
-
/** Minimum hybrid-search score for a PKB path to surface as an injection hint. */
|
|
642
|
-
const PKB_HINT_THRESHOLD = 0.5;
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Stricter hint threshold for PKB entries under `archive/`. Archive files are
|
|
646
|
-
* date-indexed dumps of older notes — they match loosely and are rarely the
|
|
647
|
-
* most relevant read, so require a higher bar before recommending them.
|
|
648
|
-
*/
|
|
649
|
-
const PKB_HINT_ARCHIVE_THRESHOLD = 0.7;
|
|
650
|
-
|
|
651
604
|
/**
|
|
652
605
|
* Read `_autoinject.md` from the PKB directory and return the list of
|
|
653
606
|
* filenames to inject.
|
|
@@ -728,49 +681,10 @@ export function readPkbContext(): string | null {
|
|
|
728
681
|
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
729
682
|
}
|
|
730
683
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
export function injectPkbContext(message: Message, content: string): Message {
|
|
736
|
-
// Escape closing tags that could break out of the XML wrapper
|
|
737
|
-
const escaped = content.replace(
|
|
738
|
-
/<\/knowledge_base\s*>/gi,
|
|
739
|
-
"</knowledge_base>",
|
|
740
|
-
);
|
|
741
|
-
const pkbBlock = {
|
|
742
|
-
type: "text" as const,
|
|
743
|
-
text: `<knowledge_base>\n${escaped}\n</knowledge_base>`,
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
// Find insertion point: skip any leading memory/image blocks
|
|
747
|
-
let insertIdx = 0;
|
|
748
|
-
for (let i = 0; i < message.content.length; i++) {
|
|
749
|
-
const block = message.content[i];
|
|
750
|
-
if (
|
|
751
|
-
block.type === "text" &&
|
|
752
|
-
(block.text.startsWith("<memory") ||
|
|
753
|
-
block.text.startsWith("</memory_image>") ||
|
|
754
|
-
block.text.startsWith("<memory_context"))
|
|
755
|
-
) {
|
|
756
|
-
insertIdx = i + 1;
|
|
757
|
-
} else if (block.type === "image") {
|
|
758
|
-
// Memory images precede the memory text block
|
|
759
|
-
insertIdx = i + 1;
|
|
760
|
-
} else {
|
|
761
|
-
break;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
return {
|
|
766
|
-
...message,
|
|
767
|
-
content: [
|
|
768
|
-
...message.content.slice(0, insertIdx),
|
|
769
|
-
pkbBlock,
|
|
770
|
-
...message.content.slice(insertIdx),
|
|
771
|
-
],
|
|
772
|
-
};
|
|
773
|
-
}
|
|
684
|
+
// The `<knowledge_base>` block is emitted by the `pkb-context` default
|
|
685
|
+
// injector (`plugins/defaults/injectors.ts`) as an `after-memory-prefix`
|
|
686
|
+
// placement, splicing immediately after any leading memory-prefix blocks.
|
|
687
|
+
// Use {@link applyRuntimeInjections} with `options.pkbContext` set.
|
|
774
688
|
|
|
775
689
|
/** Strip `<knowledge_base>` blocks injected by `injectPkbContext`. */
|
|
776
690
|
export function stripPkbContext(messages: Message[]): Message[] {
|
|
@@ -1146,18 +1060,10 @@ export function stripChannelCapabilityContext(messages: Message[]): Message[] {
|
|
|
1146
1060
|
return stripUserTextBlocksByPrefix(messages, ["<channel_capabilities>"]);
|
|
1147
1061
|
}
|
|
1148
1062
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
message: Message,
|
|
1154
|
-
contextText: string,
|
|
1155
|
-
): Message {
|
|
1156
|
-
return {
|
|
1157
|
-
...message,
|
|
1158
|
-
content: [{ type: "text", text: contextText }, ...message.content],
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1063
|
+
// The workspace top-level context block is emitted by the
|
|
1064
|
+
// `workspace-context` default injector (`plugins/defaults/injectors.ts`)
|
|
1065
|
+
// as a `prepend-user-tail` placement. Use {@link applyRuntimeInjections}
|
|
1066
|
+
// with `options.workspaceTopLevelContext` set.
|
|
1161
1067
|
|
|
1162
1068
|
/**
|
|
1163
1069
|
* Strip `<active_workspace>` (and legacy `<active_dynamic_page>`) blocks
|
|
@@ -1549,21 +1455,33 @@ function buildActiveThreadBlockFromRenderable(
|
|
|
1549
1455
|
if (members.length === 0) return null;
|
|
1550
1456
|
|
|
1551
1457
|
// The active-thread block is flattened to plain text below, which discards
|
|
1552
|
-
// `Message.role`.
|
|
1553
|
-
// `
|
|
1554
|
-
//
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1458
|
+
// `Message.role`. Assistant rows are relabeled in the post-render step:
|
|
1459
|
+
// `renderSlackTranscript` emits assistant content with no tag-line wrapper
|
|
1460
|
+
// (to prevent the model mimicking `[MM/DD/YY HH:MM]:` prefixes in outbound
|
|
1461
|
+
// replies), so we prepend an explicit `@assistant:` label to the flattened
|
|
1462
|
+
// line. Unnamed user rows (no real Slack displayName) get a `@user`
|
|
1463
|
+
// senderLabel here so their tag line carries attribution through the
|
|
1464
|
+
// renderer. Labeled user rows and assistant rows pass through unchanged.
|
|
1465
|
+
const labeledMembers = members.map((m) => {
|
|
1466
|
+
if (m.role === "assistant") return m;
|
|
1467
|
+
if (m.senderLabel !== null) return m;
|
|
1468
|
+
return { ...m, senderLabel: "@user" };
|
|
1469
|
+
});
|
|
1563
1470
|
|
|
1564
1471
|
const rendered = renderSlackTranscript(labeledMembers);
|
|
1565
1472
|
if (rendered.length === 0) return null;
|
|
1566
|
-
|
|
1473
|
+
// Reaction / overflow-trailer lines already embed `@assistant` inline, so
|
|
1474
|
+
// `isReactionTagLine` is used to skip those and avoid double-attribution
|
|
1475
|
+
// (`@assistant: [... @assistant reacted ...]`). Regular content and the
|
|
1476
|
+
// `[deleted]` sentinel get the prefix so attribution survives flattening.
|
|
1477
|
+
const lines = rendered
|
|
1478
|
+
.map((msg) => {
|
|
1479
|
+
const text = extractTagLineTexts([msg])[0] ?? "";
|
|
1480
|
+
return msg.role === "assistant" && !isReactionTagLine(text)
|
|
1481
|
+
? `@assistant: ${text}`
|
|
1482
|
+
: text;
|
|
1483
|
+
})
|
|
1484
|
+
.join("\n");
|
|
1567
1485
|
return `<active_thread>\n${lines}\n</active_thread>`;
|
|
1568
1486
|
}
|
|
1569
1487
|
|
|
@@ -1715,13 +1633,32 @@ export function findLastInjectedNowContent(messages: Message[]): string | null {
|
|
|
1715
1633
|
export type InjectionMode = "full" | "minimal";
|
|
1716
1634
|
|
|
1717
1635
|
/**
|
|
1718
|
-
* Per-turn injection bytes captured
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1636
|
+
* Per-turn injection bytes captured so `loadFromDb` can rehydrate historical
|
|
1637
|
+
* user messages byte-for-byte after a daemon restart or conversation
|
|
1638
|
+
* eviction. Persisting the exact injected text onto message metadata keeps
|
|
1639
|
+
* Anthropic's prefix cache anchored to msg[0] instead of invalidating every
|
|
1640
|
+
* turn on reload. Any field left `undefined` means that block was not
|
|
1641
|
+
* injected on this turn.
|
|
1721
1642
|
*/
|
|
1722
1643
|
export interface RuntimeInjectionBlocks {
|
|
1723
1644
|
unifiedTurnContext?: string;
|
|
1724
1645
|
pkbSystemReminder?: string;
|
|
1646
|
+
workspaceBlock?: string;
|
|
1647
|
+
nowScratchpadBlock?: string;
|
|
1648
|
+
pkbContextBlock?: string;
|
|
1649
|
+
/**
|
|
1650
|
+
* Composed output of every plugin-registered {@link Injector}, concatenated
|
|
1651
|
+
* in ascending `order`. Empty string when every injector opted out (returned
|
|
1652
|
+
* `null`). Today the default injectors (`default-injectors` plugin)
|
|
1653
|
+
* placeholder-return `null`, so this is only non-empty when a third-party
|
|
1654
|
+
* plugin registers an injector that emits content.
|
|
1655
|
+
*
|
|
1656
|
+
* Populated by {@link composeInjectorChain} during
|
|
1657
|
+
* {@link applyRuntimeInjections}. Distinct from the other `blocks` fields
|
|
1658
|
+
* because those track specific hardcoded injections today; this field is
|
|
1659
|
+
* the extensibility seam for {@link Injector} plugins.
|
|
1660
|
+
*/
|
|
1661
|
+
injectorChainBlock?: string;
|
|
1725
1662
|
}
|
|
1726
1663
|
|
|
1727
1664
|
export interface RuntimeInjectionResult {
|
|
@@ -1730,118 +1667,449 @@ export interface RuntimeInjectionResult {
|
|
|
1730
1667
|
}
|
|
1731
1668
|
|
|
1732
1669
|
/**
|
|
1733
|
-
*
|
|
1670
|
+
* Run every registered {@link Injector}'s `produce()` in ascending `order`
|
|
1671
|
+
* and return every non-null block the chain produced.
|
|
1672
|
+
*
|
|
1673
|
+
* Injectors returning `null` are omitted from the result. The returned array
|
|
1674
|
+
* preserves ascending-`order` sort so downstream callers (notably
|
|
1675
|
+
* {@link applyRuntimeInjections}) can group blocks by `placement` and apply
|
|
1676
|
+
* them declaratively without losing per-injector ordering within each slot.
|
|
1677
|
+
*/
|
|
1678
|
+
export async function collectInjectorBlocks(
|
|
1679
|
+
ctx: TurnContext,
|
|
1680
|
+
): Promise<InjectionBlock[]> {
|
|
1681
|
+
const injectors = getInjectors();
|
|
1682
|
+
if (injectors.length === 0) return [];
|
|
1683
|
+
const out: InjectionBlock[] = [];
|
|
1684
|
+
for (const injector of injectors) {
|
|
1685
|
+
const block = await injector.produce(ctx);
|
|
1686
|
+
if (block) out.push(block);
|
|
1687
|
+
}
|
|
1688
|
+
return out;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Run every registered {@link Injector}'s `produce()` in ascending
|
|
1693
|
+
* `order`, concatenate the non-null results into a single block of text,
|
|
1694
|
+
* and return it.
|
|
1695
|
+
*
|
|
1696
|
+
* Separator: blank line between blocks. Injectors returning `null` are
|
|
1697
|
+
* skipped entirely (no leading/trailing blank lines). When no injector
|
|
1698
|
+
* contributes, the function returns an empty string.
|
|
1734
1699
|
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1700
|
+
* Used by tests that assert the concatenation contract and by callers that
|
|
1701
|
+
* want a single informational string view of the chain. The canonical
|
|
1702
|
+
* integration point is {@link applyRuntimeInjections}, which uses
|
|
1703
|
+
* {@link collectInjectorBlocks} + placement-aware application to splice
|
|
1704
|
+
* each block into the per-turn message array.
|
|
1705
|
+
*/
|
|
1706
|
+
export async function composeInjectorChain(ctx: TurnContext): Promise<string> {
|
|
1707
|
+
const blocks = await collectInjectorBlocks(ctx);
|
|
1708
|
+
const pieces: string[] = [];
|
|
1709
|
+
for (const block of blocks) {
|
|
1710
|
+
if (block.text.length > 0) pieces.push(block.text);
|
|
1711
|
+
}
|
|
1712
|
+
return pieces.join("\n\n");
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Default block placement. Kept in sync with {@link InjectionBlock} so
|
|
1717
|
+
* blocks produced without an explicit `placement` (e.g. third-party
|
|
1718
|
+
* injectors written against the pre-G2.1 API) behave predictably.
|
|
1719
|
+
*/
|
|
1720
|
+
const DEFAULT_PLACEMENT: InjectionPlacement = "append-user-tail";
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Count leading memory-prefix blocks on a user message's `content`.
|
|
1724
|
+
*
|
|
1725
|
+
* Delegates to {@link countMemoryPrefixBlocks} from
|
|
1726
|
+
* `memory/graph/conversation-graph-memory.js` — the same state-machine the
|
|
1727
|
+
* pre-migration PKB-reminder branch used to find its splice point. The
|
|
1728
|
+
* pre-migration `injectPkbContext` and `injectNowScratchpad` helpers used
|
|
1729
|
+
* slightly simpler rules inline; reusing the canonical counter here
|
|
1730
|
+
* collapses the three near-identical splice rules into one source of truth
|
|
1731
|
+
* so the ordering of PKB-context / PKB-reminder / NOW blocks relative to
|
|
1732
|
+
* any memory prefix is stable and testable. For the common case (just
|
|
1733
|
+
* `<memory __injected>` text, no images), the output is byte-identical to
|
|
1734
|
+
* the pre-migration helpers.
|
|
1735
|
+
*/
|
|
1736
|
+
function countMemoryPrefixBlocksOnContent(content: ContentBlock[]): number {
|
|
1737
|
+
return countMemoryPrefixBlocks(content);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* Apply one injector block to a `runMessages` array according to its
|
|
1742
|
+
* declared {@link InjectionPlacement}.
|
|
1743
|
+
*
|
|
1744
|
+
* Preserves the byte-for-byte positional semantics of the pre-migration
|
|
1745
|
+
* `inject*` helpers:
|
|
1746
|
+
* - `"prepend-user-tail"` — prepend to the tail user message's content.
|
|
1747
|
+
* - `"append-user-tail"` — append to the tail user message's content.
|
|
1748
|
+
* - `"after-memory-prefix"` — splice immediately after any leading memory
|
|
1749
|
+
* prefix blocks (mirrors `injectPkbContext` / `injectNowScratchpad`).
|
|
1750
|
+
* - `"replace-run-messages"` — replace `runMessages` wholesale with
|
|
1751
|
+
* `block.messagesOverride`.
|
|
1752
|
+
*
|
|
1753
|
+
* Blocks with empty `text` on non-replace placements are no-ops (the
|
|
1754
|
+
* pre-migration branches also short-circuited on empty strings).
|
|
1755
|
+
*/
|
|
1756
|
+
function applyInjectionBlock(
|
|
1757
|
+
runMessages: Message[],
|
|
1758
|
+
block: InjectionBlock,
|
|
1759
|
+
): Message[] {
|
|
1760
|
+
const placement = block.placement ?? DEFAULT_PLACEMENT;
|
|
1761
|
+
|
|
1762
|
+
if (placement === "replace-run-messages") {
|
|
1763
|
+
if (!block.messagesOverride) return runMessages;
|
|
1764
|
+
return block.messagesOverride;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (block.text.length === 0) return runMessages;
|
|
1768
|
+
|
|
1769
|
+
const userTail = runMessages[runMessages.length - 1];
|
|
1770
|
+
if (!userTail || userTail.role !== "user") return runMessages;
|
|
1771
|
+
|
|
1772
|
+
const textBlock = { type: "text" as const, text: block.text };
|
|
1773
|
+
|
|
1774
|
+
switch (placement) {
|
|
1775
|
+
case "prepend-user-tail":
|
|
1776
|
+
return [
|
|
1777
|
+
...runMessages.slice(0, -1),
|
|
1778
|
+
{ ...userTail, content: [textBlock, ...userTail.content] },
|
|
1779
|
+
];
|
|
1780
|
+
case "append-user-tail":
|
|
1781
|
+
return [
|
|
1782
|
+
...runMessages.slice(0, -1),
|
|
1783
|
+
{ ...userTail, content: [...userTail.content, textBlock] },
|
|
1784
|
+
];
|
|
1785
|
+
case "after-memory-prefix": {
|
|
1786
|
+
const memoryPrefixCount = countMemoryPrefixBlocksOnContent(
|
|
1787
|
+
userTail.content,
|
|
1788
|
+
);
|
|
1789
|
+
return [
|
|
1790
|
+
...runMessages.slice(0, -1),
|
|
1791
|
+
{
|
|
1792
|
+
...userTail,
|
|
1793
|
+
content: [
|
|
1794
|
+
...userTail.content.slice(0, memoryPrefixCount),
|
|
1795
|
+
textBlock,
|
|
1796
|
+
...userTail.content.slice(memoryPrefixCount),
|
|
1797
|
+
],
|
|
1798
|
+
},
|
|
1799
|
+
];
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Per-turn options accepted by {@link applyRuntimeInjections}.
|
|
1806
|
+
*
|
|
1807
|
+
* Most fields flow through to the per-injector {@link TurnInjectionInputs}
|
|
1808
|
+
* bag attached to the {@link TurnContext} the caller provides (or to an
|
|
1809
|
+
* ephemeral {@link TurnContext} synthesized for test call sites). A small
|
|
1810
|
+
* number of fields drive hardcoded branches that live outside the injector
|
|
1811
|
+
* chain — `activeSurface`, `channelCapabilities`, `channelCommandContext`,
|
|
1812
|
+
* `voiceCallControlPrompt`, `transportHints`, and `isNonInteractive` —
|
|
1813
|
+
* because they are orchestrator-owned content that never made sense as
|
|
1814
|
+
* plugin-overridable default injectors.
|
|
1815
|
+
*/
|
|
1816
|
+
export interface RuntimeInjectionOptions {
|
|
1817
|
+
/**
|
|
1818
|
+
* Active dashboard-surface context (read from `<active_workspace>`). Kept
|
|
1819
|
+
* on the options bag rather than an injector because it is a
|
|
1820
|
+
* channel-capability concern that has never been gated as a default
|
|
1821
|
+
* injector.
|
|
1822
|
+
*/
|
|
1823
|
+
activeSurface?: ActiveSurfaceContext | null;
|
|
1824
|
+
workspaceTopLevelContext?: string | null;
|
|
1825
|
+
channelCapabilities?: ChannelCapabilities | null;
|
|
1826
|
+
channelCommandContext?: ChannelCommandContext | null;
|
|
1827
|
+
unifiedTurnContext?: string | null;
|
|
1828
|
+
voiceCallControlPrompt?: string | null;
|
|
1829
|
+
pkbContext?: string | null;
|
|
1830
|
+
pkbActive?: boolean;
|
|
1831
|
+
/**
|
|
1832
|
+
* Dense query vector surfaced from the graph memory retriever.
|
|
1833
|
+
* When present together with `pkbActive`, used to run `searchPkbFiles`
|
|
1834
|
+
* to surface relevance hints in the PKB system reminder. When missing,
|
|
1835
|
+
* the reminder falls back to the flat static text.
|
|
1836
|
+
*/
|
|
1837
|
+
pkbQueryVector?: number[];
|
|
1838
|
+
/** Optional sparse vector accompanying `pkbQueryVector`. */
|
|
1839
|
+
pkbSparseVector?: QdrantSparseVector;
|
|
1840
|
+
/** Memory scope id used to filter PKB search results. */
|
|
1841
|
+
pkbScopeId?: string;
|
|
1842
|
+
/**
|
|
1843
|
+
* The live conversation (or a minimal shape containing `messages`) used
|
|
1844
|
+
* to compute which PKB paths are already "in context" and therefore
|
|
1845
|
+
* suppressed from hint suggestions.
|
|
1846
|
+
*/
|
|
1847
|
+
pkbConversation?: PkbContextConversation;
|
|
1848
|
+
/** Auto-injected PKB filenames (resolved relative to `pkbRoot`). */
|
|
1849
|
+
pkbAutoInjectList?: string[];
|
|
1850
|
+
/** Absolute path to the PKB directory (e.g. `<workspace>/pkb`). */
|
|
1851
|
+
pkbRoot?: string;
|
|
1852
|
+
/**
|
|
1853
|
+
* Working directory against which relative `file_read` tool paths
|
|
1854
|
+
* resolve, used to detect workspace-relative reads like
|
|
1855
|
+
* `pkb/threads.md`. Falls back to `pkbRoot` when omitted.
|
|
1856
|
+
*/
|
|
1857
|
+
pkbWorkingDir?: string;
|
|
1858
|
+
nowScratchpad?: string | null;
|
|
1859
|
+
subagentStatusBlock?: string | null;
|
|
1860
|
+
isNonInteractive?: boolean;
|
|
1861
|
+
transportHints?: string[] | null;
|
|
1862
|
+
/**
|
|
1863
|
+
* Pre-rendered Slack chronological transcript that replaces the
|
|
1864
|
+
* default `runMessages` history for any Slack conversation (channels
|
|
1865
|
+
* and DMs alike).
|
|
1866
|
+
*
|
|
1867
|
+
* When `channelCapabilities` describes a Slack conversation and this
|
|
1868
|
+
* array is non-empty, the `slack-messages` default injector emits a
|
|
1869
|
+
* `replace-run-messages` block that swaps `runMessages` with this
|
|
1870
|
+
* transcript. Channel renders include sibling-thread tags; DM renders
|
|
1871
|
+
* are flat (DMs have no threads). The `transportHints` pipeline is
|
|
1872
|
+
* skipped for any Slack conversation so the persisted view isn't
|
|
1873
|
+
* duplicated by gateway-side hints.
|
|
1874
|
+
*
|
|
1875
|
+
* Callers build this via `loadSlackChronologicalMessages` (or the
|
|
1876
|
+
* underlying `assembleSlackChronologicalMessages`) before invoking
|
|
1877
|
+
* this function so the assembly path stays free of direct DB calls
|
|
1878
|
+
* and remains easy to test.
|
|
1879
|
+
*/
|
|
1880
|
+
slackChronologicalMessages?: Message[] | null;
|
|
1881
|
+
/**
|
|
1882
|
+
* Pre-rendered `<active_thread>` focus block listing the messages of
|
|
1883
|
+
* the thread the current inbound user message belongs to.
|
|
1884
|
+
*
|
|
1885
|
+
* Appended to the FINAL user message ONLY when `channelCapabilities`
|
|
1886
|
+
* describes a Slack non-DM channel. The block is non-persisted: history
|
|
1887
|
+
* rebuilds re-derive it from storage on each turn, and
|
|
1888
|
+
* `RUNTIME_INJECTION_PREFIXES` strips any `<active_thread>` blocks from
|
|
1889
|
+
* prior turns so they do not accumulate.
|
|
1890
|
+
*
|
|
1891
|
+
* Callers build this via `loadSlackActiveThreadFocusBlock` (or the
|
|
1892
|
+
* underlying `assembleSlackActiveThreadFocusBlock`). Pass `null` /
|
|
1893
|
+
* `undefined` when the inbound is a top-level (non-thread) post.
|
|
1894
|
+
*/
|
|
1895
|
+
slackActiveThreadFocusBlock?: string | null;
|
|
1896
|
+
mode?: InjectionMode;
|
|
1897
|
+
/**
|
|
1898
|
+
* Per-turn {@link TurnContext} forwarded to plugin-registered
|
|
1899
|
+
* {@link Injector}s via {@link collectInjectorBlocks}. When omitted,
|
|
1900
|
+
* `applyRuntimeInjections` synthesizes an ephemeral context (with a
|
|
1901
|
+
* fallback `trust` classification) so the default-injector chain still
|
|
1902
|
+
* runs — call sites that build the options bag without holding a full
|
|
1903
|
+
* `TurnContext` get the same chain output.
|
|
1904
|
+
*
|
|
1905
|
+
* When provided, the caller's `trust`, `conversationId`, `turnIndex`,
|
|
1906
|
+
* etc. are preserved; the function layers its per-turn
|
|
1907
|
+
* {@link TurnInjectionInputs} onto a shallow clone so the caller's
|
|
1908
|
+
* `TurnContext` is not mutated.
|
|
1909
|
+
*/
|
|
1910
|
+
turnContext?: TurnContext;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/**
|
|
1914
|
+
* Build the {@link TurnInjectionInputs} bag from the options bag.
|
|
1915
|
+
*
|
|
1916
|
+
* Exposed so callers that already hold a {@link TurnContext} can layer the
|
|
1917
|
+
* same per-turn inputs onto it before handing control to
|
|
1918
|
+
* {@link collectInjectorBlocks} directly — useful for tests and for the
|
|
1919
|
+
* overflow-reducer reinject path.
|
|
1920
|
+
*/
|
|
1921
|
+
export function buildTurnInjectionInputs(
|
|
1922
|
+
options: RuntimeInjectionOptions,
|
|
1923
|
+
): TurnInjectionInputs {
|
|
1924
|
+
return {
|
|
1925
|
+
mode: options.mode,
|
|
1926
|
+
workspaceTopLevelContext: options.workspaceTopLevelContext,
|
|
1927
|
+
unifiedTurnContext: options.unifiedTurnContext,
|
|
1928
|
+
pkbContext: options.pkbContext,
|
|
1929
|
+
pkbActive: options.pkbActive,
|
|
1930
|
+
pkbQueryVector: options.pkbQueryVector,
|
|
1931
|
+
pkbSparseVector: options.pkbSparseVector,
|
|
1932
|
+
pkbScopeId: options.pkbScopeId,
|
|
1933
|
+
pkbConversation: options.pkbConversation,
|
|
1934
|
+
pkbAutoInjectList: options.pkbAutoInjectList,
|
|
1935
|
+
pkbRoot: options.pkbRoot,
|
|
1936
|
+
pkbWorkingDir: options.pkbWorkingDir,
|
|
1937
|
+
nowScratchpad: options.nowScratchpad,
|
|
1938
|
+
subagentStatusBlock: options.subagentStatusBlock,
|
|
1939
|
+
channelCapabilities: options.channelCapabilities,
|
|
1940
|
+
slackChronologicalMessages: options.slackChronologicalMessages,
|
|
1941
|
+
slackActiveThreadFocusBlock: options.slackActiveThreadFocusBlock,
|
|
1942
|
+
activeSurface: options.activeSurface,
|
|
1943
|
+
channelCommandContext: options.channelCommandContext,
|
|
1944
|
+
voiceCallControlPrompt: options.voiceCallControlPrompt,
|
|
1945
|
+
transportHints: options.transportHints,
|
|
1946
|
+
isNonInteractive: options.isNonInteractive,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/** Minimal synthetic TurnContext used when the caller omits one. */
|
|
1951
|
+
function synthesizeFallbackTurnContext(
|
|
1952
|
+
inputs: TurnInjectionInputs,
|
|
1953
|
+
): TurnContext {
|
|
1954
|
+
return {
|
|
1955
|
+
requestId: "runtime-assembly-fallback",
|
|
1956
|
+
conversationId: "runtime-assembly-fallback",
|
|
1957
|
+
turnIndex: 0,
|
|
1958
|
+
trust: {
|
|
1959
|
+
sourceChannel: inputs.channelCapabilities?.channel
|
|
1960
|
+
? (inputs.channelCapabilities.channel as TrustContext["sourceChannel"])
|
|
1961
|
+
: "vellum",
|
|
1962
|
+
trustClass: "unknown",
|
|
1963
|
+
},
|
|
1964
|
+
injectionInputs: inputs,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Apply the runtime-injection chain to `runMessages`.
|
|
1970
|
+
*
|
|
1971
|
+
* The canonical per-turn assembly pipeline for every provider call:
|
|
1972
|
+
*
|
|
1973
|
+
* 1. Build the per-turn {@link TurnInjectionInputs} bag from `options`.
|
|
1974
|
+
* 2. Layer it onto a {@link TurnContext} — either the one the caller
|
|
1975
|
+
* supplies via `options.turnContext` (preserving its `requestId`,
|
|
1976
|
+
* trust, and other fields) or an ephemeral fallback synthesized here.
|
|
1977
|
+
* 3. Drive the default + third-party {@link Injector} chain via
|
|
1978
|
+
* {@link collectInjectorBlocks}.
|
|
1979
|
+
* 4. Apply the chain's `"replace-run-messages"` block (Slack chronological
|
|
1980
|
+
* transcript) first so subsequent branches operate on the replaced
|
|
1981
|
+
* tail. When replacement fires, re-prepend any memory-prefix blocks
|
|
1982
|
+
* that `graphMemory.prepareMemory` had attached to the original tail —
|
|
1983
|
+
* the Slack transcript is rendered fresh from persisted rows and
|
|
1984
|
+
* carries no memory prefix of its own.
|
|
1985
|
+
* 5. Apply the chain's `"after-memory-prefix"` blocks in ascending
|
|
1986
|
+
* `order`. This runs BEFORE step 6's hardcoded prepends so the
|
|
1987
|
+
* memory-prefix counter sees only the memory blocks on the tail —
|
|
1988
|
+
* any `<channel_capabilities>` / `<channel_command_context>` /
|
|
1989
|
+
* `<transport_hints>` prepended first would push the count to zero
|
|
1990
|
+
* and force PKB / NOW to splice at the top of the tail. Within the
|
|
1991
|
+
* after-memory block, each successive splice lands at the memory
|
|
1992
|
+
* boundary, pushing earlier splices further from memory — so
|
|
1993
|
+
* higher-`order` blocks end up closer to the memory prefix.
|
|
1994
|
+
* 6. Run the remaining hardcoded branches (`isNonInteractive`,
|
|
1995
|
+
* `voiceCallControlPrompt`, `activeSurface`, `channelCapabilities`,
|
|
1996
|
+
* `channelCommandContext`, `transportHints`) in their historical order.
|
|
1997
|
+
* 7. Finally, apply the chain's remaining blocks by placement:
|
|
1998
|
+
* `"append-user-tail"` in ascending `order`, then `"prepend-user-tail"`
|
|
1999
|
+
* in descending `order` so the lowest-`order` prepend lands topmost in
|
|
2000
|
+
* the user tail content.
|
|
2001
|
+
*
|
|
2002
|
+
* Returns the final message array plus a `blocks` object holding the exact
|
|
2003
|
+
* injected text for each captured block — callers persist those bytes to
|
|
2004
|
+
* message metadata for later byte-exact rehydration.
|
|
1738
2005
|
*/
|
|
1739
2006
|
export async function applyRuntimeInjections(
|
|
1740
2007
|
runMessages: Message[],
|
|
1741
|
-
options:
|
|
1742
|
-
activeSurface?: ActiveSurfaceContext | null;
|
|
1743
|
-
workspaceTopLevelContext?: string | null;
|
|
1744
|
-
channelCapabilities?: ChannelCapabilities | null;
|
|
1745
|
-
channelCommandContext?: ChannelCommandContext | null;
|
|
1746
|
-
unifiedTurnContext?: string | null;
|
|
1747
|
-
voiceCallControlPrompt?: string | null;
|
|
1748
|
-
pkbContext?: string | null;
|
|
1749
|
-
pkbActive?: boolean;
|
|
1750
|
-
/**
|
|
1751
|
-
* Dense query vector surfaced from the graph memory retriever (PR 3).
|
|
1752
|
-
* When present together with `pkbActive`, used to run `searchPkbFiles`
|
|
1753
|
-
* to surface relevance hints in the PKB system reminder. When missing,
|
|
1754
|
-
* the reminder falls back to the flat static text.
|
|
1755
|
-
*/
|
|
1756
|
-
pkbQueryVector?: number[];
|
|
1757
|
-
/** Optional sparse vector accompanying `pkbQueryVector`. */
|
|
1758
|
-
pkbSparseVector?: QdrantSparseVector;
|
|
1759
|
-
/** Memory scope id used to filter PKB search results. */
|
|
1760
|
-
pkbScopeId?: string;
|
|
1761
|
-
/**
|
|
1762
|
-
* The live conversation (or a minimal shape containing `messages`) used
|
|
1763
|
-
* to compute which PKB paths are already "in context" and therefore
|
|
1764
|
-
* suppressed from hint suggestions.
|
|
1765
|
-
*/
|
|
1766
|
-
pkbConversation?: PkbContextConversation;
|
|
1767
|
-
/** Auto-injected PKB filenames (resolved relative to `pkbRoot`). */
|
|
1768
|
-
pkbAutoInjectList?: string[];
|
|
1769
|
-
/** Absolute path to the PKB directory (e.g. `<workspace>/pkb`). */
|
|
1770
|
-
pkbRoot?: string;
|
|
1771
|
-
/**
|
|
1772
|
-
* Working directory against which relative `file_read` tool paths
|
|
1773
|
-
* resolve, used to detect workspace-relative reads like
|
|
1774
|
-
* `pkb/threads.md`. Falls back to `pkbRoot` when omitted.
|
|
1775
|
-
*/
|
|
1776
|
-
pkbWorkingDir?: string;
|
|
1777
|
-
nowScratchpad?: string | null;
|
|
1778
|
-
subagentStatusBlock?: string | null;
|
|
1779
|
-
isNonInteractive?: boolean;
|
|
1780
|
-
transportHints?: string[] | null;
|
|
1781
|
-
/**
|
|
1782
|
-
* Pre-rendered Slack chronological transcript that replaces the
|
|
1783
|
-
* default `runMessages` history for any Slack conversation (channels
|
|
1784
|
-
* and DMs alike).
|
|
1785
|
-
*
|
|
1786
|
-
* When `channelCapabilities` describes a Slack conversation and this
|
|
1787
|
-
* array is non-empty, it overrides `runMessages` so the model sees one
|
|
1788
|
-
* chronologically-ordered transcript built from the stored Slack
|
|
1789
|
-
* metadata. Channel renders include sibling-thread tags; DM renders
|
|
1790
|
-
* are flat (DMs have no threads). The `transportHints` pipeline is
|
|
1791
|
-
* skipped for any Slack conversation so the persisted view isn't
|
|
1792
|
-
* duplicated by gateway-side hints.
|
|
1793
|
-
*
|
|
1794
|
-
* Callers build this via `loadSlackChronologicalMessages` (or the
|
|
1795
|
-
* underlying `assembleSlackChronologicalMessages`) before invoking
|
|
1796
|
-
* this function so the assembly path stays free of direct DB calls
|
|
1797
|
-
* and remains easy to test.
|
|
1798
|
-
*/
|
|
1799
|
-
slackChronologicalMessages?: Message[] | null;
|
|
1800
|
-
/**
|
|
1801
|
-
* Pre-rendered `<active_thread>` focus block listing the messages of
|
|
1802
|
-
* the thread the current inbound user message belongs to.
|
|
1803
|
-
*
|
|
1804
|
-
* Appended (tail-block) to the FINAL user message ONLY when
|
|
1805
|
-
* `channelCapabilities` describes a Slack non-DM channel. The block is
|
|
1806
|
-
* non-persisted: history rebuilds re-derive it from storage on each
|
|
1807
|
-
* turn, and `RUNTIME_INJECTION_PREFIXES` strips any `<active_thread>`
|
|
1808
|
-
* blocks from prior turns so they do not accumulate.
|
|
1809
|
-
*
|
|
1810
|
-
* Callers build this via `loadSlackActiveThreadFocusBlock` (or the
|
|
1811
|
-
* underlying `assembleSlackActiveThreadFocusBlock`). Pass `null` /
|
|
1812
|
-
* `undefined` when the inbound is a top-level (non-thread) post.
|
|
1813
|
-
*/
|
|
1814
|
-
slackActiveThreadFocusBlock?: string | null;
|
|
1815
|
-
mode?: InjectionMode;
|
|
1816
|
-
},
|
|
2008
|
+
options: RuntimeInjectionOptions,
|
|
1817
2009
|
): Promise<RuntimeInjectionResult> {
|
|
1818
2010
|
const mode = options.mode ?? "full";
|
|
1819
|
-
let pkbSystemReminderCaptured: string | undefined;
|
|
1820
|
-
const slackChannel = isSlackChannelConversation(options.channelCapabilities);
|
|
1821
|
-
// Slack DMs and channels both assemble context from persisted message
|
|
1822
|
-
// rows, so suppress hint injection for any Slack conversation. Other
|
|
1823
|
-
// channels (telegram, email, etc.) keep the generic hint pipeline.
|
|
1824
2011
|
const slackConversation = options.channelCapabilities?.channel === "slack";
|
|
2012
|
+
|
|
2013
|
+
// Build the per-injector inputs and attach them to the caller's
|
|
2014
|
+
// TurnContext (without mutating it). When the caller didn't supply one,
|
|
2015
|
+
// synthesize a minimal fallback so the chain still runs — test call sites
|
|
2016
|
+
// that drive injection via `options` without constructing a full context
|
|
2017
|
+
// continue to work.
|
|
2018
|
+
const injectionInputs = buildTurnInjectionInputs(options);
|
|
2019
|
+
const turnCtx: TurnContext = options.turnContext
|
|
2020
|
+
? { ...options.turnContext, injectionInputs }
|
|
2021
|
+
: synthesizeFallbackTurnContext(injectionInputs);
|
|
2022
|
+
|
|
2023
|
+
const chainBlocks = await collectInjectorBlocks(turnCtx);
|
|
2024
|
+
|
|
2025
|
+
// Split the chain output by placement so the downstream assembly can
|
|
2026
|
+
// process each slot with the correct ordering rule.
|
|
2027
|
+
const prepends: InjectionBlock[] = [];
|
|
2028
|
+
const appends: InjectionBlock[] = [];
|
|
2029
|
+
const afterMemory: InjectionBlock[] = [];
|
|
2030
|
+
let replaceBlock: InjectionBlock | null = null;
|
|
2031
|
+
for (const block of chainBlocks) {
|
|
2032
|
+
switch (block.placement ?? "append-user-tail") {
|
|
2033
|
+
case "replace-run-messages":
|
|
2034
|
+
// Later replace-run-messages blocks would overwrite earlier ones;
|
|
2035
|
+
// the default chain only registers one (the Slack transcript).
|
|
2036
|
+
replaceBlock = block;
|
|
2037
|
+
break;
|
|
2038
|
+
case "after-memory-prefix":
|
|
2039
|
+
afterMemory.push(block);
|
|
2040
|
+
break;
|
|
2041
|
+
case "prepend-user-tail":
|
|
2042
|
+
prepends.push(block);
|
|
2043
|
+
break;
|
|
2044
|
+
case "append-user-tail":
|
|
2045
|
+
appends.push(block);
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Track captured text for metadata persistence. Each field corresponds
|
|
2051
|
+
// to a specific default-injector block id so the loop below can pick up
|
|
2052
|
+
// the right capture without re-rendering.
|
|
2053
|
+
//
|
|
2054
|
+
// The capture is gated on the tail actually being a user message — if it
|
|
2055
|
+
// isn't, `applyInjectionBlock` no-ops the block and no content is actually
|
|
2056
|
+
// injected, so the persisted metadata must be undefined (matches
|
|
2057
|
+
// pre-migration behaviour where the `inject*` helpers short-circuited the
|
|
2058
|
+
// same way).
|
|
1825
2059
|
let turnContextCaptured: string | undefined;
|
|
2060
|
+
let workspaceCaptured: string | undefined;
|
|
2061
|
+
let nowScratchpadCaptured: string | undefined;
|
|
2062
|
+
let pkbContextCaptured: string | undefined;
|
|
2063
|
+
let pkbSystemReminderCaptured: string | undefined;
|
|
2064
|
+
const initialTail = runMessages[runMessages.length - 1];
|
|
2065
|
+
const initialTailIsUser = !!initialTail && initialTail.role === "user";
|
|
2066
|
+
if (initialTailIsUser) {
|
|
2067
|
+
for (const block of chainBlocks) {
|
|
2068
|
+
switch (block.id) {
|
|
2069
|
+
case "unified-turn-context":
|
|
2070
|
+
turnContextCaptured = block.text;
|
|
2071
|
+
break;
|
|
2072
|
+
case "workspace-context":
|
|
2073
|
+
workspaceCaptured = block.text;
|
|
2074
|
+
break;
|
|
2075
|
+
case "now-md":
|
|
2076
|
+
nowScratchpadCaptured = block.text;
|
|
2077
|
+
break;
|
|
2078
|
+
case "pkb-context":
|
|
2079
|
+
pkbContextCaptured = block.text;
|
|
2080
|
+
break;
|
|
2081
|
+
case "pkb-reminder":
|
|
2082
|
+
pkbSystemReminderCaptured = block.text;
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Compose the block text into a single informational string for
|
|
2089
|
+
// `injectorChainBlock`. Matches the pre-migration behaviour where the
|
|
2090
|
+
// field captured the composed view of every third-party injector on
|
|
2091
|
+
// the turn. We include default injectors here too so downstream
|
|
2092
|
+
// observers see the full set.
|
|
2093
|
+
const injectorChainPieces: string[] = [];
|
|
2094
|
+
for (const block of chainBlocks) {
|
|
2095
|
+
if (block.text.length > 0) injectorChainPieces.push(block.text);
|
|
2096
|
+
}
|
|
2097
|
+
const injectorChainBlock =
|
|
2098
|
+
injectorChainPieces.length > 0
|
|
2099
|
+
? injectorChainPieces.join("\n\n")
|
|
2100
|
+
: undefined;
|
|
2101
|
+
|
|
1826
2102
|
let result = runMessages;
|
|
1827
|
-
|
|
1828
|
-
//
|
|
1829
|
-
|
|
1830
|
-
// for channels and a flat sequence for DMs, so the same branch handles
|
|
1831
|
-
// both. The active-thread focus block below stays gated on `slackChannel`
|
|
1832
|
-
// since DMs do not have threads.
|
|
1833
|
-
if (
|
|
1834
|
-
slackConversation &&
|
|
1835
|
-
options.slackChronologicalMessages &&
|
|
1836
|
-
options.slackChronologicalMessages.length > 0
|
|
1837
|
-
) {
|
|
2103
|
+
|
|
2104
|
+
// ── Step 1: Slack chronological replacement (chain "replace" block) ──
|
|
2105
|
+
if (replaceBlock && replaceBlock.messagesOverride) {
|
|
1838
2106
|
// `graphMemory.prepareMemory` prepends a `<memory __injected>` block
|
|
1839
2107
|
// (and any memory-image groups) to the last user message before
|
|
1840
2108
|
// runtime assembly runs. The Slack transcript is freshly rendered
|
|
1841
2109
|
// from persisted rows and has no such prefix, so swap it in and then
|
|
1842
2110
|
// re-prepend the captured prefix onto the new tail user message.
|
|
1843
2111
|
const carriedMemoryBlocks = extractMemoryPrefixBlocks(runMessages);
|
|
1844
|
-
result =
|
|
2112
|
+
result = replaceBlock.messagesOverride;
|
|
1845
2113
|
if (carriedMemoryBlocks.length > 0) {
|
|
1846
2114
|
const slackTail = result[result.length - 1];
|
|
1847
2115
|
if (slackTail && slackTail.role === "user") {
|
|
@@ -1856,6 +2124,25 @@ export async function applyRuntimeInjections(
|
|
|
1856
2124
|
}
|
|
1857
2125
|
}
|
|
1858
2126
|
|
|
2127
|
+
// ── Step 2: after-memory-prefix chain blocks ──
|
|
2128
|
+
// These splice relative to the memory-prefix count on the tail content,
|
|
2129
|
+
// so they must run BEFORE the hardcoded prepends in step 3. Otherwise
|
|
2130
|
+
// any prepended `<channel_capabilities>` / `<channel_command_context>` /
|
|
2131
|
+
// `<transport_hints>` (none of which are memory-prefix blocks) would
|
|
2132
|
+
// drop the count to 0 and PKB / NOW would splice at the very top of
|
|
2133
|
+
// the tail instead of immediately after memory.
|
|
2134
|
+
//
|
|
2135
|
+
// Ascending `order`: each splice lands at the memory-prefix boundary,
|
|
2136
|
+
// pushing any previously-spliced block one slot further from memory.
|
|
2137
|
+
// So higher-`order` blocks end up closer to the memory prefix.
|
|
2138
|
+
for (const block of afterMemory) {
|
|
2139
|
+
result = applyInjectionBlock(result, block);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// ── Step 3: hardcoded branches that stayed outside the injector chain ──
|
|
2143
|
+
// These run in the same historical order as before G2.1 so their
|
|
2144
|
+
// interleaving with any prior tail content stays stable.
|
|
2145
|
+
|
|
1859
2146
|
// For non-interactive conversations (scheduled jobs, work items), instruct the
|
|
1860
2147
|
// model to never ask for clarification — there is no human present to answer.
|
|
1861
2148
|
if (options.isNonInteractive) {
|
|
@@ -1887,117 +2174,6 @@ export async function applyRuntimeInjections(
|
|
|
1887
2174
|
}
|
|
1888
2175
|
}
|
|
1889
2176
|
|
|
1890
|
-
if (mode === "full" && options.pkbContext) {
|
|
1891
|
-
const userTail = result[result.length - 1];
|
|
1892
|
-
if (userTail && userTail.role === "user") {
|
|
1893
|
-
result = [
|
|
1894
|
-
...result.slice(0, -1),
|
|
1895
|
-
injectPkbContext(userTail, options.pkbContext),
|
|
1896
|
-
];
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// PKB behavioral nudge — injected on every turn when PKB is active so
|
|
1901
|
-
// the model keeps reading topic files and calling `remember`. When a
|
|
1902
|
-
// query vector is available from the graph memory retriever, run a
|
|
1903
|
-
// hybrid PKB search to surface up to three relevance hints; fall back
|
|
1904
|
-
// to the flat static reminder on empty results or any error.
|
|
1905
|
-
if (mode === "full" && options.pkbActive) {
|
|
1906
|
-
const userTail = result[result.length - 1];
|
|
1907
|
-
if (userTail && userTail.role === "user") {
|
|
1908
|
-
let hints: string[] = [];
|
|
1909
|
-
const queryVector = options.pkbQueryVector;
|
|
1910
|
-
if (
|
|
1911
|
-
queryVector &&
|
|
1912
|
-
queryVector.length > 0 &&
|
|
1913
|
-
options.pkbScopeId &&
|
|
1914
|
-
options.pkbConversation &&
|
|
1915
|
-
options.pkbRoot
|
|
1916
|
-
) {
|
|
1917
|
-
try {
|
|
1918
|
-
const results = await searchPkbFiles(
|
|
1919
|
-
queryVector,
|
|
1920
|
-
options.pkbSparseVector,
|
|
1921
|
-
8,
|
|
1922
|
-
[options.pkbScopeId],
|
|
1923
|
-
);
|
|
1924
|
-
const workingDir = options.pkbWorkingDir ?? options.pkbRoot;
|
|
1925
|
-
const inContext = getInContextPkbPaths(
|
|
1926
|
-
options.pkbConversation,
|
|
1927
|
-
options.pkbAutoInjectList ?? [],
|
|
1928
|
-
options.pkbRoot,
|
|
1929
|
-
workingDir,
|
|
1930
|
-
);
|
|
1931
|
-
const pkbRoot = options.pkbRoot;
|
|
1932
|
-
// Gate on `denseScore` (cosine, [0, 1]) so the quality bar is stable
|
|
1933
|
-
// regardless of whether sparse was provided. Rank by `hybridScore`
|
|
1934
|
-
// (RRF) when available — that captures the sparse signal for
|
|
1935
|
-
// re-ordering eligible hits. hybridScore and denseScore live on
|
|
1936
|
-
// different scales, so items with hybridScore are ordered together
|
|
1937
|
-
// and placed ahead of items that only have denseScore.
|
|
1938
|
-
hints = results
|
|
1939
|
-
.filter((r) => {
|
|
1940
|
-
const abs = resolve(pkbRoot, r.path);
|
|
1941
|
-
if (inContext.has(abs)) return false;
|
|
1942
|
-
const threshold = r.path
|
|
1943
|
-
.replace(/\\/g, "/")
|
|
1944
|
-
.startsWith("archive/")
|
|
1945
|
-
? PKB_HINT_ARCHIVE_THRESHOLD
|
|
1946
|
-
: PKB_HINT_THRESHOLD;
|
|
1947
|
-
return r.denseScore >= threshold;
|
|
1948
|
-
})
|
|
1949
|
-
.sort((a, b) => {
|
|
1950
|
-
const aHasHybrid = a.hybridScore !== undefined;
|
|
1951
|
-
const bHasHybrid = b.hybridScore !== undefined;
|
|
1952
|
-
if (aHasHybrid && !bHasHybrid) return -1;
|
|
1953
|
-
if (!aHasHybrid && bHasHybrid) return 1;
|
|
1954
|
-
if (aHasHybrid && bHasHybrid) {
|
|
1955
|
-
return b.hybridScore! - a.hybridScore!;
|
|
1956
|
-
}
|
|
1957
|
-
return b.denseScore - a.denseScore;
|
|
1958
|
-
})
|
|
1959
|
-
.slice(0, 3)
|
|
1960
|
-
.map((r) => r.path);
|
|
1961
|
-
} catch (err) {
|
|
1962
|
-
pkbReminderLog.warn(
|
|
1963
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
1964
|
-
"PKB hint search failed — falling back to flat reminder",
|
|
1965
|
-
);
|
|
1966
|
-
hints = [];
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
const reminder = buildPkbReminder(hints);
|
|
1971
|
-
pkbSystemReminderCaptured = reminder;
|
|
1972
|
-
// Splice the reminder in right after the memory prefix blocks so it
|
|
1973
|
-
// lands above the user's typed text, producing the tail shape
|
|
1974
|
-
// `[<turn_context>, <memory __injected>, <system_reminder>, ...your_text, ...later_appends]`
|
|
1975
|
-
// after `unifiedTurnContext` later prepends `<turn_context>` at index 0.
|
|
1976
|
-
const memoryPrefixCount = countMemoryPrefixBlocks(userTail.content);
|
|
1977
|
-
result = [
|
|
1978
|
-
...result.slice(0, -1),
|
|
1979
|
-
{
|
|
1980
|
-
...userTail,
|
|
1981
|
-
content: [
|
|
1982
|
-
...userTail.content.slice(0, memoryPrefixCount),
|
|
1983
|
-
{ type: "text" as const, text: reminder },
|
|
1984
|
-
...userTail.content.slice(memoryPrefixCount),
|
|
1985
|
-
],
|
|
1986
|
-
},
|
|
1987
|
-
];
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
if (mode === "full" && options.nowScratchpad) {
|
|
1992
|
-
const userTail = result[result.length - 1];
|
|
1993
|
-
if (userTail && userTail.role === "user") {
|
|
1994
|
-
result = [
|
|
1995
|
-
...result.slice(0, -1),
|
|
1996
|
-
injectNowScratchpad(userTail, options.nowScratchpad),
|
|
1997
|
-
];
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
2177
|
if (mode === "full" && options.activeSurface) {
|
|
2002
2178
|
const userTail = result[result.length - 1];
|
|
2003
2179
|
if (userTail && userTail.role === "user") {
|
|
@@ -2028,33 +2204,6 @@ export async function applyRuntimeInjections(
|
|
|
2028
2204
|
}
|
|
2029
2205
|
}
|
|
2030
2206
|
|
|
2031
|
-
if (mode === "full" && options.subagentStatusBlock) {
|
|
2032
|
-
const userTail = result[result.length - 1];
|
|
2033
|
-
if (userTail && userTail.role === "user") {
|
|
2034
|
-
result = [
|
|
2035
|
-
...result.slice(0, -1),
|
|
2036
|
-
injectSubagentStatus(userTail, options.subagentStatusBlock),
|
|
2037
|
-
];
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
if (options.unifiedTurnContext) {
|
|
2042
|
-
const userTail = result[result.length - 1];
|
|
2043
|
-
if (userTail && userTail.role === "user") {
|
|
2044
|
-
turnContextCaptured = options.unifiedTurnContext;
|
|
2045
|
-
result = [
|
|
2046
|
-
...result.slice(0, -1),
|
|
2047
|
-
{
|
|
2048
|
-
...userTail,
|
|
2049
|
-
content: [
|
|
2050
|
-
{ type: "text" as const, text: options.unifiedTurnContext },
|
|
2051
|
-
...userTail.content,
|
|
2052
|
-
],
|
|
2053
|
-
},
|
|
2054
|
-
];
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
2207
|
// Slack conversations (both channels and DMs) build their own
|
|
2059
2208
|
// chronological transcript from persisted messages and intentionally do
|
|
2060
2209
|
// not receive the per-turn `<transport_hints>` block — the rendered
|
|
@@ -2076,50 +2225,18 @@ export async function applyRuntimeInjections(
|
|
|
2076
2225
|
}
|
|
2077
2226
|
}
|
|
2078
2227
|
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
// `RUNTIME_INJECTION_PREFIXES` list so focus blocks never accumulate.
|
|
2085
|
-
if (
|
|
2086
|
-
mode === "full" &&
|
|
2087
|
-
slackChannel &&
|
|
2088
|
-
typeof options.slackActiveThreadFocusBlock === "string" &&
|
|
2089
|
-
options.slackActiveThreadFocusBlock.length > 0
|
|
2090
|
-
) {
|
|
2091
|
-
const userTail = result[result.length - 1];
|
|
2092
|
-
if (userTail && userTail.role === "user") {
|
|
2093
|
-
result = [
|
|
2094
|
-
...result.slice(0, -1),
|
|
2095
|
-
{
|
|
2096
|
-
...userTail,
|
|
2097
|
-
content: [
|
|
2098
|
-
...userTail.content,
|
|
2099
|
-
{
|
|
2100
|
-
type: "text" as const,
|
|
2101
|
-
text: options.slackActiveThreadFocusBlock,
|
|
2102
|
-
},
|
|
2103
|
-
],
|
|
2104
|
-
},
|
|
2105
|
-
];
|
|
2106
|
-
}
|
|
2228
|
+
// ── Step 4: apply remaining chain blocks by placement ──
|
|
2229
|
+
// append-user-tail: ascending `order` so lower-order blocks come first
|
|
2230
|
+
// in the append sequence.
|
|
2231
|
+
for (const block of appends) {
|
|
2232
|
+
result = applyInjectionBlock(result, block);
|
|
2107
2233
|
}
|
|
2108
2234
|
|
|
2109
|
-
//
|
|
2110
|
-
//
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
if (userTail && userTail.role === "user") {
|
|
2115
|
-
result = [
|
|
2116
|
-
...result.slice(0, -1),
|
|
2117
|
-
injectWorkspaceTopLevelContext(
|
|
2118
|
-
userTail,
|
|
2119
|
-
options.workspaceTopLevelContext,
|
|
2120
|
-
),
|
|
2121
|
-
];
|
|
2122
|
-
}
|
|
2235
|
+
// prepend-user-tail: descending `order` so the lowest-order block lands
|
|
2236
|
+
// topmost in the tail content (each successive prepend pushes the
|
|
2237
|
+
// previous one further down).
|
|
2238
|
+
for (let i = prepends.length - 1; i >= 0; i--) {
|
|
2239
|
+
result = applyInjectionBlock(result, prepends[i]);
|
|
2123
2240
|
}
|
|
2124
2241
|
|
|
2125
2242
|
return {
|
|
@@ -2127,6 +2244,10 @@ export async function applyRuntimeInjections(
|
|
|
2127
2244
|
blocks: {
|
|
2128
2245
|
unifiedTurnContext: turnContextCaptured,
|
|
2129
2246
|
pkbSystemReminder: pkbSystemReminderCaptured,
|
|
2247
|
+
workspaceBlock: workspaceCaptured,
|
|
2248
|
+
nowScratchpadBlock: nowScratchpadCaptured,
|
|
2249
|
+
pkbContextBlock: pkbContextCaptured,
|
|
2250
|
+
injectorChainBlock,
|
|
2130
2251
|
},
|
|
2131
2252
|
};
|
|
2132
2253
|
}
|