@data-club/ai-hub 0.0.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/dist/auth/passwordPolicy.d.ts +55 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +63 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/texts.d.ts +815 -0
- package/dist/auth/texts.d.ts.map +1 -0
- package/dist/auth/texts.js +429 -0
- package/dist/auth/texts.js.map +1 -0
- package/dist/client/AIHubClient.d.ts +1058 -0
- package/dist/client/AIHubClient.d.ts.map +1 -0
- package/dist/client/AIHubClient.js +950 -0
- package/dist/client/AIHubClient.js.map +1 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +9 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/sseParser.d.ts +41 -0
- package/dist/client/sseParser.d.ts.map +1 -0
- package/dist/client/sseParser.js +153 -0
- package/dist/client/sseParser.js.map +1 -0
- package/dist/client/tokenStorage.d.ts +56 -0
- package/dist/client/tokenStorage.d.ts.map +1 -0
- package/dist/client/tokenStorage.js +116 -0
- package/dist/client/tokenStorage.js.map +1 -0
- package/dist/components/AgentsPicker/index.d.ts +70 -0
- package/dist/components/AgentsPicker/index.d.ts.map +1 -0
- package/dist/components/AgentsPicker/index.js +63 -0
- package/dist/components/AgentsPicker/index.js.map +1 -0
- package/dist/components/AuthGate/index.d.ts +79 -0
- package/dist/components/AuthGate/index.d.ts.map +1 -0
- package/dist/components/AuthGate/index.js +61 -0
- package/dist/components/AuthGate/index.js.map +1 -0
- package/dist/components/CacheBadge/index.d.ts +88 -0
- package/dist/components/CacheBadge/index.d.ts.map +1 -0
- package/dist/components/CacheBadge/index.js +127 -0
- package/dist/components/CacheBadge/index.js.map +1 -0
- package/dist/components/ChangePassword/index.d.ts +109 -0
- package/dist/components/ChangePassword/index.d.ts.map +1 -0
- package/dist/components/ChangePassword/index.js +142 -0
- package/dist/components/ChangePassword/index.js.map +1 -0
- package/dist/components/ChatShell/ShellHeader.d.ts +44 -0
- package/dist/components/ChatShell/ShellHeader.d.ts.map +1 -0
- package/dist/components/ChatShell/ShellHeader.js +14 -0
- package/dist/components/ChatShell/ShellHeader.js.map +1 -0
- package/dist/components/ChatShell/UserMenu.d.ts +48 -0
- package/dist/components/ChatShell/UserMenu.d.ts.map +1 -0
- package/dist/components/ChatShell/UserMenu.js +86 -0
- package/dist/components/ChatShell/UserMenu.js.map +1 -0
- package/dist/components/ChatShell/index.d.ts +98 -0
- package/dist/components/ChatShell/index.d.ts.map +1 -0
- package/dist/components/ChatShell/index.js +124 -0
- package/dist/components/ChatShell/index.js.map +1 -0
- package/dist/components/DataClubAIHub/index.d.ts +132 -0
- package/dist/components/DataClubAIHub/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHub/index.js +124 -0
- package/dist/components/DataClubAIHub/index.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts +89 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js +199 -0
- package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts +33 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.js +137 -0
- package/dist/components/DataClubAIHubAdmin/AgentsPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts +50 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.js +93 -0
- package/dist/components/DataClubAIHubAdmin/CreateUserModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts +49 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js +94 -0
- package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts +37 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js +44 -0
- package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts +56 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js +158 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts +29 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js +183 -0
- package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts +38 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js +294 -0
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts +36 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js +373 -0
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts +47 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.js +233 -0
- package/dist/components/DataClubAIHubAdmin/UsersPage.js.map +1 -0
- package/dist/components/DataClubAIHubAdmin/index.d.ts +87 -0
- package/dist/components/DataClubAIHubAdmin/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHubAdmin/index.js +137 -0
- package/dist/components/DataClubAIHubAdmin/index.js.map +1 -0
- package/dist/components/DataClubAIHubLogin/index.d.ts +113 -0
- package/dist/components/DataClubAIHubLogin/index.d.ts.map +1 -0
- package/dist/components/DataClubAIHubLogin/index.js +123 -0
- package/dist/components/DataClubAIHubLogin/index.js.map +1 -0
- package/dist/components/EffortControl/index.d.ts +57 -0
- package/dist/components/EffortControl/index.d.ts.map +1 -0
- package/dist/components/EffortControl/index.js +84 -0
- package/dist/components/EffortControl/index.js.map +1 -0
- package/dist/components/Layout/index.d.ts +63 -0
- package/dist/components/Layout/index.d.ts.map +1 -0
- package/dist/components/Layout/index.js +45 -0
- package/dist/components/Layout/index.js.map +1 -0
- package/dist/components/MainColumn/index.d.ts +69 -0
- package/dist/components/MainColumn/index.d.ts.map +1 -0
- package/dist/components/MainColumn/index.js +47 -0
- package/dist/components/MainColumn/index.js.map +1 -0
- package/dist/components/MarkdownRenderer/index.d.ts +66 -0
- package/dist/components/MarkdownRenderer/index.d.ts.map +1 -0
- package/dist/components/MarkdownRenderer/index.js +153 -0
- package/dist/components/MarkdownRenderer/index.js.map +1 -0
- package/dist/components/MessageInput/index.d.ts +85 -0
- package/dist/components/MessageInput/index.d.ts.map +1 -0
- package/dist/components/MessageInput/index.js +139 -0
- package/dist/components/MessageInput/index.js.map +1 -0
- package/dist/components/MessageList/index.d.ts +156 -0
- package/dist/components/MessageList/index.d.ts.map +1 -0
- package/dist/components/MessageList/index.js +352 -0
- package/dist/components/MessageList/index.js.map +1 -0
- package/dist/components/ReasoningSelector/index.d.ts +77 -0
- package/dist/components/ReasoningSelector/index.d.ts.map +1 -0
- package/dist/components/ReasoningSelector/index.js +156 -0
- package/dist/components/ReasoningSelector/index.js.map +1 -0
- package/dist/components/ReasoningView/index.d.ts +94 -0
- package/dist/components/ReasoningView/index.d.ts.map +1 -0
- package/dist/components/ReasoningView/index.js +83 -0
- package/dist/components/ReasoningView/index.js.map +1 -0
- package/dist/components/ScrollToPresentButton/index.d.ts +54 -0
- package/dist/components/ScrollToPresentButton/index.d.ts.map +1 -0
- package/dist/components/ScrollToPresentButton/index.js +44 -0
- package/dist/components/ScrollToPresentButton/index.js.map +1 -0
- package/dist/components/SessionsPanel/SessionTitleModal.d.ts +79 -0
- package/dist/components/SessionsPanel/SessionTitleModal.d.ts.map +1 -0
- package/dist/components/SessionsPanel/SessionTitleModal.js +102 -0
- package/dist/components/SessionsPanel/SessionTitleModal.js.map +1 -0
- package/dist/components/SessionsPanel/index.d.ts +100 -0
- package/dist/components/SessionsPanel/index.d.ts.map +1 -0
- package/dist/components/SessionsPanel/index.js +176 -0
- package/dist/components/SessionsPanel/index.js.map +1 -0
- package/dist/components/SystemNotice/index.d.ts +66 -0
- package/dist/components/SystemNotice/index.d.ts.map +1 -0
- package/dist/components/SystemNotice/index.js +81 -0
- package/dist/components/SystemNotice/index.js.map +1 -0
- package/dist/components/ThemeSwitcher/index.d.ts +66 -0
- package/dist/components/ThemeSwitcher/index.d.ts.map +1 -0
- package/dist/components/ThemeSwitcher/index.js +73 -0
- package/dist/components/ThemeSwitcher/index.js.map +1 -0
- package/dist/components/ToolCallView/formatContent.d.ts +36 -0
- package/dist/components/ToolCallView/formatContent.d.ts.map +1 -0
- package/dist/components/ToolCallView/formatContent.js +53 -0
- package/dist/components/ToolCallView/formatContent.js.map +1 -0
- package/dist/components/ToolCallView/index.d.ts +120 -0
- package/dist/components/ToolCallView/index.d.ts.map +1 -0
- package/dist/components/ToolCallView/index.js +104 -0
- package/dist/components/ToolCallView/index.js.map +1 -0
- package/dist/components/TpmBanner/index.d.ts +53 -0
- package/dist/components/TpmBanner/index.d.ts.map +1 -0
- package/dist/components/TpmBanner/index.js +116 -0
- package/dist/components/TpmBanner/index.js.map +1 -0
- package/dist/components/TurnStatusIndicator/index.d.ts +77 -0
- package/dist/components/TurnStatusIndicator/index.d.ts.map +1 -0
- package/dist/components/TurnStatusIndicator/index.js +122 -0
- package/dist/components/TurnStatusIndicator/index.js.map +1 -0
- package/dist/components/internal/PasswordInput.d.ts +62 -0
- package/dist/components/internal/PasswordInput.d.ts.map +1 -0
- package/dist/components/internal/PasswordInput.js +42 -0
- package/dist/components/internal/PasswordInput.js.map +1 -0
- package/dist/context/AIHubContext.d.ts +220 -0
- package/dist/context/AIHubContext.d.ts.map +1 -0
- package/dist/context/AIHubContext.js +19 -0
- package/dist/context/AIHubContext.js.map +1 -0
- package/dist/context/AIHubProvider.d.ts +75 -0
- package/dist/context/AIHubProvider.d.ts.map +1 -0
- package/dist/context/AIHubProvider.js +954 -0
- package/dist/context/AIHubProvider.js.map +1 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +9 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/theme.d.ts +43 -0
- package/dist/context/theme.d.ts.map +1 -0
- package/dist/context/theme.js +70 -0
- package/dist/context/theme.js.map +1 -0
- package/dist/context/useAIHub.d.ts +104 -0
- package/dist/context/useAIHub.d.ts.map +1 -0
- package/dist/context/useAIHub.js +63 -0
- package/dist/context/useAIHub.js.map +1 -0
- package/dist/context/useAIHubAuth.d.ts +44 -0
- package/dist/context/useAIHubAuth.d.ts.map +1 -0
- package/dist/context/useAIHubAuth.js +35 -0
- package/dist/context/useAIHubAuth.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/index.d.ts +99 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +97 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/affordances.d.ts +182 -0
- package/dist/state/affordances.d.ts.map +1 -0
- package/dist/state/affordances.js +189 -0
- package/dist/state/affordances.js.map +1 -0
- package/dist/state/stateReducer.d.ts +72 -0
- package/dist/state/stateReducer.d.ts.map +1 -0
- package/dist/state/stateReducer.js +122 -0
- package/dist/state/stateReducer.js.map +1 -0
- package/dist/stories/EffortControl.stories.d.ts +14 -0
- package/dist/stories/EffortControl.stories.d.ts.map +1 -0
- package/dist/stories/EffortControl.stories.js +43 -0
- package/dist/stories/EffortControl.stories.js.map +1 -0
- package/dist/stories/MarkdownRenderer.stories.d.ts +25 -0
- package/dist/stories/MarkdownRenderer.stories.d.ts.map +1 -0
- package/dist/stories/MarkdownRenderer.stories.js +178 -0
- package/dist/stories/MarkdownRenderer.stories.js.map +1 -0
- package/dist/stories/MessageBubble.stories.d.ts +21 -0
- package/dist/stories/MessageBubble.stories.d.ts.map +1 -0
- package/dist/stories/MessageBubble.stories.js +59 -0
- package/dist/stories/MessageBubble.stories.js.map +1 -0
- package/dist/stories/ReasoningView.stories.d.ts +18 -0
- package/dist/stories/ReasoningView.stories.d.ts.map +1 -0
- package/dist/stories/ReasoningView.stories.js +62 -0
- package/dist/stories/ReasoningView.stories.js.map +1 -0
- package/dist/stories/ScrollToPresentButton.stories.d.ts +18 -0
- package/dist/stories/ScrollToPresentButton.stories.d.ts.map +1 -0
- package/dist/stories/ScrollToPresentButton.stories.js +34 -0
- package/dist/stories/ScrollToPresentButton.stories.js.map +1 -0
- package/dist/stories/SystemNotice.stories.d.ts +17 -0
- package/dist/stories/SystemNotice.stories.d.ts.map +1 -0
- package/dist/stories/SystemNotice.stories.js +42 -0
- package/dist/stories/SystemNotice.stories.js.map +1 -0
- package/dist/stories/ThemeSwitcher.stories.d.ts +15 -0
- package/dist/stories/ThemeSwitcher.stories.d.ts.map +1 -0
- package/dist/stories/ThemeSwitcher.stories.js +35 -0
- package/dist/stories/ThemeSwitcher.stories.js.map +1 -0
- package/dist/stories/ToolCallView.stories.d.ts +20 -0
- package/dist/stories/ToolCallView.stories.d.ts.map +1 -0
- package/dist/stories/ToolCallView.stories.js +65 -0
- package/dist/stories/ToolCallView.stories.js.map +1 -0
- package/dist/stories/TpmBanner.stories.d.ts +24 -0
- package/dist/stories/TpmBanner.stories.d.ts.map +1 -0
- package/dist/stories/TpmBanner.stories.js +63 -0
- package/dist/stories/TpmBanner.stories.js.map +1 -0
- package/dist/stories/TurnStatusIndicator.stories.d.ts +25 -0
- package/dist/stories/TurnStatusIndicator.stories.d.ts.map +1 -0
- package/dist/stories/TurnStatusIndicator.stories.js +56 -0
- package/dist/stories/TurnStatusIndicator.stories.js.map +1 -0
- package/dist/stories/_mockProvider.d.ts +44 -0
- package/dist/stories/_mockProvider.d.ts.map +1 -0
- package/dist/stories/_mockProvider.js +84 -0
- package/dist/stories/_mockProvider.js.map +1 -0
- package/dist/types.d.ts +787 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/webcomponent/defineWebComponent.d.ts +106 -0
- package/dist/webcomponent/defineWebComponent.d.ts.map +1 -0
- package/dist/webcomponent/defineWebComponent.js +126 -0
- package/dist/webcomponent/defineWebComponent.js.map +1 -0
- package/dist/webcomponent/index.d.ts +7 -0
- package/dist/webcomponent/index.d.ts.map +1 -0
- package/dist/webcomponent/index.js +7 -0
- package/dist/webcomponent/index.js.map +1 -0
- package/package.json +63 -0
- package/src/__lint-canary__/step1-lint-canary.ts.disabled +28 -0
- package/src/auth/passwordPolicy.ts +75 -0
- package/src/auth/texts.ts +1294 -0
- package/src/client/AIHubClient.ts +1718 -0
- package/src/client/index.ts +30 -0
- package/src/client/sseParser.ts +168 -0
- package/src/client/tokenStorage.ts +131 -0
- package/src/components/AgentsPicker/index.tsx +147 -0
- package/src/components/AuthGate/index.tsx +132 -0
- package/src/components/CacheBadge/index.tsx +245 -0
- package/src/components/ChangePassword/index.tsx +375 -0
- package/src/components/ChatShell/ShellHeader.tsx +80 -0
- package/src/components/ChatShell/UserMenu.tsx +196 -0
- package/src/components/ChatShell/index.tsx +307 -0
- package/src/components/DataClubAIHub/index.tsx +300 -0
- package/src/components/DataClubAIHubAdmin/AgentEditorModal.tsx +505 -0
- package/src/components/DataClubAIHubAdmin/AgentsPage.tsx +342 -0
- package/src/components/DataClubAIHubAdmin/CreateUserModal.tsx +214 -0
- package/src/components/DataClubAIHubAdmin/PasswordRevealModal.tsx +204 -0
- package/src/components/DataClubAIHubAdmin/ResetPasswordModal.tsx +122 -0
- package/src/components/DataClubAIHubAdmin/RotatingMessageEditorModal.tsx +408 -0
- package/src/components/DataClubAIHubAdmin/RotatingMessagesPage.tsx +373 -0
- package/src/components/DataClubAIHubAdmin/SessionsOverviewPage.tsx +639 -0
- package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +979 -0
- package/src/components/DataClubAIHubAdmin/UsersPage.tsx +495 -0
- package/src/components/DataClubAIHubAdmin/index.tsx +277 -0
- package/src/components/DataClubAIHubLogin/index.tsx +282 -0
- package/src/components/EffortControl/index.tsx +138 -0
- package/src/components/Layout/index.tsx +92 -0
- package/src/components/MainColumn/index.tsx +131 -0
- package/src/components/MarkdownRenderer/index.tsx +256 -0
- package/src/components/MessageInput/index.tsx +283 -0
- package/src/components/MessageList/index.tsx +753 -0
- package/src/components/ReasoningSelector/index.tsx +268 -0
- package/src/components/ReasoningView/index.tsx +183 -0
- package/src/components/ScrollToPresentButton/index.tsx +85 -0
- package/src/components/SessionsPanel/SessionTitleModal.tsx +256 -0
- package/src/components/SessionsPanel/index.tsx +406 -0
- package/src/components/SystemNotice/index.tsx +151 -0
- package/src/components/ThemeSwitcher/index.tsx +136 -0
- package/src/components/ToolCallView/formatContent.ts +50 -0
- package/src/components/ToolCallView/index.tsx +274 -0
- package/src/components/TpmBanner/index.tsx +172 -0
- package/src/components/TurnStatusIndicator/index.tsx +203 -0
- package/src/components/internal/PasswordInput.tsx +145 -0
- package/src/context/AIHubContext.ts +249 -0
- package/src/context/AIHubProvider.tsx +1084 -0
- package/src/context/index.ts +9 -0
- package/src/context/theme.ts +76 -0
- package/src/context/useAIHub.ts +151 -0
- package/src/context/useAIHubAuth.ts +65 -0
- package/src/index.ts +275 -0
- package/src/primitives/index.ts +101 -0
- package/src/state/affordances.ts +321 -0
- package/src/state/stateReducer.ts +131 -0
- package/src/stories/EffortControl.stories.tsx +63 -0
- package/src/stories/MarkdownRenderer.stories.tsx +201 -0
- package/src/stories/MessageBubble.stories.tsx +101 -0
- package/src/stories/ReasoningView.stories.tsx +81 -0
- package/src/stories/ScrollToPresentButton.stories.tsx +52 -0
- package/src/stories/SystemNotice.stories.tsx +59 -0
- package/src/stories/ThemeSwitcher.stories.tsx +55 -0
- package/src/stories/ToolCallView.stories.tsx +95 -0
- package/src/stories/TpmBanner.stories.tsx +93 -0
- package/src/stories/TurnStatusIndicator.stories.tsx +86 -0
- package/src/stories/_mockProvider.tsx +116 -0
- package/src/styles/CLASSES.md +356 -0
- package/src/styles/default.css +2937 -0
- package/src/types.ts +867 -0
- package/src/webcomponent/defineWebComponent.tsx +193 -0
- package/src/webcomponent/index.ts +11 -0
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AIHubClient` — the typed TypeScript SDK for `@data-club/ai-hub-server`.
|
|
3
|
+
*
|
|
4
|
+
* Used internally by `<AIHubProvider>` to power every component in the
|
|
5
|
+
* library; also exported for apps that build entirely custom UIs without
|
|
6
|
+
* the React layer (§7.8).
|
|
7
|
+
*
|
|
8
|
+
* Design notes:
|
|
9
|
+
*
|
|
10
|
+
* - **Wire is snake_case; SDK arguments are camelCase.** The translation is
|
|
11
|
+
* per-method and explicit so the reader can see exactly which fields cross
|
|
12
|
+
* the boundary. The only header-level camelCase translation is on
|
|
13
|
+
* `login()`'s response: `must_change_password` → `mustChangePassword`
|
|
14
|
+
* (per §6.3.4). `user.*` and `deployment.*` keep their snake_case keys —
|
|
15
|
+
* they're echoed through to React state and the BEM CSS contract is
|
|
16
|
+
* already snake_case-aware.
|
|
17
|
+
* - **SSE uses `fetch` + `ReadableStream`, not `EventSource`** (§9.5 — the
|
|
18
|
+
* browser API can't carry custom auth headers).
|
|
19
|
+
* - **No silent refresh** (§6.7). A 401 on any protected request fires the
|
|
20
|
+
* `onUnauthorized` callback exactly once; the provider then routes the
|
|
21
|
+
* user back to `<DataClubAIHubLogin />`.
|
|
22
|
+
* - **Empty-title rejection on `createSession`** is enforced client-side
|
|
23
|
+
* before the request goes out so we don't burn an HTTP round-trip on a
|
|
24
|
+
* case the server will 400 anyway.
|
|
25
|
+
*
|
|
26
|
+
* @see SPECIFICATIONS.md §7.8 — client SDK
|
|
27
|
+
* @see SPECIFICATIONS.md §6.3 — API surface
|
|
28
|
+
* @see SPECIFICATIONS.md §6.7 — auth model
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type {
|
|
32
|
+
AIHubAuthContext,
|
|
33
|
+
AIHubDeployment,
|
|
34
|
+
AIHubError,
|
|
35
|
+
AIHubEvent,
|
|
36
|
+
AIHubFeatureToggles,
|
|
37
|
+
AIHubMessage,
|
|
38
|
+
AIHubMessageOverride,
|
|
39
|
+
AIHubRole,
|
|
40
|
+
AIHubUser,
|
|
41
|
+
Session,
|
|
42
|
+
} from "../types.js";
|
|
43
|
+
|
|
44
|
+
import { parseSseStream } from "./sseParser.js";
|
|
45
|
+
import {
|
|
46
|
+
resolveTokenStorage,
|
|
47
|
+
type TokenStorage,
|
|
48
|
+
type TokenStorageMode,
|
|
49
|
+
} from "./tokenStorage.js";
|
|
50
|
+
|
|
51
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
52
|
+
/* Public shapes */
|
|
53
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Constructor options for `AIHubClient`.
|
|
57
|
+
*/
|
|
58
|
+
export interface AIHubClientOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Base URL the server is mounted at — typically `/api/ai-hub` in a
|
|
61
|
+
* per-customer wrapper (§5). No trailing slash; the client appends paths.
|
|
62
|
+
*/
|
|
63
|
+
baseUrl: string;
|
|
64
|
+
/**
|
|
65
|
+
* Custom `fetch`. Defaults to `globalThis.fetch`. Override for tests or
|
|
66
|
+
* for non-DOM environments (Workers, Bun, etc.).
|
|
67
|
+
*/
|
|
68
|
+
fetch?: typeof fetch;
|
|
69
|
+
/**
|
|
70
|
+
* How to persist the JWT. Either one of the three built-in modes or a
|
|
71
|
+
* custom adapter. Defaults to `"localStorage"`.
|
|
72
|
+
*/
|
|
73
|
+
tokenStorage?: TokenStorageMode | TokenStorage;
|
|
74
|
+
/**
|
|
75
|
+
* Fired when the server returns 401 on a protected request — the provider
|
|
76
|
+
* uses this to route back to login (§6.7 — no silent refresh in v1).
|
|
77
|
+
* The token is cleared before the callback fires.
|
|
78
|
+
*/
|
|
79
|
+
onUnauthorized?: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Response shape returned by `login()`. The `deployment` block is omitted
|
|
84
|
+
* when `mustChangePassword === true` (the change-only token grants no
|
|
85
|
+
* deployment access — §6.3.4).
|
|
86
|
+
*/
|
|
87
|
+
export interface LoginResponse {
|
|
88
|
+
/** The bearer token. Either a 24 h session JWT or a 5 min change-only JWT. */
|
|
89
|
+
token: string;
|
|
90
|
+
/**
|
|
91
|
+
* True when the user must call `changePassword` before any other endpoint
|
|
92
|
+
* will accept the token. Camelcased from the wire's `must_change_password`.
|
|
93
|
+
*/
|
|
94
|
+
mustChangePassword: boolean;
|
|
95
|
+
/** The authenticated user (per-user identity). */
|
|
96
|
+
user: AIHubUser;
|
|
97
|
+
/** Per-deployment context. Absent when `mustChangePassword === true`. */
|
|
98
|
+
deployment?: AIHubDeployment;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Filter args for `listSessions`. */
|
|
102
|
+
export interface ListSessionsOptions {
|
|
103
|
+
/** The ai-hub agent the sessions belong to. Required (§6.3.1). */
|
|
104
|
+
agentId: string;
|
|
105
|
+
/** Surface archived rows as well. Defaults to false. */
|
|
106
|
+
includeArchived?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Body for `createSession`. */
|
|
110
|
+
export interface CreateSessionOptions {
|
|
111
|
+
/** The ai-hub agent the new session is scoped to. */
|
|
112
|
+
agentId: string;
|
|
113
|
+
/** The user-entered chat title. 1..80 chars; empty is rejected client-side. */
|
|
114
|
+
title: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Body for `patchSession`. Either or both fields may be supplied. */
|
|
118
|
+
export interface PatchSessionOptions {
|
|
119
|
+
/** New title (1..80 chars). */
|
|
120
|
+
title?: string;
|
|
121
|
+
/** New archive state. */
|
|
122
|
+
isArchived?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Body for `patchSessionReasoning`. Fields are independent — the UI may
|
|
127
|
+
* send just the one the user changed. `null` explicitly clears a field
|
|
128
|
+
* (back to agent default); omitting it keeps the existing value.
|
|
129
|
+
*
|
|
130
|
+
* Enum values depend on the agent's model — server-side validation is
|
|
131
|
+
* intentionally loose so this layer stays schema-driven. The actual
|
|
132
|
+
* rejection (if any) happens when Letta receives the patched `model_settings`
|
|
133
|
+
* before the next turn.
|
|
134
|
+
*/
|
|
135
|
+
export interface PatchSessionReasoningOptions {
|
|
136
|
+
/** Anthropic/Bedrock-shape effort OR OpenAI-shape reasoning_effort — handled the same here. */
|
|
137
|
+
effort?: string | null;
|
|
138
|
+
/** OpenAI verbosity. */
|
|
139
|
+
verbosity?: string | null;
|
|
140
|
+
/** Google numeric thinking budget. */
|
|
141
|
+
thinkingBudget?: number | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Reasoning-effort enum (§7.4). `none` means no reasoning at all. */
|
|
145
|
+
export type AIHubEffort =
|
|
146
|
+
| "none"
|
|
147
|
+
| "minimal"
|
|
148
|
+
| "low"
|
|
149
|
+
| "medium"
|
|
150
|
+
| "high"
|
|
151
|
+
| "extra_high";
|
|
152
|
+
|
|
153
|
+
/** Body for `streamChat`. */
|
|
154
|
+
export interface StreamChatBody {
|
|
155
|
+
/** The ai-hub session id the message is being posted to. */
|
|
156
|
+
sessionId: string;
|
|
157
|
+
/** The user's message text. Non-empty per the server's Zod schema. */
|
|
158
|
+
userMessage: string;
|
|
159
|
+
/** Per-message reasoning effort (§7.4). */
|
|
160
|
+
effort?: AIHubEffort;
|
|
161
|
+
/**
|
|
162
|
+
* Allows reconnecting to the same turn from a different connection;
|
|
163
|
+
* see §6.3.2.
|
|
164
|
+
*/
|
|
165
|
+
idempotencyKey?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Optional tier-3 customization: the human-facing replacement to show
|
|
168
|
+
* in the chat transcript at the position the new user message lands
|
|
169
|
+
* at. When set, the server sends `userMessage` to Letta verbatim
|
|
170
|
+
* (system view), then — only after the turn terminates `done` —
|
|
171
|
+
* writes an override row whose `user_override` field is this string.
|
|
172
|
+
* Subsequent `getSessionMessages` calls will substitute it in place
|
|
173
|
+
* of the wrapped Letta content.
|
|
174
|
+
*
|
|
175
|
+
* Persisted only on `done`. On `cancelled` / `failed`, Letta drops
|
|
176
|
+
* the user message and we drop the override too (no orphan row).
|
|
177
|
+
*/
|
|
178
|
+
userOverride?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Per-call options shared by `streamChat` + `getTurnEvents`. */
|
|
182
|
+
export interface StreamChatOptions {
|
|
183
|
+
/** Aborts the in-flight stream. */
|
|
184
|
+
signal?: AbortSignal;
|
|
185
|
+
/**
|
|
186
|
+
* Resumption cursor for `getTurnEvents`; ignored on `streamChat` in v1
|
|
187
|
+
* but accepted for forward-compat with the §6.3.2 idempotency surface.
|
|
188
|
+
*/
|
|
189
|
+
fromSeq?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Per-agent entry returned by `listAgents`. */
|
|
193
|
+
export interface AgentSummary {
|
|
194
|
+
/** Stable id of the ai-hub agent. */
|
|
195
|
+
id: string;
|
|
196
|
+
/** User-visible name shown in the agents picker. */
|
|
197
|
+
display_name: string;
|
|
198
|
+
/**
|
|
199
|
+
* Caller-scoped count of the caller's own non-archived sessions against
|
|
200
|
+
* this agent — populated server-side in one round-trip (§6.3.3).
|
|
201
|
+
*/
|
|
202
|
+
session_count: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Response shape from `cancelTurn`. */
|
|
206
|
+
export interface CancelTurnResponse {
|
|
207
|
+
/** The cancelled turn's id (echoed). */
|
|
208
|
+
turn_id: string;
|
|
209
|
+
/** Terminal status after cancellation. Usually `"cancelled"`. */
|
|
210
|
+
status: "running" | "done" | "cancelled" | "failed";
|
|
211
|
+
/** True when the turn had already settled before the call landed. */
|
|
212
|
+
was_already_terminal: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Admin-view shape of a single user row, as returned by `GET /admin/users`,
|
|
217
|
+
* `POST /admin/users`, and `PATCH /admin/users/:id/role`.
|
|
218
|
+
*
|
|
219
|
+
* Excludes `password_hash` and `identity_source` (server-internal — §6.3.4).
|
|
220
|
+
* Note the surface is wider than the user-facing `AIHubUser` — admins need
|
|
221
|
+
* to see `is_initial_password` (to flag rows with the ⚠ indicator) plus the
|
|
222
|
+
* created / updated timestamps.
|
|
223
|
+
*
|
|
224
|
+
* @see SPECIFICATIONS.md §6.3.4 — admin users endpoints
|
|
225
|
+
* @see ui-sketches/05-admin-users.md
|
|
226
|
+
*/
|
|
227
|
+
export interface AdminUserView {
|
|
228
|
+
/** Stable UUID. */
|
|
229
|
+
id: string;
|
|
230
|
+
/** Login identifier (unique per deployment). */
|
|
231
|
+
username: string;
|
|
232
|
+
/** Optional human-readable name. `null` when unset. */
|
|
233
|
+
display_name: string | null;
|
|
234
|
+
/** Effective role used for access control. */
|
|
235
|
+
role: AIHubRole;
|
|
236
|
+
/**
|
|
237
|
+
* True until the user completes the change-password flow on first login
|
|
238
|
+
* (or after an admin-initiated password reset). Drives the ⚠ marker in
|
|
239
|
+
* the admin Users table.
|
|
240
|
+
*/
|
|
241
|
+
is_initial_password: boolean;
|
|
242
|
+
/** The user's stored theme preference, or `null` when not set. */
|
|
243
|
+
theme_preference: "light" | "dark" | null;
|
|
244
|
+
/** ISO-8601 timestamp. */
|
|
245
|
+
created_at: string;
|
|
246
|
+
/** ISO-8601 timestamp; bumped on role change / password reset. */
|
|
247
|
+
updated_at: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Body for `createUser`. */
|
|
251
|
+
export interface CreateUserBody {
|
|
252
|
+
/** Login identifier. Must be unique (server returns 409 on collision). */
|
|
253
|
+
username: string;
|
|
254
|
+
/** Optional human-readable name. */
|
|
255
|
+
displayName?: string | null;
|
|
256
|
+
/**
|
|
257
|
+
* Initial role. Defaults to `"user"` server-side. Admins cannot create
|
|
258
|
+
* `super_admin`s (server enforces).
|
|
259
|
+
*/
|
|
260
|
+
role?: AIHubRole;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Response from `createUser` / `resetUserPassword` — plaintext returned once. */
|
|
264
|
+
export interface AdminUserPasswordReveal {
|
|
265
|
+
/**
|
|
266
|
+
* 16-char alphanumeric plaintext password the server just generated.
|
|
267
|
+
* Returned **exactly once** — there is no path to retrieve this value
|
|
268
|
+
* again. The admin UI must surface it in a one-time-reveal modal before
|
|
269
|
+
* the user dismisses it; calling `resetUserPassword` again issues a
|
|
270
|
+
* different value (and invalidates the old).
|
|
271
|
+
*/
|
|
272
|
+
generatedPassword: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Response from `createUser` — the new user plus its one-time password. */
|
|
276
|
+
export interface CreateUserResponse extends AdminUserPasswordReveal {
|
|
277
|
+
/** The newly-created user row. */
|
|
278
|
+
user: AdminUserView;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
282
|
+
/* Admin agents (catalog) */
|
|
283
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Admin-view shape of an ai-hub agent row. Differs from the user-facing
|
|
287
|
+
* `AgentSummary` (returned by `GET /agents`) by carrying the Letta `agent_id`
|
|
288
|
+
* the admin needs to see / edit, the (deprecated-but-still-stored) TPM
|
|
289
|
+
* settings, and the archive flag.
|
|
290
|
+
*
|
|
291
|
+
* @see SPECIFICATIONS.md §6.3.4 — admin agents endpoints
|
|
292
|
+
* @see ui-sketches/06-admin-agents.md
|
|
293
|
+
*/
|
|
294
|
+
export interface AdminAgentView {
|
|
295
|
+
/** Stable UUID. */
|
|
296
|
+
id: string;
|
|
297
|
+
/** Display name surfaced to end users in the agents picker. */
|
|
298
|
+
display_name: string;
|
|
299
|
+
/** The assigned Letta agent id. */
|
|
300
|
+
letta_agent_id: string;
|
|
301
|
+
/**
|
|
302
|
+
* TPM-recovery state — kept on the row post-Step-11.5 for forward
|
|
303
|
+
* compatibility but **no longer read by runtime code** (§6.5). The admin
|
|
304
|
+
* Agents page deliberately does not surface a TPM settings modal anymore.
|
|
305
|
+
*/
|
|
306
|
+
tpm_recovery_enabled: boolean;
|
|
307
|
+
/** Same — retained for forward compatibility. */
|
|
308
|
+
max_recovery_attempts: number;
|
|
309
|
+
/** Same — retained for forward compatibility. */
|
|
310
|
+
default_retry_after_seconds: number;
|
|
311
|
+
/** Same — retained for forward compatibility. */
|
|
312
|
+
stream_idle_timeout_seconds: number;
|
|
313
|
+
/**
|
|
314
|
+
* Soft-hide flag. Archived agents disappear from `GET /agents` so no new
|
|
315
|
+
* sessions can start against them; existing sessions remain readable +
|
|
316
|
+
* writable (§6.3.4).
|
|
317
|
+
*/
|
|
318
|
+
is_archived: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* Step-12.5 — name of the seed folder under `AI_HUB_FILES_DIR/seeds/`.
|
|
321
|
+
* `null` when no sandbox is configured for this agent. See
|
|
322
|
+
* docs/server/session-sandboxes.md.
|
|
323
|
+
*/
|
|
324
|
+
sandbox_seed_name: string | null;
|
|
325
|
+
/**
|
|
326
|
+
* Step-12.5 — Letta conversation id (`conv-…`) to fork from at session
|
|
327
|
+
* create. `null` when this agent starts every session brand-new.
|
|
328
|
+
*/
|
|
329
|
+
fork_source_conversation_id: string | null;
|
|
330
|
+
/** ISO-8601 timestamp. */
|
|
331
|
+
created_at: string;
|
|
332
|
+
/** ISO-8601 timestamp. */
|
|
333
|
+
updated_at: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Body for `createAdminAgent`. */
|
|
337
|
+
export interface CreateAdminAgentBody {
|
|
338
|
+
/** Display name; must be 1..255 chars. */
|
|
339
|
+
displayName: string;
|
|
340
|
+
/**
|
|
341
|
+
* The Letta-side agent id this ai-hub agent will point at. Validated by
|
|
342
|
+
* the server against `client.agents.retrieve(...)`; if Letta doesn't
|
|
343
|
+
* recognize the id, the server returns 400 `letta_agent_not_found`.
|
|
344
|
+
*/
|
|
345
|
+
lettaAgentId: string;
|
|
346
|
+
/**
|
|
347
|
+
* Step-12.5 — optional seed folder name. When non-null, each new
|
|
348
|
+
* session for this agent gets a fresh copy of `seeds/<name>/` as its
|
|
349
|
+
* working directory. Validated against `^[a-zA-Z0-9_-]+$` + filesystem
|
|
350
|
+
* existence; misses return 400 `seed_name_invalid` / `seed_dir_not_found`.
|
|
351
|
+
*/
|
|
352
|
+
sandboxSeedName?: string | null;
|
|
353
|
+
/**
|
|
354
|
+
* Step-12.5 — optional Letta conversation id to fork from at session
|
|
355
|
+
* create. Validated against Letta + agent-id match; misses return
|
|
356
|
+
* 400 `fork_source_not_found` / `fork_source_agent_mismatch`.
|
|
357
|
+
*/
|
|
358
|
+
forkSourceConversationId?: string | null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Body for `patchAdminAgent`. Each field independently optional. */
|
|
362
|
+
export interface PatchAdminAgentBody {
|
|
363
|
+
/** New display name. */
|
|
364
|
+
displayName?: string;
|
|
365
|
+
/**
|
|
366
|
+
* New assigned Letta agent id. The server validates it before accepting;
|
|
367
|
+
* see {@link CreateAdminAgentBody.lettaAgentId} for the failure mode.
|
|
368
|
+
*/
|
|
369
|
+
lettaAgentId?: string;
|
|
370
|
+
/** Archive / unarchive flag. */
|
|
371
|
+
isArchived?: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* Step-12.5 — set / change / clear (with `null`) the sandbox seed name.
|
|
374
|
+
* Same validation as on create.
|
|
375
|
+
*/
|
|
376
|
+
sandboxSeedName?: string | null;
|
|
377
|
+
/**
|
|
378
|
+
* Step-12.5 — set / change / clear (with `null`) the fork source. Same
|
|
379
|
+
* validation as on create. May be a raw `conv-…` id or an `rmsg-…`
|
|
380
|
+
* rotating-message reference.
|
|
381
|
+
*/
|
|
382
|
+
forkSourceConversationId?: string | null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
386
|
+
/* Admin rotating messages */
|
|
387
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
388
|
+
|
|
389
|
+
/** Which scheduling shape a rotating message uses. */
|
|
390
|
+
export type RotatingMessageScheduleKind = "every" | "daily" | "cron";
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* One row from `GET /admin/rotating-messages`. A rotating starting message
|
|
394
|
+
* runs `prompt` against `letta_agent_id` on the configured schedule and stores
|
|
395
|
+
* the agent's reply as `current_conversation_id` — a forkable base conversation
|
|
396
|
+
* referenced elsewhere by its `rmsg-…` `id`.
|
|
397
|
+
*
|
|
398
|
+
* @see docs/server/rotating-messages.md
|
|
399
|
+
*/
|
|
400
|
+
export interface AdminRotatingMessageView {
|
|
401
|
+
/** The `rmsg-…` identifier — also the value pasted into an agent's Base conversation field. */
|
|
402
|
+
id: string;
|
|
403
|
+
/** Operator-facing display name. */
|
|
404
|
+
display_name: string;
|
|
405
|
+
/** The prompt sent to the agent each rotation. */
|
|
406
|
+
prompt: string;
|
|
407
|
+
/** Raw Letta agent id the prompt runs against. */
|
|
408
|
+
letta_agent_id: string;
|
|
409
|
+
/** Schedule discriminator. */
|
|
410
|
+
schedule_kind: RotatingMessageScheduleKind;
|
|
411
|
+
/** Interval (seconds) when `schedule_kind === "every"`; else null. */
|
|
412
|
+
interval_seconds: number | null;
|
|
413
|
+
/** `"HH:MM"` (UTC) when `schedule_kind === "daily"`; else null. */
|
|
414
|
+
daily_time: string | null;
|
|
415
|
+
/** 5-field cron expression when `schedule_kind === "cron"`; else null. */
|
|
416
|
+
cron_expression: string | null;
|
|
417
|
+
/** Current prepared conversation id; null until the first successful rotation. */
|
|
418
|
+
current_conversation_id: string | null;
|
|
419
|
+
/** Outcome of the most recent rotation. */
|
|
420
|
+
last_status: "pending" | "ok" | "error";
|
|
421
|
+
/** Error from the last failed rotation; null otherwise. */
|
|
422
|
+
last_error: string | null;
|
|
423
|
+
/** ISO-8601 timestamp of the last successful rotation; null until first success. */
|
|
424
|
+
last_rotated_at: string | null;
|
|
425
|
+
/** ISO-8601 timestamp the scheduler uses to decide when to rotate next. */
|
|
426
|
+
next_rotation_at: string;
|
|
427
|
+
/** Soft-hide flag. */
|
|
428
|
+
is_archived: boolean;
|
|
429
|
+
/** ISO-8601 timestamp. */
|
|
430
|
+
created_at: string;
|
|
431
|
+
/** ISO-8601 timestamp. */
|
|
432
|
+
updated_at: string;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Body for `createAdminRotatingMessage`. */
|
|
436
|
+
export interface CreateAdminRotatingMessageBody {
|
|
437
|
+
displayName: string;
|
|
438
|
+
prompt: string;
|
|
439
|
+
lettaAgentId: string;
|
|
440
|
+
scheduleKind: RotatingMessageScheduleKind;
|
|
441
|
+
/** Required when `scheduleKind === "every"`. */
|
|
442
|
+
intervalSeconds?: number | null;
|
|
443
|
+
/** Required when `scheduleKind === "daily"` (`"HH:MM"`, UTC). */
|
|
444
|
+
dailyTime?: string | null;
|
|
445
|
+
/** Required when `scheduleKind === "cron"` (5-field cron). */
|
|
446
|
+
cronExpression?: string | null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Body for `patchAdminRotatingMessage`. Each field independently optional. */
|
|
450
|
+
export interface PatchAdminRotatingMessageBody {
|
|
451
|
+
displayName?: string;
|
|
452
|
+
prompt?: string;
|
|
453
|
+
lettaAgentId?: string;
|
|
454
|
+
scheduleKind?: RotatingMessageScheduleKind;
|
|
455
|
+
intervalSeconds?: number | null;
|
|
456
|
+
dailyTime?: string | null;
|
|
457
|
+
cronExpression?: string | null;
|
|
458
|
+
isArchived?: boolean;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Response from `rotateAdminRotatingMessageNow`. */
|
|
462
|
+
export interface RotateNowResponse {
|
|
463
|
+
/** The refreshed row after the inline rotation attempt. */
|
|
464
|
+
rotating_message: AdminRotatingMessageView;
|
|
465
|
+
/** The attempt's outcome. */
|
|
466
|
+
outcome: { status: "ok" | "error"; conversationId?: string; error?: string };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
470
|
+
/* Admin sessions overview */
|
|
471
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* One row in `GET /admin/sessions` — the session row joined to its owner
|
|
475
|
+
* username + agent display name so the admin table can render labels without
|
|
476
|
+
* a follow-up round-trip.
|
|
477
|
+
*
|
|
478
|
+
* @see SPECIFICATIONS.md §6.3.4 — admin sessions endpoint
|
|
479
|
+
* @see ui-sketches/07-admin-sessions-overview.md
|
|
480
|
+
*/
|
|
481
|
+
export interface AdminSessionView {
|
|
482
|
+
/** Stable session id. */
|
|
483
|
+
id: string;
|
|
484
|
+
/** Owner's user id. */
|
|
485
|
+
user_id: string;
|
|
486
|
+
/** Agent (ai-hub catalog) id. */
|
|
487
|
+
agent_id: string;
|
|
488
|
+
/** Session title. */
|
|
489
|
+
title: string;
|
|
490
|
+
/** Soft-hide flag. */
|
|
491
|
+
is_archived: boolean;
|
|
492
|
+
/**
|
|
493
|
+
* Step-12.5 — `true` when the session has a per-session sandbox folder
|
|
494
|
+
* on disk; the super_admin's `[Purge sandbox]` button is shown only
|
|
495
|
+
* when this is true. The absolute path is deliberately NOT exposed
|
|
496
|
+
* over the wire (server filesystem-layout leak).
|
|
497
|
+
*/
|
|
498
|
+
has_sandbox: boolean;
|
|
499
|
+
/** ISO-8601 timestamp. */
|
|
500
|
+
created_at: string;
|
|
501
|
+
/** ISO-8601 timestamp; bumped on rename / archive flip / new turn. */
|
|
502
|
+
updated_at: string;
|
|
503
|
+
/** Owner's username — server-joined. */
|
|
504
|
+
owner_username: string;
|
|
505
|
+
/** Agent's display name — server-joined. */
|
|
506
|
+
agent_display_name: string;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Filter args for `listAdminSessions`. */
|
|
510
|
+
export interface ListAdminSessionsOptions {
|
|
511
|
+
/** Filter by owner. */
|
|
512
|
+
ownerId?: string;
|
|
513
|
+
/** Filter by ai-hub agent. */
|
|
514
|
+
agentId?: string;
|
|
515
|
+
/** Filter by archive state. Defaults to `"active"` per the server. */
|
|
516
|
+
status?: "active" | "archived" | "all";
|
|
517
|
+
/** Case-insensitive substring match against the session title. */
|
|
518
|
+
search?: string;
|
|
519
|
+
/** Opaque pagination cursor returned in the previous page's `nextCursor`. */
|
|
520
|
+
cursor?: string;
|
|
521
|
+
/** Page size; 1..200. Defaults to 50 server-side. */
|
|
522
|
+
limit?: number;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Response shape from `listAdminSessions` — a page of rows plus a cursor for
|
|
527
|
+
* the next page.
|
|
528
|
+
*/
|
|
529
|
+
export interface ListAdminSessionsResponse {
|
|
530
|
+
/** The page of sessions. */
|
|
531
|
+
sessions: AdminSessionView[];
|
|
532
|
+
/** Cursor to pass back as `cursor` for the next page; `null` on last page. */
|
|
533
|
+
nextCursor: string | null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
537
|
+
/* Admin settings */
|
|
538
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Response shape from `GET /admin/settings` — deployment-wide configuration
|
|
542
|
+
* the super-admin sees on the settings page.
|
|
543
|
+
*
|
|
544
|
+
* Encrypted Letta API key is **never** returned in plaintext through this
|
|
545
|
+
* endpoint. `letta_api_key_set` + `letta_api_key_masked` let the UI display
|
|
546
|
+
* "configured / not configured" without exposing the value.
|
|
547
|
+
*
|
|
548
|
+
* @see SPECIFICATIONS.md §6.3.4 — settings endpoints
|
|
549
|
+
* @see ui-sketches/08-admin-settings.md
|
|
550
|
+
*/
|
|
551
|
+
export interface AdminSettingsView {
|
|
552
|
+
/** Letta API base URL (e.g. `http://letta:8283`). `null` until set. */
|
|
553
|
+
letta_base_url: string | null;
|
|
554
|
+
/** Optional override for the "Open Letta ADE" link target. `null` when unset. */
|
|
555
|
+
letta_ade_url: string | null;
|
|
556
|
+
/** Masked placeholder (`••••••••`) when a key is stored in the DB; `null` otherwise. */
|
|
557
|
+
letta_api_key_masked: string | null;
|
|
558
|
+
/**
|
|
559
|
+
* True when the runtime has a usable API key from EITHER source — DB or
|
|
560
|
+
* env. The admin UI uses this to decide whether to render "configured"
|
|
561
|
+
* vs. "missing." Pair with `letta_api_key_source` for source-aware copy.
|
|
562
|
+
*/
|
|
563
|
+
letta_api_key_set: boolean;
|
|
564
|
+
/**
|
|
565
|
+
* Which source the runtime is *currently* using for the Letta API key —
|
|
566
|
+
* `"db"` when a super-admin set it via the admin panel (DB wins over
|
|
567
|
+
* env per spec), `"env"` when the DB column is empty and the server's
|
|
568
|
+
* `LETTA_API_KEY` env var is set, `"none"` when no usable value exists
|
|
569
|
+
* anywhere (chat path will throw). Lets the UI label the field
|
|
570
|
+
* "(from env)" / "(from admin panel)" so operators aren't confused
|
|
571
|
+
* about why their `.env` change didn't appear to take effect (or did).
|
|
572
|
+
*/
|
|
573
|
+
letta_api_key_source: "db" | "env" | "none";
|
|
574
|
+
/** Theme settings, stored as separate columns server-side. */
|
|
575
|
+
theme: {
|
|
576
|
+
default: "light" | "dark";
|
|
577
|
+
switcher_enabled: boolean;
|
|
578
|
+
};
|
|
579
|
+
/** The flat §8.4 feature-toggles blob. */
|
|
580
|
+
feature_toggles: AIHubFeatureToggles;
|
|
581
|
+
/** ISO-8601 of the last signing-key rotation. `null` until first rotation. */
|
|
582
|
+
signing_key_rotated_at: string | null;
|
|
583
|
+
/** ISO-8601 of the last metrics-token rotation. `null` until first rotation. */
|
|
584
|
+
metrics_scrape_token_rotated_at: string | null;
|
|
585
|
+
/** ISO-8601 of the last settings update. */
|
|
586
|
+
updated_at: string;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Body for `patchAdminSettings`. Each field independently optional. */
|
|
590
|
+
export interface PatchAdminSettingsBody {
|
|
591
|
+
/** Letta base URL. Pass `null` to clear (the schema accepts null + URL). */
|
|
592
|
+
lettaBaseUrl?: string | null;
|
|
593
|
+
/** Letta ADE URL. Pass `null` to clear. */
|
|
594
|
+
lettaAdeUrl?: string | null;
|
|
595
|
+
/** New Letta API key (plaintext over HTTPS; encrypted server-side at rest). */
|
|
596
|
+
lettaApiKey?: string;
|
|
597
|
+
/** Theme partial — either or both fields. */
|
|
598
|
+
theme?: {
|
|
599
|
+
default?: "light" | "dark";
|
|
600
|
+
switcherEnabled?: boolean;
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Response from `rotateAdminMetricsToken` — the new token returned **once**. */
|
|
605
|
+
export interface AdminMetricsTokenRotateResponse {
|
|
606
|
+
/** The freshly-generated 32-byte hex token. Shown to the admin exactly once. */
|
|
607
|
+
token: string;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
611
|
+
/* Internal helpers */
|
|
612
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Thrown for any HTTP-level failure (non-2xx, network drop, etc.). The
|
|
616
|
+
* `code` discriminates by §7.2's `AIHubError` codes; the message is safe
|
|
617
|
+
* to render to end-users (it's either the server's `error.message` or a
|
|
618
|
+
* fixed transport-level string).
|
|
619
|
+
*/
|
|
620
|
+
export class AIHubRequestError extends Error implements AIHubError {
|
|
621
|
+
/** Machine-readable code; clients branch on this rather than the message. */
|
|
622
|
+
override readonly name = "AIHubRequestError";
|
|
623
|
+
/** Discriminator matching `AIHubError.code`. */
|
|
624
|
+
readonly code: AIHubError["code"];
|
|
625
|
+
/** HTTP status when one was available. */
|
|
626
|
+
readonly status?: number;
|
|
627
|
+
/** Pino `request_id` from the server for log correlation. */
|
|
628
|
+
readonly request_id?: string;
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @param code - the error discriminator.
|
|
632
|
+
* @param message - the human-readable message.
|
|
633
|
+
* @param status - HTTP status, when available.
|
|
634
|
+
* @param requestId - request id, when available.
|
|
635
|
+
*/
|
|
636
|
+
constructor(
|
|
637
|
+
code: AIHubError["code"],
|
|
638
|
+
message: string,
|
|
639
|
+
status?: number,
|
|
640
|
+
requestId?: string,
|
|
641
|
+
) {
|
|
642
|
+
super(message);
|
|
643
|
+
this.code = code;
|
|
644
|
+
if (status !== undefined) this.status = status;
|
|
645
|
+
if (requestId !== undefined) this.request_id = requestId;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
650
|
+
/* AIHubClient */
|
|
651
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* The typed SDK every React surface in the library is built on.
|
|
655
|
+
*
|
|
656
|
+
* The client owns the JWT lifecycle (storage + clearing on 401) and the
|
|
657
|
+
* SSE plumbing; everything else is a thin HTTP wrapper that translates
|
|
658
|
+
* camelCase arguments into the server's snake_case wire format.
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```ts
|
|
662
|
+
* const client = new AIHubClient({ baseUrl: "/api/ai-hub" });
|
|
663
|
+
* const { token, mustChangePassword, user } = await client.login("alice", "secret");
|
|
664
|
+
* if (!mustChangePassword) {
|
|
665
|
+
* for await (const event of client.streamChat({
|
|
666
|
+
* sessionId: session.id,
|
|
667
|
+
* userMessage: "Hello.",
|
|
668
|
+
* })) {
|
|
669
|
+
* console.log(event);
|
|
670
|
+
* }
|
|
671
|
+
* }
|
|
672
|
+
* ```
|
|
673
|
+
*/
|
|
674
|
+
export class AIHubClient {
|
|
675
|
+
private readonly baseUrl: string;
|
|
676
|
+
private readonly fetchImpl: typeof fetch;
|
|
677
|
+
private readonly tokenStore: TokenStorage;
|
|
678
|
+
private readonly onUnauthorized?: () => void;
|
|
679
|
+
/** Guards against firing `onUnauthorized` more than once per 401 storm. */
|
|
680
|
+
private unauthorizedFired = false;
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* @param options - constructor options. `baseUrl` is required; everything
|
|
684
|
+
* else has a sensible default.
|
|
685
|
+
*/
|
|
686
|
+
constructor(options: AIHubClientOptions) {
|
|
687
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
688
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
689
|
+
this.tokenStore = resolveTokenStorage(options.tokenStorage ?? "localStorage");
|
|
690
|
+
if (options.onUnauthorized) this.onUnauthorized = options.onUnauthorized;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* ───── token surface ──────────────────────────────────────────────── */
|
|
694
|
+
|
|
695
|
+
/** Returns the currently-stored token, or null. */
|
|
696
|
+
getToken(): string | null {
|
|
697
|
+
return this.tokenStore.get();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Replaces the stored token. Used by the provider after `login` /
|
|
702
|
+
* `changePassword` to persist the JWT before the next request fires.
|
|
703
|
+
*
|
|
704
|
+
* @param token - the bearer JWT to store.
|
|
705
|
+
*/
|
|
706
|
+
setToken(token: string): void {
|
|
707
|
+
this.tokenStore.set(token);
|
|
708
|
+
this.unauthorizedFired = false;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Clears the stored token. Idempotent. */
|
|
712
|
+
clearToken(): void {
|
|
713
|
+
this.tokenStore.clear();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* ───── auth ───────────────────────────────────────────────────────── */
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* `POST /auth/login`. On success, persists the returned token automatically
|
|
720
|
+
* so subsequent calls authenticate without the caller threading the JWT
|
|
721
|
+
* through.
|
|
722
|
+
*
|
|
723
|
+
* @param username - the user's login id.
|
|
724
|
+
* @param password - plaintext password (sent over HTTPS to the server).
|
|
725
|
+
* @returns the login response, with `mustChangePassword` camelcased.
|
|
726
|
+
*/
|
|
727
|
+
async login(username: string, password: string): Promise<LoginResponse> {
|
|
728
|
+
const raw = await this.requestJson<{
|
|
729
|
+
token: string;
|
|
730
|
+
must_change_password: boolean;
|
|
731
|
+
user: AIHubUser;
|
|
732
|
+
deployment?: AIHubDeployment;
|
|
733
|
+
}>("POST", "/auth/login", {
|
|
734
|
+
body: { username, password },
|
|
735
|
+
authenticated: false,
|
|
736
|
+
});
|
|
737
|
+
this.setToken(raw.token);
|
|
738
|
+
const response: LoginResponse = {
|
|
739
|
+
token: raw.token,
|
|
740
|
+
mustChangePassword: raw.must_change_password,
|
|
741
|
+
user: raw.user,
|
|
742
|
+
};
|
|
743
|
+
if (raw.deployment) response.deployment = raw.deployment;
|
|
744
|
+
return response;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* `POST /auth/change-password`. Returns the fresh session token (the
|
|
749
|
+
* change-only token is consumed by this call).
|
|
750
|
+
*
|
|
751
|
+
* @param currentPassword - the user's existing password, or null when
|
|
752
|
+
* the caller is authenticating with a password-change-only token
|
|
753
|
+
* (forced-change flow — §8.5).
|
|
754
|
+
* @param newPassword - the new password. Must satisfy the §8.5 policy
|
|
755
|
+
* (≥8 chars, ≥2 of {letters, digits, special}).
|
|
756
|
+
* @returns the new session token, persisted automatically.
|
|
757
|
+
*/
|
|
758
|
+
async changePassword(
|
|
759
|
+
currentPassword: string | null,
|
|
760
|
+
newPassword: string,
|
|
761
|
+
): Promise<{ token: string }> {
|
|
762
|
+
const body: Record<string, unknown> = { new_password: newPassword };
|
|
763
|
+
if (currentPassword !== null) body.current_password = currentPassword;
|
|
764
|
+
const result = await this.requestJson<{ token: string }>(
|
|
765
|
+
"POST",
|
|
766
|
+
"/auth/change-password",
|
|
767
|
+
{ body },
|
|
768
|
+
);
|
|
769
|
+
this.setToken(result.token);
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* `POST /auth/logout`. Server-side this is a no-op in v1 (no token
|
|
775
|
+
* blacklist — §6.3.4); we issue it for symmetry and to give the server
|
|
776
|
+
* an audit-log entry. Always clears the local token afterward, even
|
|
777
|
+
* when the request fails.
|
|
778
|
+
*/
|
|
779
|
+
async logout(): Promise<void> {
|
|
780
|
+
try {
|
|
781
|
+
await this.requestJson<unknown>("POST", "/auth/logout", { body: {} });
|
|
782
|
+
} catch {
|
|
783
|
+
/* network errors here are not fatal — clear and move on */
|
|
784
|
+
} finally {
|
|
785
|
+
this.clearToken();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* `GET /auth/me` — fetches the `{ user, deployment }` envelope.
|
|
791
|
+
*/
|
|
792
|
+
async me(): Promise<AIHubAuthContext> {
|
|
793
|
+
return this.requestJson<AIHubAuthContext>("GET", "/auth/me");
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* `GET /feature-toggles` — fetches the deployment-wide UI feature
|
|
798
|
+
* toggles set by `super_admin` (§8.4). **Unauthenticated** — no token
|
|
799
|
+
* required, every role gets the same response. The provider calls this
|
|
800
|
+
* on mount so the §8.4 precedence ("server toggle > React prop >
|
|
801
|
+
* built-in default") applies before the chat shell first renders.
|
|
802
|
+
*
|
|
803
|
+
* Missing keys are returned absent in the response (not coerced to
|
|
804
|
+
* `false`) so the client can distinguish "explicitly off" from "fall
|
|
805
|
+
* through to the React prop default."
|
|
806
|
+
*
|
|
807
|
+
* @returns the wire-format toggles blob; may be empty (`{}`) when the
|
|
808
|
+
* super_admin hasn't set anything.
|
|
809
|
+
*/
|
|
810
|
+
async getFeatureToggles(): Promise<AIHubFeatureToggles> {
|
|
811
|
+
const result = await this.requestJson<{
|
|
812
|
+
feature_toggles?: AIHubFeatureToggles;
|
|
813
|
+
}>("GET", "/feature-toggles");
|
|
814
|
+
return result.feature_toggles ?? {};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* `PATCH /auth/me/preferences` — updates the caller's stored
|
|
819
|
+
* `theme_preference`. Returns the refreshed envelope.
|
|
820
|
+
*
|
|
821
|
+
* Rejected with `AIHubRequestError(code: "theme_switcher_disabled")` (HTTP
|
|
822
|
+
* 409) when the deployment has the switcher turned off (§7.6).
|
|
823
|
+
*
|
|
824
|
+
* @param prefs - the preferences patch (currently only `theme`).
|
|
825
|
+
* @param prefs.theme - new theme preference (`"light"` or `"dark"`).
|
|
826
|
+
*/
|
|
827
|
+
async updatePreferences(
|
|
828
|
+
prefs: { theme?: "light" | "dark" },
|
|
829
|
+
): Promise<AIHubAuthContext> {
|
|
830
|
+
return this.requestJson<AIHubAuthContext>(
|
|
831
|
+
"PATCH",
|
|
832
|
+
"/auth/me/preferences",
|
|
833
|
+
{ body: prefs },
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* ───── sessions ───────────────────────────────────────────────────── */
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* `GET /sessions?agent_id=...&include_archived=...`.
|
|
841
|
+
*
|
|
842
|
+
* @param options - filter arguments; `agentId` is required.
|
|
843
|
+
*/
|
|
844
|
+
async listSessions(options: ListSessionsOptions): Promise<Session[]> {
|
|
845
|
+
const query = new URLSearchParams({ agent_id: options.agentId });
|
|
846
|
+
if (options.includeArchived) query.set("include_archived", "true");
|
|
847
|
+
const result = await this.requestJson<{ sessions: Session[] }>(
|
|
848
|
+
"GET",
|
|
849
|
+
`/sessions?${query.toString()}`,
|
|
850
|
+
);
|
|
851
|
+
return result.sessions;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* `POST /sessions`.
|
|
856
|
+
*
|
|
857
|
+
* Client-side validation: title must be 1..80 chars. Empty / oversize
|
|
858
|
+
* titles throw `AIHubRequestError(code: "validation")` synchronously
|
|
859
|
+
* without an HTTP round-trip.
|
|
860
|
+
*
|
|
861
|
+
* @param options - the agent id + non-empty title.
|
|
862
|
+
* @returns the created session row.
|
|
863
|
+
*/
|
|
864
|
+
async createSession(options: CreateSessionOptions): Promise<Session> {
|
|
865
|
+
const title = options.title;
|
|
866
|
+
if (typeof title !== "string" || title.length === 0) {
|
|
867
|
+
throw new AIHubRequestError(
|
|
868
|
+
"validation",
|
|
869
|
+
"Session title is required (1..80 characters).",
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
if (title.length > 80) {
|
|
873
|
+
throw new AIHubRequestError(
|
|
874
|
+
"validation",
|
|
875
|
+
"Session title must be 80 characters or fewer.",
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return this.requestJson<Session>("POST", "/sessions", {
|
|
879
|
+
body: { agent_id: options.agentId, title },
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** `GET /sessions/:id`. */
|
|
884
|
+
async getSession(id: string): Promise<Session> {
|
|
885
|
+
return this.requestJson<Session>("GET", `/sessions/${encodeURIComponent(id)}`);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* `PATCH /sessions/:id` — rename and/or archive in one call. An empty
|
|
890
|
+
* patch (`{}`) returns the row unchanged.
|
|
891
|
+
*
|
|
892
|
+
* @param id - the session id.
|
|
893
|
+
* @param patch - the fields to update.
|
|
894
|
+
*/
|
|
895
|
+
async patchSession(id: string, patch: PatchSessionOptions): Promise<Session> {
|
|
896
|
+
const body: Record<string, unknown> = {};
|
|
897
|
+
if (patch.title !== undefined) body.title = patch.title;
|
|
898
|
+
if (patch.isArchived !== undefined) body.is_archived = patch.isArchived;
|
|
899
|
+
return this.requestJson<Session>(
|
|
900
|
+
"PATCH",
|
|
901
|
+
`/sessions/${encodeURIComponent(id)}`,
|
|
902
|
+
{ body },
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* `PATCH /sessions/:id/reasoning` — update the user's per-session reasoning
|
|
908
|
+
* settings. The server applies them to the agent's `model_settings`
|
|
909
|
+
* automatically before the next chat turn.
|
|
910
|
+
*
|
|
911
|
+
* Returns the refreshed session including `capabilities`, so the caller
|
|
912
|
+
* can re-render UI controls without a separate GET.
|
|
913
|
+
*/
|
|
914
|
+
async patchSessionReasoning(
|
|
915
|
+
id: string,
|
|
916
|
+
patch: PatchSessionReasoningOptions,
|
|
917
|
+
): Promise<Session> {
|
|
918
|
+
const body: Record<string, unknown> = {};
|
|
919
|
+
if (patch.effort !== undefined) body.effort = patch.effort;
|
|
920
|
+
if (patch.verbosity !== undefined) body.verbosity = patch.verbosity;
|
|
921
|
+
if (patch.thinkingBudget !== undefined) body.thinking_budget = patch.thinkingBudget;
|
|
922
|
+
return this.requestJson<Session>(
|
|
923
|
+
"PATCH",
|
|
924
|
+
`/sessions/${encodeURIComponent(id)}/reasoning`,
|
|
925
|
+
{ body },
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** `GET /sessions/:id/messages`. */
|
|
930
|
+
async getSessionMessages(id: string): Promise<AIHubMessage[]> {
|
|
931
|
+
const result = await this.requestJson<{ messages: AIHubMessage[] }>(
|
|
932
|
+
"GET",
|
|
933
|
+
`/sessions/${encodeURIComponent(id)}/messages`,
|
|
934
|
+
);
|
|
935
|
+
return result.messages;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* ───── message overrides ──────────────────────────────────────────── */
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* `GET /sessions/:id/message-overrides` — returns the caller's per-message
|
|
942
|
+
* content substitutions for this session's underlying Letta conversation.
|
|
943
|
+
*
|
|
944
|
+
* Overrides are keyed by `ordinal_position` (0-based index into Letta's
|
|
945
|
+
* chronologically-sorted message list). They're applied automatically by
|
|
946
|
+
* `getSessionMessages` — callers don't need to merge manually; this
|
|
947
|
+
* endpoint exists so custom Tier-3 UIs can render an edit affordance
|
|
948
|
+
* next to each message that has an override.
|
|
949
|
+
*
|
|
950
|
+
* @param sessionId - the ai-hub session id.
|
|
951
|
+
* @returns the overrides sorted by `ordinal_position` ascending.
|
|
952
|
+
*/
|
|
953
|
+
async listMessageOverrides(sessionId: string): Promise<AIHubMessageOverride[]> {
|
|
954
|
+
const result = await this.requestJson<{ overrides: AIHubMessageOverride[] }>(
|
|
955
|
+
"GET",
|
|
956
|
+
`/sessions/${encodeURIComponent(sessionId)}/message-overrides`,
|
|
957
|
+
);
|
|
958
|
+
return result.overrides;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* `PUT /sessions/:id/message-overrides/:position` — upsert one or both
|
|
963
|
+
* override layers for the message at `ordinalPosition`.
|
|
964
|
+
*
|
|
965
|
+
* Pass `systemOverride` to set the LLM-facing view, `userOverride` to
|
|
966
|
+
* set the UI-facing view, or both. At least one must be provided.
|
|
967
|
+
* **Merge semantics**: only the layer(s) you pass are written; an
|
|
968
|
+
* existing value on the other layer is preserved.
|
|
969
|
+
*
|
|
970
|
+
* **Validation gates** (server-enforced):
|
|
971
|
+
*
|
|
972
|
+
* - `409 turn_in_progress` — a turn is currently live for this session.
|
|
973
|
+
* - `409 message_not_found` — no Letta message exists at this position
|
|
974
|
+
* yet (typical when a tier-3 caller tries to override a message
|
|
975
|
+
* before it has landed; wait for the turn to finish first).
|
|
976
|
+
*
|
|
977
|
+
* Pass empty string for `userOverride` to visually hide the message
|
|
978
|
+
* (the bubble slot stays — use `deleteMessageOverride` to remove the
|
|
979
|
+
* slot entirely).
|
|
980
|
+
*
|
|
981
|
+
* @param sessionId - the ai-hub session id.
|
|
982
|
+
* @param ordinalPosition - the 0-based index in Letta's message list.
|
|
983
|
+
* @param override - one or both layers to write.
|
|
984
|
+
* @returns the resulting override row.
|
|
985
|
+
*/
|
|
986
|
+
async setMessageOverride(
|
|
987
|
+
sessionId: string,
|
|
988
|
+
ordinalPosition: number,
|
|
989
|
+
override: { systemOverride?: string; userOverride?: string },
|
|
990
|
+
): Promise<AIHubMessageOverride> {
|
|
991
|
+
const body: Record<string, string> = {};
|
|
992
|
+
if (override.systemOverride !== undefined) body.system_override = override.systemOverride;
|
|
993
|
+
if (override.userOverride !== undefined) body.user_override = override.userOverride;
|
|
994
|
+
return this.requestJson<AIHubMessageOverride>(
|
|
995
|
+
"PUT",
|
|
996
|
+
`/sessions/${encodeURIComponent(sessionId)}/message-overrides/${ordinalPosition}`,
|
|
997
|
+
{ body },
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* `DELETE /sessions/:id/message-overrides/:position` — remove an override.
|
|
1003
|
+
*
|
|
1004
|
+
* Idempotent: returns successfully whether or not the row existed.
|
|
1005
|
+
*
|
|
1006
|
+
* @param sessionId - the ai-hub session id.
|
|
1007
|
+
* @param ordinalPosition - the 0-based index in Letta's message list.
|
|
1008
|
+
*/
|
|
1009
|
+
async deleteMessageOverride(sessionId: string, ordinalPosition: number): Promise<void> {
|
|
1010
|
+
await this.requestJson<void>(
|
|
1011
|
+
"DELETE",
|
|
1012
|
+
`/sessions/${encodeURIComponent(sessionId)}/message-overrides/${ordinalPosition}`,
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/* ───── agents ─────────────────────────────────────────────────────── */
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* `GET /agents` — returns the user-facing agent catalog (non-archived
|
|
1020
|
+
* only) with each entry's `session_count` scoped to the caller.
|
|
1021
|
+
*/
|
|
1022
|
+
async listAgents(): Promise<AgentSummary[]> {
|
|
1023
|
+
const result = await this.requestJson<{ agents: AgentSummary[] }>(
|
|
1024
|
+
"GET",
|
|
1025
|
+
"/agents",
|
|
1026
|
+
);
|
|
1027
|
+
return result.agents;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/* ───── chat ───────────────────────────────────────────────────────── */
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* `POST /chat` — opens an SSE stream. Returns an async iterable of
|
|
1034
|
+
* `AIHubEvent`s; consume with `for await (const event of …)`.
|
|
1035
|
+
*
|
|
1036
|
+
* The iterable terminates when the server emits `stream_end` (the bytes
|
|
1037
|
+
* stop after that). To abort early, pass `opts.signal`.
|
|
1038
|
+
*
|
|
1039
|
+
* @param body - the request body (sessionId, userMessage, optional effort,
|
|
1040
|
+
* optional idempotencyKey).
|
|
1041
|
+
* @param opts - optional abort signal and resumption hint.
|
|
1042
|
+
*/
|
|
1043
|
+
streamChat(
|
|
1044
|
+
body: StreamChatBody,
|
|
1045
|
+
opts: StreamChatOptions = {},
|
|
1046
|
+
): AsyncIterable<AIHubEvent> {
|
|
1047
|
+
const wireBody: Record<string, unknown> = {
|
|
1048
|
+
session_id: body.sessionId,
|
|
1049
|
+
user_message: body.userMessage,
|
|
1050
|
+
};
|
|
1051
|
+
if (body.effort !== undefined) wireBody.effort = body.effort;
|
|
1052
|
+
if (body.idempotencyKey !== undefined) {
|
|
1053
|
+
wireBody.idempotency_key = body.idempotencyKey;
|
|
1054
|
+
}
|
|
1055
|
+
if (body.userOverride !== undefined) {
|
|
1056
|
+
wireBody.user_override = body.userOverride;
|
|
1057
|
+
}
|
|
1058
|
+
return this.streamRequest("POST", "/chat", wireBody, opts);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* `GET /chat/:turn_id/events?from_seq=N` — resumption replay then
|
|
1063
|
+
* live-attach.
|
|
1064
|
+
*
|
|
1065
|
+
* @param turnId - the turn to resume.
|
|
1066
|
+
* @param opts - optional abort signal and `fromSeq` cursor (defaults to
|
|
1067
|
+
* `-1` so every persisted event is replayed).
|
|
1068
|
+
*/
|
|
1069
|
+
getTurnEvents(
|
|
1070
|
+
turnId: string,
|
|
1071
|
+
opts: StreamChatOptions = {},
|
|
1072
|
+
): AsyncIterable<AIHubEvent> {
|
|
1073
|
+
const fromSeq = opts.fromSeq ?? -1;
|
|
1074
|
+
const path = `/chat/${encodeURIComponent(turnId)}/events?from_seq=${encodeURIComponent(
|
|
1075
|
+
String(fromSeq),
|
|
1076
|
+
)}`;
|
|
1077
|
+
return this.streamRequest("GET", path, undefined, opts);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** `POST /chat/:turn_id/cancel`. */
|
|
1081
|
+
async cancelTurn(turnId: string): Promise<CancelTurnResponse> {
|
|
1082
|
+
return this.requestJson<CancelTurnResponse>(
|
|
1083
|
+
"POST",
|
|
1084
|
+
`/chat/${encodeURIComponent(turnId)}/cancel`,
|
|
1085
|
+
{ body: {} },
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/* ───── admin: users ───────────────────────────────────────────────── */
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* `GET /admin/users` — fetches every user row in the deployment.
|
|
1093
|
+
*
|
|
1094
|
+
* Caller must be `admin` or `super_admin`; the server returns 403
|
|
1095
|
+
* otherwise (the SDK surfaces it as `AIHubRequestError(code: "forbidden")`).
|
|
1096
|
+
*
|
|
1097
|
+
* @returns the user rows, sorted alphabetically by username.
|
|
1098
|
+
* @throws {AIHubRequestError} `forbidden` for `user` / `power_user` callers.
|
|
1099
|
+
*/
|
|
1100
|
+
async listAdminUsers(): Promise<AdminUserView[]> {
|
|
1101
|
+
const result = await this.requestJson<{ users: AdminUserView[] }>(
|
|
1102
|
+
"GET",
|
|
1103
|
+
"/admin/users",
|
|
1104
|
+
);
|
|
1105
|
+
return result.users;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* `POST /admin/users` — creates a user; the server generates the password.
|
|
1110
|
+
*
|
|
1111
|
+
* Returns the new row plus the **one-time** plaintext password
|
|
1112
|
+
* (`generatedPassword`). The caller is responsible for surfacing it
|
|
1113
|
+
* exactly once in the UI — there is no way to retrieve it later.
|
|
1114
|
+
*
|
|
1115
|
+
* Admins cannot create `super_admin`s (server returns 403 if the body
|
|
1116
|
+
* requests it). On username collision returns 409 `username_taken`.
|
|
1117
|
+
*
|
|
1118
|
+
* @param body - the new user's identity + initial role.
|
|
1119
|
+
* @returns the created row and the one-time password.
|
|
1120
|
+
* @throws {AIHubRequestError} `forbidden` (caller role insufficient or
|
|
1121
|
+
* admin tried to grant super_admin), `validation` (malformed body),
|
|
1122
|
+
* or `username_taken` (409).
|
|
1123
|
+
*/
|
|
1124
|
+
async createAdminUser(body: CreateUserBody): Promise<CreateUserResponse> {
|
|
1125
|
+
const wireBody: Record<string, unknown> = { username: body.username };
|
|
1126
|
+
if (body.displayName !== undefined) {
|
|
1127
|
+
wireBody.display_name = body.displayName;
|
|
1128
|
+
}
|
|
1129
|
+
if (body.role !== undefined) wireBody.role = body.role;
|
|
1130
|
+
const result = await this.requestJson<{
|
|
1131
|
+
user: AdminUserView;
|
|
1132
|
+
generated_password: string;
|
|
1133
|
+
}>("POST", "/admin/users", { body: wireBody });
|
|
1134
|
+
return {
|
|
1135
|
+
user: result.user,
|
|
1136
|
+
generatedPassword: result.generated_password,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* `PATCH /admin/users/:id/role` — changes a user's role.
|
|
1142
|
+
*
|
|
1143
|
+
* The server enforces the admin-shield + super-admin-min-count rules
|
|
1144
|
+
* transactionally (§8.1). Violations come back as 403 with codes like
|
|
1145
|
+
* `admin_shield`, `super_admin_min_count`, or `forbidden`.
|
|
1146
|
+
*
|
|
1147
|
+
* @param id - the target user's id.
|
|
1148
|
+
* @param role - the new role.
|
|
1149
|
+
* @returns the updated row.
|
|
1150
|
+
* @throws {AIHubRequestError} `forbidden` (shield / min-count / caller
|
|
1151
|
+
* cannot grant this role).
|
|
1152
|
+
*/
|
|
1153
|
+
async patchAdminUserRole(
|
|
1154
|
+
id: string,
|
|
1155
|
+
role: AIHubRole,
|
|
1156
|
+
): Promise<AdminUserView> {
|
|
1157
|
+
const result = await this.requestJson<{ user: AdminUserView }>(
|
|
1158
|
+
"PATCH",
|
|
1159
|
+
`/admin/users/${encodeURIComponent(id)}/role`,
|
|
1160
|
+
{ body: { role } },
|
|
1161
|
+
);
|
|
1162
|
+
return result.user;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* `POST /admin/users/:id/reset-password` — issues a fresh password.
|
|
1167
|
+
*
|
|
1168
|
+
* Sets `is_initial_password = true` on the target. The new plaintext is
|
|
1169
|
+
* returned **exactly once** in `generatedPassword`; the admin UI must
|
|
1170
|
+
* surface it via the one-time-reveal modal.
|
|
1171
|
+
*
|
|
1172
|
+
* @param id - the target user's id.
|
|
1173
|
+
* @returns the one-time plaintext.
|
|
1174
|
+
* @throws {AIHubRequestError} `forbidden` (shield / min-count).
|
|
1175
|
+
*/
|
|
1176
|
+
async resetAdminUserPassword(
|
|
1177
|
+
id: string,
|
|
1178
|
+
): Promise<AdminUserPasswordReveal> {
|
|
1179
|
+
const result = await this.requestJson<{ generated_password: string }>(
|
|
1180
|
+
"POST",
|
|
1181
|
+
`/admin/users/${encodeURIComponent(id)}/reset-password`,
|
|
1182
|
+
{ body: {} },
|
|
1183
|
+
);
|
|
1184
|
+
return { generatedPassword: result.generated_password };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* `DELETE /admin/users/:id` — hard-deletes a user.
|
|
1189
|
+
*
|
|
1190
|
+
* Cascades to that user's sessions via the FK. The server blocks
|
|
1191
|
+
* self-delete (returns 403) so a careless click can't brick the caller's
|
|
1192
|
+
* own account; shield + min-count apply as for role change.
|
|
1193
|
+
*
|
|
1194
|
+
* @param id - the target user's id.
|
|
1195
|
+
* @throws {AIHubRequestError} `forbidden` (shield, min-count, or
|
|
1196
|
+
* attempting to delete oneself).
|
|
1197
|
+
*/
|
|
1198
|
+
async deleteAdminUser(id: string): Promise<void> {
|
|
1199
|
+
await this.requestJson<unknown>(
|
|
1200
|
+
"DELETE",
|
|
1201
|
+
`/admin/users/${encodeURIComponent(id)}`,
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/* ───── admin: agents ──────────────────────────────────────────────── */
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* `GET /admin/agents` — every ai-hub agent in the deployment, including
|
|
1209
|
+
* archived ones. Caller must be `power_user`+ per §8.2.
|
|
1210
|
+
*
|
|
1211
|
+
* Sort order: ascending by `display_name` (the server's `ORDER BY`).
|
|
1212
|
+
*
|
|
1213
|
+
* @returns the agent rows.
|
|
1214
|
+
* @throws {AIHubRequestError} `forbidden` for `user` callers.
|
|
1215
|
+
*/
|
|
1216
|
+
async listAdminAgents(): Promise<AdminAgentView[]> {
|
|
1217
|
+
const result = await this.requestJson<{ agents: AdminAgentView[] }>(
|
|
1218
|
+
"GET",
|
|
1219
|
+
"/admin/agents",
|
|
1220
|
+
);
|
|
1221
|
+
return result.agents;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* `POST /admin/agents` — register a new ai-hub agent in the catalog.
|
|
1226
|
+
*
|
|
1227
|
+
* The server validates `lettaAgentId` against Letta; if the id isn't
|
|
1228
|
+
* resolvable the response is 400 `letta_agent_not_found` (surface inline
|
|
1229
|
+
* on the form field).
|
|
1230
|
+
*
|
|
1231
|
+
* @param body - the display name + assigned Letta agent id.
|
|
1232
|
+
* @returns the newly-created row.
|
|
1233
|
+
* @throws {AIHubRequestError} `letta_agent_not_found`, `validation`, `forbidden`.
|
|
1234
|
+
*/
|
|
1235
|
+
async createAdminAgent(body: CreateAdminAgentBody): Promise<AdminAgentView> {
|
|
1236
|
+
const wire: Record<string, unknown> = {
|
|
1237
|
+
display_name: body.displayName,
|
|
1238
|
+
letta_agent_id: body.lettaAgentId,
|
|
1239
|
+
};
|
|
1240
|
+
if (body.sandboxSeedName !== undefined) {
|
|
1241
|
+
wire.sandbox_seed_name = body.sandboxSeedName;
|
|
1242
|
+
}
|
|
1243
|
+
if (body.forkSourceConversationId !== undefined) {
|
|
1244
|
+
wire.fork_source_conversation_id = body.forkSourceConversationId;
|
|
1245
|
+
}
|
|
1246
|
+
return this.requestJson<AdminAgentView>("POST", "/admin/agents", {
|
|
1247
|
+
body: wire,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* `PATCH /admin/agents/:id` — update one or more fields. Every field is
|
|
1253
|
+
* optional; an empty patch is a 200 no-op.
|
|
1254
|
+
*
|
|
1255
|
+
* Archive / unarchive flows through this same endpoint via `isArchived`.
|
|
1256
|
+
*
|
|
1257
|
+
* @param id - the ai-hub agent id.
|
|
1258
|
+
* @param body - the fields to update.
|
|
1259
|
+
* @returns the refreshed row.
|
|
1260
|
+
* @throws {AIHubRequestError} `letta_agent_not_found` if `lettaAgentId`
|
|
1261
|
+
* was supplied and doesn't resolve in Letta.
|
|
1262
|
+
*/
|
|
1263
|
+
async patchAdminAgent(
|
|
1264
|
+
id: string,
|
|
1265
|
+
body: PatchAdminAgentBody,
|
|
1266
|
+
): Promise<AdminAgentView> {
|
|
1267
|
+
const wire: Record<string, unknown> = {};
|
|
1268
|
+
if (body.displayName !== undefined) wire.display_name = body.displayName;
|
|
1269
|
+
if (body.lettaAgentId !== undefined) wire.letta_agent_id = body.lettaAgentId;
|
|
1270
|
+
if (body.isArchived !== undefined) wire.is_archived = body.isArchived;
|
|
1271
|
+
if (body.sandboxSeedName !== undefined) {
|
|
1272
|
+
wire.sandbox_seed_name = body.sandboxSeedName;
|
|
1273
|
+
}
|
|
1274
|
+
if (body.forkSourceConversationId !== undefined) {
|
|
1275
|
+
wire.fork_source_conversation_id = body.forkSourceConversationId;
|
|
1276
|
+
}
|
|
1277
|
+
return this.requestJson<AdminAgentView>(
|
|
1278
|
+
"PATCH",
|
|
1279
|
+
`/admin/agents/${encodeURIComponent(id)}`,
|
|
1280
|
+
{ body: wire },
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/* ───── admin: rotating messages ────────────────────────────────────── */
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* `GET /admin/rotating-messages` — every rotating starting message in the
|
|
1288
|
+
* deployment (archived included), sorted by display name. `power_user`+.
|
|
1289
|
+
*
|
|
1290
|
+
* @returns the rows.
|
|
1291
|
+
* @throws {AIHubRequestError} `forbidden` for `user` callers.
|
|
1292
|
+
*/
|
|
1293
|
+
async listAdminRotatingMessages(): Promise<AdminRotatingMessageView[]> {
|
|
1294
|
+
const result = await this.requestJson<{
|
|
1295
|
+
rotating_messages: AdminRotatingMessageView[];
|
|
1296
|
+
}>("GET", "/admin/rotating-messages");
|
|
1297
|
+
return result.rotating_messages;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* `POST /admin/rotating-messages` — create a rotating starting message. The
|
|
1302
|
+
* server validates `lettaAgentId` against Letta (`letta_agent_not_found`)
|
|
1303
|
+
* and the schedule shape (`invalid_schedule`).
|
|
1304
|
+
*
|
|
1305
|
+
* @param body - the prompt, agent, and schedule config.
|
|
1306
|
+
* @returns the newly-created row (including its `rmsg-…` id).
|
|
1307
|
+
* @throws {AIHubRequestError} `letta_agent_not_found`, `invalid_schedule`, `validation`, `forbidden`.
|
|
1308
|
+
*/
|
|
1309
|
+
async createAdminRotatingMessage(
|
|
1310
|
+
body: CreateAdminRotatingMessageBody,
|
|
1311
|
+
): Promise<AdminRotatingMessageView> {
|
|
1312
|
+
const wire: Record<string, unknown> = {
|
|
1313
|
+
display_name: body.displayName,
|
|
1314
|
+
prompt: body.prompt,
|
|
1315
|
+
letta_agent_id: body.lettaAgentId,
|
|
1316
|
+
schedule_kind: body.scheduleKind,
|
|
1317
|
+
};
|
|
1318
|
+
if (body.intervalSeconds !== undefined) wire.interval_seconds = body.intervalSeconds;
|
|
1319
|
+
if (body.dailyTime !== undefined) wire.daily_time = body.dailyTime;
|
|
1320
|
+
if (body.cronExpression !== undefined) wire.cron_expression = body.cronExpression;
|
|
1321
|
+
return this.requestJson<AdminRotatingMessageView>(
|
|
1322
|
+
"POST",
|
|
1323
|
+
"/admin/rotating-messages",
|
|
1324
|
+
{ body: wire },
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* `PATCH /admin/rotating-messages/:id` — update one or more fields. Every
|
|
1330
|
+
* field is optional; an empty patch is a 200 no-op. Changing the schedule
|
|
1331
|
+
* recomputes the next rotation time.
|
|
1332
|
+
*
|
|
1333
|
+
* @param id - the rotating message id.
|
|
1334
|
+
* @param body - the fields to update.
|
|
1335
|
+
* @returns the refreshed row.
|
|
1336
|
+
* @throws {AIHubRequestError} `letta_agent_not_found`, `invalid_schedule`, `not_found`, `forbidden`.
|
|
1337
|
+
*/
|
|
1338
|
+
async patchAdminRotatingMessage(
|
|
1339
|
+
id: string,
|
|
1340
|
+
body: PatchAdminRotatingMessageBody,
|
|
1341
|
+
): Promise<AdminRotatingMessageView> {
|
|
1342
|
+
const wire: Record<string, unknown> = {};
|
|
1343
|
+
if (body.displayName !== undefined) wire.display_name = body.displayName;
|
|
1344
|
+
if (body.prompt !== undefined) wire.prompt = body.prompt;
|
|
1345
|
+
if (body.lettaAgentId !== undefined) wire.letta_agent_id = body.lettaAgentId;
|
|
1346
|
+
if (body.scheduleKind !== undefined) wire.schedule_kind = body.scheduleKind;
|
|
1347
|
+
if (body.intervalSeconds !== undefined) wire.interval_seconds = body.intervalSeconds;
|
|
1348
|
+
if (body.dailyTime !== undefined) wire.daily_time = body.dailyTime;
|
|
1349
|
+
if (body.cronExpression !== undefined) wire.cron_expression = body.cronExpression;
|
|
1350
|
+
if (body.isArchived !== undefined) wire.is_archived = body.isArchived;
|
|
1351
|
+
return this.requestJson<AdminRotatingMessageView>(
|
|
1352
|
+
"PATCH",
|
|
1353
|
+
`/admin/rotating-messages/${encodeURIComponent(id)}`,
|
|
1354
|
+
{ body: wire },
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* `POST /admin/rotating-messages/:id/rotate-now` — run a rotation inline
|
|
1360
|
+
* (preview / force-refresh). May take a while — it spawns the Letta Code
|
|
1361
|
+
* subprocess like a real chat turn.
|
|
1362
|
+
*
|
|
1363
|
+
* @param id - the rotating message id.
|
|
1364
|
+
* @returns the refreshed row plus the attempt's outcome.
|
|
1365
|
+
* @throws {AIHubRequestError} `not_found`, `forbidden`.
|
|
1366
|
+
*/
|
|
1367
|
+
async rotateAdminRotatingMessageNow(id: string): Promise<RotateNowResponse> {
|
|
1368
|
+
return this.requestJson<RotateNowResponse>(
|
|
1369
|
+
"POST",
|
|
1370
|
+
`/admin/rotating-messages/${encodeURIComponent(id)}/rotate-now`,
|
|
1371
|
+
{ body: {} },
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* ───── admin: sessions overview ───────────────────────────────────── */
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* `GET /admin/sessions` — paginated read-only browser over every session
|
|
1379
|
+
* in the deployment. `power_user`+ per §8.2.
|
|
1380
|
+
*
|
|
1381
|
+
* Each row carries `owner_username` + `agent_display_name` joined
|
|
1382
|
+
* server-side. Use `nextCursor` (when non-null) as the `cursor` for the
|
|
1383
|
+
* next page.
|
|
1384
|
+
*
|
|
1385
|
+
* @param options - filter + pagination args. All optional.
|
|
1386
|
+
* @returns the page plus the next-cursor.
|
|
1387
|
+
*/
|
|
1388
|
+
async listAdminSessions(
|
|
1389
|
+
options: ListAdminSessionsOptions = {},
|
|
1390
|
+
): Promise<ListAdminSessionsResponse> {
|
|
1391
|
+
const query = new URLSearchParams();
|
|
1392
|
+
if (options.ownerId !== undefined) query.set("owner_id", options.ownerId);
|
|
1393
|
+
if (options.agentId !== undefined) query.set("agent_id", options.agentId);
|
|
1394
|
+
if (options.status !== undefined) query.set("status", options.status);
|
|
1395
|
+
if (options.search !== undefined && options.search.length > 0) {
|
|
1396
|
+
query.set("search", options.search);
|
|
1397
|
+
}
|
|
1398
|
+
if (options.cursor !== undefined) query.set("cursor", options.cursor);
|
|
1399
|
+
if (options.limit !== undefined) query.set("limit", String(options.limit));
|
|
1400
|
+
const qs = query.toString();
|
|
1401
|
+
const path = qs.length > 0 ? `/admin/sessions?${qs}` : "/admin/sessions";
|
|
1402
|
+
const result = await this.requestJson<{
|
|
1403
|
+
sessions: AdminSessionView[];
|
|
1404
|
+
next_cursor: string | null;
|
|
1405
|
+
}>("GET", path);
|
|
1406
|
+
return { sessions: result.sessions, nextCursor: result.next_cursor };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* `GET /admin/sessions/:id/messages` — read-only history for any session
|
|
1411
|
+
* (cross-user). Returns the same `AIHubMessage[]` shape as the user-facing
|
|
1412
|
+
* `getSessionMessages` so the same `<MessageBubble />` rendering can be
|
|
1413
|
+
* reused.
|
|
1414
|
+
*
|
|
1415
|
+
* @param id - any session id in the deployment.
|
|
1416
|
+
* @returns the projected message list.
|
|
1417
|
+
* @throws {AIHubRequestError} `forbidden` for `user` callers, `not_found`
|
|
1418
|
+
* if the session id doesn't exist.
|
|
1419
|
+
*/
|
|
1420
|
+
async getAdminSessionMessages(id: string): Promise<AIHubMessage[]> {
|
|
1421
|
+
const result = await this.requestJson<{ messages: AIHubMessage[] }>(
|
|
1422
|
+
"GET",
|
|
1423
|
+
`/admin/sessions/${encodeURIComponent(id)}/messages`,
|
|
1424
|
+
);
|
|
1425
|
+
return result.messages;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Step-12.5 — `POST /admin/sessions/:id/purge-sandbox`.
|
|
1430
|
+
*
|
|
1431
|
+
* Removes the per-session sandbox folder from disk + clears
|
|
1432
|
+
* `session.sandbox_dir`. Idempotent: 204 on success regardless of
|
|
1433
|
+
* whether the session ever had a sandbox. Rejects with
|
|
1434
|
+
* `409 turn_running` if a chat turn is live for the session.
|
|
1435
|
+
*
|
|
1436
|
+
* `super_admin` only — `admin` / `power_user` callers get 403.
|
|
1437
|
+
*
|
|
1438
|
+
* @param id - the ai-hub session id.
|
|
1439
|
+
* @throws {AIHubRequestError} `turn_running`, `forbidden`, `not_found`.
|
|
1440
|
+
*/
|
|
1441
|
+
async purgeAdminSessionSandbox(id: string): Promise<void> {
|
|
1442
|
+
await this.requestJson<void>(
|
|
1443
|
+
"POST",
|
|
1444
|
+
`/admin/sessions/${encodeURIComponent(id)}/purge-sandbox`,
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/* ───── admin: settings ────────────────────────────────────────────── */
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* `GET /admin/settings` — deployment-wide configuration (super_admin only).
|
|
1452
|
+
*
|
|
1453
|
+
* The Letta API key is never returned in plaintext; the response carries
|
|
1454
|
+
* `letta_api_key_set` + `letta_api_key_masked` so the UI can render
|
|
1455
|
+
* "configured / not configured" affordances.
|
|
1456
|
+
*/
|
|
1457
|
+
async getAdminSettings(): Promise<AdminSettingsView> {
|
|
1458
|
+
return this.requestJson<AdminSettingsView>("GET", "/admin/settings");
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* `PATCH /admin/settings` — update one or more deployment settings.
|
|
1463
|
+
*
|
|
1464
|
+
* The API key, when supplied, is encrypted under `AI_HUB_ENCRYPTION_KEY`
|
|
1465
|
+
* server-side. Theme fields live as separate columns and follow the same
|
|
1466
|
+
* precedence as the React props (server > prop > built-in default).
|
|
1467
|
+
*
|
|
1468
|
+
* @param body - the patch. Each field independent.
|
|
1469
|
+
* @returns the refreshed settings view.
|
|
1470
|
+
*/
|
|
1471
|
+
async patchAdminSettings(
|
|
1472
|
+
body: PatchAdminSettingsBody,
|
|
1473
|
+
): Promise<AdminSettingsView> {
|
|
1474
|
+
const wire: Record<string, unknown> = {};
|
|
1475
|
+
if (body.lettaBaseUrl !== undefined) wire.letta_base_url = body.lettaBaseUrl;
|
|
1476
|
+
if (body.lettaAdeUrl !== undefined) wire.letta_ade_url = body.lettaAdeUrl;
|
|
1477
|
+
if (body.lettaApiKey !== undefined) wire.letta_api_key = body.lettaApiKey;
|
|
1478
|
+
if (body.theme !== undefined) {
|
|
1479
|
+
const themeWire: Record<string, unknown> = {};
|
|
1480
|
+
if (body.theme.default !== undefined) themeWire.default = body.theme.default;
|
|
1481
|
+
if (body.theme.switcherEnabled !== undefined) {
|
|
1482
|
+
themeWire.switcher_enabled = body.theme.switcherEnabled;
|
|
1483
|
+
}
|
|
1484
|
+
wire.theme = themeWire;
|
|
1485
|
+
}
|
|
1486
|
+
return this.requestJson<AdminSettingsView>("PATCH", "/admin/settings", {
|
|
1487
|
+
body: wire,
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* `GET /admin/settings/feature-toggles` — the §8.4 toggle blob the
|
|
1493
|
+
* super-admin sees. Same payload the `getFeatureToggles` (unauthenticated)
|
|
1494
|
+
* endpoint returns, but gated and intended for the settings editor.
|
|
1495
|
+
*/
|
|
1496
|
+
async getAdminFeatureToggles(): Promise<AIHubFeatureToggles> {
|
|
1497
|
+
const result = await this.requestJson<{
|
|
1498
|
+
feature_toggles?: AIHubFeatureToggles;
|
|
1499
|
+
}>("GET", "/admin/settings/feature-toggles");
|
|
1500
|
+
return result.feature_toggles ?? {};
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* `PATCH /admin/settings/feature-toggles` — merge-patch the §8.4 toggle
|
|
1505
|
+
* blob. Unknown keys are rejected with 400; the server's strict schema is
|
|
1506
|
+
* the source of truth for which toggles are accepted.
|
|
1507
|
+
*
|
|
1508
|
+
* @param toggles - the partial — only the keys you want to write.
|
|
1509
|
+
*/
|
|
1510
|
+
async patchAdminFeatureToggles(
|
|
1511
|
+
toggles: AIHubFeatureToggles,
|
|
1512
|
+
): Promise<AIHubFeatureToggles> {
|
|
1513
|
+
const result = await this.requestJson<{ feature_toggles: AIHubFeatureToggles }>(
|
|
1514
|
+
"PATCH",
|
|
1515
|
+
"/admin/settings/feature-toggles",
|
|
1516
|
+
{ body: toggles },
|
|
1517
|
+
);
|
|
1518
|
+
return result.feature_toggles;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* `POST /admin/settings/rotate-signing-key` — replace the JWT signing key
|
|
1523
|
+
* in place. **Every outstanding JWT is invalidated immediately**, including
|
|
1524
|
+
* the caller's own. The next protected request from the UI will 401 and
|
|
1525
|
+
* the provider routes back to login (§6.7).
|
|
1526
|
+
*
|
|
1527
|
+
* Returns nothing — the new key never leaves the server.
|
|
1528
|
+
*/
|
|
1529
|
+
async rotateAdminSigningKey(): Promise<void> {
|
|
1530
|
+
await this.requestJson<unknown>("POST", "/admin/settings/rotate-signing-key", {
|
|
1531
|
+
body: {},
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* `POST /admin/settings/rotate-metrics-token` — rotate the `/metrics`
|
|
1537
|
+
* bearer token. The new token is returned **exactly once** in
|
|
1538
|
+
* `{ token }`; subsequent `getAdminSettings` calls do NOT echo it back.
|
|
1539
|
+
* The admin must copy it into their Prometheus / Alloy config before
|
|
1540
|
+
* dismissing the reveal modal (§6.10).
|
|
1541
|
+
*
|
|
1542
|
+
* Does NOT invalidate the caller's JWT — only the metrics-scraper bearer
|
|
1543
|
+
* changes.
|
|
1544
|
+
*/
|
|
1545
|
+
async rotateAdminMetricsToken(): Promise<AdminMetricsTokenRotateResponse> {
|
|
1546
|
+
return this.requestJson<AdminMetricsTokenRotateResponse>(
|
|
1547
|
+
"POST",
|
|
1548
|
+
"/admin/settings/rotate-metrics-token",
|
|
1549
|
+
{ body: {} },
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/* ───── internal request plumbing ─────────────────────────────────── */
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Sends a JSON request and parses the JSON response. Centralizes auth
|
|
1557
|
+
* header injection, 401 handling, and error normalization.
|
|
1558
|
+
*
|
|
1559
|
+
* @param method - HTTP method.
|
|
1560
|
+
* @param path - path under `baseUrl`.
|
|
1561
|
+
* @param opts - optional body and unauthenticated-mode flag.
|
|
1562
|
+
* @param opts.body - JSON body to send (omit for GET / DELETE).
|
|
1563
|
+
* @param opts.authenticated - set to `false` to skip the bearer header
|
|
1564
|
+
* (login is the only such caller). Defaults to true.
|
|
1565
|
+
* @returns the parsed response.
|
|
1566
|
+
*/
|
|
1567
|
+
private async requestJson<T>(
|
|
1568
|
+
method: string,
|
|
1569
|
+
path: string,
|
|
1570
|
+
opts: { body?: unknown; authenticated?: boolean } = {},
|
|
1571
|
+
): Promise<T> {
|
|
1572
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
1573
|
+
const authenticated = opts.authenticated !== false;
|
|
1574
|
+
if (authenticated) {
|
|
1575
|
+
const token = this.tokenStore.get();
|
|
1576
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1577
|
+
}
|
|
1578
|
+
let body: BodyInit | undefined;
|
|
1579
|
+
if (opts.body !== undefined) {
|
|
1580
|
+
headers["Content-Type"] = "application/json";
|
|
1581
|
+
body = JSON.stringify(opts.body);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
let response: Response;
|
|
1585
|
+
try {
|
|
1586
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
1587
|
+
method,
|
|
1588
|
+
headers,
|
|
1589
|
+
...(body !== undefined ? { body } : {}),
|
|
1590
|
+
});
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
throw new AIHubRequestError(
|
|
1593
|
+
"network",
|
|
1594
|
+
err instanceof Error ? err.message : "Network request failed.",
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (response.status === 401) {
|
|
1599
|
+
this.handleUnauthorized();
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (!response.ok) {
|
|
1603
|
+
throw await this.errorFromResponse(response);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// 204 / 205 / empty body
|
|
1607
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
1608
|
+
if (response.status === 204 || !ct.includes("application/json")) {
|
|
1609
|
+
return undefined as T;
|
|
1610
|
+
}
|
|
1611
|
+
return (await response.json()) as T;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* Shared backbone for `streamChat` and `getTurnEvents`. Yields events
|
|
1616
|
+
* from an SSE response; throws `AIHubRequestError` on 4xx/5xx before the
|
|
1617
|
+
* stream opens.
|
|
1618
|
+
*
|
|
1619
|
+
* @param method - HTTP method.
|
|
1620
|
+
* @param path - path under `baseUrl`.
|
|
1621
|
+
* @param body - optional JSON body (POST /chat only).
|
|
1622
|
+
* @param opts - abort signal.
|
|
1623
|
+
* @yields each ai-hub event from the SSE stream until `stream_end`.
|
|
1624
|
+
*/
|
|
1625
|
+
private async *streamRequest(
|
|
1626
|
+
method: string,
|
|
1627
|
+
path: string,
|
|
1628
|
+
body: unknown,
|
|
1629
|
+
opts: StreamChatOptions,
|
|
1630
|
+
): AsyncIterable<AIHubEvent> {
|
|
1631
|
+
const headers: Record<string, string> = { Accept: "text/event-stream" };
|
|
1632
|
+
const token = this.tokenStore.get();
|
|
1633
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1634
|
+
let bodyInit: BodyInit | undefined;
|
|
1635
|
+
if (body !== undefined) {
|
|
1636
|
+
headers["Content-Type"] = "application/json";
|
|
1637
|
+
bodyInit = JSON.stringify(body);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
let response: Response;
|
|
1641
|
+
try {
|
|
1642
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
1643
|
+
method,
|
|
1644
|
+
headers,
|
|
1645
|
+
...(bodyInit !== undefined ? { body: bodyInit } : {}),
|
|
1646
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
1647
|
+
});
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
throw new AIHubRequestError(
|
|
1650
|
+
"network",
|
|
1651
|
+
err instanceof Error ? err.message : "SSE request failed.",
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (response.status === 401) this.handleUnauthorized();
|
|
1656
|
+
if (!response.ok) throw await this.errorFromResponse(response);
|
|
1657
|
+
if (!response.body) {
|
|
1658
|
+
throw new AIHubRequestError("internal", "SSE response had no body.");
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
for await (const event of parseSseStream(response.body)) {
|
|
1662
|
+
yield event;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Clears the token and fires the unauthorized callback (at most once per
|
|
1668
|
+
* 401 storm — the flag resets on next `setToken`).
|
|
1669
|
+
*/
|
|
1670
|
+
private handleUnauthorized(): void {
|
|
1671
|
+
this.clearToken();
|
|
1672
|
+
if (this.unauthorizedFired) return;
|
|
1673
|
+
this.unauthorizedFired = true;
|
|
1674
|
+
this.onUnauthorized?.();
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Builds an `AIHubRequestError` from a non-ok server response. Reads the
|
|
1679
|
+
* server's `{ error: { code, message, request_id } }` envelope when
|
|
1680
|
+
* present; falls back to status-code-derived defaults otherwise.
|
|
1681
|
+
*
|
|
1682
|
+
* @param response - the fetch response (status >= 400).
|
|
1683
|
+
* @returns the normalized error.
|
|
1684
|
+
*/
|
|
1685
|
+
private async errorFromResponse(response: Response): Promise<AIHubRequestError> {
|
|
1686
|
+
interface ErrorEnvelope {
|
|
1687
|
+
error?: { code?: string; message?: string; request_id?: string };
|
|
1688
|
+
}
|
|
1689
|
+
let envelope: ErrorEnvelope = {};
|
|
1690
|
+
try {
|
|
1691
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
1692
|
+
if (ct.includes("application/json")) {
|
|
1693
|
+
envelope = (await response.json()) as ErrorEnvelope;
|
|
1694
|
+
}
|
|
1695
|
+
} catch {
|
|
1696
|
+
/* malformed body — fall through to defaults */
|
|
1697
|
+
}
|
|
1698
|
+
const errorBlock = envelope.error;
|
|
1699
|
+
const wireCode = errorBlock?.code;
|
|
1700
|
+
const message =
|
|
1701
|
+
errorBlock?.message ?? `Request failed with status ${response.status}.`;
|
|
1702
|
+
const code = wireCode ?? defaultCodeForStatus(response.status);
|
|
1703
|
+
return new AIHubRequestError(code, message, response.status, errorBlock?.request_id);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Maps an HTTP status to a default `AIHubError.code` when the server didn't
|
|
1709
|
+
* include one in the response envelope.
|
|
1710
|
+
*/
|
|
1711
|
+
function defaultCodeForStatus(status: number): AIHubError["code"] {
|
|
1712
|
+
if (status === 401) return "unauthorized";
|
|
1713
|
+
if (status === 403) return "forbidden";
|
|
1714
|
+
if (status === 404) return "not_found";
|
|
1715
|
+
if (status >= 400 && status < 500) return "validation";
|
|
1716
|
+
if (status >= 500) return "internal";
|
|
1717
|
+
return "internal";
|
|
1718
|
+
}
|