libre-webui 0.36.0 → 0.37.1
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/README.md +59 -0
- package/backend/dist/cordis/contracts.d.ts +270 -0
- package/backend/dist/cordis/contracts.d.ts.map +1 -0
- package/backend/dist/cordis/contracts.js +32 -0
- package/backend/dist/cordis/contracts.js.map +1 -0
- package/backend/dist/cordis/dsh/chat-model.d.ts +16 -0
- package/backend/dist/cordis/dsh/chat-model.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/chat-model.js +104 -0
- package/backend/dist/cordis/dsh/chat-model.js.map +1 -0
- package/backend/dist/cordis/dsh/engine-plugin.d.ts +186 -0
- package/backend/dist/cordis/dsh/engine-plugin.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/engine-plugin.js +1116 -0
- package/backend/dist/cordis/dsh/engine-plugin.js.map +1 -0
- package/backend/dist/cordis/dsh/librewebui-llm-adapter.d.ts +175 -0
- package/backend/dist/cordis/dsh/librewebui-llm-adapter.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/librewebui-llm-adapter.js +480 -0
- package/backend/dist/cordis/dsh/librewebui-llm-adapter.js.map +1 -0
- package/backend/dist/cordis/dsh/model-identity.d.ts +12 -0
- package/backend/dist/cordis/dsh/model-identity.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/model-identity.js +87 -0
- package/backend/dist/cordis/dsh/model-identity.js.map +1 -0
- package/backend/dist/cordis/dsh/native-provider-client.d.ts +44 -0
- package/backend/dist/cordis/dsh/native-provider-client.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/native-provider-client.js +524 -0
- package/backend/dist/cordis/dsh/native-provider-client.js.map +1 -0
- package/backend/dist/cordis/dsh/native-provider-plugin.d.ts +11 -0
- package/backend/dist/cordis/dsh/native-provider-plugin.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/native-provider-plugin.js +416 -0
- package/backend/dist/cordis/dsh/native-provider-plugin.js.map +1 -0
- package/backend/dist/cordis/dsh/native-provider-protocol.d.ts +37 -0
- package/backend/dist/cordis/dsh/native-provider-protocol.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/native-provider-protocol.js +325 -0
- package/backend/dist/cordis/dsh/native-provider-protocol.js.map +1 -0
- package/backend/dist/cordis/dsh/provider-handler.d.ts +36 -0
- package/backend/dist/cordis/dsh/provider-handler.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/provider-handler.js +582 -0
- package/backend/dist/cordis/dsh/provider-handler.js.map +1 -0
- package/backend/dist/cordis/dsh/session-files.d.ts +9 -0
- package/backend/dist/cordis/dsh/session-files.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/session-files.js +60 -0
- package/backend/dist/cordis/dsh/session-files.js.map +1 -0
- package/backend/dist/cordis/dsh/session-settings.d.ts +15 -0
- package/backend/dist/cordis/dsh/session-settings.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/session-settings.js +51 -0
- package/backend/dist/cordis/dsh/session-settings.js.map +1 -0
- package/backend/dist/cordis/dsh/work-driver.d.ts +12 -0
- package/backend/dist/cordis/dsh/work-driver.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/work-driver.js +402 -0
- package/backend/dist/cordis/dsh/work-driver.js.map +1 -0
- package/backend/dist/cordis/dsh/workspace-policy.d.ts +10 -0
- package/backend/dist/cordis/dsh/workspace-policy.d.ts.map +1 -0
- package/backend/dist/cordis/dsh/workspace-policy.js +44 -0
- package/backend/dist/cordis/dsh/workspace-policy.js.map +1 -0
- package/backend/dist/cordis/host/composition.d.ts +73 -0
- package/backend/dist/cordis/host/composition.d.ts.map +1 -0
- package/backend/dist/cordis/host/composition.js +205 -0
- package/backend/dist/cordis/host/composition.js.map +1 -0
- package/backend/dist/cordis/host/config.d.ts +159 -0
- package/backend/dist/cordis/host/config.d.ts.map +1 -0
- package/backend/dist/cordis/host/config.js +184 -0
- package/backend/dist/cordis/host/config.js.map +1 -0
- package/backend/dist/cordis/host/host.d.ts +74 -0
- package/backend/dist/cordis/host/host.d.ts.map +1 -0
- package/backend/dist/cordis/host/host.js +420 -0
- package/backend/dist/cordis/host/host.js.map +1 -0
- package/backend/dist/cordis/host/loader-entry.d.ts +41 -0
- package/backend/dist/cordis/host/loader-entry.d.ts.map +1 -0
- package/backend/dist/cordis/host/loader-entry.js +52 -0
- package/backend/dist/cordis/host/loader-entry.js.map +1 -0
- package/backend/dist/cordis/host/model.d.ts +45 -0
- package/backend/dist/cordis/host/model.d.ts.map +1 -0
- package/backend/dist/cordis/host/model.js +54 -0
- package/backend/dist/cordis/host/model.js.map +1 -0
- package/backend/dist/cordis/host/privacy.d.ts +8 -0
- package/backend/dist/cordis/host/privacy.d.ts.map +1 -0
- package/backend/dist/cordis/host/privacy.js +73 -0
- package/backend/dist/cordis/host/privacy.js.map +1 -0
- package/backend/dist/cordis/index.d.ts +14 -0
- package/backend/dist/cordis/index.d.ts.map +1 -0
- package/backend/dist/cordis/index.js +30 -0
- package/backend/dist/cordis/index.js.map +1 -0
- package/backend/dist/cordis/runtime.d.ts +97 -0
- package/backend/dist/cordis/runtime.d.ts.map +1 -0
- package/backend/dist/cordis/runtime.js +231 -0
- package/backend/dist/cordis/runtime.js.map +1 -0
- package/backend/dist/cordis/work-model.d.ts +7 -0
- package/backend/dist/cordis/work-model.d.ts.map +1 -0
- package/backend/dist/cordis/work-model.js +30 -0
- package/backend/dist/cordis/work-model.js.map +1 -0
- package/backend/dist/db.js +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +9 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/main.js +12 -0
- package/backend/dist/main.js.map +1 -1
- package/backend/dist/persistence/extensionTypes.d.ts +2 -0
- package/backend/dist/persistence/extensionTypes.d.ts.map +1 -1
- package/backend/dist/persistence/extensionTypes.js.map +1 -1
- package/backend/dist/persistence/postgresExtensionRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresExtensionRepositories.js +38 -0
- package/backend/dist/persistence/postgresExtensionRepositories.js.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.js +6 -1
- package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/resourceTypes.d.ts +3 -1
- package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
- package/backend/dist/persistence/resourceTypes.js.map +1 -1
- package/backend/dist/persistence/sqliteExtensionRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteExtensionRepositories.js +40 -0
- package/backend/dist/persistence/sqliteExtensionRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.js +7 -1
- package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
- package/backend/dist/routes/agentCli.d.ts.map +1 -1
- package/backend/dist/routes/agentCli.js +34 -2
- package/backend/dist/routes/agentCli.js.map +1 -1
- package/backend/dist/routes/chat.d.ts.map +1 -1
- package/backend/dist/routes/chat.js +9 -1
- package/backend/dist/routes/chat.js.map +1 -1
- package/backend/dist/routes/cordis.d.ts +3 -0
- package/backend/dist/routes/cordis.d.ts.map +1 -0
- package/backend/dist/routes/cordis.js +440 -0
- package/backend/dist/routes/cordis.js.map +1 -0
- package/backend/dist/routes/libreClaw.js +1 -1
- package/backend/dist/routes/libreClaw.js.map +1 -1
- package/backend/dist/routes/work.d.ts.map +1 -1
- package/backend/dist/routes/work.js +23 -8
- package/backend/dist/routes/work.js.map +1 -1
- package/backend/dist/services/agentAccessService.d.ts +4 -0
- package/backend/dist/services/agentAccessService.d.ts.map +1 -1
- package/backend/dist/services/agentAccessService.js +48 -16
- package/backend/dist/services/agentAccessService.js.map +1 -1
- package/backend/dist/services/agentCliService.d.ts +42 -1
- package/backend/dist/services/agentCliService.d.ts.map +1 -1
- package/backend/dist/services/agentCliService.js +206 -7
- package/backend/dist/services/agentCliService.js.map +1 -1
- package/backend/dist/services/agentCliUsage.d.ts +22 -0
- package/backend/dist/services/agentCliUsage.d.ts.map +1 -0
- package/backend/dist/services/agentCliUsage.js +192 -0
- package/backend/dist/services/agentCliUsage.js.map +1 -0
- package/backend/dist/services/authService.d.ts +3 -0
- package/backend/dist/services/authService.d.ts.map +1 -1
- package/backend/dist/services/authService.js +4 -1
- package/backend/dist/services/authService.js.map +1 -1
- package/backend/dist/services/chatGenerationService.d.ts +7 -0
- package/backend/dist/services/chatGenerationService.d.ts.map +1 -1
- package/backend/dist/services/chatGenerationService.js +32 -1
- package/backend/dist/services/chatGenerationService.js.map +1 -1
- package/backend/dist/services/cordisAccessService.d.ts +56 -0
- package/backend/dist/services/cordisAccessService.d.ts.map +1 -0
- package/backend/dist/services/cordisAccessService.js +98 -0
- package/backend/dist/services/cordisAccessService.js.map +1 -0
- package/backend/dist/services/durableChatGenerationService.d.ts.map +1 -1
- package/backend/dist/services/durableChatGenerationService.js +9 -1
- package/backend/dist/services/durableChatGenerationService.js.map +1 -1
- package/backend/dist/services/emailService.d.ts.map +1 -1
- package/backend/dist/services/emailService.js +8 -1
- package/backend/dist/services/emailService.js.map +1 -1
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +7 -26
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/services/pluginUsageService.d.ts +52 -0
- package/backend/dist/services/pluginUsageService.d.ts.map +1 -1
- package/backend/dist/services/pluginUsageService.js +89 -1
- package/backend/dist/services/pluginUsageService.js.map +1 -1
- package/backend/dist/services/systemSettingsService.d.ts +1 -1
- package/backend/dist/services/systemSettingsService.d.ts.map +1 -1
- package/backend/dist/services/systemSettingsService.js +1 -1
- package/backend/dist/services/systemSettingsService.js.map +1 -1
- package/backend/dist/services/thinkingSummaryService.d.ts +3 -0
- package/backend/dist/services/thinkingSummaryService.d.ts.map +1 -1
- package/backend/dist/services/thinkingSummaryService.js +56 -17
- package/backend/dist/services/thinkingSummaryService.js.map +1 -1
- package/backend/dist/services/titleGenerationService.d.ts +22 -2
- package/backend/dist/services/titleGenerationService.d.ts.map +1 -1
- package/backend/dist/services/titleGenerationService.js +63 -8
- package/backend/dist/services/titleGenerationService.js.map +1 -1
- package/backend/dist/services/workAgentService.d.ts +1 -1
- package/backend/dist/services/workAgentService.d.ts.map +1 -1
- package/backend/dist/services/workAgentService.js +76 -12
- package/backend/dist/services/workAgentService.js.map +1 -1
- package/backend/dist/services/workModelProviderService.d.ts +5 -0
- package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
- package/backend/dist/services/workModelProviderService.js +52 -3
- package/backend/dist/services/workModelProviderService.js.map +1 -1
- package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeService.js +4 -1
- package/backend/dist/services/workRuntimeService.js.map +1 -1
- package/backend/dist/services/workTaskService.d.ts.map +1 -1
- package/backend/dist/services/workTaskService.js +6 -6
- package/backend/dist/services/workTaskService.js.map +1 -1
- package/backend/dist/types/work.d.ts +12 -1
- package/backend/dist/types/work.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.js +18 -4
- package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
- package/backend/dist/utils/pluginStreaming.d.ts +6 -0
- package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreaming.js +10 -0
- package/backend/dist/utils/pluginStreaming.js.map +1 -1
- package/backend/dist/utils/smtpClient.d.ts +5 -1
- package/backend/dist/utils/smtpClient.d.ts.map +1 -1
- package/backend/dist/utils/smtpClient.js +29 -3
- package/backend/dist/utils/smtpClient.js.map +1 -1
- package/backend/dist/websocketServer.d.ts.map +1 -1
- package/backend/dist/websocketServer.js +4 -0
- package/backend/dist/websocketServer.js.map +1 -1
- package/backend/package.json +37 -2
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/artifact-runtime/framer-motion.js +3 -3
- package/frontend/dist/artifact-runtime/lucide-react.js +1 -1
- package/frontend/dist/artifact-runtime/react.js +5 -5
- package/frontend/dist/artifact-runtime/recharts.js +45 -45
- package/frontend/dist/assets/index-BrDSO609.js +20 -0
- package/frontend/dist/assets/index-C8_AByBq.css +1 -0
- package/frontend/dist/index.html +33 -31
- package/frontend/dist/js/{ArtifactContainer-B8tyPtWd.js → ArtifactContainer-B1-GVzVf.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-BwRfWoSK.js → ArtifactDemoPage-goftRHdk.js} +1 -1
- package/frontend/dist/js/{ArtifactSandboxFrame-Ctcwt_l2.js → ArtifactSandboxFrame-_8RA8_JV.js} +1 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-DlkOZI5N.js +1 -0
- package/frontend/dist/js/{AutomationModal-mTYs9LrJ.js → AutomationModal-BaaxZF1M.js} +1 -1
- package/frontend/dist/js/AutomationsPage-DY2x7zVm.js +1 -0
- package/frontend/dist/js/{AvatarUpload-wWUAi3e2.js → AvatarUpload-CH_n1Ef0.js} +1 -1
- package/frontend/dist/js/{Button-ZQ6BePid.js → Button-CGMYia4g.js} +1 -1
- package/frontend/dist/js/{CalendarPage-Ch4racU2.js → CalendarPage-DLo5nm3Q.js} +1 -1
- package/frontend/dist/js/ChannelsPage-DnQI7mzq.js +1 -0
- package/frontend/dist/js/ChatPage-BaDkJnEC.js +14 -0
- package/frontend/dist/js/ConnectModels-B4dGb5yc.js +1 -0
- package/frontend/dist/js/CordisPage-CPiYn2Bx.js +1 -0
- package/frontend/dist/js/{EvaluationsPage-DHPnNBkW.js → EvaluationsPage-DpdGpd7x.js} +2 -2
- package/frontend/dist/js/{FirstTimeSetup-CoF83yY8.js → FirstTimeSetup-By8OE1JS.js} +1 -1
- package/frontend/dist/js/GalleryPage-ByZyIhZQ.js +1 -0
- package/frontend/dist/js/HomePage-Btc-Mvin.js +1 -0
- package/frontend/dist/js/{LanguageSwitcher-CogfczNm.js → LanguageSwitcher-B3CxRW6J.js} +1 -1
- package/frontend/dist/js/LibreClawPage-3Sonm1gu.js +1 -0
- package/frontend/dist/js/{Logo-C9fsEUaY.js → Logo-HZvdAk2c.js} +1 -1
- package/frontend/dist/js/{LogoMark-Cvu5CQOW.js → LogoMark-GJnfMXsk.js} +1 -1
- package/frontend/dist/js/{ModelManagerModals-Bc2Woor7.js → ModelManagerModals-Bk6OQzJI.js} +1 -1
- package/frontend/dist/js/NotesPage-lFhfjMqJ.js +3 -0
- package/frontend/dist/js/{OptimizedSyntaxHighlighter-aKWyHj_c.js → OptimizedSyntaxHighlighter-CzzV6ONG.js} +1 -1
- package/frontend/dist/js/OptimizedSyntaxHighlighter-ajqwyBQm.js +1 -0
- package/frontend/dist/js/{PasswordStrengthMeter-BJr4eBbo.js → PasswordStrengthMeter-DasfJa4P.js} +1 -1
- package/frontend/dist/js/{PersonasPage-ff5opf9B.js → PersonasPage-nOThOpXp.js} +2 -2
- package/frontend/dist/js/PluginUsagePage-BRbHeRf7.js +1 -0
- package/frontend/dist/js/{RichMessageContent-BNNylVAb.js → RichMessageContent-BZ7KKJKK.js} +2 -2
- package/frontend/dist/js/RichMessageContent-oyydJjgc.js +1 -0
- package/frontend/dist/js/SettingsModal-VpsAx2Ns.js +19 -0
- package/frontend/dist/js/{SettingsToggle-CKLHOUt-.js → SettingsToggle-oxvw7ulA.js} +1 -1
- package/frontend/dist/js/{ShareDialog-DykD70uB.js → ShareDialog-CnxPGPb_.js} +1 -1
- package/frontend/dist/js/SystemPage-D2ikvhDp.js +1 -0
- package/frontend/dist/js/{TurnstileWidget-BO2ceTt8.js → TurnstileWidget-CVepEMEQ.js} +1 -1
- package/frontend/dist/js/{UserManagementPage-Ch-PvYQ9.js → UserManagementPage-2yHv89ZA.js} +1 -1
- package/frontend/dist/js/UserManagementPanel-vxpiyiTZ.js +1 -0
- package/frontend/dist/js/{WhatsNewModal-Bu4H4MEb.js → WhatsNewModal-DAAFzY0d.js} +1 -1
- package/frontend/dist/js/WorkPage-BYpYD9mr.js +8 -0
- package/frontend/dist/js/api-C0ligMmL.js +1 -0
- package/frontend/dist/js/api-CVUaOaq4.js +14 -0
- package/frontend/dist/js/{appNavigation-CVsQuDI1.js → appNavigation-CqtrZ9bm.js} +1 -1
- package/frontend/dist/js/{appStore-Din-tTD9.js → appStore-Biw63DtJ.js} +2 -2
- package/frontend/dist/js/appVersion-D78eXAb-.js +1 -0
- package/frontend/dist/js/ar-Cq-DrSYL.js +2 -0
- package/frontend/dist/js/authStore-2DsDTcIu.js +1 -0
- package/frontend/dist/js/{authStore-Cm8P6rYs.js → authStore-C5qKrmn4.js} +3 -3
- package/frontend/dist/js/bn-BqxLh-KL.js +2 -0
- package/frontend/dist/js/chatStore-DQhEqb9F.js +3 -0
- package/frontend/dist/js/client-gf5bjBrn.js +2 -0
- package/frontend/dist/js/composerStyles-C83jssgZ.js +1 -0
- package/frontend/dist/js/{config-DHea4b8N.js → config-CNvgxwAm.js} +1 -1
- package/frontend/dist/js/cordisApi-BYjA4EOQ.js +2 -0
- package/frontend/dist/js/cs-D2DDNZf-.js +2 -0
- package/frontend/dist/js/da-BciY-TSG.js +2 -0
- package/frontend/dist/js/de-DjU_eXQ-.js +2 -0
- package/frontend/dist/js/es-ChIEs80I.js +2 -0
- package/frontend/dist/js/{estree-DCT3Jm65.js → estree-DGmLqopC.js} +17 -17
- package/frontend/dist/js/evaluationsApi-Bg12ekHo.js +1 -0
- package/frontend/dist/js/fr-Bi8BGRW4.js +2 -0
- package/frontend/dist/js/hi-BMsCnWg7.js +2 -0
- package/frontend/dist/js/{html-CBXMuS97.js → html-BryNQ_yx.js} +6 -6
- package/frontend/dist/js/id-CpcN1vhk.js +2 -0
- package/frontend/dist/js/is-DkUy2cmP.js +2 -0
- package/frontend/dist/js/it-BglK9DRZ.js +2 -0
- package/frontend/dist/js/ja-IrprzwLJ.js +2 -0
- package/frontend/dist/js/ko--wwEsMPi.js +2 -0
- package/frontend/dist/js/{markdown-Bo71X2X2.js → markdown-CZ1tYEW8.js} +17 -17
- package/frontend/dist/js/{markdown-core-BFkBYv2v.js → markdown-core-KjS96SYW.js} +1 -1
- package/frontend/dist/js/{markdown-html-CwjnwJbl.js → markdown-html-DQEwrWcf.js} +1 -1
- package/frontend/dist/js/{markdown-math-CJvHdD1l.js → markdown-math-CKi32S5d.js} +1 -1
- package/frontend/dist/js/{markdownHtml-egpK3_YY.js → markdownHtml-lrXhxBhK.js} +1 -1
- package/frontend/dist/js/markdownMath-B0BpyFBS.js +1 -0
- package/frontend/dist/js/{messageCodeStyles-euWoxwoo.js → messageCodeStyles-DJZ4QMVq.js} +2 -2
- package/frontend/dist/js/ms-BWj59y7-.js +2 -0
- package/frontend/dist/js/nl-D-QndW_A.js +2 -0
- package/frontend/dist/js/pl-Dw7d7iDV.js +2 -0
- package/frontend/dist/js/pt-BI53uHJw.js +2 -0
- package/frontend/dist/js/react-vendor-wiHys0m2.js +9 -0
- package/frontend/dist/js/router-vendor-HZm4FR0A.js +1 -0
- package/frontend/dist/js/ru-JKyTgPg9.js +2 -0
- package/frontend/dist/js/{standalone-6z5x8CWe.js → standalone-DbyFjvTU.js} +1 -1
- package/frontend/dist/js/sv-BIkBLPSQ.js +2 -0
- package/frontend/dist/js/{syntax-highlight-7t8DGG2n.js → syntax-highlight-DNc3FFCa.js} +1 -1
- package/frontend/dist/js/{teamEventStream-DGgjrVqw.js → teamEventStream-PXiwR5Kr.js} +1 -1
- package/frontend/dist/js/th-CZ_LcCU0.js +2 -0
- package/frontend/dist/js/tr-B265byx1.js +2 -0
- package/frontend/dist/js/{typescript-CslHIgIJ.js → typescript-DUM5niRM.js} +1 -1
- package/frontend/dist/js/{ui-CNcEDQ95.js → ui-DDMjO5dd.js} +2 -2
- package/frontend/dist/js/ui-vendor-BhHHyHk3.js +1 -0
- package/frontend/dist/js/uk-VTAlChwf.js +2 -0
- package/frontend/dist/js/{useChat-YRODF1y1.js → useChat-DaIZ2Dbu.js} +1 -1
- package/frontend/dist/js/{useDialogFocus-BQLdAGYH.js → useDialogFocus-BPTG_Zmy.js} +1 -1
- package/frontend/dist/js/useDictation-BXyX1U8r.js +1 -0
- package/frontend/dist/js/useQuery-hWmQjdLu.js +1 -0
- package/frontend/dist/js/{useTranslation-Kn7_iLGj.js → useTranslation-B9kflK9S.js} +1 -1
- package/frontend/dist/js/{utils-Dq1w7K0c.js → utils-p8Gt72YP.js} +1 -1
- package/frontend/dist/js/{utils-vendor-dx9vGAt3.js → utils-vendor-DISWt2y9.js} +1 -1
- package/frontend/dist/js/vi-D8rXh-53.js +2 -0
- package/frontend/dist/js/{workStatus-oU04ZrUq.js → work-DmJpVPPO.js} +1 -1
- package/frontend/dist/js/workApi-BIS9RxRi.js +1 -0
- package/frontend/dist/js/workApi-C3Td_lFD.js +5 -0
- package/frontend/dist/js/workModels-MzU2Ab7l.js +1 -0
- package/frontend/dist/js/zh-BEGd9iBD.js +2 -0
- package/package.json +43 -8
- package/scripts/prepare-dsh-provider.mjs +124 -0
- package/frontend/dist/assets/index-BXTzjRcS.js +0 -92
- package/frontend/dist/assets/index-uzzNXp5R.css +0 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-DPWGjHhp.js +0 -1
- package/frontend/dist/js/AutomationsPage-DvsLZWtg.js +0 -1
- package/frontend/dist/js/ChannelsPage-D252gXwf.js +0 -1
- package/frontend/dist/js/ChatPage-D_CzOiHw.js +0 -14
- package/frontend/dist/js/ConnectModels-DYOtIOEY.js +0 -1
- package/frontend/dist/js/GalleryPage-BpK-vrvd.js +0 -1
- package/frontend/dist/js/HomePage-zJkkDNJl.js +0 -1
- package/frontend/dist/js/LibreClawPage-CAgNj9GM.js +0 -1
- package/frontend/dist/js/NotesPage-CNTqlwNc.js +0 -3
- package/frontend/dist/js/OptimizedSyntaxHighlighter-BFjOBavF.js +0 -1
- package/frontend/dist/js/PluginUsagePage-Cp4j4-VZ.js +0 -1
- package/frontend/dist/js/RichMessageContent-BOY5jqj-.js +0 -1
- package/frontend/dist/js/SettingsModal-WAdVG4-g.js +0 -19
- package/frontend/dist/js/SystemPage-Dwi8leEQ.js +0 -1
- package/frontend/dist/js/UserManagementPanel-CfG67c_k.js +0 -1
- package/frontend/dist/js/WorkPage-D_cL9wp4.js +0 -8
- package/frontend/dist/js/api-29NO19CP.js +0 -14
- package/frontend/dist/js/api-DhafOTdF.js +0 -1
- package/frontend/dist/js/appVersion-BNuquyI6.js +0 -1
- package/frontend/dist/js/ar-WHne6B0o.js +0 -2
- package/frontend/dist/js/authStore-snvjjOuX.js +0 -1
- package/frontend/dist/js/bn-BZvCjqwO.js +0 -2
- package/frontend/dist/js/chatStore-BPpcL4I5.js +0 -3
- package/frontend/dist/js/composerStyles-bNlrIugf.js +0 -1
- package/frontend/dist/js/cs-DfSqWQ9V.js +0 -2
- package/frontend/dist/js/da-BmMLW4Dl.js +0 -2
- package/frontend/dist/js/de-BQMiQwQT.js +0 -2
- package/frontend/dist/js/es-DXf6jt4Z.js +0 -2
- package/frontend/dist/js/evaluationsApi-Btc9Mg9z.js +0 -1
- package/frontend/dist/js/fr-BD6nT1oc.js +0 -2
- package/frontend/dist/js/hi-BSuNic-8.js +0 -2
- package/frontend/dist/js/id-wodbwt3B.js +0 -2
- package/frontend/dist/js/is-CEjw-OJE.js +0 -2
- package/frontend/dist/js/it-X_pq8C7q.js +0 -2
- package/frontend/dist/js/ja-DWM2HZZd.js +0 -2
- package/frontend/dist/js/ko-DFi2khBB.js +0 -2
- package/frontend/dist/js/markdownMath-mesq3ZcY.js +0 -1
- package/frontend/dist/js/ms-DNQateTR.js +0 -2
- package/frontend/dist/js/nl-DSWhi8W8.js +0 -2
- package/frontend/dist/js/pl-W9WEDpZ3.js +0 -2
- package/frontend/dist/js/pt-DCCGvdNt.js +0 -2
- package/frontend/dist/js/react-vendor-D0Ec-185.js +0 -9
- package/frontend/dist/js/router-vendor-CM0PhOMl.js +0 -1
- package/frontend/dist/js/ru-D-Ww6dXe.js +0 -2
- package/frontend/dist/js/sv-B0Iry-H3.js +0 -2
- package/frontend/dist/js/th-CEZS0WeE.js +0 -2
- package/frontend/dist/js/tr-BFJ8TeYb.js +0 -2
- package/frontend/dist/js/ui-vendor-LatG6rFF.js +0 -177
- package/frontend/dist/js/uk-BmW3yDel.js +0 -2
- package/frontend/dist/js/useQuery-C3O4CZ54.js +0 -1
- package/frontend/dist/js/vi-Cu25FXnp.js +0 -2
- package/frontend/dist/js/work-DVgm81bJ.js +0 -1
- package/frontend/dist/js/workApi-BdUNugC3.js +0 -1
- package/frontend/dist/js/workApi-OieM8qEL.js +0 -6
- package/frontend/dist/js/zh-CuCAx3oH.js +0 -2
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The DSH engine bridge: a Cordis plugin that turns a mounted DSH composition
|
|
19
|
+
* into the `libreDshEngine` contract.
|
|
20
|
+
*
|
|
21
|
+
* This is the only Libre WebUI module that names concrete `@deepseek-ai/dsh-*`
|
|
22
|
+
* packages, and it does so for types alone. Everything it publishes is defined
|
|
23
|
+
* in `cordis/contracts.ts`, so routes, the WebSocket bridge, and the frontend
|
|
24
|
+
* client never learn that DSH exists.
|
|
25
|
+
*
|
|
26
|
+
* The plugin is mounted as a Loader row, which means Cordis owns it. Its
|
|
27
|
+
* dependency injector, its `session/event` subscription, every agent handle it
|
|
28
|
+
* creates, and the service it publishes all belong to that row's fiber.
|
|
29
|
+
* Removing the row disposes the fiber, and all of it goes away together — that
|
|
30
|
+
* single ownership edge is the rollback guarantee.
|
|
31
|
+
*
|
|
32
|
+
* @module cordis/dsh/engine-plugin
|
|
33
|
+
*/
|
|
34
|
+
import { randomUUID } from 'node:crypto';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { mkdir, realpath } from 'node:fs/promises';
|
|
37
|
+
import { Session } from '@deepseek-ai/dsh-session';
|
|
38
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
39
|
+
import { deleteJsonlSession } from './session-files.js';
|
|
40
|
+
import { installWorkspaceToolPolicy } from './workspace-policy.js';
|
|
41
|
+
import { appendSessionSettings, sessionSettings } from './session-settings.js';
|
|
42
|
+
import { isProviderModelIdentity } from './model-identity.js';
|
|
43
|
+
import { installModelSelection, } from '@deepseek-ai/dsh-agent';
|
|
44
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
45
|
+
import { DSH_ENGINE_SERVICE } from '../contracts.js';
|
|
46
|
+
import { createLogger } from '../../utils/logger.js';
|
|
47
|
+
import { resolveDataDirectory } from '../../utils/dataDirectory.js';
|
|
48
|
+
const logger = createLogger('cordis-dsh-engine');
|
|
49
|
+
/** Plugin name reported to Cordis diagnostics. */
|
|
50
|
+
export const name = 'libre-webui-dsh-engine';
|
|
51
|
+
/**
|
|
52
|
+
* Services this plugin waits for.
|
|
53
|
+
*
|
|
54
|
+
* Declaring them is what makes the bridge start only once the engine is
|
|
55
|
+
* usable, and — just as importantly — refuse to start at all when the engine
|
|
56
|
+
* is absent. A bridge that published itself against a missing engine would
|
|
57
|
+
* turn a configuration mistake into silently empty session lists.
|
|
58
|
+
*/
|
|
59
|
+
export const inject = ['sessions', 'agents', 'tools', 'llm'];
|
|
60
|
+
/**
|
|
61
|
+
* One in-flight response for a session.
|
|
62
|
+
*
|
|
63
|
+
* Chunks are buffered until a subscriber attaches, because the HTTP handler
|
|
64
|
+
* must await this handle before it can write response headers — a model that
|
|
65
|
+
* answers faster than the round trip would otherwise lose its opening tokens.
|
|
66
|
+
*/
|
|
67
|
+
export class ResponseStream {
|
|
68
|
+
constructor(sessionId, onClose, onDone) {
|
|
69
|
+
this.sessionId = sessionId;
|
|
70
|
+
this.onClose = onClose;
|
|
71
|
+
this.onDone = onDone;
|
|
72
|
+
this.buffer = [];
|
|
73
|
+
this.listeners = new Set();
|
|
74
|
+
this.closed = false;
|
|
75
|
+
this.terminated = false;
|
|
76
|
+
this.errorReported = false;
|
|
77
|
+
this.bufferBytes = 0;
|
|
78
|
+
}
|
|
79
|
+
/** Deliver a chunk to the buffer, or straight to attached listeners. */
|
|
80
|
+
push(chunk) {
|
|
81
|
+
if (this.terminated)
|
|
82
|
+
return;
|
|
83
|
+
const bytes = Buffer.byteLength(JSON.stringify(chunk));
|
|
84
|
+
if (chunk.type !== 'error' &&
|
|
85
|
+
chunk.type !== 'done' &&
|
|
86
|
+
(this.bufferBytes + bytes > ResponseStream.MAX_BUFFER_BYTES ||
|
|
87
|
+
this.buffer.length >= ResponseStream.MAX_BUFFER_CHUNKS)) {
|
|
88
|
+
this.onClose?.();
|
|
89
|
+
this.buffer.length = 0;
|
|
90
|
+
this.bufferBytes = 0;
|
|
91
|
+
this.push({
|
|
92
|
+
type: 'error',
|
|
93
|
+
code: 'CORDIS_STREAM_LIMIT',
|
|
94
|
+
message: 'The engine response exceeded the stream buffer limit.',
|
|
95
|
+
});
|
|
96
|
+
this.push({ type: 'done', reason: 'error' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (chunk.type === 'error') {
|
|
100
|
+
if (this.errorReported)
|
|
101
|
+
return;
|
|
102
|
+
this.errorReported = true;
|
|
103
|
+
}
|
|
104
|
+
if (chunk.type === 'done') {
|
|
105
|
+
this.terminated = true;
|
|
106
|
+
this.onDone?.();
|
|
107
|
+
}
|
|
108
|
+
if (this.closed)
|
|
109
|
+
return;
|
|
110
|
+
// Recorded here rather than read back from the buffer: a chunk delivered to
|
|
111
|
+
// a live listener is never buffered, so the terminal chunk would be
|
|
112
|
+
// invisible to a later check and the session would look busy forever.
|
|
113
|
+
this.buffer.push(chunk);
|
|
114
|
+
this.bufferBytes += bytes;
|
|
115
|
+
for (const listener of this.listeners) {
|
|
116
|
+
try {
|
|
117
|
+
listener(chunk);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// A misbehaving consumer must not abort the model stream for other
|
|
121
|
+
// consumers, nor for the engine that is publishing it.
|
|
122
|
+
logger.warn('stream listener threw', {
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (this.terminated)
|
|
128
|
+
this.listeners.clear();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Mark the response finished and release every listener.
|
|
132
|
+
*
|
|
133
|
+
* Also called when a client disconnects, so an abandoned response stops
|
|
134
|
+
* buffering for a reader that is gone.
|
|
135
|
+
*/
|
|
136
|
+
close() {
|
|
137
|
+
if (this.closed)
|
|
138
|
+
return;
|
|
139
|
+
this.closed = true;
|
|
140
|
+
this.listeners.clear();
|
|
141
|
+
if (!this.terminated) {
|
|
142
|
+
this.buffer.length = 0;
|
|
143
|
+
this.bufferBytes = 0;
|
|
144
|
+
this.onClose?.();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Whether the response has already terminated. */
|
|
148
|
+
get isClosed() {
|
|
149
|
+
return this.closed;
|
|
150
|
+
}
|
|
151
|
+
/** Chunks buffered but not yet delivered. */
|
|
152
|
+
get pending() {
|
|
153
|
+
return this.buffer.length;
|
|
154
|
+
}
|
|
155
|
+
subscribe(listener) {
|
|
156
|
+
// Replay first, so a subscriber attaching after a fast model answer still
|
|
157
|
+
// sees the complete response. The buffer is retained rather than drained:
|
|
158
|
+
// a turn can reach `done` before the HTTP handler attaches, and discarding
|
|
159
|
+
// the chunks would deliver an empty response for a turn that succeeded.
|
|
160
|
+
for (const chunk of this.buffer)
|
|
161
|
+
listener(chunk);
|
|
162
|
+
if (this.closed || this.terminated)
|
|
163
|
+
return { unsubscribe: () => undefined };
|
|
164
|
+
this.listeners.add(listener);
|
|
165
|
+
return {
|
|
166
|
+
unsubscribe: () => {
|
|
167
|
+
this.listeners.delete(listener);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** Whether this response has produced its terminal chunk. */
|
|
172
|
+
get isTerminated() {
|
|
173
|
+
return this.terminated;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
ResponseStream.MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
177
|
+
ResponseStream.MAX_BUFFER_CHUNKS = 10000;
|
|
178
|
+
/** Extract readable text from one DSH message content value. */
|
|
179
|
+
export function extractText(content) {
|
|
180
|
+
if (typeof content === 'string')
|
|
181
|
+
return content;
|
|
182
|
+
if (!Array.isArray(content))
|
|
183
|
+
return '';
|
|
184
|
+
const parts = [];
|
|
185
|
+
for (const block of content) {
|
|
186
|
+
if (typeof block === 'string') {
|
|
187
|
+
parts.push(block);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (block === null || typeof block !== 'object')
|
|
191
|
+
continue;
|
|
192
|
+
const record = block;
|
|
193
|
+
// Content blocks are a union of text, image, and tool shapes. Only text is
|
|
194
|
+
// projectable into a chat transcript; the rest is deliberately dropped
|
|
195
|
+
// rather than stringified into something misleading.
|
|
196
|
+
if (record.type === 'text' && typeof record.text === 'string') {
|
|
197
|
+
parts.push(record.text);
|
|
198
|
+
}
|
|
199
|
+
else if (record.type === 'tool-result') {
|
|
200
|
+
parts.push(extractText(record.content));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return parts.join('');
|
|
204
|
+
}
|
|
205
|
+
/** Normalize a DSH message role into the contract's role vocabulary. */
|
|
206
|
+
export function normalizeRole(role) {
|
|
207
|
+
if (role === 'user' ||
|
|
208
|
+
role === 'assistant' ||
|
|
209
|
+
role === 'system' ||
|
|
210
|
+
role === 'tool') {
|
|
211
|
+
return role;
|
|
212
|
+
}
|
|
213
|
+
return 'unknown';
|
|
214
|
+
}
|
|
215
|
+
/** Project one DSH message into the contract's message shape. */
|
|
216
|
+
export function projectMessage(raw, index) {
|
|
217
|
+
if (raw === null || typeof raw !== 'object') {
|
|
218
|
+
return { id: `message-${index}`, role: 'unknown', text: '' };
|
|
219
|
+
}
|
|
220
|
+
const record = raw;
|
|
221
|
+
const id = typeof record.id === 'string' ? record.id : `message-${index}`;
|
|
222
|
+
const source = record.source && typeof record.source === 'object'
|
|
223
|
+
? record.source
|
|
224
|
+
: undefined;
|
|
225
|
+
const reasoning = Array.isArray(record.content)
|
|
226
|
+
? record.content
|
|
227
|
+
.filter((block) => block !== null &&
|
|
228
|
+
typeof block === 'object' &&
|
|
229
|
+
block.type === 'reasoning')
|
|
230
|
+
.map((block) => block.text ?? '')
|
|
231
|
+
.join('')
|
|
232
|
+
: '';
|
|
233
|
+
const blocks = Array.isArray(record.content)
|
|
234
|
+
? record.content.filter((block) => !!block && typeof block === 'object')
|
|
235
|
+
: [];
|
|
236
|
+
const toolCalls = blocks
|
|
237
|
+
.filter(block => block.type === 'tool-call')
|
|
238
|
+
.map(block => ({
|
|
239
|
+
callId: typeof block.id === 'string' ? block.id : '',
|
|
240
|
+
name: typeof block.name === 'string' ? block.name : '',
|
|
241
|
+
arguments: typeof block.arguments === 'string'
|
|
242
|
+
? block.arguments
|
|
243
|
+
: JSON.stringify(block.arguments ?? {}),
|
|
244
|
+
}));
|
|
245
|
+
const toolResults = blocks
|
|
246
|
+
.filter(block => block.type === 'tool-result')
|
|
247
|
+
.map(block => ({
|
|
248
|
+
callId: typeof block.toolCallId === 'string' ? block.toolCallId : '',
|
|
249
|
+
output: extractText(block.content),
|
|
250
|
+
isError: block.isError === true,
|
|
251
|
+
}));
|
|
252
|
+
const messageSource = record.role === 'system'
|
|
253
|
+
? 'system'
|
|
254
|
+
: source?.kind === 'plugin'
|
|
255
|
+
? 'context'
|
|
256
|
+
: source?.kind === 'tool'
|
|
257
|
+
? 'tool'
|
|
258
|
+
: source?.kind === 'model' || record.role === 'assistant'
|
|
259
|
+
? 'model'
|
|
260
|
+
: source?.kind === 'user' || record.role === 'user'
|
|
261
|
+
? 'user'
|
|
262
|
+
: undefined;
|
|
263
|
+
const seq = typeof record.seq === 'number' ? record.seq : undefined;
|
|
264
|
+
return {
|
|
265
|
+
id,
|
|
266
|
+
role: source?.kind === 'tool' ? 'tool' : normalizeRole(record.role),
|
|
267
|
+
text: extractText(record.content),
|
|
268
|
+
...(reasoning ? { reasoning } : {}),
|
|
269
|
+
...(messageSource ? { source: messageSource } : {}),
|
|
270
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
271
|
+
...(toolResults.length ? { toolResults } : {}),
|
|
272
|
+
...(seq === undefined ? {} : { seq }),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Map one DSH session event onto a contract stream chunk.
|
|
277
|
+
*
|
|
278
|
+
* A durable session event is an envelope: `type` and `seq` sit beside a `data`
|
|
279
|
+
* object that carries the payload for that type. Reading the payload off the
|
|
280
|
+
* envelope instead of `data` yields no chunk at all rather than an error, so
|
|
281
|
+
* each branch unwraps `data` explicitly.
|
|
282
|
+
* @param event - the session event envelope.
|
|
283
|
+
* @returns the projected chunk, or undefined for an event the transcript omits.
|
|
284
|
+
*/
|
|
285
|
+
export function projectStreamEvent(event) {
|
|
286
|
+
const type = event.type;
|
|
287
|
+
const data = event.data !== null &&
|
|
288
|
+
typeof event.data === 'object' &&
|
|
289
|
+
!Array.isArray(event.data)
|
|
290
|
+
? event.data
|
|
291
|
+
: {};
|
|
292
|
+
if (type === 'assistant/message') {
|
|
293
|
+
const message = data.message;
|
|
294
|
+
const text = message !== null && typeof message === 'object'
|
|
295
|
+
? extractText(message.content)
|
|
296
|
+
: '';
|
|
297
|
+
return text.length > 0 ? { type: 'text', text } : undefined;
|
|
298
|
+
}
|
|
299
|
+
if (type === 'tool/call') {
|
|
300
|
+
return {
|
|
301
|
+
type: 'tool-call',
|
|
302
|
+
callId: typeof data.callId === 'string' ? data.callId : '',
|
|
303
|
+
name: typeof data.name === 'string' ? data.name : '',
|
|
304
|
+
...(typeof data.arguments === 'string'
|
|
305
|
+
? { arguments: data.arguments }
|
|
306
|
+
: {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (type === 'tool/result') {
|
|
310
|
+
// Current DSH stores correlation and error state in the tool-result
|
|
311
|
+
// content block; retain legacy message fields for already-recorded logs.
|
|
312
|
+
const message = data.message !== null &&
|
|
313
|
+
typeof data.message === 'object' &&
|
|
314
|
+
!Array.isArray(data.message)
|
|
315
|
+
? data.message
|
|
316
|
+
: {};
|
|
317
|
+
const error = data.error !== null &&
|
|
318
|
+
typeof data.error === 'object' &&
|
|
319
|
+
!Array.isArray(data.error)
|
|
320
|
+
? data.error
|
|
321
|
+
: undefined;
|
|
322
|
+
const block = Array.isArray(message.content)
|
|
323
|
+
? message.content.find((value) => value !== null &&
|
|
324
|
+
typeof value === 'object' &&
|
|
325
|
+
value.type === 'tool-result')
|
|
326
|
+
: undefined;
|
|
327
|
+
const source = message.source && typeof message.source === 'object'
|
|
328
|
+
? message.source
|
|
329
|
+
: undefined;
|
|
330
|
+
const callId = block?.toolCallId ?? source?.callId ?? message.toolCallId;
|
|
331
|
+
return {
|
|
332
|
+
type: 'tool-result',
|
|
333
|
+
callId: typeof callId === 'string' ? callId : '',
|
|
334
|
+
// A tool result carries no tool name; the failure identity is the closest
|
|
335
|
+
// thing, and an empty name is honest when the call succeeded.
|
|
336
|
+
name: typeof error?.name === 'string' ? error.name : '',
|
|
337
|
+
isError: block?.isError === true || message.isError === true,
|
|
338
|
+
...(block ? { output: extractText(block.content) } : {}),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (type === 'turn/end') {
|
|
342
|
+
// The reason is a structured value, not a label: a failed turn carries the
|
|
343
|
+
// provider's own error there. Reporting a bare status instead is how a
|
|
344
|
+
// precise failure ("this route lists no models") reached the client as
|
|
345
|
+
// "the turn ended".
|
|
346
|
+
const reason = data.reason;
|
|
347
|
+
const kind = reason !== null && typeof reason === 'object' && !Array.isArray(reason)
|
|
348
|
+
? reason.kind
|
|
349
|
+
: undefined;
|
|
350
|
+
return {
|
|
351
|
+
type: 'done',
|
|
352
|
+
reason: typeof kind === 'string' ? kind : 'unknown',
|
|
353
|
+
...(data.interrupted === true ||
|
|
354
|
+
kind === 'aborted' ||
|
|
355
|
+
kind === 'interrupted'
|
|
356
|
+
? { interrupted: true }
|
|
357
|
+
: {}),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Project the failure a finished turn recorded, if it recorded one.
|
|
364
|
+
*
|
|
365
|
+
* @param event - the `turn/end` envelope.
|
|
366
|
+
* @returns the error chunk, or undefined when the turn ended normally.
|
|
367
|
+
*/
|
|
368
|
+
export function projectTurnFailure(event) {
|
|
369
|
+
const data = event.data !== null &&
|
|
370
|
+
typeof event.data === 'object' &&
|
|
371
|
+
!Array.isArray(event.data)
|
|
372
|
+
? event.data
|
|
373
|
+
: {};
|
|
374
|
+
const reason = data.reason;
|
|
375
|
+
if (reason === null || typeof reason !== 'object' || Array.isArray(reason)) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
const record = reason;
|
|
379
|
+
if (record.kind !== 'error')
|
|
380
|
+
return undefined;
|
|
381
|
+
const error = record.error;
|
|
382
|
+
if (error === null || typeof error !== 'object') {
|
|
383
|
+
return { type: 'error', message: 'The engine reported a failed turn.' };
|
|
384
|
+
}
|
|
385
|
+
const detail = error;
|
|
386
|
+
const message = typeof detail.message === 'string' && detail.message.trim() !== ''
|
|
387
|
+
? detail.message
|
|
388
|
+
: 'The engine reported a failed turn.';
|
|
389
|
+
return {
|
|
390
|
+
type: 'error',
|
|
391
|
+
message,
|
|
392
|
+
...(typeof detail.code === 'string' ? { code: detail.code } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Build the session-id minter for one engine instance.
|
|
397
|
+
*
|
|
398
|
+
* A shared counter is not enough. `dsh-session-persistence-jsonl` indexes live
|
|
399
|
+
* sessions process-wide, so two engine instances in one process that both mint
|
|
400
|
+
* `session-1` collide, and the second fails with `SessionAlreadyExistsError`
|
|
401
|
+
* even though the two hosts use different store directories. Prefixing with a
|
|
402
|
+
* per-instance random component keeps ids unique across every host in the
|
|
403
|
+
* process while staying readable in a session list.
|
|
404
|
+
*
|
|
405
|
+
* @returns a function minting an id no other engine instance will produce.
|
|
406
|
+
*/
|
|
407
|
+
function createSessionIdMinter() {
|
|
408
|
+
const prefix = randomUUID().slice(0, 8);
|
|
409
|
+
let counter = 0;
|
|
410
|
+
return () => {
|
|
411
|
+
counter += 1;
|
|
412
|
+
return `session-${prefix}-${counter}`;
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** Read a session's event count without depending on a concrete class. */
|
|
416
|
+
function eventCountOf(session) {
|
|
417
|
+
return typeof session.seq === 'number' && Number.isFinite(session.seq)
|
|
418
|
+
? session.seq
|
|
419
|
+
: 0;
|
|
420
|
+
}
|
|
421
|
+
/** Project a session into its summary shape. */
|
|
422
|
+
export function projectSummary(session) {
|
|
423
|
+
const createdAt = session.header?.createdAt;
|
|
424
|
+
const firstInput = session
|
|
425
|
+
.deriveMessages()
|
|
426
|
+
.map(projectMessage)
|
|
427
|
+
.find(message => message.source === 'user' && message.text.trim());
|
|
428
|
+
const title = firstInput?.text.replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
429
|
+
return {
|
|
430
|
+
id: session.id,
|
|
431
|
+
eventCount: eventCountOf(session),
|
|
432
|
+
...(title ? { title } : {}),
|
|
433
|
+
...(session.header?.cwd ? { workspacePath: session.header.cwd } : {}),
|
|
434
|
+
...(typeof createdAt === 'number' ? { createdAt } : {}),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The engine contract, published as the `libreDshEngine` Cordis service.
|
|
439
|
+
*
|
|
440
|
+
* It is a `Service` rather than a plain object so that Cordis owns its
|
|
441
|
+
* lifecycle: the name is declared once, `ctx.get('libreDshEngine')` resolves it
|
|
442
|
+
* for every consumer, and removing the bridge row withdraws it automatically.
|
|
443
|
+
*/
|
|
444
|
+
export class LibreDshEngineService extends Service {
|
|
445
|
+
constructor(ctx, config = {}) {
|
|
446
|
+
super(ctx, DSH_ENGINE_SERVICE);
|
|
447
|
+
this.streams = new Map();
|
|
448
|
+
this.handles = new Map();
|
|
449
|
+
/**
|
|
450
|
+
* Sessions created by the UI that no agent owns yet.
|
|
451
|
+
*
|
|
452
|
+
* They are deliberately absent from the DSH store until the first message,
|
|
453
|
+
* because the agent that will own one must be the component that enters it.
|
|
454
|
+
*/
|
|
455
|
+
this.pending = new Map();
|
|
456
|
+
/** Working directory requested for each not-yet-owned session. */
|
|
457
|
+
this.pendingCwd = new Map();
|
|
458
|
+
this.pendingNative = new Map();
|
|
459
|
+
this.selections = new Map();
|
|
460
|
+
this.settingsUpdates = new Map();
|
|
461
|
+
this.approvals = new Map();
|
|
462
|
+
/** Caller identity exists only while a model turn is in flight. */
|
|
463
|
+
this.requestUsers = new Map();
|
|
464
|
+
this.agentUsers = new Map();
|
|
465
|
+
this.streamedText = new Set();
|
|
466
|
+
this.starting = new Map();
|
|
467
|
+
this.controllers = new Map();
|
|
468
|
+
this.stopping = new Map();
|
|
469
|
+
this.deleting = new Map();
|
|
470
|
+
this.disposed = false;
|
|
471
|
+
/** Mints session ids unique across every engine in this process. */
|
|
472
|
+
this.mintSessionId = createSessionIdMinter();
|
|
473
|
+
this.workspacePath = path.resolve(config.workspacePath?.trim()
|
|
474
|
+
? config.workspacePath
|
|
475
|
+
: path.join(resolveDataDirectory(), 'cordis-workspace'));
|
|
476
|
+
installWorkspaceToolPolicy(ctx, this.workspacePath);
|
|
477
|
+
this.allowStreaming = config.streaming !== false;
|
|
478
|
+
this.allowTools = config.tools !== false;
|
|
479
|
+
if (!this.allowTools)
|
|
480
|
+
ctx.tools.guard(() => 'Cordis tools are disabled by configuration.');
|
|
481
|
+
this.sessionStorePath = config.sessionStorePath;
|
|
482
|
+
this.defaultProvider = config.defaultProvider;
|
|
483
|
+
this.defaultModel = config.defaultModel;
|
|
484
|
+
ctx.tools.guard(exec => {
|
|
485
|
+
const args = exec.arguments;
|
|
486
|
+
if (args?.sandbox_permissions === 'danger-full-access')
|
|
487
|
+
return 'Unrestricted filesystem access is not available in the Cordis bridge.';
|
|
488
|
+
const policy = this.ctx.get('sandboxPolicy');
|
|
489
|
+
if ((exec.name === 'write' || exec.name === 'edit') && policy) {
|
|
490
|
+
const mode = policy.resolve({
|
|
491
|
+
...(exec.agent ? { session: exec.agent.session } : {}),
|
|
492
|
+
}).mode;
|
|
493
|
+
if (mode === 'danger-full-access')
|
|
494
|
+
return 'This session must select read-only or workspace-write permissions.';
|
|
495
|
+
if (mode === 'read-only' &&
|
|
496
|
+
args?.sandbox_permissions !== 'workspace-write')
|
|
497
|
+
return 'File changes require workspace-write mode or explicit approval for this operation.';
|
|
498
|
+
}
|
|
499
|
+
return undefined;
|
|
500
|
+
});
|
|
501
|
+
ctx.on('approval/request', async (request, next) => {
|
|
502
|
+
const sessionId = request.agent.session.id;
|
|
503
|
+
if (/-transient$/.test(sessionId))
|
|
504
|
+
return 'unavailable';
|
|
505
|
+
if (!this.streams.has(sessionId))
|
|
506
|
+
return next();
|
|
507
|
+
const pending = [...this.approvals.values()].find(value => value.approval.sessionId === sessionId &&
|
|
508
|
+
value.approval.callId === request.callId &&
|
|
509
|
+
value.approval.toolName === request.toolName);
|
|
510
|
+
if (!pending)
|
|
511
|
+
return 'unavailable';
|
|
512
|
+
const abort = () => {
|
|
513
|
+
pending.decided = true;
|
|
514
|
+
pending.resolve('cancelled');
|
|
515
|
+
};
|
|
516
|
+
request.signal?.addEventListener('abort', abort, { once: true });
|
|
517
|
+
if (request.signal?.aborted)
|
|
518
|
+
abort();
|
|
519
|
+
try {
|
|
520
|
+
return await pending.promise;
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
request.signal?.removeEventListener('abort', abort);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// Each live frame carries its exact agent and session. Finished session
|
|
527
|
+
// events still provide terminal/tool state and the durable transcript.
|
|
528
|
+
// Live assistant frames are the token-level channel. Durable session events
|
|
529
|
+
// carry a turn's finished message, so a client reading only those receives
|
|
530
|
+
// a whole reply at once; these frames arrive per delta, which is what makes
|
|
531
|
+
// a reply appear as it is written.
|
|
532
|
+
ctx.on('agent/assistant-stream', ((payload) => {
|
|
533
|
+
const { agent, frame } = payload;
|
|
534
|
+
const sessionId = agent?.session?.id ?? agent?.id;
|
|
535
|
+
if (!sessionId || !frame)
|
|
536
|
+
return;
|
|
537
|
+
const stream = this.streams.get(sessionId);
|
|
538
|
+
if (!stream || stream.isClosed || stream.isTerminated)
|
|
539
|
+
return;
|
|
540
|
+
if (frame.type === 'start') {
|
|
541
|
+
this.streamedText.delete(sessionId);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (frame.type !== 'chunk' ||
|
|
545
|
+
!frame.chunk ||
|
|
546
|
+
typeof frame.chunk !== 'object')
|
|
547
|
+
return;
|
|
548
|
+
const chunk = frame.chunk;
|
|
549
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
550
|
+
this.streamedText.add(sessionId);
|
|
551
|
+
stream.push({ type: 'text', text: chunk.text });
|
|
552
|
+
}
|
|
553
|
+
else if (chunk.type === 'reasoning-delta' &&
|
|
554
|
+
typeof chunk.text === 'string') {
|
|
555
|
+
stream.push({ type: 'reasoning', text: chunk.text });
|
|
556
|
+
}
|
|
557
|
+
}));
|
|
558
|
+
ctx.on('agent/error', ((payload) => {
|
|
559
|
+
const sessionId = payload.agent?.session?.id ?? payload.agent?.id;
|
|
560
|
+
const stream = sessionId ? this.streams.get(sessionId) : undefined;
|
|
561
|
+
if (!stream || stream.isTerminated)
|
|
562
|
+
return;
|
|
563
|
+
stream.push({
|
|
564
|
+
type: 'error',
|
|
565
|
+
message: payload.error instanceof Error
|
|
566
|
+
? payload.error.message
|
|
567
|
+
: String(payload.error),
|
|
568
|
+
});
|
|
569
|
+
// Disposal settles errors that happened before a durable turn boundary.
|
|
570
|
+
if (sessionId)
|
|
571
|
+
void this.cancel(sessionId).catch(error => logger.warn('failed agent cleanup', { error: String(error) }));
|
|
572
|
+
}));
|
|
573
|
+
ctx.on('session/event', ((session, event) => {
|
|
574
|
+
if (event === null || typeof event !== 'object')
|
|
575
|
+
return;
|
|
576
|
+
const sessionId = session?.id;
|
|
577
|
+
if (typeof sessionId !== 'string')
|
|
578
|
+
return;
|
|
579
|
+
const stream = this.streams.get(sessionId);
|
|
580
|
+
if (!stream || stream.isTerminated)
|
|
581
|
+
return;
|
|
582
|
+
const envelope = event;
|
|
583
|
+
const data = envelope.data;
|
|
584
|
+
if (envelope.type === 'approval/asked' &&
|
|
585
|
+
typeof data?.id === 'string' &&
|
|
586
|
+
typeof data.toolName === 'string') {
|
|
587
|
+
if (/-transient$/.test(sessionId))
|
|
588
|
+
return;
|
|
589
|
+
const approval = {
|
|
590
|
+
id: data.id,
|
|
591
|
+
sessionId,
|
|
592
|
+
toolName: data.toolName,
|
|
593
|
+
...(typeof data.callId === 'string' ? { callId: data.callId } : {}),
|
|
594
|
+
...(typeof data.reason === 'string' ? { reason: data.reason } : {}),
|
|
595
|
+
};
|
|
596
|
+
let answer;
|
|
597
|
+
const promise = new Promise(resolve => {
|
|
598
|
+
answer = resolve;
|
|
599
|
+
});
|
|
600
|
+
this.approvals.set(approval.id, {
|
|
601
|
+
approval,
|
|
602
|
+
promise,
|
|
603
|
+
resolve: answer,
|
|
604
|
+
decided: false,
|
|
605
|
+
});
|
|
606
|
+
stream.push({ type: 'approval-request', approval });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (envelope.type === 'approval/decided' &&
|
|
610
|
+
typeof data?.id === 'string') {
|
|
611
|
+
this.approvals.delete(data.id);
|
|
612
|
+
stream.push({
|
|
613
|
+
type: 'approval-decision',
|
|
614
|
+
approvalId: data.id,
|
|
615
|
+
outcome: data.outcome,
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
// A failed turn is reported before its terminal chunk, so a consumer
|
|
620
|
+
// that stops reading at `done` has already seen the reason.
|
|
621
|
+
const failure = projectTurnFailure(envelope);
|
|
622
|
+
if (failure)
|
|
623
|
+
stream.push(failure);
|
|
624
|
+
if (envelope.type === 'assistant/message' &&
|
|
625
|
+
this.streamedText.has(sessionId))
|
|
626
|
+
return;
|
|
627
|
+
const projected = projectStreamEvent(envelope);
|
|
628
|
+
if (!projected)
|
|
629
|
+
return;
|
|
630
|
+
stream.push(projected);
|
|
631
|
+
}));
|
|
632
|
+
// Every in-flight response and agent handle is owned by this fiber.
|
|
633
|
+
// Disposing the bridge row releases all of them, so removing the plugin
|
|
634
|
+
// cannot leave a model stream or a live agent behind.
|
|
635
|
+
ctx.effect(() => async () => {
|
|
636
|
+
this.disposed = true;
|
|
637
|
+
await Promise.allSettled([...this.streams.keys()].map(id => this.cancel(id)));
|
|
638
|
+
await Promise.allSettled([...this.handles.values()].map(handle => handle.dispose()));
|
|
639
|
+
this.handles.clear();
|
|
640
|
+
this.pending.clear();
|
|
641
|
+
this.pendingCwd.clear();
|
|
642
|
+
this.pendingNative.clear();
|
|
643
|
+
for (const pending of this.approvals.values())
|
|
644
|
+
pending.resolve('cancelled');
|
|
645
|
+
this.approvals.clear();
|
|
646
|
+
logger.info('libreDshEngine service withdrawn');
|
|
647
|
+
}, 'libre-webui-dsh-engine.teardown');
|
|
648
|
+
}
|
|
649
|
+
/** The DSH session store this bridge reads through. */
|
|
650
|
+
get sessions() {
|
|
651
|
+
return this.ctx.get('sessions');
|
|
652
|
+
}
|
|
653
|
+
/** The DSH agent registry this bridge drives. */
|
|
654
|
+
get agents() {
|
|
655
|
+
return this.ctx.get('agents');
|
|
656
|
+
}
|
|
657
|
+
/** The DSH tool runtime this bridge reads through. */
|
|
658
|
+
get tools() {
|
|
659
|
+
return this.ctx.get('tools');
|
|
660
|
+
}
|
|
661
|
+
modelConfiguration() {
|
|
662
|
+
return { provider: this.defaultProvider, model: this.defaultModel };
|
|
663
|
+
}
|
|
664
|
+
status() {
|
|
665
|
+
const names = [
|
|
666
|
+
'llm',
|
|
667
|
+
'systemPrompt',
|
|
668
|
+
'sessions',
|
|
669
|
+
'tools',
|
|
670
|
+
'agents',
|
|
671
|
+
];
|
|
672
|
+
return names.map(serviceName => ({
|
|
673
|
+
name: serviceName,
|
|
674
|
+
state: this.ctx.get(serviceName) === undefined ? 'pending' : 'ready',
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
get persistence() {
|
|
678
|
+
return this.ctx.get('sessionPersistence');
|
|
679
|
+
}
|
|
680
|
+
async listSessions() {
|
|
681
|
+
const summaries = new Map();
|
|
682
|
+
for (const stored of (await this.persistence?.list()) ?? []) {
|
|
683
|
+
const summary = {
|
|
684
|
+
id: stored.header.id,
|
|
685
|
+
createdAt: stored.header.createdAt,
|
|
686
|
+
eventCount: stored.eventCount ?? 0,
|
|
687
|
+
};
|
|
688
|
+
try {
|
|
689
|
+
const session = await this.nativeSession(stored.header.id);
|
|
690
|
+
summaries.set(stored.header.id, session ? projectSummary(session) : summary);
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
if (!(error instanceof Error) ||
|
|
694
|
+
error.name !== 'SessionPersistenceCorruptionError')
|
|
695
|
+
throw error;
|
|
696
|
+
summaries.set(stored.header.id, summary);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (const session of this.sessions.list())
|
|
700
|
+
summaries.set(session.id, projectSummary(session));
|
|
701
|
+
for (const session of this.pending.values()) {
|
|
702
|
+
if (!summaries.has(session.id))
|
|
703
|
+
summaries.set(session.id, session);
|
|
704
|
+
}
|
|
705
|
+
return [...summaries.values()]
|
|
706
|
+
.filter(session => !/-transient$/.test(session.id))
|
|
707
|
+
.sort((left, right) => (right.createdAt ?? 0) - (left.createdAt ?? 0));
|
|
708
|
+
}
|
|
709
|
+
async nativeSession(sessionId) {
|
|
710
|
+
const live = this.sessions.get(sessionId);
|
|
711
|
+
if (live)
|
|
712
|
+
return live;
|
|
713
|
+
const pending = this.pendingNative.get(sessionId);
|
|
714
|
+
if (pending)
|
|
715
|
+
return pending;
|
|
716
|
+
if (this.persistence &&
|
|
717
|
+
(await this.persistence.stat(sessionId))) {
|
|
718
|
+
const handle = await this.persistence.open(sessionId, 'read');
|
|
719
|
+
try {
|
|
720
|
+
const read = await handle.read();
|
|
721
|
+
return Session.fromRestore(handle.id, read.events, handle.header, handle.inheritedEventCount, read.eventState, this.sessions.messageProjections);
|
|
722
|
+
}
|
|
723
|
+
finally {
|
|
724
|
+
await handle.close();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return this.pendingNative.get(sessionId);
|
|
728
|
+
}
|
|
729
|
+
async getSession(sessionId) {
|
|
730
|
+
const session = await this.nativeSession(sessionId);
|
|
731
|
+
if (!session)
|
|
732
|
+
return undefined;
|
|
733
|
+
return {
|
|
734
|
+
...projectSummary(session),
|
|
735
|
+
...(this.pending.get(sessionId)?.title
|
|
736
|
+
? { title: this.pending.get(sessionId).title }
|
|
737
|
+
: {}),
|
|
738
|
+
active: this.streams.has(sessionId) ||
|
|
739
|
+
this.starting.has(sessionId) ||
|
|
740
|
+
this.stopping.has(sessionId),
|
|
741
|
+
settings: sessionSettings(session),
|
|
742
|
+
capabilities: {
|
|
743
|
+
permissions: !!this.ctx.get('sandboxPolicy'),
|
|
744
|
+
approvals: !!this.ctx.get('approval') && !/-transient$/.test(sessionId),
|
|
745
|
+
},
|
|
746
|
+
approvals: [...this.approvals.values()]
|
|
747
|
+
.filter(value => value.approval.sessionId === sessionId && !value.decided)
|
|
748
|
+
.map(value => value.approval),
|
|
749
|
+
messages: session.deriveMessages().map(projectMessage),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async validateSettings(settings, userId) {
|
|
753
|
+
if (settings.permissionMode !== undefined) {
|
|
754
|
+
if (settings.permissionMode !== 'read-only' &&
|
|
755
|
+
settings.permissionMode !== 'workspace-write')
|
|
756
|
+
throw new Error('Choose read-only or workspace-write permissions.');
|
|
757
|
+
if (!this.ctx.get('sandboxPolicy'))
|
|
758
|
+
throw new Error('This composition does not provide filesystem permission controls.');
|
|
759
|
+
}
|
|
760
|
+
if (settings.model !== undefined) {
|
|
761
|
+
if (!isProviderModelIdentity(settings.model))
|
|
762
|
+
throw new Error('Choose an available provider model.');
|
|
763
|
+
const provider = this.ctx.get('libreCordisProvider');
|
|
764
|
+
if (this.defaultProvider === 'libre-webui' &&
|
|
765
|
+
!(await provider?.resolveModel?.(settings.model, userId)))
|
|
766
|
+
throw new Error('The selected provider model is not available for this account.');
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async updateSessionSettings(sessionId, settings, options) {
|
|
770
|
+
if (this.streams.has(sessionId) ||
|
|
771
|
+
this.starting.has(sessionId) ||
|
|
772
|
+
this.stopping.has(sessionId) ||
|
|
773
|
+
this.deleting.has(sessionId) ||
|
|
774
|
+
this.settingsUpdates.has(sessionId))
|
|
775
|
+
throw Object.assign(new Error('Session settings cannot change while a turn or another operation is active.'), { code: 'CORDIS_SESSION_BUSY' });
|
|
776
|
+
const update = (async () => {
|
|
777
|
+
await this.validateSettings(settings, options?.userId);
|
|
778
|
+
const owned = this.handles.get(sessionId);
|
|
779
|
+
if (owned) {
|
|
780
|
+
await owned.agent.whenIdle?.();
|
|
781
|
+
if (!this.persistence && owned.agent.session) {
|
|
782
|
+
appendSessionSettings(owned.agent.session, settings, this.defaultProvider ?? 'libre-webui');
|
|
783
|
+
if (settings.model && this.selections.has(sessionId))
|
|
784
|
+
this.selections.get(sessionId).current = {
|
|
785
|
+
provider: this.defaultProvider ?? 'libre-webui',
|
|
786
|
+
model: settings.model,
|
|
787
|
+
};
|
|
788
|
+
return (await this.getSession(sessionId));
|
|
789
|
+
}
|
|
790
|
+
await owned.dispose();
|
|
791
|
+
this.handles.delete(sessionId);
|
|
792
|
+
}
|
|
793
|
+
const stored = await this.persistence?.stat(sessionId);
|
|
794
|
+
if (stored && this.persistence) {
|
|
795
|
+
const handle = await this.persistence.open(sessionId, 'write');
|
|
796
|
+
try {
|
|
797
|
+
const read = await handle.read();
|
|
798
|
+
const session = Session.fromRestore(handle.id, read.events, handle.header, handle.inheritedEventCount, read.eventState, this.sessions.messageProjections);
|
|
799
|
+
// fromRestore may add a native end-seed marker before our settings.
|
|
800
|
+
// Persist that marker too so the appended suffix stays contiguous.
|
|
801
|
+
const start = read.events.length;
|
|
802
|
+
appendSessionSettings(session, settings, this.defaultProvider ?? 'libre-webui');
|
|
803
|
+
await handle.append(session.snapshotEvents().slice(start));
|
|
804
|
+
await handle.flush();
|
|
805
|
+
this.pendingNative.delete(sessionId);
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
await handle.close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
const session = this.pendingNative.get(sessionId) ??
|
|
813
|
+
this.sessions.get(sessionId);
|
|
814
|
+
if (!session)
|
|
815
|
+
throw new Error('Session not found.');
|
|
816
|
+
appendSessionSettings(session, settings, this.defaultProvider ?? 'libre-webui');
|
|
817
|
+
}
|
|
818
|
+
const result = await this.getSession(sessionId);
|
|
819
|
+
if (!result)
|
|
820
|
+
throw new Error('Session not found.');
|
|
821
|
+
return result;
|
|
822
|
+
})();
|
|
823
|
+
this.settingsUpdates.set(sessionId, update);
|
|
824
|
+
try {
|
|
825
|
+
return await update;
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
this.settingsUpdates.delete(sessionId);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async decideApproval(sessionId, approvalId, outcome) {
|
|
832
|
+
if (outcome !== 'allowed-once' && outcome !== 'rejected')
|
|
833
|
+
throw new Error('Invalid approval outcome.');
|
|
834
|
+
const pending = this.approvals.get(approvalId);
|
|
835
|
+
if (!pending || pending.decided || pending.approval.sessionId !== sessionId)
|
|
836
|
+
return false;
|
|
837
|
+
pending.decided = true;
|
|
838
|
+
pending.resolve(outcome);
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
async resolveWorkspace(cwd) {
|
|
842
|
+
await mkdir(this.workspacePath, { recursive: true });
|
|
843
|
+
const root = await realpath(this.workspacePath);
|
|
844
|
+
const requested = await realpath(cwd || root);
|
|
845
|
+
const relative = path.relative(root, requested);
|
|
846
|
+
if (relative === '..' ||
|
|
847
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
848
|
+
path.isAbsolute(relative)) {
|
|
849
|
+
throw new Error('The session workspace must be inside the configured Cordis workspace.');
|
|
850
|
+
}
|
|
851
|
+
return requested;
|
|
852
|
+
}
|
|
853
|
+
async createSession(options) {
|
|
854
|
+
if (this.disposed)
|
|
855
|
+
throw new Error('The Cordis engine is stopped.');
|
|
856
|
+
await this.validateSettings(options, options.userId);
|
|
857
|
+
const cwd = await this.resolveWorkspace(options.cwd);
|
|
858
|
+
const id = this.mintSessionId() + (options.transient ? '-transient' : '');
|
|
859
|
+
const summary = {
|
|
860
|
+
id,
|
|
861
|
+
eventCount: 0,
|
|
862
|
+
createdAt: Date.now(),
|
|
863
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
864
|
+
};
|
|
865
|
+
const prepared = this.sessions.prepare(id, { meta: { cwd } });
|
|
866
|
+
appendSessionSettings(prepared, {
|
|
867
|
+
...options,
|
|
868
|
+
...(this.ctx.get('sandboxPolicy')
|
|
869
|
+
? { permissionMode: options.permissionMode ?? 'read-only' }
|
|
870
|
+
: {}),
|
|
871
|
+
}, this.defaultProvider ?? 'libre-webui');
|
|
872
|
+
if (this.persistence) {
|
|
873
|
+
const handle = await this.persistence.create(prepared.header);
|
|
874
|
+
try {
|
|
875
|
+
if (prepared.seq)
|
|
876
|
+
await handle.append(prepared.snapshotEvents());
|
|
877
|
+
await handle.flush();
|
|
878
|
+
}
|
|
879
|
+
finally {
|
|
880
|
+
await handle.close();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
this.pendingNative.set(id, prepared);
|
|
884
|
+
this.pending.set(id, summary);
|
|
885
|
+
this.pendingCwd.set(id, cwd);
|
|
886
|
+
return (await this.getSession(id));
|
|
887
|
+
}
|
|
888
|
+
async deleteSession(sessionId) {
|
|
889
|
+
const existing = this.deleting.get(sessionId);
|
|
890
|
+
if (existing)
|
|
891
|
+
return existing;
|
|
892
|
+
const remove = (async () => {
|
|
893
|
+
// Removing an invalid transcript must not require replaying its body.
|
|
894
|
+
// The persistence catalog still validates its identity and header; the
|
|
895
|
+
// deletion adapter separately validates the artifact's canonical path.
|
|
896
|
+
await this.settingsUpdates.get(sessionId)?.catch(() => undefined);
|
|
897
|
+
const exists = this.pending.has(sessionId) ||
|
|
898
|
+
this.sessions.get(sessionId) ||
|
|
899
|
+
(await this.persistence?.stat(sessionId));
|
|
900
|
+
if (!exists)
|
|
901
|
+
return false;
|
|
902
|
+
await this.cancel(sessionId);
|
|
903
|
+
const handle = this.handles.get(sessionId);
|
|
904
|
+
if (handle) {
|
|
905
|
+
await handle.dispose();
|
|
906
|
+
this.handles.delete(sessionId);
|
|
907
|
+
}
|
|
908
|
+
const stored = await this.persistence?.stat(sessionId);
|
|
909
|
+
if (stored) {
|
|
910
|
+
if (!this.sessionStorePath)
|
|
911
|
+
throw new Error('The configured persistence backend does not support session deletion.');
|
|
912
|
+
await deleteJsonlSession(this.sessionStorePath, stored.header);
|
|
913
|
+
}
|
|
914
|
+
this.pending.delete(sessionId);
|
|
915
|
+
this.pendingCwd.delete(sessionId);
|
|
916
|
+
this.pendingNative.delete(sessionId);
|
|
917
|
+
this.agentUsers.delete(sessionId);
|
|
918
|
+
return true;
|
|
919
|
+
})();
|
|
920
|
+
this.deleting.set(sessionId, remove);
|
|
921
|
+
try {
|
|
922
|
+
return await remove;
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
this.deleting.delete(sessionId);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async listAgents() {
|
|
929
|
+
const roots = new Set(this.agents.roots().map(agent => agent.id));
|
|
930
|
+
return this.agents
|
|
931
|
+
.list()
|
|
932
|
+
.map(agent => ({ id: agent.id, root: roots.has(agent.id) }));
|
|
933
|
+
}
|
|
934
|
+
async listTools() {
|
|
935
|
+
if (!this.allowTools)
|
|
936
|
+
return [];
|
|
937
|
+
// No scope yields the deployment-wide registry; per-agent restrictions are
|
|
938
|
+
// applied by the tool runtime at dispatch time, not at listing time.
|
|
939
|
+
return this.tools
|
|
940
|
+
.schemas()
|
|
941
|
+
.filter((schema) => typeof schema.name === 'string')
|
|
942
|
+
.map(schema => ({
|
|
943
|
+
name: schema.name,
|
|
944
|
+
description: typeof schema.description === 'string' ? schema.description : '',
|
|
945
|
+
}))
|
|
946
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
947
|
+
}
|
|
948
|
+
async sendMessage(sessionId, text, options) {
|
|
949
|
+
if (this.disposed)
|
|
950
|
+
throw new Error('The Cordis engine is stopped.');
|
|
951
|
+
if (!this.allowStreaming)
|
|
952
|
+
throw new Error('cordis dsh engine: streaming is disabled by configuration');
|
|
953
|
+
if (this.streams.has(sessionId) ||
|
|
954
|
+
this.stopping.has(sessionId) ||
|
|
955
|
+
this.deleting.has(sessionId) ||
|
|
956
|
+
this.settingsUpdates.has(sessionId)) {
|
|
957
|
+
throw new Error(`cordis dsh engine: session "${sessionId}" already has a response in flight`);
|
|
958
|
+
}
|
|
959
|
+
if (options?.userId)
|
|
960
|
+
this.requestUsers.set(sessionId, options.userId);
|
|
961
|
+
const controller = new AbortController();
|
|
962
|
+
this.controllers.set(sessionId, controller);
|
|
963
|
+
const stream = new ResponseStream(sessionId, () => {
|
|
964
|
+
void this.cancel(sessionId).catch(error => logger.warn('stream cancellation failed', { error: String(error) }));
|
|
965
|
+
}, () => {
|
|
966
|
+
if (this.streams.get(sessionId) === stream)
|
|
967
|
+
this.streams.delete(sessionId);
|
|
968
|
+
this.streamedText.delete(sessionId);
|
|
969
|
+
this.controllers.delete(sessionId);
|
|
970
|
+
this.requestUsers.delete(sessionId);
|
|
971
|
+
});
|
|
972
|
+
this.streams.set(sessionId, stream);
|
|
973
|
+
const start = (async () => {
|
|
974
|
+
const details = await this.getSession(sessionId);
|
|
975
|
+
if (!details)
|
|
976
|
+
throw new Error(`cordis dsh engine: session "${sessionId}" does not exist; create it first`);
|
|
977
|
+
let handle = this.handles.get(sessionId);
|
|
978
|
+
if (handle &&
|
|
979
|
+
(this.agentUsers.get(sessionId) !== options?.userId ||
|
|
980
|
+
details.settings.model === undefined ||
|
|
981
|
+
!isProviderModelIdentity(this.selections.get(sessionId)?.current?.model))) {
|
|
982
|
+
await handle.dispose();
|
|
983
|
+
this.handles.delete(sessionId);
|
|
984
|
+
handle = undefined;
|
|
985
|
+
}
|
|
986
|
+
if (!handle) {
|
|
987
|
+
const userId = options?.userId;
|
|
988
|
+
const provider = this.ctx.get('libreCordisProvider');
|
|
989
|
+
const selectedModel = details.settings.model;
|
|
990
|
+
const model = isProviderModelIdentity(selectedModel)
|
|
991
|
+
? selectedModel
|
|
992
|
+
: isProviderModelIdentity(this.defaultModel)
|
|
993
|
+
? this.defaultModel
|
|
994
|
+
: await provider?.defaultModel?.(userId);
|
|
995
|
+
const agentOptions = {};
|
|
996
|
+
if (this.defaultProvider)
|
|
997
|
+
agentOptions.provider = this.defaultProvider;
|
|
998
|
+
if (model)
|
|
999
|
+
agentOptions.model = model;
|
|
1000
|
+
const setup = (agentCtx, agent) => {
|
|
1001
|
+
if (!this.allowTools)
|
|
1002
|
+
agentCtx.tools.restrict({ allow: [] });
|
|
1003
|
+
if (agent.session && this.ctx.get('sandboxPolicy')) {
|
|
1004
|
+
// Even an operator's broader default cannot widen this bridge's
|
|
1005
|
+
// two-mode contract. Persist the actual mode enforced on resume.
|
|
1006
|
+
appendSessionSettings(agent.session, { permissionMode: details.settings.permissionMode }, this.defaultProvider ?? 'libre-webui');
|
|
1007
|
+
}
|
|
1008
|
+
if (model && this.defaultProvider) {
|
|
1009
|
+
const selection = {
|
|
1010
|
+
current: { provider: this.defaultProvider, model },
|
|
1011
|
+
assembled: undefined,
|
|
1012
|
+
};
|
|
1013
|
+
this.selections.set(sessionId, selection);
|
|
1014
|
+
installModelSelection(agentCtx, selection);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
const stored = await this.persistence?.stat(sessionId);
|
|
1018
|
+
if (stored) {
|
|
1019
|
+
await this.resolveWorkspace(stored.header.cwd ?? '');
|
|
1020
|
+
handle = await this.agents.resume({
|
|
1021
|
+
resumeSessionId: sessionId,
|
|
1022
|
+
agentOptions,
|
|
1023
|
+
signal: controller.signal,
|
|
1024
|
+
setup,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
const cwd = await this.resolveWorkspace(options?.cwd ?? this.pendingCwd.get(sessionId) ?? '');
|
|
1029
|
+
handle = await this.agents.create({
|
|
1030
|
+
sessionId,
|
|
1031
|
+
meta: { cwd },
|
|
1032
|
+
agentOptions,
|
|
1033
|
+
signal: controller.signal,
|
|
1034
|
+
setup,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
this.handles.set(sessionId, handle);
|
|
1038
|
+
this.agentUsers.set(sessionId, userId);
|
|
1039
|
+
this.pending.delete(sessionId);
|
|
1040
|
+
this.pendingCwd.delete(sessionId);
|
|
1041
|
+
this.pendingNative.delete(sessionId);
|
|
1042
|
+
}
|
|
1043
|
+
controller.signal.throwIfAborted();
|
|
1044
|
+
if (typeof handle.agent.followup !== 'function')
|
|
1045
|
+
throw new Error('cordis dsh engine: the mounted engine has no agent driver that accepts messages');
|
|
1046
|
+
handle.agent.followup(createUserMessage({
|
|
1047
|
+
source: { kind: 'user' },
|
|
1048
|
+
content: [{ type: 'text', text }],
|
|
1049
|
+
}));
|
|
1050
|
+
})();
|
|
1051
|
+
this.starting.set(sessionId, start);
|
|
1052
|
+
try {
|
|
1053
|
+
await start;
|
|
1054
|
+
return stream;
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
stream.push({ type: 'done', reason: 'error' });
|
|
1058
|
+
stream.close();
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
finally {
|
|
1062
|
+
this.starting.delete(sessionId);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
requestUserId(sessionId) {
|
|
1066
|
+
return this.requestUsers.get(sessionId);
|
|
1067
|
+
}
|
|
1068
|
+
async cancel(sessionId) {
|
|
1069
|
+
const inProgress = this.stopping.get(sessionId);
|
|
1070
|
+
if (inProgress) {
|
|
1071
|
+
await inProgress;
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
const stream = this.streams.get(sessionId);
|
|
1075
|
+
const handle = this.handles.get(sessionId);
|
|
1076
|
+
if (!stream && !handle && !this.starting.has(sessionId))
|
|
1077
|
+
return false;
|
|
1078
|
+
this.controllers.get(sessionId)?.abort();
|
|
1079
|
+
const stop = (async () => {
|
|
1080
|
+
await this.starting.get(sessionId)?.catch(() => undefined);
|
|
1081
|
+
const owned = this.handles.get(sessionId);
|
|
1082
|
+
if (owned) {
|
|
1083
|
+
owned.agent.cancel?.({ kind: 'user' });
|
|
1084
|
+
if (!this.persistence && owned.agent.whenIdle) {
|
|
1085
|
+
// Without a persistence backend, the live agent owns the only copy
|
|
1086
|
+
// of the transcript. Drain cancellation without deleting that copy.
|
|
1087
|
+
await owned.agent.whenIdle();
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
await owned.dispose();
|
|
1091
|
+
this.handles.delete(sessionId);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
stream?.push({ type: 'done', reason: 'cancelled', interrupted: true });
|
|
1095
|
+
})();
|
|
1096
|
+
this.stopping.set(sessionId, stop);
|
|
1097
|
+
try {
|
|
1098
|
+
await stop;
|
|
1099
|
+
}
|
|
1100
|
+
finally {
|
|
1101
|
+
this.stopping.delete(sessionId);
|
|
1102
|
+
}
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/** Contract name under which this service is registered. */
|
|
1107
|
+
LibreDshEngineService.provide = DSH_ENGINE_SERVICE;
|
|
1108
|
+
/**
|
|
1109
|
+
* Mount the bridge on a context that already provides the engine services.
|
|
1110
|
+
* @param ctx - context carrying `sessions`, `agents`, and `tools`.
|
|
1111
|
+
* @param config - bridge configuration from the composition row.
|
|
1112
|
+
*/
|
|
1113
|
+
export function apply(ctx, config = {}) {
|
|
1114
|
+
ctx.plugin(LibreDshEngineService, config);
|
|
1115
|
+
}
|
|
1116
|
+
//# sourceMappingURL=engine-plugin.js.map
|