@vellumai/assistant 0.11.3 → 0.11.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -147,6 +147,11 @@ const { _resetAdapterInstallCacheForTests } =
|
|
|
147
147
|
await import("../../acp/auto-install.js");
|
|
148
148
|
const { ACP_CLAUDE_OAUTH_MISSING_CODE } =
|
|
149
149
|
await import("../../acp/prepare-agent-env.js");
|
|
150
|
+
const { ACP_CLAUDE_AUTH_REQUIRED_CODE, AcpAuthRequiredError } =
|
|
151
|
+
await import("../../acp/auth-required.js");
|
|
152
|
+
const { hasAcpConnectCardRaised } =
|
|
153
|
+
await import("../../acp/acp-connect-card-state.js");
|
|
154
|
+
const { AcpAgentProcess } = await import("../../acp/agent-process.js");
|
|
150
155
|
|
|
151
156
|
// ---------------------------------------------------------------------------
|
|
152
157
|
// Helpers
|
|
@@ -547,3 +552,95 @@ describe("executeAcpSpawn — CLAUDE_CODE_OAUTH_TOKEN injection", () => {
|
|
|
547
552
|
expect(result.errorCode).toBe(ACP_CLAUDE_OAUTH_MISSING_CODE);
|
|
548
553
|
});
|
|
549
554
|
});
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Pre-spawn Claude auth rejection
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
describe("pre-spawn Claude auth rejection", () => {
|
|
561
|
+
test("raises the recovery surface: errorCode, card guidance, registry mark", async () => {
|
|
562
|
+
// The adapter can reject session creation with auth_required (a stored
|
|
563
|
+
// credential the server refuses). The error thrown here is produced by
|
|
564
|
+
// the REAL conversion seam, not hand-built: a wire-shaped JSON-RPC
|
|
565
|
+
// auth_required rejection driven through AcpAgentProcess.createSession's
|
|
566
|
+
// actual retry path, so this test fails if that seam stops yielding the
|
|
567
|
+
// typed error the tool boundary maps to the recovery surface.
|
|
568
|
+
const proc = new AcpAgentProcess(
|
|
569
|
+
"claude",
|
|
570
|
+
{ command: "claude-agent-acp", args: [] },
|
|
571
|
+
(() => ({})) as never,
|
|
572
|
+
);
|
|
573
|
+
(proc as unknown as { connection: unknown }).connection = {
|
|
574
|
+
newSession: () =>
|
|
575
|
+
Promise.reject({ code: -32000, message: "Authentication required" }),
|
|
576
|
+
};
|
|
577
|
+
(proc as unknown as { initializeResponse: unknown }).initializeResponse = {
|
|
578
|
+
authMethods: [
|
|
579
|
+
{
|
|
580
|
+
id: "claude-ai-login",
|
|
581
|
+
name: "Claude Subscription",
|
|
582
|
+
type: "terminal",
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
(proc as unknown as { spawnedEnv: unknown }).spawnedEnv = {};
|
|
587
|
+
const realSeamError: unknown = await proc.createSession("/tmp").then(
|
|
588
|
+
() => null,
|
|
589
|
+
(e: unknown) => e,
|
|
590
|
+
);
|
|
591
|
+
expect(realSeamError).toBeInstanceOf(AcpAuthRequiredError);
|
|
592
|
+
|
|
593
|
+
spawnMock.mockImplementationOnce(async () => {
|
|
594
|
+
throw realSeamError;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const context = { ...makeContext(), conversationId: "conv-prespawn-auth" };
|
|
598
|
+
const result = await executeAcpSpawn(
|
|
599
|
+
{ agent: "claude", task: "do something" },
|
|
600
|
+
context,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
expect(result.isError).toBe(true);
|
|
604
|
+
expect(result.errorCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
|
|
605
|
+
expect(result.content).toContain("Connect Claude Code");
|
|
606
|
+
expect(hasAcpConnectCardRaised("conv-prespawn-auth")).toBe(true);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("a raw wire auth_required object (unwrapped initialize path) gets the same surface", async () => {
|
|
610
|
+
// initialize() cannot ride withAuthRetry (its response advertises the
|
|
611
|
+
// methods the retry selects from), so its rejection arrives as the plain
|
|
612
|
+
// JSON-RPC object rather than the typed error. The gate must recognize
|
|
613
|
+
// both shapes or this path silently flattens to a generic failure.
|
|
614
|
+
spawnMock.mockImplementationOnce(async () => {
|
|
615
|
+
throw { code: -32000, message: "Authentication required" };
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const context = { ...makeContext(), conversationId: "conv-prespawn-raw" };
|
|
619
|
+
const result = await executeAcpSpawn(
|
|
620
|
+
{ agent: "claude", task: "do something" },
|
|
621
|
+
context,
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
expect(result.isError).toBe(true);
|
|
625
|
+
expect(result.errorCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
|
|
626
|
+
expect(result.content).toContain("Connect Claude Code");
|
|
627
|
+
expect(hasAcpConnectCardRaised("conv-prespawn-raw")).toBe(true);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("a non-auth spawn failure keeps the plain flattened result", async () => {
|
|
631
|
+
spawnMock.mockImplementationOnce(async () => {
|
|
632
|
+
throw new Error("adapter exploded");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const context = { ...makeContext(), conversationId: "conv-prespawn-boom" };
|
|
636
|
+
const result = await executeAcpSpawn(
|
|
637
|
+
{ agent: "claude", task: "do something" },
|
|
638
|
+
context,
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
expect(result.isError).toBe(true);
|
|
642
|
+
expect(result.errorCode).toBeUndefined();
|
|
643
|
+
expect(result.content).toContain("adapter exploded");
|
|
644
|
+
expect(hasAcpConnectCardRaised("conv-prespawn-boom")).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
});
|
package/src/tools/acp/spawn.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { basename } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { markAcpConnectCardRaised } from "../../acp/acp-connect-card-state.js";
|
|
6
|
+
import {
|
|
7
|
+
ACP_AUTH_RECOVERY_GUIDANCE,
|
|
8
|
+
ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
9
|
+
CLAUDE_ACP_COMMAND,
|
|
10
|
+
isAcpAuthRequired,
|
|
11
|
+
} from "../../acp/auth-required.js";
|
|
6
12
|
import { resolveAgentWithAutoInstall } from "../../acp/auto-install.js";
|
|
7
13
|
import { getAcpSessionManager } from "../../acp/index.js";
|
|
8
14
|
import {
|
|
@@ -147,6 +153,32 @@ export async function executeAcpSpawn(
|
|
|
147
153
|
|
|
148
154
|
return { content: payload, isError: false };
|
|
149
155
|
} catch (err) {
|
|
156
|
+
// A pre-spawn rejection of the stored Claude credential (the adapter
|
|
157
|
+
// raises auth_required during session creation) gets the same recovery
|
|
158
|
+
// surface as the missing-token preflight: the errorCode raises the inline
|
|
159
|
+
// Connect card off this failed tool call, the registry mark dedups a
|
|
160
|
+
// redundant secure prompt, and the guidance keeps the model pointed at
|
|
161
|
+
// the card. Adapter-gated: the card repairs Claude credentials only.
|
|
162
|
+
// `isAcpAuthRequired` rather than an instanceof: it also recognizes the
|
|
163
|
+
// raw JSON-RPC auth_required object, which is how a rejection reaches
|
|
164
|
+
// this catch from the one protocol call `withAuthRetry` cannot wrap
|
|
165
|
+
// (`initialize`, whose response is what advertises the auth methods the
|
|
166
|
+
// retry would need).
|
|
167
|
+
if (
|
|
168
|
+
isAcpAuthRequired(err) &&
|
|
169
|
+
basename(agentConfig.command) === CLAUDE_ACP_COMMAND
|
|
170
|
+
) {
|
|
171
|
+
const message =
|
|
172
|
+
err instanceof Error
|
|
173
|
+
? err.message
|
|
174
|
+
: String((err as { message?: unknown }).message ?? err);
|
|
175
|
+
markAcpConnectCardRaised(context.conversationId);
|
|
176
|
+
return {
|
|
177
|
+
content: `${message}\n\n${ACP_AUTH_RECOVERY_GUIDANCE}`,
|
|
178
|
+
isError: true,
|
|
179
|
+
errorCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
150
182
|
const msg =
|
|
151
183
|
err instanceof Error
|
|
152
184
|
? err.message
|
|
@@ -426,13 +426,22 @@ export function executeDocumentUpdate(
|
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// The store drops append content that merely restates the document's tail,
|
|
430
|
+
// so the client renders what actually landed rather than the submission.
|
|
431
|
+
const applied = result.appliedMarkdown;
|
|
432
|
+
const message = result.duplicateLeadingContentSkipped
|
|
433
|
+
? applied.length === 0
|
|
434
|
+
? "Nothing appended: the content is already at the end of the document"
|
|
435
|
+
: "Document content updated (leading content already at the end of the document was skipped)"
|
|
436
|
+
: "Document content updated";
|
|
437
|
+
|
|
429
438
|
// Send document_editor_update message to update the built-in RTE
|
|
430
439
|
if (context.sendToClient) {
|
|
431
440
|
context.sendToClient({
|
|
432
441
|
type: "document_editor_update",
|
|
433
442
|
conversationId: context.conversationId,
|
|
434
443
|
surfaceId,
|
|
435
|
-
markdown:
|
|
444
|
+
markdown: applied,
|
|
436
445
|
mode,
|
|
437
446
|
});
|
|
438
447
|
|
|
@@ -441,7 +450,7 @@ export function executeDocumentUpdate(
|
|
|
441
450
|
success: true,
|
|
442
451
|
surface_id: surfaceId,
|
|
443
452
|
mode,
|
|
444
|
-
message
|
|
453
|
+
message,
|
|
445
454
|
}),
|
|
446
455
|
isError: false,
|
|
447
456
|
};
|
|
@@ -458,7 +467,7 @@ export function executeDocumentUpdate(
|
|
|
458
467
|
success: true,
|
|
459
468
|
surface_id: surfaceId,
|
|
460
469
|
mode,
|
|
461
|
-
message:
|
|
470
|
+
message: `${message} (no client connected to render it)`,
|
|
462
471
|
}),
|
|
463
472
|
isError: false,
|
|
464
473
|
};
|
package/src/tools/registry.ts
CHANGED
|
@@ -431,7 +431,8 @@ export function unregisterPluginTools(pluginName: string): void {
|
|
|
431
431
|
* {@link import("../plugins/mtime-cache.js").getActiveUserPluginTools} and diffs
|
|
432
432
|
* the result into the registry: plugins that vanished are unregistered, new
|
|
433
433
|
* plugins register, and a plugin whose per-tool source mtimes moved is
|
|
434
|
-
* re-registered. Mirrors how hooks resolve through `getUserHookEntriesFor
|
|
434
|
+
* re-registered. Mirrors how hooks resolve through `getUserHookEntriesFor`
|
|
435
|
+
* in `hooks/registry.ts`.
|
|
435
436
|
*
|
|
436
437
|
* This is a pure read of the plugin cache — it does NOT reconcile the cache
|
|
437
438
|
* against the source-versions sentinel, so it never activates a plugin or runs
|
|
@@ -211,6 +211,17 @@ export const SURFACE_SHAPE_DOCS: Record<string, SurfaceShapeDoc> = {
|
|
|
211
211
|
? null
|
|
212
212
|
: '`data.channel` must be one of "slack", "telegram", "phone"',
|
|
213
213
|
},
|
|
214
|
+
voice_picker: {
|
|
215
|
+
// The steering lives in `purpose` because that is the only part of a cold
|
|
216
|
+
// type the model ever sees: `shape` ships solely inside a teaching error,
|
|
217
|
+
// and an empty payload has no missing-content state to raise one. The
|
|
218
|
+
// surface exists to replace prose voice tours, so losing the directive
|
|
219
|
+
// would leave nothing but a renderer nobody invokes.
|
|
220
|
+
purpose:
|
|
221
|
+
"inline picker for the voice the assistant speaks in; show it when the user asks to change, hear, or pick a voice rather than describing voices in prose or sending them to Settings; attach no actions and never set await_action; during a live voice call answer in speech instead, the room already carries its own picker",
|
|
222
|
+
shape:
|
|
223
|
+
"{}: no payload, the card reads the current voice and the catalog itself. Selecting a voice applies on the assistant's next spoken turn",
|
|
224
|
+
},
|
|
214
225
|
};
|
|
215
226
|
|
|
216
227
|
/** Model-facing surface_type enum, derived so docs and schema cannot drift. */
|
|
@@ -55,8 +55,7 @@ export const runWorkflowInputSchema = z.looseObject({
|
|
|
55
55
|
*/
|
|
56
56
|
function resolveTrustContext(context: ToolContext): TrustContext {
|
|
57
57
|
const conversation = findConversation(context.conversationId);
|
|
58
|
-
const fromConversation =
|
|
59
|
-
conversation?.currentTurnTrustContext ?? conversation?.trustContext;
|
|
58
|
+
const fromConversation = conversation?.getTurnOrRestingTrust();
|
|
60
59
|
if (fromConversation) {
|
|
61
60
|
return fromConversation;
|
|
62
61
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createReasoningTagFilter } from "../reasoning-tag-filter.js";
|
|
4
|
+
|
|
5
|
+
function run(chunks: string[]): string {
|
|
6
|
+
const filter = createReasoningTagFilter();
|
|
7
|
+
let out = "";
|
|
8
|
+
for (const chunk of chunks) {
|
|
9
|
+
out += filter.push(chunk);
|
|
10
|
+
}
|
|
11
|
+
out += filter.flush();
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("ReasoningTagFilter", () => {
|
|
16
|
+
test("passes plain text through unchanged", () => {
|
|
17
|
+
expect(run(["Hello ", "there."])).toBe("Hello there.");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("drops a complete think span within one delta", () => {
|
|
21
|
+
expect(run(["<think>hm, let me reason</think>Answer."])).toBe("Answer.");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("drops a reasoning span crossing many deltas", () => {
|
|
25
|
+
expect(
|
|
26
|
+
run(["<thi", "nk>step one", " step two", "</th", "ink>Spoken reply"]),
|
|
27
|
+
).toBe("Spoken reply");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("drops thinking-tag variant and is case-insensitive", () => {
|
|
31
|
+
expect(run(["<Thinking>inner</Thinking>ok"])).toBe("ok");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps text before and after the span", () => {
|
|
35
|
+
expect(run(["Sure. <think>secret</think> Done."])).toBe("Sure. Done.");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("handles multiple spans in one turn", () => {
|
|
39
|
+
expect(run(["a<think>x</think>b", "<think>y", "z</think>c"])).toBe("abc");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("an unclosed span at end of stream stays dropped", () => {
|
|
43
|
+
expect(run(["ok<think>never closes"])).toBe("ok");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a dangling partial open tag is released as literal text at flush", () => {
|
|
47
|
+
expect(run(["price <th"])).toBe("price <th");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a false partial that disambiguates to text is released", () => {
|
|
51
|
+
expect(run(["a <th", "ree-step plan"])).toBe("a <three-step plan");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("does not touch angle brackets that are not think tags", () => {
|
|
55
|
+
expect(run(["use <b>bold</b> and x < y"])).toBe(
|
|
56
|
+
"use <b>bold</b> and x < y",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("holds back nothing across an empty stream", () => {
|
|
61
|
+
expect(run([""])).toBe("");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("does not shift indexes for characters whose lowercase form is longer", () => {
|
|
65
|
+
// U+0130 (dotted capital I) lowers to two code units; positional
|
|
66
|
+
// scanning must never lowercase the haystack wholesale.
|
|
67
|
+
expect(run(["\u0130stanbul rocks. <think>hidden</think> Done."])).toBe(
|
|
68
|
+
"\u0130stanbul rocks. Done.",
|
|
69
|
+
);
|
|
70
|
+
expect(run(["\u0130\u0130\u0130<THINK>x</THINK>ok"])).toBe(
|
|
71
|
+
"\u0130\u0130\u0130ok",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("prefers the longer tag when both match at one index", () => {
|
|
76
|
+
expect(run(["a<thinking>x</thinking>b"])).toBe("ab");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming reasoning-tag filter for the TTS pipelines.
|
|
3
|
+
*
|
|
4
|
+
* Some OpenAI-compatible reasoning models emit their chain of thought inline
|
|
5
|
+
* in the content stream as `<think>...</think>` (or `<thinking>...</thinking>`)
|
|
6
|
+
* spans. When a profile has not opted into the chat-completions provider's
|
|
7
|
+
* `parseThinkTags` (which re-routes those spans to `thinking_delta`), the raw
|
|
8
|
+
* reasoning rides `text_delta`, and anything downstream that speaks text
|
|
9
|
+
* deltas would read the model's inner monologue aloud and delay real speech
|
|
10
|
+
* by the full length of the reasoning.
|
|
11
|
+
*
|
|
12
|
+
* This filter is the TTS-boundary guard: voice output must never speak
|
|
13
|
+
* reasoning regardless of profile configuration. It is stateful because a
|
|
14
|
+
* tag or a reasoning span routinely crosses delta boundaries; feed every
|
|
15
|
+
* delta through {@link ReasoningTagFilter.push} and call
|
|
16
|
+
* {@link ReasoningTagFilter.flush} when the turn's stream ends.
|
|
17
|
+
*
|
|
18
|
+
* Display paths are deliberately untouched: `assistant_text_delta` frames
|
|
19
|
+
* keep the raw text, exactly like the markdown handling in
|
|
20
|
+
* `calls/tts-text-sanitizer.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Tag scanning lives in `util/think-tag-stream.ts`, shared with the
|
|
23
|
+
* chat-completions provider's think-tag router, and is positionally exact
|
|
24
|
+
* on the original string (no full-string lowercasing, which shifts indexes
|
|
25
|
+
* for characters like the dotted capital I).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { indexOfTag, partialTagSuffix } from "../util/think-tag-stream.js";
|
|
29
|
+
|
|
30
|
+
const OPEN_TAGS = ["<think>", "<thinking>"] as const;
|
|
31
|
+
const CLOSE_TAGS = ["</think>", "</thinking>"] as const;
|
|
32
|
+
|
|
33
|
+
export class ReasoningTagFilter {
|
|
34
|
+
private insideReasoning = false;
|
|
35
|
+
private pending = "";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Feed one streamed delta; returns the speakable text it releases.
|
|
39
|
+
* Reasoning spans are dropped; a partial tag at the end of the buffer is
|
|
40
|
+
* held back until the next delta disambiguates it.
|
|
41
|
+
*/
|
|
42
|
+
push(text: string): string {
|
|
43
|
+
this.pending += text;
|
|
44
|
+
let out = "";
|
|
45
|
+
for (;;) {
|
|
46
|
+
if (this.insideReasoning) {
|
|
47
|
+
const close = indexOfTag(this.pending, CLOSE_TAGS, true);
|
|
48
|
+
if (close) {
|
|
49
|
+
this.pending = this.pending.slice(close.index + close.tag.length);
|
|
50
|
+
this.insideReasoning = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Everything buffered is reasoning except a possible partial close
|
|
54
|
+
// tag at the end; drop the reasoning, keep the partial.
|
|
55
|
+
const partial = partialTagSuffix(this.pending, CLOSE_TAGS, true);
|
|
56
|
+
this.pending = partial > 0 ? this.pending.slice(-partial) : "";
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
const open = indexOfTag(this.pending, OPEN_TAGS, true);
|
|
60
|
+
if (open) {
|
|
61
|
+
out += this.pending.slice(0, open.index);
|
|
62
|
+
this.pending = this.pending.slice(open.index + open.tag.length);
|
|
63
|
+
this.insideReasoning = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const partial = partialTagSuffix(this.pending, OPEN_TAGS, true);
|
|
67
|
+
const safeLength = this.pending.length - partial;
|
|
68
|
+
out += this.pending.slice(0, safeLength);
|
|
69
|
+
this.pending = partial > 0 ? this.pending.slice(safeLength) : "";
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* End of stream: release any held-back partial. Outside a reasoning span a
|
|
76
|
+
* dangling "<thin" was ordinary text after all; inside one, the span never
|
|
77
|
+
* closed and its content stays dropped.
|
|
78
|
+
*/
|
|
79
|
+
flush(): string {
|
|
80
|
+
const rest = this.insideReasoning ? "" : this.pending;
|
|
81
|
+
this.pending = "";
|
|
82
|
+
this.insideReasoning = false;
|
|
83
|
+
return rest;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createReasoningTagFilter(): ReasoningTagFilter {
|
|
88
|
+
return new ReasoningTagFilter();
|
|
89
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared primitives for streaming think-tag scanning.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers parse inline `<think>` spans out of streamed model content
|
|
5
|
+
* and must not drift apart (single source of truth):
|
|
6
|
+
*
|
|
7
|
+
* - `providers/openai/chat-completions-provider.ts` ROUTES the spans to
|
|
8
|
+
* `thinking_delta` events (content preserved, case-sensitive `<think>`
|
|
9
|
+
* only, matching the models that emit it).
|
|
10
|
+
* - `tts/reasoning-tag-filter.ts` DROPS the spans from the spoken stream
|
|
11
|
+
* (case-insensitive, `<think>` and `<thinking>` variants).
|
|
12
|
+
*
|
|
13
|
+
* The state machines stay separate because they do different things with
|
|
14
|
+
* the reasoning; what they share is the tag scanning below. All scanning is
|
|
15
|
+
* positionally exact on the ORIGINAL string: case-insensitive mode folds
|
|
16
|
+
* A-Z per character code and never calls `toLowerCase()` on the haystack,
|
|
17
|
+
* because Unicode lowercasing can change string length (e.g. dotted capital
|
|
18
|
+
* I, U+0130, lowers to two code units) and shift every subsequent index.
|
|
19
|
+
* Tags must be given in lowercase ASCII.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function foldedCharCode(code: number, caseInsensitive: boolean): number {
|
|
23
|
+
return caseInsensitive && code >= 65 && code <= 90 ? code + 32 : code;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tagMatchesAt(
|
|
27
|
+
haystack: string,
|
|
28
|
+
start: number,
|
|
29
|
+
tag: string,
|
|
30
|
+
caseInsensitive: boolean,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (start + tag.length > haystack.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
for (let k = 0; k < tag.length; k += 1) {
|
|
36
|
+
if (
|
|
37
|
+
foldedCharCode(haystack.charCodeAt(start + k), caseInsensitive) !==
|
|
38
|
+
tag.charCodeAt(k)
|
|
39
|
+
) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* First occurrence of any tag in `haystack`, by original-string index.
|
|
48
|
+
* When several tags match at the same index the longest wins, so
|
|
49
|
+
* `<thinking>` is not consumed as `<think>` plus stray text.
|
|
50
|
+
*/
|
|
51
|
+
export function indexOfTag(
|
|
52
|
+
haystack: string,
|
|
53
|
+
tags: readonly string[],
|
|
54
|
+
caseInsensitive: boolean,
|
|
55
|
+
): { index: number; tag: string } | null {
|
|
56
|
+
for (let i = 0; i < haystack.length; i += 1) {
|
|
57
|
+
let bestTag: string | null = null;
|
|
58
|
+
for (const tag of tags) {
|
|
59
|
+
if (tagMatchesAt(haystack, i, tag, caseInsensitive)) {
|
|
60
|
+
if (bestTag === null || tag.length > bestTag.length) {
|
|
61
|
+
bestTag = tag;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (bestTag !== null) {
|
|
66
|
+
return { index: i, tag: bestTag };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Length of the longest suffix of `text` that is a proper prefix of any
|
|
74
|
+
* listed tag. Streaming scanners hold that many characters back so a tag
|
|
75
|
+
* split across deltas is not emitted before it can be recognized.
|
|
76
|
+
*/
|
|
77
|
+
export function partialTagSuffix(
|
|
78
|
+
text: string,
|
|
79
|
+
tags: readonly string[],
|
|
80
|
+
caseInsensitive: boolean,
|
|
81
|
+
): number {
|
|
82
|
+
const longest = Math.max(...tags.map((tag) => tag.length)) - 1;
|
|
83
|
+
for (let len = Math.min(text.length, longest); len > 0; len -= 1) {
|
|
84
|
+
const start = text.length - len;
|
|
85
|
+
for (const tag of tags) {
|
|
86
|
+
if (
|
|
87
|
+
len < tag.length &&
|
|
88
|
+
tagMatchesAt(text, start, tag.slice(0, len), caseInsensitive)
|
|
89
|
+
) {
|
|
90
|
+
return len;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|