glirastes 0.3.0 → 0.3.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/CHANGELOG.md +86 -0
- package/README.md +154 -9
- package/codegen/package.json +5 -0
- package/dist/cjs/codegen/asset-sync.d.ts +20 -0
- package/dist/cjs/codegen/asset-sync.js +28 -0
- package/dist/cjs/codegen/coverage.d.ts +66 -0
- package/dist/cjs/codegen/coverage.js +408 -0
- package/dist/cjs/codegen/endpoint-scanner.d.ts +16 -0
- package/dist/cjs/codegen/endpoint-scanner.js +197 -0
- package/dist/cjs/codegen/format.d.ts +17 -0
- package/dist/cjs/codegen/format.js +34 -0
- package/dist/cjs/codegen/fs-utils.d.ts +45 -0
- package/dist/cjs/codegen/fs-utils.js +165 -0
- package/dist/cjs/codegen/index.d.ts +13 -0
- package/dist/cjs/codegen/index.js +33 -0
- package/dist/cjs/codegen/mcp-generator.d.ts +50 -0
- package/dist/cjs/codegen/mcp-generator.js +803 -0
- package/dist/cjs/codegen/module-generator.d.ts +11 -0
- package/dist/cjs/codegen/module-generator.js +353 -0
- package/dist/cjs/codegen/nestjs-scanner.d.ts +43 -0
- package/dist/cjs/codegen/nestjs-scanner.js +241 -0
- package/dist/cjs/codegen/output-recommender.d.ts +23 -0
- package/dist/cjs/codegen/output-recommender.js +33 -0
- package/dist/cjs/codegen/scaffolder.d.ts +36 -0
- package/dist/cjs/codegen/scaffolder.js +262 -0
- package/dist/cjs/codegen/skill-generator.d.ts +49 -0
- package/dist/cjs/codegen/skill-generator.js +967 -0
- package/dist/cjs/codegen/types.d.ts +52 -0
- package/dist/cjs/codegen/types.js +16 -0
- package/dist/cjs/codegen/ui-scanner.d.ts +24 -0
- package/dist/cjs/codegen/ui-scanner.js +202 -0
- package/dist/cjs/codegen/validator.d.ts +35 -0
- package/dist/cjs/codegen/validator.js +292 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/openapi/index.d.ts +78 -0
- package/dist/cjs/openapi/index.js +347 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/react/audio-worklet-processor.d.ts +16 -0
- package/dist/cjs/react/audio-worklet-processor.js +27 -0
- package/dist/cjs/react/bridges/rest-session-bridge.d.ts +113 -0
- package/dist/cjs/react/bridges/rest-session-bridge.js +209 -0
- package/dist/cjs/react/components/ai-chat-panel.d.ts +12 -0
- package/dist/cjs/react/components/ai-chat-panel.js +67 -0
- package/dist/cjs/react/components/ai-trigger-button.d.ts +2 -0
- package/dist/cjs/react/components/ai-trigger-button.js +239 -0
- package/dist/cjs/react/components/approval-card.d.ts +9 -0
- package/dist/cjs/react/components/approval-card.js +24 -0
- package/dist/cjs/react/components/bulk-approval-card.d.ts +9 -0
- package/dist/cjs/react/components/bulk-approval-card.js +36 -0
- package/dist/cjs/react/components/chat-input.d.ts +10 -0
- package/dist/cjs/react/components/chat-input.js +60 -0
- package/dist/cjs/react/components/chat-window.d.ts +2 -0
- package/dist/cjs/react/components/chat-window.js +134 -0
- package/dist/cjs/react/components/confirm-dialog.d.ts +32 -0
- package/dist/cjs/react/components/confirm-dialog.js +48 -0
- package/dist/cjs/react/components/floating-chat-ui.d.ts +90 -0
- package/dist/cjs/react/components/floating-chat-ui.js +163 -0
- package/dist/cjs/react/components/mention-chip.d.ts +16 -0
- package/dist/cjs/react/components/mention-chip.js +42 -0
- package/dist/cjs/react/components/mention-input.d.ts +8 -0
- package/dist/cjs/react/components/mention-input.js +117 -0
- package/dist/cjs/react/components/message-bubble.d.ts +11 -0
- package/dist/cjs/react/components/message-bubble.js +171 -0
- package/dist/cjs/react/components/message-list.d.ts +8 -0
- package/dist/cjs/react/components/message-list.js +52 -0
- package/dist/cjs/react/components/pipeline-timeline.d.ts +7 -0
- package/dist/cjs/react/components/pipeline-timeline.js +72 -0
- package/dist/cjs/react/components/recording-bar.d.ts +2 -0
- package/dist/cjs/react/components/recording-bar.js +81 -0
- package/dist/cjs/react/components/recording-controls.d.ts +18 -0
- package/dist/cjs/react/components/recording-controls.js +38 -0
- package/dist/cjs/react/components/rich-mention-input.d.ts +2 -0
- package/dist/cjs/react/components/rich-mention-input.js +571 -0
- package/dist/cjs/react/components/session-switcher.d.ts +14 -0
- package/dist/cjs/react/components/session-switcher.js +133 -0
- package/dist/cjs/react/components/suggestion-bar.d.ts +9 -0
- package/dist/cjs/react/components/suggestion-bar.js +47 -0
- package/dist/cjs/react/components/suggestion-chip.d.ts +9 -0
- package/dist/cjs/react/components/suggestion-chip.js +14 -0
- package/dist/cjs/react/components/voice-input-addon.d.ts +13 -0
- package/dist/cjs/react/components/voice-input-addon.js +31 -0
- package/dist/cjs/react/components/voice-input-button.d.ts +2 -0
- package/dist/cjs/react/components/voice-input-button.js +55 -0
- package/dist/cjs/react/components/wavesurfer-loader.d.ts +5 -0
- package/dist/cjs/react/components/wavesurfer-loader.js +36 -0
- package/dist/cjs/react/hooks/use-ai-chat.d.ts +22 -0
- package/dist/cjs/react/hooks/use-ai-chat.js +51 -0
- package/dist/cjs/react/hooks/use-approvals.d.ts +23 -0
- package/dist/cjs/react/hooks/use-approvals.js +63 -0
- package/dist/cjs/react/hooks/use-deepgram-transcription.d.ts +2 -0
- package/dist/cjs/react/hooks/use-deepgram-transcription.js +329 -0
- package/dist/cjs/react/hooks/use-drag-resize-panel.d.ts +48 -0
- package/dist/cjs/react/hooks/use-drag-resize-panel.js +308 -0
- package/dist/cjs/react/hooks/use-draggable-position.d.ts +25 -0
- package/dist/cjs/react/hooks/use-draggable-position.js +134 -0
- package/dist/cjs/react/hooks/use-keyboard-shortcut.d.ts +20 -0
- package/dist/cjs/react/hooks/use-keyboard-shortcut.js +93 -0
- package/dist/cjs/react/hooks/use-mentions.d.ts +23 -0
- package/dist/cjs/react/hooks/use-mentions.js +42 -0
- package/dist/cjs/react/hooks/use-suggestions.d.ts +22 -0
- package/dist/cjs/react/hooks/use-suggestions.js +39 -0
- package/dist/cjs/react/index.d.ts +43 -0
- package/dist/cjs/react/index.js +111 -0
- package/dist/cjs/react/provider/ai-chat-provider.d.ts +45 -0
- package/dist/cjs/react/provider/ai-chat-provider.js +1024 -0
- package/dist/cjs/react/provider/chat-context.d.ts +84 -0
- package/dist/cjs/react/provider/chat-context.js +13 -0
- package/dist/cjs/react/template.d.ts +84 -0
- package/dist/cjs/react/template.js +90 -0
- package/dist/cjs/react/themes.d.ts +24 -0
- package/dist/cjs/react/themes.js +273 -0
- package/dist/cjs/react/transports/langgraph/index.d.ts +2 -0
- package/dist/cjs/react/transports/langgraph/index.js +8 -0
- package/dist/cjs/react/transports/langgraph/langgraph-ai-chat.d.ts +62 -0
- package/dist/cjs/react/transports/langgraph/langgraph-ai-chat.js +94 -0
- package/dist/cjs/react/transports/langgraph.d.ts +71 -0
- package/dist/cjs/react/transports/langgraph.js +369 -0
- package/dist/cjs/react/transports/vercel/index.d.ts +2 -0
- package/dist/cjs/react/transports/vercel/index.js +8 -0
- package/dist/cjs/react/transports/vercel/vercel-ai-chat.d.ts +49 -0
- package/dist/cjs/react/transports/vercel/vercel-ai-chat.js +81 -0
- package/dist/cjs/react/transports/vercel.d.ts +30 -0
- package/dist/cjs/react/transports/vercel.js +69 -0
- package/dist/cjs/react/types.d.ts +760 -0
- package/dist/cjs/react/types.js +2 -0
- package/dist/cjs/react/ui.d.ts +73 -0
- package/dist/cjs/react/ui.js +183 -0
- package/dist/cjs/react/utils/create-approval-description.d.ts +53 -0
- package/dist/cjs/react/utils/create-approval-description.js +122 -0
- package/dist/cjs/react/utils/create-tool-result-handler.d.ts +90 -0
- package/dist/cjs/react/utils/create-tool-result-handler.js +168 -0
- package/dist/cjs/react/utils/mention-markup.d.ts +13 -0
- package/dist/cjs/react/utils/mention-markup.js +117 -0
- package/dist/cjs/react/utils/merge-pipeline-steps.d.ts +19 -0
- package/dist/cjs/react/utils/merge-pipeline-steps.js +81 -0
- package/dist/cjs/react/utils/sanitize-stored-messages.d.ts +23 -0
- package/dist/cjs/react/utils/sanitize-stored-messages.js +52 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat-auth.guard.d.ts +10 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat-auth.guard.js +42 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat-module-options.interface.d.ts +32 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat-module-options.interface.js +4 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat.controller.d.ts +11 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat.controller.js +80 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat.module.d.ts +9 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat.module.js +119 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat.service.d.ts +13 -0
- package/dist/cjs/server/adapters/nestjs/chat/ai-chat.service.js +131 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/ai-chat-request.dto.d.ts +7 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/ai-chat-request.dto.js +52 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/chat-message-part.dto.d.ts +19 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/chat-message-part.dto.js +108 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/chat-message.dto.d.ts +8 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/chat-message.dto.js +59 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/index.d.ts +3 -0
- package/dist/cjs/server/adapters/nestjs/chat/dto/index.js +9 -0
- package/dist/cjs/server/adapters/nestjs/chat/index.d.ts +3 -0
- package/dist/cjs/server/adapters/nestjs/chat/index.js +9 -0
- package/dist/cjs/server/adapters/nestjs/chat/speech-stream.gateway.d.ts +4 -0
- package/dist/cjs/server/adapters/nestjs/chat/speech-stream.gateway.js +73 -0
- package/dist/cjs/server/adapters/nestjs/chat/ws-loader.d.ts +5 -0
- package/dist/cjs/server/adapters/nestjs/chat/ws-loader.js +72 -0
- package/dist/cjs/server/adapters/nestjs/decorators/ai-module.decorator.d.ts +6 -0
- package/dist/cjs/server/adapters/nestjs/decorators/ai-module.decorator.js +13 -0
- package/dist/cjs/server/adapters/nestjs/decorators/ai-param.decorator.d.ts +5 -0
- package/dist/cjs/server/adapters/nestjs/decorators/ai-param.decorator.js +15 -0
- package/dist/cjs/server/adapters/nestjs/decorators/ai-tool.decorator.d.ts +6 -0
- package/dist/cjs/server/adapters/nestjs/decorators/ai-tool.decorator.js +22 -0
- package/dist/cjs/server/adapters/nestjs/decorators/constants.d.ts +4 -0
- package/dist/cjs/server/adapters/nestjs/decorators/constants.js +7 -0
- package/dist/cjs/server/adapters/nestjs/index.d.ts +14 -0
- package/dist/cjs/server/adapters/nestjs/index.js +42 -0
- package/dist/cjs/server/adapters/nestjs/metadata.d.ts +39 -0
- package/dist/cjs/server/adapters/nestjs/metadata.js +36 -0
- package/dist/cjs/server/adapters/nestjs/module/ai-tools-explorer.service.d.ts +36 -0
- package/dist/cjs/server/adapters/nestjs/module/ai-tools-explorer.service.js +159 -0
- package/dist/cjs/server/adapters/nestjs/module/ai-tools.module.d.ts +37 -0
- package/dist/cjs/server/adapters/nestjs/module/ai-tools.module.js +44 -0
- package/dist/cjs/server/adapters/nestjs/module/ai-tools.service.d.ts +28 -0
- package/dist/cjs/server/adapters/nestjs/module/ai-tools.service.js +41 -0
- package/dist/cjs/server/adapters/nestjs/scanner/nestjs-scanner.d.ts +16 -0
- package/dist/cjs/server/adapters/nestjs/scanner/nestjs-scanner.js +117 -0
- package/dist/cjs/server/adapters/nestjs/scanner/schema-builder.d.ts +26 -0
- package/dist/cjs/server/adapters/nestjs/scanner/schema-builder.js +259 -0
- package/dist/cjs/server/adapters/nestjs/swagger.d.ts +7 -0
- package/dist/cjs/server/adapters/nestjs/swagger.js +39 -0
- package/dist/cjs/server/adapters/nextjs/context-helpers.d.ts +22 -0
- package/dist/cjs/server/adapters/nextjs/context-helpers.js +60 -0
- package/dist/cjs/server/adapters/nextjs/create-ai-chat-handler.d.ts +94 -0
- package/dist/cjs/server/adapters/nextjs/create-ai-chat-handler.js +965 -0
- package/dist/cjs/server/adapters/nextjs/filter-fallback-error.d.ts +11 -0
- package/dist/cjs/server/adapters/nextjs/filter-fallback-error.js +41 -0
- package/dist/cjs/server/adapters/nextjs/index.d.ts +8 -0
- package/dist/cjs/server/adapters/nextjs/index.js +22 -0
- package/dist/cjs/server/adapters/nextjs/prepare-model-messages.d.ts +58 -0
- package/dist/cjs/server/adapters/nextjs/prepare-model-messages.js +114 -0
- package/dist/cjs/server/adapters/nextjs/sanitize-model-messages.d.ts +64 -0
- package/dist/cjs/server/adapters/nextjs/sanitize-model-messages.js +489 -0
- package/dist/cjs/server/adapters/nextjs/server-action-helper.d.ts +7 -0
- package/dist/cjs/server/adapters/nextjs/server-action-helper.js +40 -0
- package/dist/cjs/server/adapters/nextjs/types.d.ts +246 -0
- package/dist/cjs/server/adapters/nextjs/types.js +2 -0
- package/dist/cjs/server/adapters/nextjs/with-ai-tool.d.ts +9 -0
- package/dist/cjs/server/adapters/nextjs/with-ai-tool.js +12 -0
- package/dist/cjs/server/core/followup-tool.d.ts +9 -0
- package/dist/cjs/server/core/followup-tool.js +82 -0
- package/dist/cjs/server/core/index.d.ts +139 -0
- package/dist/cjs/server/core/index.js +818 -0
- package/dist/cjs/server/core/intent-classifier.d.ts +94 -0
- package/dist/cjs/server/core/intent-classifier.js +324 -0
- package/dist/cjs/server/core/model-selector.d.ts +12 -0
- package/dist/cjs/server/core/model-selector.js +48 -0
- package/dist/cjs/server/core/module-router.d.ts +14 -0
- package/dist/cjs/server/core/module-router.js +110 -0
- package/dist/cjs/server/core/zod-compact.d.ts +18 -0
- package/dist/cjs/server/core/zod-compact.js +129 -0
- package/dist/cjs/server/index.d.ts +6 -0
- package/dist/cjs/server/index.js +28 -0
- package/dist/cjs/server/lancer/cache.d.ts +9 -0
- package/dist/cjs/server/lancer/cache.js +33 -0
- package/dist/cjs/server/lancer/client.d.ts +2 -0
- package/dist/cjs/server/lancer/client.js +279 -0
- package/dist/cjs/server/lancer/errors.d.ts +13 -0
- package/dist/cjs/server/lancer/errors.js +21 -0
- package/dist/cjs/server/lancer/index.d.ts +3 -0
- package/dist/cjs/server/lancer/index.js +7 -0
- package/dist/cjs/server/lancer/telemetry-buffer.d.ts +15 -0
- package/dist/cjs/server/lancer/telemetry-buffer.js +64 -0
- package/dist/cjs/server/lancer/types.d.ts +201 -0
- package/dist/cjs/server/lancer/types.js +5 -0
- package/dist/cjs/server/pii-shield/aegis-detector.d.ts +26 -0
- package/dist/cjs/server/pii-shield/aegis-detector.js +26 -0
- package/dist/cjs/server/pii-shield/create-pii-shield.d.ts +2 -0
- package/dist/cjs/server/pii-shield/create-pii-shield.js +191 -0
- package/dist/cjs/server/pii-shield/deep-walk.d.ts +1 -0
- package/dist/cjs/server/pii-shield/deep-walk.js +19 -0
- package/dist/cjs/server/pii-shield/fake-data.d.ts +5 -0
- package/dist/cjs/server/pii-shield/fake-data.js +170 -0
- package/dist/cjs/server/pii-shield/index.d.ts +4 -0
- package/dist/cjs/server/pii-shield/index.js +9 -0
- package/dist/cjs/server/pii-shield/leakage.d.ts +1 -0
- package/dist/cjs/server/pii-shield/leakage.js +15 -0
- package/dist/cjs/server/pii-shield/session-mapping.d.ts +12 -0
- package/dist/cjs/server/pii-shield/session-mapping.js +70 -0
- package/dist/cjs/server/pipeline.d.ts +26 -0
- package/dist/cjs/server/pipeline.js +127 -0
- package/dist/cjs/server/testing/approvals.d.ts +11 -0
- package/dist/cjs/server/testing/approvals.js +41 -0
- package/dist/cjs/server/testing/combination.d.ts +3 -0
- package/dist/cjs/server/testing/combination.js +83 -0
- package/dist/cjs/server/testing/create-test-suite.d.ts +29 -0
- package/dist/cjs/server/testing/create-test-suite.js +81 -0
- package/dist/cjs/server/testing/edge-cases.d.ts +8 -0
- package/dist/cjs/server/testing/edge-cases.js +277 -0
- package/dist/cjs/server/testing/guardrails.d.ts +14 -0
- package/dist/cjs/server/testing/guardrails.js +152 -0
- package/dist/cjs/server/testing/helpers.d.ts +14 -0
- package/dist/cjs/server/testing/helpers.js +119 -0
- package/dist/cjs/server/testing/index.d.ts +12 -0
- package/dist/cjs/server/testing/index.js +37 -0
- package/dist/cjs/server/testing/mock-executor.d.ts +8 -0
- package/dist/cjs/server/testing/mock-executor.js +40 -0
- package/dist/cjs/server/testing/regression.d.ts +7 -0
- package/dist/cjs/server/testing/regression.js +27 -0
- package/dist/cjs/server/testing/routing.d.ts +12 -0
- package/dist/cjs/server/testing/routing.js +63 -0
- package/dist/cjs/server/testing/schema-consistency.d.ts +6 -0
- package/dist/cjs/server/testing/schema-consistency.js +56 -0
- package/dist/cjs/server/testing/smoke.d.ts +11 -0
- package/dist/cjs/server/testing/smoke.js +64 -0
- package/dist/cjs/server/testing/types.d.ts +156 -0
- package/dist/cjs/server/testing/types.js +2 -0
- package/dist/cjs/types.d.ts +696 -0
- package/dist/cjs/types.js +91 -0
- package/dist/cli/commands/install-skills.d.ts +17 -0
- package/dist/cli/commands/install-skills.js +185 -0
- package/dist/cli/commands/install-skills.js.map +1 -0
- package/dist/cli/index.js +25 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/react/components/ai-trigger-button.js +4 -6
- package/dist/react/components/ai-trigger-button.js.map +1 -1
- package/dist/react/components/recording-bar.js +4 -6
- package/dist/react/components/recording-bar.js.map +1 -1
- package/dist/react/components/wavesurfer-loader.d.ts +5 -0
- package/dist/react/components/wavesurfer-loader.js +34 -0
- package/dist/react/components/wavesurfer-loader.js.map +1 -0
- package/dist/server/adapters/nestjs/chat/ai-chat.module.d.ts +1 -1
- package/dist/server/adapters/nestjs/chat/ai-chat.module.js +3 -2
- package/dist/server/adapters/nestjs/chat/ai-chat.module.js.map +1 -1
- package/dist/server/adapters/nestjs/chat/speech-stream.gateway.d.ts +1 -1
- package/dist/server/adapters/nestjs/chat/speech-stream.gateway.js +10 -8
- package/dist/server/adapters/nestjs/chat/speech-stream.gateway.js.map +1 -1
- package/dist/server/adapters/nestjs/chat/ws-loader.d.ts +5 -0
- package/dist/server/adapters/nestjs/chat/ws-loader.js +37 -0
- package/dist/server/adapters/nestjs/chat/ws-loader.js.map +1 -0
- package/openapi/package.json +5 -0
- package/package.json +59 -16
- package/react/langgraph/package.json +5 -0
- package/react/package.json +5 -0
- package/react/vercel/package.json +5 -0
- package/server/nestjs/package.json +5 -0
- package/server/nextjs/package.json +5 -0
- package/server/package.json +5 -0
- package/server/testing/package.json +5 -0
- package/skills/README.md +107 -0
- package/skills/building-glirastes-chat-ui/SKILL.md +378 -0
- package/skills/integrating-glirastes-nestjs/SKILL.md +346 -0
- package/skills/integrating-glirastes-nextjs/SKILL.md +312 -0
- package/skills/integrating-glirastes-nextjs-with-nestjs-backend/SKILL.md +359 -0
- package/skills/maintaining-glirastes-tools/SKILL.md +467 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_SAFETY_MAX_STEPS = void 0;
|
|
4
|
+
exports.resolveStepLimit = resolveStepLimit;
|
|
5
|
+
exports.createAiChatHandler = createAiChatHandler;
|
|
6
|
+
const index_js_1 = require("../../core/index.js");
|
|
7
|
+
const types_js_1 = require("../../../types.js");
|
|
8
|
+
const ai_1 = require("ai");
|
|
9
|
+
const context_helpers_js_1 = require("./context-helpers.js");
|
|
10
|
+
const prepare_model_messages_js_1 = require("./prepare-model-messages.js");
|
|
11
|
+
const filter_fallback_error_js_1 = require("./filter-fallback-error.js");
|
|
12
|
+
/**
|
|
13
|
+
* Default soft cap on reasoning steps when no module-level or explicit
|
|
14
|
+
* `maxSteps` is configured. Acts as a last-line safety against runaway
|
|
15
|
+
* tool loops; not as a normal-operation cap (modules default to 4 via
|
|
16
|
+
* EXECUTION_DEFAULTS). Override per request via `createAiChatHandler({
|
|
17
|
+
* safetyMaxSteps })`.
|
|
18
|
+
*/
|
|
19
|
+
exports.DEFAULT_SAFETY_MAX_STEPS = 24;
|
|
20
|
+
function asPositiveInt(value) {
|
|
21
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
22
|
+
return null;
|
|
23
|
+
const normalized = Math.floor(value);
|
|
24
|
+
return normalized > 0 ? normalized : null;
|
|
25
|
+
}
|
|
26
|
+
function resolveStepLimit(options) {
|
|
27
|
+
const moduleMaxSteps = asPositiveInt(options.moduleMaxSteps);
|
|
28
|
+
if (moduleMaxSteps) {
|
|
29
|
+
return { maxSteps: moduleMaxSteps, stepLimitSource: 'module' };
|
|
30
|
+
}
|
|
31
|
+
const configuredMaxSteps = asPositiveInt(options.configuredMaxSteps);
|
|
32
|
+
if (configuredMaxSteps) {
|
|
33
|
+
return { maxSteps: configuredMaxSteps, stepLimitSource: 'explicit' };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
maxSteps: asPositiveInt(options.safetyMaxSteps) ?? exports.DEFAULT_SAFETY_MAX_STEPS,
|
|
37
|
+
stepLimitSource: 'safety',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function compactToolNames(toolCalls) {
|
|
41
|
+
const unique = new Set();
|
|
42
|
+
for (const call of toolCalls) {
|
|
43
|
+
if (typeof call.toolName === 'string' && call.toolName.length > 0) {
|
|
44
|
+
unique.add(call.toolName);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return Array.from(unique);
|
|
48
|
+
}
|
|
49
|
+
function shorten(text, maxLength = 120) {
|
|
50
|
+
const normalized = text.trim().replace(/\s+/g, ' ');
|
|
51
|
+
if (normalized.length <= maxLength)
|
|
52
|
+
return normalized;
|
|
53
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
54
|
+
}
|
|
55
|
+
function buildStepSummary(args) {
|
|
56
|
+
if (args.requiresApproval) {
|
|
57
|
+
if (args.toolNames.length > 0) {
|
|
58
|
+
return `Awaiting approval: ${args.toolNames.join(', ')}`;
|
|
59
|
+
}
|
|
60
|
+
return 'Awaiting approval.';
|
|
61
|
+
}
|
|
62
|
+
if (args.toolCallCount > 0) {
|
|
63
|
+
const label = args.toolNames.length > 0
|
|
64
|
+
? args.toolNames.join(', ')
|
|
65
|
+
: `${args.toolCallCount} tool call(s)`;
|
|
66
|
+
return `Executed tools: ${label}`;
|
|
67
|
+
}
|
|
68
|
+
if (args.text.trim().length > 0) {
|
|
69
|
+
return shorten(args.text);
|
|
70
|
+
}
|
|
71
|
+
return `Finished step (${args.finishReason}).`;
|
|
72
|
+
}
|
|
73
|
+
function buildStepReport(step) {
|
|
74
|
+
const toolNames = compactToolNames(step.toolCalls);
|
|
75
|
+
const toolCallCount = step.toolCalls.length;
|
|
76
|
+
const toolResultCount = step.toolResults.length;
|
|
77
|
+
const pendingApprovals = Math.max(toolCallCount - toolResultCount, 0);
|
|
78
|
+
const requiresApproval = step.finishReason === 'tool-calls' && pendingApprovals > 0;
|
|
79
|
+
return {
|
|
80
|
+
stepNumber: step.stepNumber,
|
|
81
|
+
finishReason: step.finishReason,
|
|
82
|
+
toolCalls: toolNames,
|
|
83
|
+
toolResults: toolResultCount,
|
|
84
|
+
requiresApproval,
|
|
85
|
+
pendingApprovals,
|
|
86
|
+
summary: buildStepSummary({
|
|
87
|
+
finishReason: step.finishReason,
|
|
88
|
+
text: step.text,
|
|
89
|
+
toolNames,
|
|
90
|
+
toolCallCount,
|
|
91
|
+
toolResultCount,
|
|
92
|
+
requiresApproval,
|
|
93
|
+
}),
|
|
94
|
+
usage: {
|
|
95
|
+
inputTokens: step.usage.inputTokens ?? 0,
|
|
96
|
+
outputTokens: step.usage.outputTokens ?? 0,
|
|
97
|
+
totalTokens: step.usage.totalTokens ?? 0,
|
|
98
|
+
},
|
|
99
|
+
createdAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function createPipelineState(options) {
|
|
103
|
+
return {
|
|
104
|
+
status: options.status,
|
|
105
|
+
totalSteps: options.totalSteps,
|
|
106
|
+
finishReason: options.finishReason,
|
|
107
|
+
maxSteps: options.maxSteps,
|
|
108
|
+
stepLimitSource: options.stepLimitSource,
|
|
109
|
+
message: options.message,
|
|
110
|
+
updatedAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Built-in default stream handler.
|
|
115
|
+
*
|
|
116
|
+
* Converts UI messages → model messages, wraps tools, calls streamText,
|
|
117
|
+
* and returns a UI message stream response.
|
|
118
|
+
*/
|
|
119
|
+
async function defaultStreamHandler(ctx, onFinish) {
|
|
120
|
+
const vercelTools = (0, prepare_model_messages_js_1.wrapToolsForVercelAi)(ctx.tools);
|
|
121
|
+
const modelMessages = await (0, prepare_model_messages_js_1.prepareModelMessages)(ctx.messages, { maxContextMessages: ctx.maxContextMessages });
|
|
122
|
+
let wasTruncated = false;
|
|
123
|
+
let stoppedByLoop = false;
|
|
124
|
+
// Stop condition: step count OR consecutive identical tool calls (loop detection).
|
|
125
|
+
const LOOP_REPEAT_THRESHOLD = 3;
|
|
126
|
+
const shouldStop = (event) => {
|
|
127
|
+
if (event.steps.length >= ctx.maxSteps)
|
|
128
|
+
return true;
|
|
129
|
+
if (event.steps.length >= LOOP_REPEAT_THRESHOLD) {
|
|
130
|
+
const recent = event.steps.slice(-LOOP_REPEAT_THRESHOLD);
|
|
131
|
+
const signatures = recent.map((s) => s.toolCalls
|
|
132
|
+
.map((c) => c.toolName)
|
|
133
|
+
.sort()
|
|
134
|
+
.join(','));
|
|
135
|
+
if (signatures[0].length > 0 &&
|
|
136
|
+
signatures.every((sig) => sig === signatures[0])) {
|
|
137
|
+
stoppedByLoop = true;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
};
|
|
143
|
+
const stream = (0, ai_1.createUIMessageStream)({
|
|
144
|
+
execute: async ({ writer }) => {
|
|
145
|
+
let totalSteps = 0;
|
|
146
|
+
let finalStateWritten = false;
|
|
147
|
+
const writePipelineState = (state, transient = false) => {
|
|
148
|
+
writer.write({
|
|
149
|
+
type: 'data-pipeline-state',
|
|
150
|
+
data: state,
|
|
151
|
+
transient,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
writePipelineState(createPipelineState({
|
|
155
|
+
status: 'running',
|
|
156
|
+
totalSteps: 0,
|
|
157
|
+
maxSteps: ctx.maxSteps,
|
|
158
|
+
stepLimitSource: ctx.stepLimitSource,
|
|
159
|
+
}), true);
|
|
160
|
+
const result = (0, ai_1.streamText)({
|
|
161
|
+
model: ctx.model,
|
|
162
|
+
messages: modelMessages,
|
|
163
|
+
system: ctx.systemPrompt,
|
|
164
|
+
tools: vercelTools,
|
|
165
|
+
maxOutputTokens: ctx.maxOutputTokens,
|
|
166
|
+
temperature: ctx.temperature,
|
|
167
|
+
stopWhen: shouldStop,
|
|
168
|
+
onStepFinish: (event) => {
|
|
169
|
+
const report = buildStepReport({
|
|
170
|
+
stepNumber: event.stepNumber,
|
|
171
|
+
finishReason: event.finishReason,
|
|
172
|
+
toolCalls: event.toolCalls.map((call) => ({
|
|
173
|
+
toolName: call.toolName,
|
|
174
|
+
})),
|
|
175
|
+
toolResults: event.toolResults,
|
|
176
|
+
text: event.text,
|
|
177
|
+
usage: {
|
|
178
|
+
inputTokens: event.usage.inputTokens,
|
|
179
|
+
outputTokens: event.usage.outputTokens,
|
|
180
|
+
totalTokens: event.usage.totalTokens,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
totalSteps = Math.max(totalSteps, report.stepNumber + 1);
|
|
184
|
+
if (ctx.emitStepReports) {
|
|
185
|
+
writer.write({
|
|
186
|
+
type: 'data-step-report',
|
|
187
|
+
data: report,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
ctx.onStepReport?.(report);
|
|
191
|
+
},
|
|
192
|
+
onAbort: (event) => {
|
|
193
|
+
if (finalStateWritten)
|
|
194
|
+
return;
|
|
195
|
+
totalSteps = Math.max(totalSteps, event.steps.length);
|
|
196
|
+
writePipelineState(createPipelineState({
|
|
197
|
+
status: 'aborted',
|
|
198
|
+
totalSteps,
|
|
199
|
+
finishReason: 'stop',
|
|
200
|
+
maxSteps: ctx.maxSteps,
|
|
201
|
+
stepLimitSource: ctx.stepLimitSource,
|
|
202
|
+
message: 'Stopped by user.',
|
|
203
|
+
}));
|
|
204
|
+
finalStateWritten = true;
|
|
205
|
+
},
|
|
206
|
+
onFinish: (event) => {
|
|
207
|
+
wasTruncated =
|
|
208
|
+
event.finishReason === 'tool-calls' &&
|
|
209
|
+
(event.steps.length >= ctx.maxSteps || stoppedByLoop);
|
|
210
|
+
totalSteps = Math.max(totalSteps, event.steps.length);
|
|
211
|
+
const isSafetyStop = wasTruncated && ctx.stepLimitSource === 'safety' && !stoppedByLoop;
|
|
212
|
+
const status = stoppedByLoop
|
|
213
|
+
? 'safety-stop'
|
|
214
|
+
: isSafetyStop
|
|
215
|
+
? 'safety-stop'
|
|
216
|
+
: event.finishReason === 'error'
|
|
217
|
+
? 'error'
|
|
218
|
+
: 'completed';
|
|
219
|
+
if (!finalStateWritten) {
|
|
220
|
+
writePipelineState(createPipelineState({
|
|
221
|
+
status,
|
|
222
|
+
totalSteps,
|
|
223
|
+
finishReason: event.finishReason,
|
|
224
|
+
maxSteps: ctx.maxSteps,
|
|
225
|
+
stepLimitSource: ctx.stepLimitSource,
|
|
226
|
+
message: stoppedByLoop
|
|
227
|
+
? 'Stopped: repeated tool call loop detected.'
|
|
228
|
+
: wasTruncated
|
|
229
|
+
? isSafetyStop
|
|
230
|
+
? 'Paused at step safety cap. You can continue or stop.'
|
|
231
|
+
: 'Paused at configured step cap.'
|
|
232
|
+
: undefined,
|
|
233
|
+
}));
|
|
234
|
+
finalStateWritten = true;
|
|
235
|
+
}
|
|
236
|
+
onFinish?.({
|
|
237
|
+
totalSteps: event.steps.length,
|
|
238
|
+
finishReason: event.finishReason,
|
|
239
|
+
usage: {
|
|
240
|
+
inputTokens: event.totalUsage.inputTokens ?? 0,
|
|
241
|
+
outputTokens: event.totalUsage.outputTokens ?? 0,
|
|
242
|
+
totalTokens: event.totalUsage.totalTokens ?? 0,
|
|
243
|
+
},
|
|
244
|
+
wasTruncated,
|
|
245
|
+
maxSteps: ctx.maxSteps,
|
|
246
|
+
stepLimitSource: ctx.stepLimitSource,
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
writer.merge(result.toUIMessageStream());
|
|
251
|
+
try {
|
|
252
|
+
await result.response;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (finalStateWritten)
|
|
256
|
+
return;
|
|
257
|
+
writePipelineState(createPipelineState({
|
|
258
|
+
status: 'error',
|
|
259
|
+
totalSteps,
|
|
260
|
+
finishReason: 'error',
|
|
261
|
+
maxSteps: ctx.maxSteps,
|
|
262
|
+
stepLimitSource: ctx.stepLimitSource,
|
|
263
|
+
message: 'Pipeline failed.',
|
|
264
|
+
}));
|
|
265
|
+
finalStateWritten = true;
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
// Wrap the outer stream so the filter sees BOTH the inner result chunks AND
|
|
271
|
+
// the error chunk that createUIMessageStream emits when execute() throws —
|
|
272
|
+
// the latter is where the duplicate "No output generated" event comes from.
|
|
273
|
+
return (0, ai_1.createUIMessageStreamResponse)({ stream: (0, filter_fallback_error_js_1.filterFallbackError)(stream) });
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Creates a complete Next.js App Router POST handler for AI chat.
|
|
277
|
+
*
|
|
278
|
+
* Supports two modes:
|
|
279
|
+
*
|
|
280
|
+
* **Pipeline mode** (Pro): When `pipeline` is provided, uses the server-pro
|
|
281
|
+
* pipeline for guardrails, intent classification, tool selection, and model
|
|
282
|
+
* selection.
|
|
283
|
+
*
|
|
284
|
+
* **Legacy mode**: When no `pipeline` is provided, uses the original
|
|
285
|
+
* `guardrails` + `intentRouter` flow.
|
|
286
|
+
*
|
|
287
|
+
* The SDK does not perform authentication or authorization. Tools forward the
|
|
288
|
+
* caller's cookies to backend APIs via `runtime.cookieHeader`; permission
|
|
289
|
+
* checks happen on the backend.
|
|
290
|
+
*/
|
|
291
|
+
function createAiChatHandler(config) {
|
|
292
|
+
const { tools: staticTools, pipeline, followups, loadTools,
|
|
293
|
+
// PII Shield
|
|
294
|
+
piiShield,
|
|
295
|
+
// Audit & Telemetry
|
|
296
|
+
onAudit, telemetry,
|
|
297
|
+
// Lancer (Pro: runtime prompt + model overrides)
|
|
298
|
+
lancer, modelDefaults, modelInstanceFactory,
|
|
299
|
+
// Shared
|
|
300
|
+
model: modelConfig, systemPrompt: systemPromptConfig, locale = 'en-US', uiActionSchema, maxOutputTokens = 600, temperature = 0.2, maxSteps: configuredMaxSteps, safetyMaxSteps = exports.DEFAULT_SAFETY_MAX_STEPS, emitStepReports = true, maxContextMessages = 12, maxConversationHistoryPairs = 4, guardrails, intentRouter, wrapHandler, onError, onToolError, onPipelineResult, onStepReport, onFinish, streamHandler: customStreamHandler, } = config;
|
|
301
|
+
// Report model defaults to Glirastes (fire-and-forget, once)
|
|
302
|
+
let defaultsReported = false;
|
|
303
|
+
function reportDefaultsOnce() {
|
|
304
|
+
if (defaultsReported || !lancer || !modelDefaults)
|
|
305
|
+
return;
|
|
306
|
+
defaultsReported = true;
|
|
307
|
+
lancer.config.reportModels(modelDefaults).catch(() => { });
|
|
308
|
+
}
|
|
309
|
+
// Build stream handler factory — per-request to allow session.ended audit
|
|
310
|
+
function createStreamHandler(auditContext) {
|
|
311
|
+
const wrappedOnFinish = (event) => {
|
|
312
|
+
onFinish?.(event);
|
|
313
|
+
if (auditContext && (onAudit || telemetry)) {
|
|
314
|
+
const sessionEndEvent = {
|
|
315
|
+
type: 'session.ended',
|
|
316
|
+
timestamp: new Date().toISOString(),
|
|
317
|
+
sessionId: auditContext.sessionId,
|
|
318
|
+
source: 'adapter',
|
|
319
|
+
details: {
|
|
320
|
+
totalSteps: event.totalSteps,
|
|
321
|
+
totalTokens: event.usage.totalTokens,
|
|
322
|
+
durationMs: Date.now() - auditContext.requestStart,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
onAudit?.(sessionEndEvent);
|
|
326
|
+
try {
|
|
327
|
+
telemetry?.emit({
|
|
328
|
+
eventType: 'session.ended',
|
|
329
|
+
traceId: auditContext.sessionId,
|
|
330
|
+
tokensIn: event.usage.inputTokens,
|
|
331
|
+
tokensOut: event.usage.outputTokens,
|
|
332
|
+
latencyMs: Date.now() - auditContext.requestStart,
|
|
333
|
+
modelId: auditContext.modelId,
|
|
334
|
+
timestamp: new Date().toISOString(),
|
|
335
|
+
payload: sessionEndEvent.details,
|
|
336
|
+
actor: { sessionId: auditContext.sessionId },
|
|
337
|
+
});
|
|
338
|
+
void telemetry?.flush();
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Telemetry is fire-and-forget
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return customStreamHandler
|
|
346
|
+
? customStreamHandler
|
|
347
|
+
: (ctx) => defaultStreamHandler(ctx, wrappedOnFinish);
|
|
348
|
+
}
|
|
349
|
+
if (!staticTools && !loadTools) {
|
|
350
|
+
throw new Error('createAiChatHandler: either tools or loadTools must be provided');
|
|
351
|
+
}
|
|
352
|
+
async function handler(req) {
|
|
353
|
+
reportDefaultsOnce();
|
|
354
|
+
try {
|
|
355
|
+
const { messages } = await req.json();
|
|
356
|
+
if (!Array.isArray(messages)) {
|
|
357
|
+
return new Response(JSON.stringify({ error: 'messages must be an array' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
358
|
+
}
|
|
359
|
+
// Build tool context
|
|
360
|
+
const toolContext = (0, context_helpers_js_1.buildToolContext)(req, { locale });
|
|
361
|
+
const requestStart = Date.now();
|
|
362
|
+
// Derive session ID — stable per conversation turn (groups approval flows)
|
|
363
|
+
const sessionId = (onAudit || piiShield || telemetry) ? deriveSessionId(messages) : '';
|
|
364
|
+
const toolOptions = {
|
|
365
|
+
uiActionSchema,
|
|
366
|
+
onError: onToolError,
|
|
367
|
+
onAudit,
|
|
368
|
+
sessionId,
|
|
369
|
+
traceId: sessionId || undefined,
|
|
370
|
+
telemetry,
|
|
371
|
+
};
|
|
372
|
+
// Emit session.started for audit + telemetry
|
|
373
|
+
if (sessionId && (onAudit || telemetry)) {
|
|
374
|
+
const sessionStartEvent = {
|
|
375
|
+
type: 'session.started',
|
|
376
|
+
timestamp: new Date().toISOString(),
|
|
377
|
+
sessionId,
|
|
378
|
+
source: 'adapter',
|
|
379
|
+
details: { locale },
|
|
380
|
+
};
|
|
381
|
+
onAudit?.(sessionStartEvent);
|
|
382
|
+
try {
|
|
383
|
+
telemetry?.emit({
|
|
384
|
+
eventType: 'session.started',
|
|
385
|
+
traceId: sessionId,
|
|
386
|
+
timestamp: new Date().toISOString(),
|
|
387
|
+
payload: sessionStartEvent.details,
|
|
388
|
+
actor: { sessionId },
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Telemetry is fire-and-forget
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// ================================================================
|
|
396
|
+
// Pipeline mode
|
|
397
|
+
// ================================================================
|
|
398
|
+
if (pipeline) {
|
|
399
|
+
// Create callEndpoint for endpoint tools (needed regardless of static/dynamic)
|
|
400
|
+
const callEndpoint = (0, context_helpers_js_1.createInternalApiCallerFromRequest)(req);
|
|
401
|
+
const endpointToolContext = { ...toolContext, callEndpoint };
|
|
402
|
+
// Get tools (static or dynamic)
|
|
403
|
+
let registry;
|
|
404
|
+
if (staticTools) {
|
|
405
|
+
registry = staticTools;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
registry = await loadTools(endpointToolContext);
|
|
409
|
+
}
|
|
410
|
+
// Add followup tool if configured
|
|
411
|
+
if (followups) {
|
|
412
|
+
const followupTool = (0, index_js_1.createFollowupTool)(followups);
|
|
413
|
+
if (followupTool) {
|
|
414
|
+
registry = { ...registry, [followupTool.id]: followupTool };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Extract user input for pipeline
|
|
418
|
+
let userText = extractLastUserText(messages);
|
|
419
|
+
if (piiShield) {
|
|
420
|
+
userText = await piiShield.outbound(userText, sessionId);
|
|
421
|
+
}
|
|
422
|
+
// Build lightweight conversation history for intent classification.
|
|
423
|
+
// Include the last few user/assistant text exchanges (excluding the
|
|
424
|
+
// current message) so the classifier can understand follow-ups.
|
|
425
|
+
const conversationHistory = extractConversationHistory(messages, maxConversationHistoryPairs);
|
|
426
|
+
// Run pipeline: guardrails + classification + routing + model selection
|
|
427
|
+
const pipelineResult = await pipeline.process(userText, toolContext, conversationHistory.length > 0 ? conversationHistory : undefined);
|
|
428
|
+
// Notify consumer about pipeline decision
|
|
429
|
+
onPipelineResult?.(pipelineResult);
|
|
430
|
+
// Emit pipeline-stage telemetry events for request trace
|
|
431
|
+
if (telemetry && sessionId) {
|
|
432
|
+
const pipelineActor = { sessionId };
|
|
433
|
+
try {
|
|
434
|
+
// Use explicit blocked flag from pipeline instead of heuristic
|
|
435
|
+
const wardenBlocked = pipelineResult.blocked === true;
|
|
436
|
+
telemetry.emit({
|
|
437
|
+
eventType: wardenBlocked ? 'warden.block' : 'warden.pass',
|
|
438
|
+
traceId: sessionId,
|
|
439
|
+
timestamp: new Date().toISOString(),
|
|
440
|
+
payload: { blocked: wardenBlocked, inputLength: userText.length },
|
|
441
|
+
actor: pipelineActor,
|
|
442
|
+
});
|
|
443
|
+
// Primus classification
|
|
444
|
+
telemetry.emit({
|
|
445
|
+
eventType: 'primus.classify',
|
|
446
|
+
traceId: sessionId,
|
|
447
|
+
timestamp: new Date().toISOString(),
|
|
448
|
+
payload: {
|
|
449
|
+
moduleId: pipelineResult.intent.intent,
|
|
450
|
+
confidence: pipelineResult.intent.confidence,
|
|
451
|
+
module: pipelineResult.module?.id ?? null,
|
|
452
|
+
},
|
|
453
|
+
actor: pipelineActor,
|
|
454
|
+
});
|
|
455
|
+
// Tool routing
|
|
456
|
+
telemetry.emit({
|
|
457
|
+
eventType: 'pipeline.routing',
|
|
458
|
+
traceId: sessionId,
|
|
459
|
+
timestamp: new Date().toISOString(),
|
|
460
|
+
payload: {
|
|
461
|
+
toolCount: pipelineResult.tools.length,
|
|
462
|
+
tools: pipelineResult.tools,
|
|
463
|
+
module: pipelineResult.module?.id ?? null,
|
|
464
|
+
modelId: pipelineResult.model.modelId,
|
|
465
|
+
},
|
|
466
|
+
actor: pipelineActor,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Telemetry is fire-and-forget
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Convert all tools to AI tools
|
|
474
|
+
let aiTools = await (0, index_js_1.toolsToAiTools)(registry, endpointToolContext, toolOptions);
|
|
475
|
+
// Scope tools to pipeline selection
|
|
476
|
+
if (pipelineResult.blocked) {
|
|
477
|
+
// Guardrails blocked — no tools allowed
|
|
478
|
+
aiTools = {};
|
|
479
|
+
}
|
|
480
|
+
else if (pipelineResult.tools.length > 0) {
|
|
481
|
+
const scoped = {};
|
|
482
|
+
for (const name of pipelineResult.tools) {
|
|
483
|
+
if (aiTools[name])
|
|
484
|
+
scoped[name] = aiTools[name];
|
|
485
|
+
}
|
|
486
|
+
if (Object.keys(scoped).length > 0)
|
|
487
|
+
aiTools = scoped;
|
|
488
|
+
}
|
|
489
|
+
// Resolve model — config callback > pipeline instance > error
|
|
490
|
+
let model;
|
|
491
|
+
if (modelConfig) {
|
|
492
|
+
model =
|
|
493
|
+
typeof modelConfig === 'function'
|
|
494
|
+
? modelConfig({ pipelineResult })
|
|
495
|
+
: modelConfig;
|
|
496
|
+
}
|
|
497
|
+
else if (pipelineResult.model.instance) {
|
|
498
|
+
model = pipelineResult.model.instance;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
return new Response(JSON.stringify({
|
|
502
|
+
error: 'No model configured. Provide model on handler config or as instance on ModelSelectorConfig.',
|
|
503
|
+
}), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
504
|
+
}
|
|
505
|
+
// Resolve system prompt
|
|
506
|
+
let systemPrompt = typeof systemPromptConfig === 'function'
|
|
507
|
+
? systemPromptConfig({
|
|
508
|
+
pipelineResult,
|
|
509
|
+
currentDate: new Date(),
|
|
510
|
+
})
|
|
511
|
+
: systemPromptConfig;
|
|
512
|
+
// Track the actual model ID used (may be overridden by Glirastes config)
|
|
513
|
+
let resolvedModelId = pipelineResult.model.modelId;
|
|
514
|
+
// Apply runtime prompt + model overrides from Glirastes (Pro feature)
|
|
515
|
+
if (lancer) {
|
|
516
|
+
try {
|
|
517
|
+
const configResult = await lancer.config.fetch(['prompts', 'models']);
|
|
518
|
+
// Prompt overrides
|
|
519
|
+
if (pipelineResult.module) {
|
|
520
|
+
const prompts = configResult.prompts;
|
|
521
|
+
if (prompts) {
|
|
522
|
+
const override = prompts.find((p) => p.moduleId === pipelineResult.module.id);
|
|
523
|
+
if (override) {
|
|
524
|
+
systemPrompt = (0, types_js_1.applyPromptOverride)(systemPrompt, override);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Model overrides — resolve tier from pipeline, apply override if configured
|
|
529
|
+
if (modelInstanceFactory) {
|
|
530
|
+
const modelOverrides = configResult.models;
|
|
531
|
+
if (modelOverrides) {
|
|
532
|
+
const tier = pipelineResult.module?.executionDefaults?.modelTier ?? 'standard';
|
|
533
|
+
const overrideModelId = modelOverrides[tier];
|
|
534
|
+
if (overrideModelId) {
|
|
535
|
+
resolvedModelId = overrideModelId;
|
|
536
|
+
model = modelInstanceFactory(overrideModelId);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// Graceful degradation: use local prompt/model if Glirastes is unreachable
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const stepLimit = resolveStepLimit({
|
|
546
|
+
moduleMaxSteps: pipelineResult.module?.executionDefaults?.maxSteps,
|
|
547
|
+
configuredMaxSteps,
|
|
548
|
+
safetyMaxSteps,
|
|
549
|
+
});
|
|
550
|
+
const contextWindow = pipelineResult.module?.executionDefaults?.contextWindow ??
|
|
551
|
+
maxContextMessages;
|
|
552
|
+
let recentMessages = messages.slice(-contextWindow);
|
|
553
|
+
// PII Shield: anonymize message history + wrap tools
|
|
554
|
+
if (piiShield) {
|
|
555
|
+
recentMessages = await anonymizeMessages(recentMessages, piiShield, sessionId);
|
|
556
|
+
aiTools = wrapToolsWithShield(aiTools, piiShield, sessionId);
|
|
557
|
+
}
|
|
558
|
+
// Lancer Approvals: delegate approval decisions to Glirastes
|
|
559
|
+
if (lancer?.approvals) {
|
|
560
|
+
aiTools = wrapToolsWithLancerApprovals(aiTools, lancer.approvals, sessionId);
|
|
561
|
+
}
|
|
562
|
+
const pipelineStreamHandler = createStreamHandler((onAudit || telemetry) ? {
|
|
563
|
+
sessionId,
|
|
564
|
+
requestStart,
|
|
565
|
+
modelId: resolvedModelId,
|
|
566
|
+
} : undefined);
|
|
567
|
+
return pipelineStreamHandler({
|
|
568
|
+
tools: aiTools,
|
|
569
|
+
messages: recentMessages,
|
|
570
|
+
systemPrompt,
|
|
571
|
+
model,
|
|
572
|
+
maxSteps: stepLimit.maxSteps,
|
|
573
|
+
stepLimitSource: stepLimit.stepLimitSource,
|
|
574
|
+
emitStepReports,
|
|
575
|
+
onStepReport,
|
|
576
|
+
maxOutputTokens,
|
|
577
|
+
temperature,
|
|
578
|
+
maxContextMessages,
|
|
579
|
+
modelTier: pipelineResult.model.modelId,
|
|
580
|
+
pipelineResult,
|
|
581
|
+
piiShield: piiShield ? { shield: piiShield, sessionId } : undefined,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
// ================================================================
|
|
585
|
+
// Legacy mode (without pipeline)
|
|
586
|
+
// ================================================================
|
|
587
|
+
// Guardrails
|
|
588
|
+
let processedMessages = messages;
|
|
589
|
+
if (guardrails) {
|
|
590
|
+
const check = await guardrails(messages);
|
|
591
|
+
if (!check.ok) {
|
|
592
|
+
return new Response(JSON.stringify({ error: check.reason ?? 'Invalid input' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
593
|
+
}
|
|
594
|
+
if (check.sanitizedMessages) {
|
|
595
|
+
processedMessages = check.sanitizedMessages;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Create callEndpoint for endpoint tools (needed regardless of static/dynamic)
|
|
599
|
+
const callEndpoint = (0, context_helpers_js_1.createInternalApiCallerFromRequest)(req);
|
|
600
|
+
const endpointToolContext = { ...toolContext, callEndpoint };
|
|
601
|
+
// Load tools
|
|
602
|
+
let registry;
|
|
603
|
+
if (staticTools) {
|
|
604
|
+
registry = staticTools;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
registry = await loadTools(endpointToolContext);
|
|
608
|
+
}
|
|
609
|
+
const allToolNames = Object.keys(registry);
|
|
610
|
+
// PII Shield: anonymize for legacy mode
|
|
611
|
+
const legacySessionId = piiShield ? deriveSessionId(processedMessages) : '';
|
|
612
|
+
if (piiShield) {
|
|
613
|
+
const lastUserText = extractLastUserText(processedMessages);
|
|
614
|
+
await piiShield.outbound(lastUserText, legacySessionId);
|
|
615
|
+
}
|
|
616
|
+
// Intent routing
|
|
617
|
+
let routing;
|
|
618
|
+
if (intentRouter) {
|
|
619
|
+
const text = extractLastUserText(processedMessages);
|
|
620
|
+
routing = await intentRouter(text, allToolNames);
|
|
621
|
+
}
|
|
622
|
+
// Convert to AI tools
|
|
623
|
+
let aiTools = await (0, index_js_1.toolsToAiTools)(registry, endpointToolContext, toolOptions);
|
|
624
|
+
// Scope tools by intent
|
|
625
|
+
if (routing?.toolNames && routing.toolNames.length > 0) {
|
|
626
|
+
const scoped = {};
|
|
627
|
+
for (const name of routing.toolNames) {
|
|
628
|
+
if (aiTools[name])
|
|
629
|
+
scoped[name] = aiTools[name];
|
|
630
|
+
}
|
|
631
|
+
if (Object.keys(scoped).length > 0)
|
|
632
|
+
aiTools = scoped;
|
|
633
|
+
}
|
|
634
|
+
// Resolve model + system prompt (legacy mode — no pipeline fallback)
|
|
635
|
+
if (!modelConfig) {
|
|
636
|
+
return new Response(JSON.stringify({
|
|
637
|
+
error: 'No model configured. In legacy mode (without pipeline), model is required.',
|
|
638
|
+
}), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
639
|
+
}
|
|
640
|
+
const model = typeof modelConfig === 'function'
|
|
641
|
+
? modelConfig({ routing })
|
|
642
|
+
: modelConfig;
|
|
643
|
+
const systemPrompt = typeof systemPromptConfig === 'function'
|
|
644
|
+
? systemPromptConfig({
|
|
645
|
+
routing,
|
|
646
|
+
currentDate: new Date(),
|
|
647
|
+
})
|
|
648
|
+
: systemPromptConfig;
|
|
649
|
+
const stepLimit = resolveStepLimit({
|
|
650
|
+
configuredMaxSteps: routing?.maxSteps ?? configuredMaxSteps,
|
|
651
|
+
safetyMaxSteps,
|
|
652
|
+
});
|
|
653
|
+
const contextWindow = routing?.contextWindow ?? maxContextMessages;
|
|
654
|
+
let recentMessages = processedMessages.slice(-contextWindow);
|
|
655
|
+
// PII Shield: anonymize message history + wrap tools (legacy mode)
|
|
656
|
+
if (piiShield) {
|
|
657
|
+
recentMessages = await anonymizeMessages(recentMessages, piiShield, legacySessionId);
|
|
658
|
+
aiTools = wrapToolsWithShield(aiTools, piiShield, legacySessionId);
|
|
659
|
+
}
|
|
660
|
+
// Lancer Approvals: delegate approval decisions to Glirastes (legacy mode)
|
|
661
|
+
if (lancer?.approvals) {
|
|
662
|
+
aiTools = wrapToolsWithLancerApprovals(aiTools, lancer.approvals, legacySessionId || sessionId);
|
|
663
|
+
}
|
|
664
|
+
const legacyStreamHandler = createStreamHandler((onAudit || telemetry) ? {
|
|
665
|
+
sessionId: legacySessionId || sessionId,
|
|
666
|
+
requestStart,
|
|
667
|
+
} : undefined);
|
|
668
|
+
// Delegate streaming to consumer's handler
|
|
669
|
+
return legacyStreamHandler({
|
|
670
|
+
tools: aiTools,
|
|
671
|
+
messages: recentMessages,
|
|
672
|
+
systemPrompt,
|
|
673
|
+
model,
|
|
674
|
+
maxSteps: stepLimit.maxSteps,
|
|
675
|
+
stepLimitSource: stepLimit.stepLimitSource,
|
|
676
|
+
emitStepReports,
|
|
677
|
+
onStepReport,
|
|
678
|
+
maxOutputTokens,
|
|
679
|
+
temperature,
|
|
680
|
+
maxContextMessages,
|
|
681
|
+
modelTier: routing?.modelTier,
|
|
682
|
+
routing,
|
|
683
|
+
piiShield: piiShield ? { shield: piiShield, sessionId: legacySessionId } : undefined,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
onError?.(error, req);
|
|
688
|
+
return new Response(JSON.stringify({ error: 'AI request failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (wrapHandler) {
|
|
692
|
+
return async (req) => wrapHandler(() => handler(req), req);
|
|
693
|
+
}
|
|
694
|
+
return handler;
|
|
695
|
+
}
|
|
696
|
+
// ============================================================================
|
|
697
|
+
// PII Shield Helpers
|
|
698
|
+
// ============================================================================
|
|
699
|
+
function deriveSessionId(messages) {
|
|
700
|
+
// Use the last user message's ID — groups approval continuations (same
|
|
701
|
+
// last user message) but separates distinct questions (new user message).
|
|
702
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
703
|
+
const msg = messages[i];
|
|
704
|
+
if (msg?.role === 'user' && msg?.id && typeof msg.id === 'string') {
|
|
705
|
+
return `ai-${msg.id}`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return `ai-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
709
|
+
}
|
|
710
|
+
async function anonymizeMessages(messages, shield, sessionId) {
|
|
711
|
+
const result = [];
|
|
712
|
+
for (const msg of messages) {
|
|
713
|
+
if (typeof msg !== 'object' || msg === null) {
|
|
714
|
+
result.push(msg);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const record = msg;
|
|
718
|
+
// Only anonymize user messages
|
|
719
|
+
if (record.role !== 'user') {
|
|
720
|
+
result.push(msg);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
// Anonymize string content
|
|
724
|
+
if (typeof record.content === 'string') {
|
|
725
|
+
const anonymized = await shield.outbound(record.content, sessionId);
|
|
726
|
+
result.push({ ...record, content: anonymized });
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
// Anonymize parts array
|
|
730
|
+
if (Array.isArray(record.parts)) {
|
|
731
|
+
const anonParts = await Promise.all(record.parts.map(async (part) => {
|
|
732
|
+
if (typeof part === 'object' && part !== null &&
|
|
733
|
+
part.type === 'text') {
|
|
734
|
+
const p = part;
|
|
735
|
+
if (typeof p.text === 'string') {
|
|
736
|
+
const anonymized = await shield.outbound(p.text, sessionId);
|
|
737
|
+
return { ...p, text: anonymized };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return part;
|
|
741
|
+
}));
|
|
742
|
+
result.push({ ...record, parts: anonParts });
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
result.push(msg);
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
function wrapToolsWithShield(aiTools, shield, sessionId) {
|
|
750
|
+
const wrapped = {};
|
|
751
|
+
for (const [name, raw] of Object.entries(aiTools)) {
|
|
752
|
+
const sdkTool = raw;
|
|
753
|
+
const originalExecute = sdkTool.execute;
|
|
754
|
+
wrapped[name] = {
|
|
755
|
+
...sdkTool,
|
|
756
|
+
execute: async (...executeArgs) => {
|
|
757
|
+
// Rehydrate args (first argument is the tool input).
|
|
758
|
+
// Uses the async path for Lancer Aegis rehydration.
|
|
759
|
+
// The sync rehydrateArgs is a legacy no-op kept for backward compat.
|
|
760
|
+
let args = executeArgs[0];
|
|
761
|
+
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
762
|
+
if (shield.rehydrateArgsAsync) {
|
|
763
|
+
args = await shield.rehydrateArgsAsync(args, sessionId);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
args = shield.rehydrateArgs(args, sessionId);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Execute with real args
|
|
770
|
+
const result = await originalExecute(args, ...executeArgs.slice(1));
|
|
771
|
+
// Anonymize result before LLM sees it
|
|
772
|
+
return shield.anonymizeResult(result, sessionId);
|
|
773
|
+
},
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return wrapped;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Wrap tool `needsApproval` with Lancer approval checks.
|
|
780
|
+
*
|
|
781
|
+
* When Lancer approvals are configured, each tool's approval decision is
|
|
782
|
+
* delegated to Glirastes instead of relying on the local `needsApproval` flag.
|
|
783
|
+
* If Lancer is unreachable, falls back to the existing local approval check.
|
|
784
|
+
*
|
|
785
|
+
* When a tool requires approval (Lancer returns `required: true`), the
|
|
786
|
+
* requestId is stored so that `decide()` can be called when the user
|
|
787
|
+
* approves and the tool's `execute` fires.
|
|
788
|
+
*/
|
|
789
|
+
function wrapToolsWithLancerApprovals(aiTools, approvals, traceId) {
|
|
790
|
+
// Map toolName → requestId for pending approvals (cleared after decide)
|
|
791
|
+
const pendingRequestIds = new Map();
|
|
792
|
+
// Track tools that actually required approval (survives cross-request boundary via resolve fallback)
|
|
793
|
+
const approvalRequired = new Set();
|
|
794
|
+
const wrapped = {};
|
|
795
|
+
for (const [name, raw] of Object.entries(aiTools)) {
|
|
796
|
+
const sdkTool = raw;
|
|
797
|
+
const originalNeedsApproval = sdkTool.needsApproval;
|
|
798
|
+
const originalExecute = sdkTool.execute;
|
|
799
|
+
// Respect local needsApproval: false — never delegate to Lancer for these tools
|
|
800
|
+
if (originalNeedsApproval === false) {
|
|
801
|
+
wrapped[name] = sdkTool;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
wrapped[name] = {
|
|
805
|
+
...sdkTool,
|
|
806
|
+
// Override needsApproval with Lancer check
|
|
807
|
+
needsApproval: async (input) => {
|
|
808
|
+
try {
|
|
809
|
+
const inputHash = approvals.hashInput((input && typeof input === 'object' ? input : {}));
|
|
810
|
+
const check = await approvals.check({
|
|
811
|
+
toolId: sdkTool._toolId ?? name,
|
|
812
|
+
inputHash,
|
|
813
|
+
...(traceId ? { traceId } : {}),
|
|
814
|
+
});
|
|
815
|
+
if (check.required) {
|
|
816
|
+
// Store requestId for later decide() call
|
|
817
|
+
if (check.requestId) {
|
|
818
|
+
pendingRequestIds.set(name, check.requestId);
|
|
819
|
+
}
|
|
820
|
+
approvalRequired.add(name);
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
// Lancer says no approval required (auto-approved or no matching flow)
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
// Graceful degradation: fall back to local needsApproval
|
|
828
|
+
if (typeof originalNeedsApproval === 'function') {
|
|
829
|
+
return originalNeedsApproval(input);
|
|
830
|
+
}
|
|
831
|
+
return originalNeedsApproval === true;
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
// Wrap execute to call decide() when approved.
|
|
835
|
+
// The requestId may be missing because needsApproval and execute run in
|
|
836
|
+
// different HTTP requests (Vercel AI SDK approval flow), so the in-memory
|
|
837
|
+
// pendingRequestIds map is recreated empty on the second request.
|
|
838
|
+
// Fallback: resolve by toolId + inputHash via the /v1/approvals/resolve endpoint.
|
|
839
|
+
execute: async (...executeArgs) => {
|
|
840
|
+
const requestId = pendingRequestIds.get(name);
|
|
841
|
+
const toolId = sdkTool._toolId ?? name;
|
|
842
|
+
const decidedBy = 'unknown';
|
|
843
|
+
if (requestId) {
|
|
844
|
+
pendingRequestIds.delete(name);
|
|
845
|
+
try {
|
|
846
|
+
await approvals.decide(requestId, {
|
|
847
|
+
decision: 'approved',
|
|
848
|
+
decidedBy,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
// Fire-and-forget: don't block tool execution if decide fails
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else if (approvalRequired.has(name)) {
|
|
856
|
+
// Cross-request fallback: resolve pending approval by toolId + inputHash.
|
|
857
|
+
// Only attempt when needsApproval previously returned true (the requestId
|
|
858
|
+
// may be missing because needsApproval and execute run in different HTTP
|
|
859
|
+
// requests, recreating the in-memory map).
|
|
860
|
+
approvalRequired.delete(name);
|
|
861
|
+
try {
|
|
862
|
+
const input = executeArgs[0];
|
|
863
|
+
const inputHash = approvals.hashInput((input && typeof input === 'object' ? input : {}));
|
|
864
|
+
await approvals.resolve({
|
|
865
|
+
toolId,
|
|
866
|
+
inputHash,
|
|
867
|
+
decision: 'approved',
|
|
868
|
+
decidedBy,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
// Fire-and-forget: don't block tool execution if resolve fails
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return originalExecute(...executeArgs);
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return wrapped;
|
|
880
|
+
}
|
|
881
|
+
function extractLastUserText(messages) {
|
|
882
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
883
|
+
const msg = messages[i];
|
|
884
|
+
if (typeof msg !== 'object' || msg === null)
|
|
885
|
+
continue;
|
|
886
|
+
const record = msg;
|
|
887
|
+
if (record.role !== 'user')
|
|
888
|
+
continue;
|
|
889
|
+
if (typeof record.content === 'string')
|
|
890
|
+
return record.content;
|
|
891
|
+
if (Array.isArray(record.parts)) {
|
|
892
|
+
for (const part of record.parts) {
|
|
893
|
+
if (typeof part === 'object' &&
|
|
894
|
+
part !== null &&
|
|
895
|
+
part.type === 'text') {
|
|
896
|
+
const text = part.text;
|
|
897
|
+
if (typeof text === 'string')
|
|
898
|
+
return text;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (Array.isArray(record.content)) {
|
|
903
|
+
for (const part of record.content) {
|
|
904
|
+
if (typeof part === 'object' &&
|
|
905
|
+
part !== null &&
|
|
906
|
+
part.type === 'text') {
|
|
907
|
+
const text = part.text;
|
|
908
|
+
if (typeof text === 'string')
|
|
909
|
+
return text;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return '';
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Extract a lightweight conversation history from UIMessages for intent
|
|
918
|
+
* classification. Returns the last `maxPairs` user/assistant text exchanges
|
|
919
|
+
* EXCLUDING the current (latest) user message.
|
|
920
|
+
*
|
|
921
|
+
* Tool calls/results are stripped — only human-readable text is included
|
|
922
|
+
* to keep the classification prompt small and fast.
|
|
923
|
+
*/
|
|
924
|
+
function extractConversationHistory(messages, maxPairs) {
|
|
925
|
+
// Collect text from all messages except the last user message
|
|
926
|
+
const textMessages = [];
|
|
927
|
+
for (let i = 0; i < messages.length; i++) {
|
|
928
|
+
const msg = messages[i];
|
|
929
|
+
if (typeof msg !== 'object' || msg === null)
|
|
930
|
+
continue;
|
|
931
|
+
const record = msg;
|
|
932
|
+
const role = record.role;
|
|
933
|
+
if (role !== 'user' && role !== 'assistant')
|
|
934
|
+
continue;
|
|
935
|
+
const text = extractTextFromMessage(record);
|
|
936
|
+
if (!text.trim())
|
|
937
|
+
continue;
|
|
938
|
+
textMessages.push({ role: role, content: text });
|
|
939
|
+
}
|
|
940
|
+
// Remove the last user message (that's the current input, not history)
|
|
941
|
+
if (textMessages.length > 0 && textMessages[textMessages.length - 1].role === 'user') {
|
|
942
|
+
textMessages.pop();
|
|
943
|
+
}
|
|
944
|
+
// Return the last N messages (up to maxPairs * 2 for user+assistant pairs)
|
|
945
|
+
return textMessages.slice(-(maxPairs * 2));
|
|
946
|
+
}
|
|
947
|
+
function extractTextFromMessage(msg) {
|
|
948
|
+
if (typeof msg.content === 'string')
|
|
949
|
+
return msg.content;
|
|
950
|
+
const parts = Array.isArray(msg.parts)
|
|
951
|
+
? msg.parts
|
|
952
|
+
: Array.isArray(msg.content)
|
|
953
|
+
? msg.content
|
|
954
|
+
: [];
|
|
955
|
+
const texts = [];
|
|
956
|
+
for (const part of parts) {
|
|
957
|
+
if (typeof part === 'object' && part !== null) {
|
|
958
|
+
const p = part;
|
|
959
|
+
if (p.type === 'text' && typeof p.text === 'string') {
|
|
960
|
+
texts.push(p.text);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return texts.join(' ');
|
|
965
|
+
}
|