crca 1.4.0__py3-none-any.whl
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.
- .github/ISSUE_TEMPLATE/bug_report.md +65 -0
- .github/ISSUE_TEMPLATE/feature_request.md +41 -0
- .github/PULL_REQUEST_TEMPLATE.md +20 -0
- .github/workflows/publish-manual.yml +61 -0
- .github/workflows/publish.yml +64 -0
- .gitignore +214 -0
- CRCA.py +4156 -0
- LICENSE +201 -0
- MANIFEST.in +43 -0
- PKG-INFO +5035 -0
- README.md +4959 -0
- __init__.py +17 -0
- branches/CRCA-Q.py +2728 -0
- branches/crca_cg/corposwarm.py +9065 -0
- branches/crca_cg/fix_rancher_docker_creds.ps1 +155 -0
- branches/crca_cg/package.json +5 -0
- branches/crca_cg/test_bolt_integration.py +446 -0
- branches/crca_cg/test_corposwarm_comprehensive.py +773 -0
- branches/crca_cg/test_new_features.py +163 -0
- branches/crca_sd/__init__.py +149 -0
- branches/crca_sd/crca_sd_core.py +770 -0
- branches/crca_sd/crca_sd_governance.py +1325 -0
- branches/crca_sd/crca_sd_mpc.py +1130 -0
- branches/crca_sd/crca_sd_realtime.py +1844 -0
- branches/crca_sd/crca_sd_tui.py +1133 -0
- crca-1.4.0.dist-info/METADATA +5035 -0
- crca-1.4.0.dist-info/RECORD +501 -0
- crca-1.4.0.dist-info/WHEEL +4 -0
- crca-1.4.0.dist-info/licenses/LICENSE +201 -0
- docs/CRCA-Q.md +2333 -0
- examples/config.yaml.example +25 -0
- examples/crca_sd_example.py +513 -0
- examples/data_broker_example.py +294 -0
- examples/logistics_corporation.py +861 -0
- examples/palantir_example.py +299 -0
- examples/policy_bench.py +934 -0
- examples/pridnestrovia-sd.py +705 -0
- examples/pridnestrovia_realtime.py +1902 -0
- prompts/__init__.py +10 -0
- prompts/default_crca.py +101 -0
- pyproject.toml +151 -0
- requirements.txt +76 -0
- schemas/__init__.py +43 -0
- schemas/mcpSchemas.py +51 -0
- schemas/policy.py +458 -0
- templates/__init__.py +38 -0
- templates/base_specialized_agent.py +195 -0
- templates/drift_detection.py +325 -0
- templates/examples/causal_agent_template.py +309 -0
- templates/examples/drag_drop_example.py +213 -0
- templates/examples/logistics_agent_template.py +207 -0
- templates/examples/trading_agent_template.py +206 -0
- templates/feature_mixins.py +253 -0
- templates/graph_management.py +442 -0
- templates/llm_integration.py +194 -0
- templates/module_registry.py +276 -0
- templates/mpc_planner.py +280 -0
- templates/policy_loop.py +1168 -0
- templates/prediction_framework.py +448 -0
- templates/statistical_methods.py +778 -0
- tests/sanity.yml +31 -0
- tests/sanity_check +406 -0
- tests/test_core.py +47 -0
- tests/test_crca_excel.py +166 -0
- tests/test_crca_sd.py +780 -0
- tests/test_data_broker.py +424 -0
- tests/test_palantir.py +349 -0
- tools/__init__.py +38 -0
- tools/actuators.py +437 -0
- tools/bolt.diy/Dockerfile +103 -0
- tools/bolt.diy/app/components/@settings/core/AvatarDropdown.tsx +175 -0
- tools/bolt.diy/app/components/@settings/core/ControlPanel.tsx +345 -0
- tools/bolt.diy/app/components/@settings/core/constants.tsx +108 -0
- tools/bolt.diy/app/components/@settings/core/types.ts +114 -0
- tools/bolt.diy/app/components/@settings/index.ts +12 -0
- tools/bolt.diy/app/components/@settings/shared/components/TabTile.tsx +151 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionForm.tsx +193 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionTestIndicator.tsx +60 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ErrorState.tsx +102 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/LoadingState.tsx +94 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ServiceHeader.tsx +72 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/index.ts +6 -0
- tools/bolt.diy/app/components/@settings/tabs/data/DataTab.tsx +721 -0
- tools/bolt.diy/app/components/@settings/tabs/data/DataVisualization.tsx +384 -0
- tools/bolt.diy/app/components/@settings/tabs/event-logs/EventLogsTab.tsx +1013 -0
- tools/bolt.diy/app/components/@settings/tabs/features/FeaturesTab.tsx +295 -0
- tools/bolt.diy/app/components/@settings/tabs/github/GitHubTab.tsx +281 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubAuthDialog.tsx +173 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubCacheManager.tsx +367 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubConnection.tsx +233 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubErrorBoundary.tsx +105 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubProgressiveLoader.tsx +266 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositoryCard.tsx +121 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositorySelector.tsx +312 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubStats.tsx +291 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubUserProfile.tsx +46 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/shared/GitHubStateIndicators.tsx +264 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/shared/RepositoryCard.tsx +361 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/shared/index.ts +11 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/GitLabTab.tsx +305 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabAuthDialog.tsx +186 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabConnection.tsx +253 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabRepositorySelector.tsx +358 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryCard.tsx +79 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryList.tsx +142 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/StatsDisplay.tsx +91 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/index.ts +4 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerList.tsx +99 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerListItem.tsx +70 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpStatusBadge.tsx +37 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpTab.tsx +239 -0
- tools/bolt.diy/app/components/@settings/tabs/netlify/NetlifyTab.tsx +1393 -0
- tools/bolt.diy/app/components/@settings/tabs/netlify/components/NetlifyConnection.tsx +990 -0
- tools/bolt.diy/app/components/@settings/tabs/netlify/components/index.ts +1 -0
- tools/bolt.diy/app/components/@settings/tabs/notifications/NotificationsTab.tsx +300 -0
- tools/bolt.diy/app/components/@settings/tabs/profile/ProfileTab.tsx +181 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx +308 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/ErrorBoundary.tsx +68 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/HealthStatusBadge.tsx +64 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/LoadingSkeleton.tsx +107 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx +556 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/ModelCard.tsx +106 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/ProviderCard.tsx +120 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/SetupGuide.tsx +671 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/StatusDashboard.tsx +91 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/types.ts +44 -0
- tools/bolt.diy/app/components/@settings/tabs/settings/SettingsTab.tsx +215 -0
- tools/bolt.diy/app/components/@settings/tabs/supabase/SupabaseTab.tsx +1089 -0
- tools/bolt.diy/app/components/@settings/tabs/vercel/VercelTab.tsx +909 -0
- tools/bolt.diy/app/components/@settings/tabs/vercel/components/VercelConnection.tsx +368 -0
- tools/bolt.diy/app/components/@settings/tabs/vercel/components/index.ts +1 -0
- tools/bolt.diy/app/components/@settings/utils/tab-helpers.ts +54 -0
- tools/bolt.diy/app/components/chat/APIKeyManager.tsx +169 -0
- tools/bolt.diy/app/components/chat/Artifact.tsx +296 -0
- tools/bolt.diy/app/components/chat/AssistantMessage.tsx +192 -0
- tools/bolt.diy/app/components/chat/BaseChat.module.scss +47 -0
- tools/bolt.diy/app/components/chat/BaseChat.tsx +522 -0
- tools/bolt.diy/app/components/chat/Chat.client.tsx +670 -0
- tools/bolt.diy/app/components/chat/ChatAlert.tsx +108 -0
- tools/bolt.diy/app/components/chat/ChatBox.tsx +334 -0
- tools/bolt.diy/app/components/chat/CodeBlock.module.scss +10 -0
- tools/bolt.diy/app/components/chat/CodeBlock.tsx +85 -0
- tools/bolt.diy/app/components/chat/DicussMode.tsx +17 -0
- tools/bolt.diy/app/components/chat/ExamplePrompts.tsx +37 -0
- tools/bolt.diy/app/components/chat/FilePreview.tsx +38 -0
- tools/bolt.diy/app/components/chat/GitCloneButton.tsx +327 -0
- tools/bolt.diy/app/components/chat/ImportFolderButton.tsx +141 -0
- tools/bolt.diy/app/components/chat/LLMApiAlert.tsx +109 -0
- tools/bolt.diy/app/components/chat/MCPTools.tsx +129 -0
- tools/bolt.diy/app/components/chat/Markdown.module.scss +171 -0
- tools/bolt.diy/app/components/chat/Markdown.spec.ts +48 -0
- tools/bolt.diy/app/components/chat/Markdown.tsx +252 -0
- tools/bolt.diy/app/components/chat/Messages.client.tsx +102 -0
- tools/bolt.diy/app/components/chat/ModelSelector.tsx +797 -0
- tools/bolt.diy/app/components/chat/NetlifyDeploymentLink.client.tsx +51 -0
- tools/bolt.diy/app/components/chat/ProgressCompilation.tsx +110 -0
- tools/bolt.diy/app/components/chat/ScreenshotStateManager.tsx +33 -0
- tools/bolt.diy/app/components/chat/SendButton.client.tsx +39 -0
- tools/bolt.diy/app/components/chat/SpeechRecognition.tsx +28 -0
- tools/bolt.diy/app/components/chat/StarterTemplates.tsx +38 -0
- tools/bolt.diy/app/components/chat/SupabaseAlert.tsx +199 -0
- tools/bolt.diy/app/components/chat/SupabaseConnection.tsx +339 -0
- tools/bolt.diy/app/components/chat/ThoughtBox.tsx +43 -0
- tools/bolt.diy/app/components/chat/ToolInvocations.tsx +409 -0
- tools/bolt.diy/app/components/chat/UserMessage.tsx +101 -0
- tools/bolt.diy/app/components/chat/VercelDeploymentLink.client.tsx +158 -0
- tools/bolt.diy/app/components/chat/chatExportAndImport/ExportChatButton.tsx +49 -0
- tools/bolt.diy/app/components/chat/chatExportAndImport/ImportButtons.tsx +96 -0
- tools/bolt.diy/app/components/deploy/DeployAlert.tsx +197 -0
- tools/bolt.diy/app/components/deploy/DeployButton.tsx +277 -0
- tools/bolt.diy/app/components/deploy/GitHubDeploy.client.tsx +171 -0
- tools/bolt.diy/app/components/deploy/GitHubDeploymentDialog.tsx +1041 -0
- tools/bolt.diy/app/components/deploy/GitLabDeploy.client.tsx +171 -0
- tools/bolt.diy/app/components/deploy/GitLabDeploymentDialog.tsx +764 -0
- tools/bolt.diy/app/components/deploy/NetlifyDeploy.client.tsx +246 -0
- tools/bolt.diy/app/components/deploy/VercelDeploy.client.tsx +235 -0
- tools/bolt.diy/app/components/editor/codemirror/BinaryContent.tsx +7 -0
- tools/bolt.diy/app/components/editor/codemirror/CodeMirrorEditor.tsx +555 -0
- tools/bolt.diy/app/components/editor/codemirror/EnvMasking.ts +80 -0
- tools/bolt.diy/app/components/editor/codemirror/cm-theme.ts +192 -0
- tools/bolt.diy/app/components/editor/codemirror/indent.ts +68 -0
- tools/bolt.diy/app/components/editor/codemirror/languages.ts +112 -0
- tools/bolt.diy/app/components/git/GitUrlImport.client.tsx +147 -0
- tools/bolt.diy/app/components/header/Header.tsx +42 -0
- tools/bolt.diy/app/components/header/HeaderActionButtons.client.tsx +54 -0
- tools/bolt.diy/app/components/mandate/MandateSubmission.tsx +167 -0
- tools/bolt.diy/app/components/observability/DeploymentStatus.tsx +168 -0
- tools/bolt.diy/app/components/observability/EventTimeline.tsx +119 -0
- tools/bolt.diy/app/components/observability/FileDiffViewer.tsx +121 -0
- tools/bolt.diy/app/components/observability/GovernanceStatus.tsx +197 -0
- tools/bolt.diy/app/components/observability/GovernorMetrics.tsx +246 -0
- tools/bolt.diy/app/components/observability/LogStream.tsx +244 -0
- tools/bolt.diy/app/components/observability/MandateDetails.tsx +201 -0
- tools/bolt.diy/app/components/observability/ObservabilityDashboard.tsx +200 -0
- tools/bolt.diy/app/components/sidebar/HistoryItem.tsx +187 -0
- tools/bolt.diy/app/components/sidebar/Menu.client.tsx +536 -0
- tools/bolt.diy/app/components/sidebar/date-binning.ts +59 -0
- tools/bolt.diy/app/components/txt +1 -0
- tools/bolt.diy/app/components/ui/BackgroundRays/index.tsx +18 -0
- tools/bolt.diy/app/components/ui/BackgroundRays/styles.module.scss +246 -0
- tools/bolt.diy/app/components/ui/Badge.tsx +53 -0
- tools/bolt.diy/app/components/ui/BranchSelector.tsx +270 -0
- tools/bolt.diy/app/components/ui/Breadcrumbs.tsx +101 -0
- tools/bolt.diy/app/components/ui/Button.tsx +46 -0
- tools/bolt.diy/app/components/ui/Card.tsx +55 -0
- tools/bolt.diy/app/components/ui/Checkbox.tsx +32 -0
- tools/bolt.diy/app/components/ui/CloseButton.tsx +49 -0
- tools/bolt.diy/app/components/ui/CodeBlock.tsx +103 -0
- tools/bolt.diy/app/components/ui/Collapsible.tsx +9 -0
- tools/bolt.diy/app/components/ui/ColorSchemeDialog.tsx +378 -0
- tools/bolt.diy/app/components/ui/Dialog.tsx +449 -0
- tools/bolt.diy/app/components/ui/Dropdown.tsx +63 -0
- tools/bolt.diy/app/components/ui/EmptyState.tsx +154 -0
- tools/bolt.diy/app/components/ui/FileIcon.tsx +346 -0
- tools/bolt.diy/app/components/ui/FilterChip.tsx +92 -0
- tools/bolt.diy/app/components/ui/GlowingEffect.tsx +192 -0
- tools/bolt.diy/app/components/ui/GradientCard.tsx +100 -0
- tools/bolt.diy/app/components/ui/IconButton.tsx +84 -0
- tools/bolt.diy/app/components/ui/Input.tsx +22 -0
- tools/bolt.diy/app/components/ui/Label.tsx +20 -0
- tools/bolt.diy/app/components/ui/LoadingDots.tsx +27 -0
- tools/bolt.diy/app/components/ui/LoadingOverlay.tsx +32 -0
- tools/bolt.diy/app/components/ui/PanelHeader.tsx +20 -0
- tools/bolt.diy/app/components/ui/PanelHeaderButton.tsx +36 -0
- tools/bolt.diy/app/components/ui/Popover.tsx +29 -0
- tools/bolt.diy/app/components/ui/Progress.tsx +22 -0
- tools/bolt.diy/app/components/ui/RepositoryStats.tsx +87 -0
- tools/bolt.diy/app/components/ui/ScrollArea.tsx +41 -0
- tools/bolt.diy/app/components/ui/SearchInput.tsx +80 -0
- tools/bolt.diy/app/components/ui/SearchResultItem.tsx +134 -0
- tools/bolt.diy/app/components/ui/Separator.tsx +22 -0
- tools/bolt.diy/app/components/ui/SettingsButton.tsx +35 -0
- tools/bolt.diy/app/components/ui/Slider.tsx +73 -0
- tools/bolt.diy/app/components/ui/StatusIndicator.tsx +90 -0
- tools/bolt.diy/app/components/ui/Switch.tsx +37 -0
- tools/bolt.diy/app/components/ui/Tabs.tsx +52 -0
- tools/bolt.diy/app/components/ui/TabsWithSlider.tsx +112 -0
- tools/bolt.diy/app/components/ui/ThemeSwitch.tsx +29 -0
- tools/bolt.diy/app/components/ui/Tooltip.tsx +122 -0
- tools/bolt.diy/app/components/ui/index.ts +38 -0
- tools/bolt.diy/app/components/ui/use-toast.ts +66 -0
- tools/bolt.diy/app/components/workbench/DiffView.tsx +796 -0
- tools/bolt.diy/app/components/workbench/EditorPanel.tsx +174 -0
- tools/bolt.diy/app/components/workbench/ExpoQrModal.tsx +55 -0
- tools/bolt.diy/app/components/workbench/FileBreadcrumb.tsx +150 -0
- tools/bolt.diy/app/components/workbench/FileTree.tsx +565 -0
- tools/bolt.diy/app/components/workbench/Inspector.tsx +126 -0
- tools/bolt.diy/app/components/workbench/InspectorPanel.tsx +146 -0
- tools/bolt.diy/app/components/workbench/LockManager.tsx +262 -0
- tools/bolt.diy/app/components/workbench/PortDropdown.tsx +91 -0
- tools/bolt.diy/app/components/workbench/Preview.tsx +1049 -0
- tools/bolt.diy/app/components/workbench/ScreenshotSelector.tsx +293 -0
- tools/bolt.diy/app/components/workbench/Search.tsx +257 -0
- tools/bolt.diy/app/components/workbench/Workbench.client.tsx +506 -0
- tools/bolt.diy/app/components/workbench/terminal/Terminal.tsx +131 -0
- tools/bolt.diy/app/components/workbench/terminal/TerminalManager.tsx +68 -0
- tools/bolt.diy/app/components/workbench/terminal/TerminalTabs.tsx +277 -0
- tools/bolt.diy/app/components/workbench/terminal/theme.ts +36 -0
- tools/bolt.diy/app/components/workflow/WorkflowPhase.tsx +109 -0
- tools/bolt.diy/app/components/workflow/WorkflowStatus.tsx +60 -0
- tools/bolt.diy/app/components/workflow/WorkflowTimeline.tsx +150 -0
- tools/bolt.diy/app/entry.client.tsx +7 -0
- tools/bolt.diy/app/entry.server.tsx +80 -0
- tools/bolt.diy/app/root.tsx +156 -0
- tools/bolt.diy/app/routes/_index.tsx +175 -0
- tools/bolt.diy/app/routes/api.bug-report.ts +254 -0
- tools/bolt.diy/app/routes/api.chat.ts +463 -0
- tools/bolt.diy/app/routes/api.check-env-key.ts +41 -0
- tools/bolt.diy/app/routes/api.configured-providers.ts +110 -0
- tools/bolt.diy/app/routes/api.corporate-swarm-status.ts +55 -0
- tools/bolt.diy/app/routes/api.enhancer.ts +137 -0
- tools/bolt.diy/app/routes/api.export-api-keys.ts +44 -0
- tools/bolt.diy/app/routes/api.git-info.ts +69 -0
- tools/bolt.diy/app/routes/api.git-proxy.$.ts +178 -0
- tools/bolt.diy/app/routes/api.github-branches.ts +166 -0
- tools/bolt.diy/app/routes/api.github-deploy.ts +67 -0
- tools/bolt.diy/app/routes/api.github-stats.ts +198 -0
- tools/bolt.diy/app/routes/api.github-template.ts +242 -0
- tools/bolt.diy/app/routes/api.github-user.ts +287 -0
- tools/bolt.diy/app/routes/api.gitlab-branches.ts +143 -0
- tools/bolt.diy/app/routes/api.gitlab-deploy.ts +67 -0
- tools/bolt.diy/app/routes/api.gitlab-projects.ts +105 -0
- tools/bolt.diy/app/routes/api.health.ts +8 -0
- tools/bolt.diy/app/routes/api.llmcall.ts +298 -0
- tools/bolt.diy/app/routes/api.mandate.ts +351 -0
- tools/bolt.diy/app/routes/api.mcp-check.ts +16 -0
- tools/bolt.diy/app/routes/api.mcp-update-config.ts +23 -0
- tools/bolt.diy/app/routes/api.models.$provider.ts +2 -0
- tools/bolt.diy/app/routes/api.models.ts +90 -0
- tools/bolt.diy/app/routes/api.netlify-deploy.ts +240 -0
- tools/bolt.diy/app/routes/api.netlify-user.ts +142 -0
- tools/bolt.diy/app/routes/api.supabase-user.ts +199 -0
- tools/bolt.diy/app/routes/api.supabase.query.ts +92 -0
- tools/bolt.diy/app/routes/api.supabase.ts +56 -0
- tools/bolt.diy/app/routes/api.supabase.variables.ts +32 -0
- tools/bolt.diy/app/routes/api.system.diagnostics.ts +142 -0
- tools/bolt.diy/app/routes/api.system.disk-info.ts +311 -0
- tools/bolt.diy/app/routes/api.system.git-info.ts +332 -0
- tools/bolt.diy/app/routes/api.update.ts +21 -0
- tools/bolt.diy/app/routes/api.vercel-deploy.ts +497 -0
- tools/bolt.diy/app/routes/api.vercel-user.ts +161 -0
- tools/bolt.diy/app/routes/api.workflow-status.$proposalId.ts +309 -0
- tools/bolt.diy/app/routes/chat.$id.tsx +8 -0
- tools/bolt.diy/app/routes/execute.$mandateId.tsx +432 -0
- tools/bolt.diy/app/routes/git.tsx +25 -0
- tools/bolt.diy/app/routes/observability.$mandateId.tsx +50 -0
- tools/bolt.diy/app/routes/webcontainer.connect.$id.tsx +32 -0
- tools/bolt.diy/app/routes/webcontainer.preview.$id.tsx +97 -0
- tools/bolt.diy/app/routes/workflow.$proposalId.tsx +170 -0
- tools/bolt.diy/app/styles/animations.scss +49 -0
- tools/bolt.diy/app/styles/components/code.scss +9 -0
- tools/bolt.diy/app/styles/components/editor.scss +135 -0
- tools/bolt.diy/app/styles/components/resize-handle.scss +30 -0
- tools/bolt.diy/app/styles/components/terminal.scss +3 -0
- tools/bolt.diy/app/styles/components/toast.scss +23 -0
- tools/bolt.diy/app/styles/diff-view.css +72 -0
- tools/bolt.diy/app/styles/index.scss +73 -0
- tools/bolt.diy/app/styles/variables.scss +255 -0
- tools/bolt.diy/app/styles/z-index.scss +37 -0
- tools/bolt.diy/app/types/GitHub.ts +182 -0
- tools/bolt.diy/app/types/GitLab.ts +103 -0
- tools/bolt.diy/app/types/actions.ts +85 -0
- tools/bolt.diy/app/types/artifact.ts +5 -0
- tools/bolt.diy/app/types/context.ts +26 -0
- tools/bolt.diy/app/types/design-scheme.ts +93 -0
- tools/bolt.diy/app/types/global.d.ts +13 -0
- tools/bolt.diy/app/types/mandate.ts +333 -0
- tools/bolt.diy/app/types/model.ts +25 -0
- tools/bolt.diy/app/types/netlify.ts +94 -0
- tools/bolt.diy/app/types/supabase.ts +54 -0
- tools/bolt.diy/app/types/template.ts +8 -0
- tools/bolt.diy/app/types/terminal.ts +9 -0
- tools/bolt.diy/app/types/theme.ts +1 -0
- tools/bolt.diy/app/types/vercel.ts +67 -0
- tools/bolt.diy/app/utils/buffer.ts +29 -0
- tools/bolt.diy/app/utils/classNames.ts +65 -0
- tools/bolt.diy/app/utils/constants.ts +147 -0
- tools/bolt.diy/app/utils/debounce.ts +13 -0
- tools/bolt.diy/app/utils/debugLogger.ts +1284 -0
- tools/bolt.diy/app/utils/diff.spec.ts +11 -0
- tools/bolt.diy/app/utils/diff.ts +117 -0
- tools/bolt.diy/app/utils/easings.ts +3 -0
- tools/bolt.diy/app/utils/fileLocks.ts +96 -0
- tools/bolt.diy/app/utils/fileUtils.ts +121 -0
- tools/bolt.diy/app/utils/folderImport.ts +73 -0
- tools/bolt.diy/app/utils/formatSize.ts +12 -0
- tools/bolt.diy/app/utils/getLanguageFromExtension.ts +24 -0
- tools/bolt.diy/app/utils/githubStats.ts +9 -0
- tools/bolt.diy/app/utils/gitlabStats.ts +54 -0
- tools/bolt.diy/app/utils/logger.ts +162 -0
- tools/bolt.diy/app/utils/markdown.ts +155 -0
- tools/bolt.diy/app/utils/mobile.ts +4 -0
- tools/bolt.diy/app/utils/os.ts +4 -0
- tools/bolt.diy/app/utils/path.ts +19 -0
- tools/bolt.diy/app/utils/projectCommands.ts +197 -0
- tools/bolt.diy/app/utils/promises.ts +19 -0
- tools/bolt.diy/app/utils/react.ts +6 -0
- tools/bolt.diy/app/utils/sampler.ts +49 -0
- tools/bolt.diy/app/utils/selectStarterTemplate.ts +255 -0
- tools/bolt.diy/app/utils/shell.ts +384 -0
- tools/bolt.diy/app/utils/stacktrace.ts +27 -0
- tools/bolt.diy/app/utils/stripIndent.ts +23 -0
- tools/bolt.diy/app/utils/terminal.ts +11 -0
- tools/bolt.diy/app/utils/unreachable.ts +3 -0
- tools/bolt.diy/app/vite-env.d.ts +2 -0
- tools/bolt.diy/assets/entitlements.mac.plist +25 -0
- tools/bolt.diy/assets/icons/icon.icns +0 -0
- tools/bolt.diy/assets/icons/icon.ico +0 -0
- tools/bolt.diy/assets/icons/icon.png +0 -0
- tools/bolt.diy/bindings.js +78 -0
- tools/bolt.diy/bindings.sh +33 -0
- tools/bolt.diy/docker-compose.yaml +145 -0
- tools/bolt.diy/electron/main/index.ts +201 -0
- tools/bolt.diy/electron/main/tsconfig.json +30 -0
- tools/bolt.diy/electron/main/ui/menu.ts +29 -0
- tools/bolt.diy/electron/main/ui/window.ts +54 -0
- tools/bolt.diy/electron/main/utils/auto-update.ts +110 -0
- tools/bolt.diy/electron/main/utils/constants.ts +4 -0
- tools/bolt.diy/electron/main/utils/cookie.ts +40 -0
- tools/bolt.diy/electron/main/utils/reload.ts +35 -0
- tools/bolt.diy/electron/main/utils/serve.ts +71 -0
- tools/bolt.diy/electron/main/utils/store.ts +3 -0
- tools/bolt.diy/electron/main/utils/vite-server.ts +44 -0
- tools/bolt.diy/electron/main/vite.config.ts +44 -0
- tools/bolt.diy/electron/preload/index.ts +22 -0
- tools/bolt.diy/electron/preload/tsconfig.json +7 -0
- tools/bolt.diy/electron/preload/vite.config.ts +31 -0
- tools/bolt.diy/electron-builder.yml +64 -0
- tools/bolt.diy/electron-update.yml +4 -0
- tools/bolt.diy/eslint.config.mjs +57 -0
- tools/bolt.diy/functions/[[path]].ts +12 -0
- tools/bolt.diy/icons/angular.svg +1 -0
- tools/bolt.diy/icons/astro.svg +8 -0
- tools/bolt.diy/icons/chat.svg +1 -0
- tools/bolt.diy/icons/expo-brand.svg +1 -0
- tools/bolt.diy/icons/expo.svg +4 -0
- tools/bolt.diy/icons/logo-text.svg +1 -0
- tools/bolt.diy/icons/logo.svg +4 -0
- tools/bolt.diy/icons/mcp.svg +1 -0
- tools/bolt.diy/icons/nativescript.svg +1 -0
- tools/bolt.diy/icons/netlify.svg +10 -0
- tools/bolt.diy/icons/nextjs.svg +1 -0
- tools/bolt.diy/icons/nuxt.svg +1 -0
- tools/bolt.diy/icons/qwik.svg +1 -0
- tools/bolt.diy/icons/react.svg +1 -0
- tools/bolt.diy/icons/remix.svg +24 -0
- tools/bolt.diy/icons/remotion.svg +1 -0
- tools/bolt.diy/icons/shadcn.svg +21 -0
- tools/bolt.diy/icons/slidev.svg +60 -0
- tools/bolt.diy/icons/solidjs.svg +1 -0
- tools/bolt.diy/icons/stars.svg +1 -0
- tools/bolt.diy/icons/svelte.svg +1 -0
- tools/bolt.diy/icons/typescript.svg +1 -0
- tools/bolt.diy/icons/vite.svg +1 -0
- tools/bolt.diy/icons/vue.svg +1 -0
- tools/bolt.diy/load-context.ts +9 -0
- tools/bolt.diy/notarize.cjs +31 -0
- tools/bolt.diy/package.json +218 -0
- tools/bolt.diy/playwright.config.preview.ts +35 -0
- tools/bolt.diy/pre-start.cjs +26 -0
- tools/bolt.diy/public/apple-touch-icon-precomposed.png +0 -0
- tools/bolt.diy/public/apple-touch-icon.png +0 -0
- tools/bolt.diy/public/favicon.ico +0 -0
- tools/bolt.diy/public/favicon.svg +4 -0
- tools/bolt.diy/public/icons/AmazonBedrock.svg +1 -0
- tools/bolt.diy/public/icons/Anthropic.svg +4 -0
- tools/bolt.diy/public/icons/Cohere.svg +4 -0
- tools/bolt.diy/public/icons/Deepseek.svg +5 -0
- tools/bolt.diy/public/icons/Default.svg +4 -0
- tools/bolt.diy/public/icons/Google.svg +4 -0
- tools/bolt.diy/public/icons/Groq.svg +4 -0
- tools/bolt.diy/public/icons/HuggingFace.svg +4 -0
- tools/bolt.diy/public/icons/Hyperbolic.svg +3 -0
- tools/bolt.diy/public/icons/LMStudio.svg +5 -0
- tools/bolt.diy/public/icons/Mistral.svg +4 -0
- tools/bolt.diy/public/icons/Ollama.svg +4 -0
- tools/bolt.diy/public/icons/OpenAI.svg +4 -0
- tools/bolt.diy/public/icons/OpenAILike.svg +4 -0
- tools/bolt.diy/public/icons/OpenRouter.svg +4 -0
- tools/bolt.diy/public/icons/Perplexity.svg +4 -0
- tools/bolt.diy/public/icons/Together.svg +4 -0
- tools/bolt.diy/public/icons/xAI.svg +5 -0
- tools/bolt.diy/public/inspector-script.js +292 -0
- tools/bolt.diy/public/logo-dark-styled.png +0 -0
- tools/bolt.diy/public/logo-dark.png +0 -0
- tools/bolt.diy/public/logo-light-styled.png +0 -0
- tools/bolt.diy/public/logo-light.png +0 -0
- tools/bolt.diy/public/logo.svg +15 -0
- tools/bolt.diy/public/social_preview_index.jpg +0 -0
- tools/bolt.diy/scripts/clean.js +45 -0
- tools/bolt.diy/scripts/electron-dev.mjs +181 -0
- tools/bolt.diy/scripts/setup-env.sh +41 -0
- tools/bolt.diy/scripts/update-imports.sh +7 -0
- tools/bolt.diy/scripts/update.sh +52 -0
- tools/bolt.diy/services/execution-governor/Dockerfile +41 -0
- tools/bolt.diy/services/execution-governor/config.ts +42 -0
- tools/bolt.diy/services/execution-governor/index.ts +683 -0
- tools/bolt.diy/services/execution-governor/metrics.ts +141 -0
- tools/bolt.diy/services/execution-governor/package.json +31 -0
- tools/bolt.diy/services/execution-governor/priority-queue.ts +139 -0
- tools/bolt.diy/services/execution-governor/tsconfig.json +21 -0
- tools/bolt.diy/services/execution-governor/types.ts +145 -0
- tools/bolt.diy/services/headless-executor/Dockerfile +43 -0
- tools/bolt.diy/services/headless-executor/executor.ts +210 -0
- tools/bolt.diy/services/headless-executor/index.ts +323 -0
- tools/bolt.diy/services/headless-executor/package.json +27 -0
- tools/bolt.diy/services/headless-executor/tsconfig.json +21 -0
- tools/bolt.diy/services/headless-executor/types.ts +38 -0
- tools/bolt.diy/test-workflows.sh +240 -0
- tools/bolt.diy/tests/integration/corporate-swarm.test.ts +208 -0
- tools/bolt.diy/tests/mandates/budget-limited.json +34 -0
- tools/bolt.diy/tests/mandates/complex.json +53 -0
- tools/bolt.diy/tests/mandates/constraint-enforced.json +36 -0
- tools/bolt.diy/tests/mandates/simple.json +35 -0
- tools/bolt.diy/tsconfig.json +37 -0
- tools/bolt.diy/types/istextorbinary.d.ts +15 -0
- tools/bolt.diy/uno.config.ts +279 -0
- tools/bolt.diy/vite-electron.config.ts +76 -0
- tools/bolt.diy/vite.config.ts +112 -0
- tools/bolt.diy/worker-configuration.d.ts +22 -0
- tools/bolt.diy/wrangler.toml +6 -0
- tools/code_generator.py +461 -0
- tools/file_operations.py +465 -0
- tools/mandate_generator.py +337 -0
- tools/mcpClientUtils.py +1216 -0
- tools/sensors.py +285 -0
- utils/Agent_types.py +15 -0
- utils/AnyToStr.py +0 -0
- utils/HHCS.py +277 -0
- utils/__init__.py +30 -0
- utils/agent.py +3627 -0
- utils/aop.py +2948 -0
- utils/canonical.py +143 -0
- utils/conversation.py +1195 -0
- utils/doctrine_versioning +230 -0
- utils/formatter.py +474 -0
- utils/ledger.py +311 -0
- utils/out_types.py +16 -0
- utils/rollback.py +339 -0
- utils/router.py +929 -0
- utils/tui.py +1908 -0
templates/policy_loop.py
ADDED
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
"""Policy loop mixin for temporal policy engine.
|
|
2
|
+
|
|
3
|
+
This module provides PolicyLoopMixin for executing temporal policy loops with:
|
|
4
|
+
- Online learning (RLS + Bayesian regression)
|
|
5
|
+
- Multi-objective constrained optimization
|
|
6
|
+
- Drift detection (CUSUM)
|
|
7
|
+
- Deterministic decision-making
|
|
8
|
+
|
|
9
|
+
This is a feature mixin that can be composed into agent classes (R1 requirement).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
import numpy as np
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from schemas.policy import (
|
|
18
|
+
DoctrineV1,
|
|
19
|
+
CompiledPolicy,
|
|
20
|
+
ObservationEvent,
|
|
21
|
+
DecisionEvent,
|
|
22
|
+
OutcomeEvent,
|
|
23
|
+
InterventionSpec,
|
|
24
|
+
Intervention,
|
|
25
|
+
ModelState,
|
|
26
|
+
LedgerEvent
|
|
27
|
+
)
|
|
28
|
+
from utils.canonical import stable_hash
|
|
29
|
+
from utils.ledger import Ledger
|
|
30
|
+
|
|
31
|
+
# Try to import scipy for numerical stability
|
|
32
|
+
try:
|
|
33
|
+
from scipy import linalg as scipy_linalg
|
|
34
|
+
SCIPY_AVAILABLE = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
SCIPY_AVAILABLE = False
|
|
37
|
+
logger.warning("scipy not available - using numpy for matrix operations")
|
|
38
|
+
|
|
39
|
+
# Try to import sensors/actuators
|
|
40
|
+
try:
|
|
41
|
+
from tools.sensors import SensorRegistry
|
|
42
|
+
from tools.actuators import ActuatorRegistry
|
|
43
|
+
SENSORS_AVAILABLE = True
|
|
44
|
+
except ImportError:
|
|
45
|
+
SENSORS_AVAILABLE = False
|
|
46
|
+
SensorRegistry = None
|
|
47
|
+
ActuatorRegistry = None
|
|
48
|
+
logger.warning("tools.sensors/actuators not available - using callable functions only")
|
|
49
|
+
|
|
50
|
+
# Try to import MPC planner
|
|
51
|
+
try:
|
|
52
|
+
from templates.mpc_planner import MPCPlanner
|
|
53
|
+
MPC_AVAILABLE = True
|
|
54
|
+
except ImportError:
|
|
55
|
+
MPC_AVAILABLE = False
|
|
56
|
+
MPCPlanner = None
|
|
57
|
+
logger.warning("templates.mpc_planner not available - MPC planning disabled")
|
|
58
|
+
|
|
59
|
+
# Try to import drift detection
|
|
60
|
+
try:
|
|
61
|
+
from templates.drift_detection import DriftDetector, HybridDriftDetector
|
|
62
|
+
DRIFT_DETECTION_AVAILABLE = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
DRIFT_DETECTION_AVAILABLE = False
|
|
65
|
+
DriftDetector = None
|
|
66
|
+
HybridDriftDetector = None
|
|
67
|
+
logger.warning("templates.drift_detection not available - using CUSUM only")
|
|
68
|
+
|
|
69
|
+
# Try to import rollback manager
|
|
70
|
+
try:
|
|
71
|
+
from utils.rollback import RollbackManager
|
|
72
|
+
ROLLBACK_AVAILABLE = True
|
|
73
|
+
except ImportError:
|
|
74
|
+
ROLLBACK_AVAILABLE = False
|
|
75
|
+
RollbackManager = None
|
|
76
|
+
logger.warning("utils.rollback not available - rollback functionality disabled")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InterventionCatalog:
|
|
80
|
+
"""Registry mapping lever IDs to intervention constructors.
|
|
81
|
+
|
|
82
|
+
Provides deterministic intervention creation with bounds enforcement.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, doctrine: DoctrineV1):
|
|
86
|
+
"""
|
|
87
|
+
Initialize catalog from doctrine.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
doctrine: Doctrine containing lever specifications
|
|
91
|
+
"""
|
|
92
|
+
self.doctrine = doctrine
|
|
93
|
+
self.constructors: Dict[str, Callable] = {}
|
|
94
|
+
self._register_default_constructors()
|
|
95
|
+
|
|
96
|
+
def _register_default_constructors(self) -> None:
|
|
97
|
+
"""Register default intervention constructors."""
|
|
98
|
+
for lever_id, lever_spec in self.doctrine.levers.items():
|
|
99
|
+
self.constructors[lever_id] = self._create_constructor(lever_id, lever_spec)
|
|
100
|
+
|
|
101
|
+
def _create_constructor(self, lever_id: str, lever_spec) -> Callable:
|
|
102
|
+
"""Create a constructor function for a lever."""
|
|
103
|
+
def constructor(**params) -> InterventionSpec:
|
|
104
|
+
# Validate bounds
|
|
105
|
+
bounds = lever_spec.bounds
|
|
106
|
+
for param_name, param_value in params.items():
|
|
107
|
+
if param_name in bounds:
|
|
108
|
+
if "min" in bounds[param_name] and param_value < bounds[param_name]["min"]:
|
|
109
|
+
raise ValueError(f"Parameter {param_name} below minimum: {param_value} < {bounds[param_name]['min']}")
|
|
110
|
+
if "max" in bounds[param_name] and param_value > bounds[param_name]["max"]:
|
|
111
|
+
raise ValueError(f"Parameter {param_name} above maximum: {param_value} > {bounds[param_name]['max']}")
|
|
112
|
+
|
|
113
|
+
return InterventionSpec(
|
|
114
|
+
lever_id=lever_id,
|
|
115
|
+
parameters=params,
|
|
116
|
+
rollback_descriptor={"lever_id": lever_id, "parameters": params} if lever_spec.rollback_required else None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return constructor
|
|
120
|
+
|
|
121
|
+
def create_intervention(self, lever_id: str, **params) -> InterventionSpec:
|
|
122
|
+
"""
|
|
123
|
+
Create an intervention from a lever ID and parameters.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
lever_id: ID of the lever
|
|
127
|
+
**params: Intervention parameters
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
InterventionSpec: Intervention specification
|
|
131
|
+
"""
|
|
132
|
+
if lever_id not in self.constructors:
|
|
133
|
+
raise ValueError(f"Unknown lever ID: {lever_id}")
|
|
134
|
+
return self.constructors[lever_id](**params)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PolicyLoopMixin:
|
|
138
|
+
"""Mixin class for temporal policy loop execution.
|
|
139
|
+
|
|
140
|
+
Provides methods for:
|
|
141
|
+
- run_epoch(): Execute single epoch (observe → plan → act → update)
|
|
142
|
+
- plan_actions(): Multi-objective constrained optimization
|
|
143
|
+
- update_models(): Online learning (RLS + Bayesian regression)
|
|
144
|
+
- explain_decision(): Generate rationale with decision hash
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
doctrine: DoctrineV1,
|
|
150
|
+
ledger: Ledger,
|
|
151
|
+
seed: int = 42,
|
|
152
|
+
sensor_registry: Optional[Any] = None,
|
|
153
|
+
actuator_registry: Optional[Any] = None
|
|
154
|
+
):
|
|
155
|
+
"""
|
|
156
|
+
Initialize policy loop mixin.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
doctrine: Policy doctrine
|
|
160
|
+
ledger: Event ledger
|
|
161
|
+
seed: Random seed for determinism
|
|
162
|
+
sensor_registry: Optional SensorRegistry instance
|
|
163
|
+
actuator_registry: Optional ActuatorRegistry instance
|
|
164
|
+
"""
|
|
165
|
+
self.doctrine = doctrine
|
|
166
|
+
self.compiled_policy = CompiledPolicy.compile(doctrine)
|
|
167
|
+
self.ledger = ledger
|
|
168
|
+
self.seed = seed
|
|
169
|
+
self.rng = np.random.default_rng(seed)
|
|
170
|
+
|
|
171
|
+
# Sensor and actuator registries
|
|
172
|
+
self.sensor_registry = sensor_registry
|
|
173
|
+
self.actuator_registry = actuator_registry
|
|
174
|
+
|
|
175
|
+
# Auto-wire sensors/actuators from doctrine if registries provided
|
|
176
|
+
if self.sensor_registry and SENSORS_AVAILABLE:
|
|
177
|
+
# Auto-discover sensors
|
|
178
|
+
self.sensor_registry.auto_discover()
|
|
179
|
+
|
|
180
|
+
# MPC planner (optional)
|
|
181
|
+
self.use_mpc = False # Can be enabled via set_mpc_mode()
|
|
182
|
+
self.mpc_planner: Optional[Any] = None
|
|
183
|
+
if MPC_AVAILABLE and MPCPlanner:
|
|
184
|
+
try:
|
|
185
|
+
self.mpc_planner = MPCPlanner(doctrine, horizon=5)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(f"Failed to initialize MPC planner: {e}")
|
|
188
|
+
|
|
189
|
+
# Intervention catalog
|
|
190
|
+
self.intervention_catalog = InterventionCatalog(doctrine)
|
|
191
|
+
|
|
192
|
+
# Model state (learned parameters θ)
|
|
193
|
+
self.model_state: Optional[ModelState] = None
|
|
194
|
+
|
|
195
|
+
# Feature history for lag/EMA features
|
|
196
|
+
self.feature_history: List[Dict[str, float]] = []
|
|
197
|
+
self.max_history = 10 # Keep last 10 epochs
|
|
198
|
+
|
|
199
|
+
# EMA parameters
|
|
200
|
+
self.ema_alpha = 0.3 # EMA smoothing factor
|
|
201
|
+
self.ema_features: Dict[str, float] = {}
|
|
202
|
+
|
|
203
|
+
# Drift detection (CUSUM + optional ruptures)
|
|
204
|
+
self.drift_detection_mode = "cusum" # "cusum", "ruptures", "hybrid"
|
|
205
|
+
self.cusum_stat = 0.0
|
|
206
|
+
self.cusum_k = 0.5 # CUSUM threshold parameter
|
|
207
|
+
self.cusum_h = 5.0 # CUSUM alarm threshold
|
|
208
|
+
self.conservative_mode = False
|
|
209
|
+
|
|
210
|
+
# Ruptures-based drift detection (optional)
|
|
211
|
+
self.drift_detector: Optional[Any] = None
|
|
212
|
+
self.metric_history: Dict[str, List[float]] = {} # Per-metric history for ruptures
|
|
213
|
+
if DRIFT_DETECTION_AVAILABLE and HybridDriftDetector:
|
|
214
|
+
try:
|
|
215
|
+
self.drift_detector = HybridDriftDetector(use_ruptures=True)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.warning(f"Failed to initialize drift detector: {e}")
|
|
218
|
+
|
|
219
|
+
# Rollback manager (optional)
|
|
220
|
+
self.rollback_manager: Optional[Any] = None
|
|
221
|
+
self.auto_rollback_on_error = False # Auto-rollback on actuator errors
|
|
222
|
+
self.auto_rollback_on_invariant = False # Auto-rollback on invariant violations
|
|
223
|
+
|
|
224
|
+
# RLS parameters for transition model
|
|
225
|
+
self.rls_lambda = 0.95 # Forgetting factor
|
|
226
|
+
self.rls_A: Optional[np.ndarray] = None # State transition matrix
|
|
227
|
+
self.rls_B: Optional[np.ndarray] = None # Action effect matrix
|
|
228
|
+
self.rls_P: Dict[int, np.ndarray] = {} # Parameter covariance (per output)
|
|
229
|
+
self.rls_theta: Dict[int, np.ndarray] = {} # Parameter vector (per output)
|
|
230
|
+
|
|
231
|
+
# Bayesian regression for causal effects
|
|
232
|
+
self.bayesian_tau = 1.0 # Prior variance
|
|
233
|
+
self.bayesian_sigma = 0.1 # Observation noise
|
|
234
|
+
self.bayesian_beta_mu: Optional[np.ndarray] = None # Posterior mean
|
|
235
|
+
self.bayesian_beta_sigma: Optional[np.ndarray] = None # Posterior covariance
|
|
236
|
+
|
|
237
|
+
# Action history for causal effect estimation
|
|
238
|
+
self.action_history: List[Dict[str, Any]] = []
|
|
239
|
+
self.metric_delta_history: List[Dict[str, float]] = []
|
|
240
|
+
|
|
241
|
+
def extract_features(
|
|
242
|
+
self,
|
|
243
|
+
snapshot: Dict[str, float],
|
|
244
|
+
k: int = 3
|
|
245
|
+
) -> np.ndarray:
|
|
246
|
+
"""
|
|
247
|
+
Extract feature vector x_t = φ(s_t, L_{t-k:t}).
|
|
248
|
+
|
|
249
|
+
Includes:
|
|
250
|
+
- Instantaneous metrics
|
|
251
|
+
- Lag features (x_{t-1}, x_{t-2}, ...)
|
|
252
|
+
- EMA features
|
|
253
|
+
- Volatility (windowed std)
|
|
254
|
+
- Drift features (CUSUM/z-score)
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
snapshot: Current state snapshot (metric_name -> value)
|
|
258
|
+
k: Number of lag features to include
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
np.ndarray: Feature vector
|
|
262
|
+
"""
|
|
263
|
+
# Get metric values in deterministic order
|
|
264
|
+
metric_names = sorted(self.doctrine.metrics.keys())
|
|
265
|
+
n_metrics = len(metric_names)
|
|
266
|
+
|
|
267
|
+
# Instantaneous features
|
|
268
|
+
instant_features = np.array([snapshot.get(name, 0.0) for name in metric_names])
|
|
269
|
+
|
|
270
|
+
# Lag features
|
|
271
|
+
lag_features = []
|
|
272
|
+
for lag in range(1, min(k + 1, len(self.feature_history) + 1)):
|
|
273
|
+
if len(self.feature_history) >= lag:
|
|
274
|
+
lag_snapshot = self.feature_history[-lag]
|
|
275
|
+
lag_vals = np.array([lag_snapshot.get(name, 0.0) for name in metric_names])
|
|
276
|
+
else:
|
|
277
|
+
lag_vals = np.zeros(n_metrics)
|
|
278
|
+
lag_features.append(lag_vals)
|
|
279
|
+
|
|
280
|
+
# Pad if needed
|
|
281
|
+
while len(lag_features) < k:
|
|
282
|
+
lag_features.insert(0, np.zeros(n_metrics))
|
|
283
|
+
|
|
284
|
+
lag_features = np.concatenate(lag_features) if lag_features else np.array([])
|
|
285
|
+
|
|
286
|
+
# EMA features
|
|
287
|
+
ema_features = []
|
|
288
|
+
for name in metric_names:
|
|
289
|
+
current_val = snapshot.get(name, 0.0)
|
|
290
|
+
if name in self.ema_features:
|
|
291
|
+
ema_val = self.ema_alpha * current_val + (1 - self.ema_alpha) * self.ema_features[name]
|
|
292
|
+
else:
|
|
293
|
+
ema_val = current_val
|
|
294
|
+
self.ema_features[name] = ema_val
|
|
295
|
+
ema_features.append(ema_val)
|
|
296
|
+
ema_features = np.array(ema_features)
|
|
297
|
+
|
|
298
|
+
# Volatility (windowed std)
|
|
299
|
+
if len(self.feature_history) >= 3:
|
|
300
|
+
recent_values = [
|
|
301
|
+
[hist.get(name, 0.0) for name in metric_names]
|
|
302
|
+
for hist in self.feature_history[-3:]
|
|
303
|
+
]
|
|
304
|
+
volatility = np.std(recent_values, axis=0)
|
|
305
|
+
else:
|
|
306
|
+
volatility = np.zeros(n_metrics)
|
|
307
|
+
|
|
308
|
+
# Drift features (z-score of current vs EMA)
|
|
309
|
+
drift_features = []
|
|
310
|
+
for i, name in enumerate(metric_names):
|
|
311
|
+
current_val = snapshot.get(name, 0.0)
|
|
312
|
+
ema_val = self.ema_features.get(name, current_val)
|
|
313
|
+
if volatility[i] > 1e-6:
|
|
314
|
+
z_score = (current_val - ema_val) / volatility[i]
|
|
315
|
+
else:
|
|
316
|
+
z_score = 0.0
|
|
317
|
+
drift_features.append(z_score)
|
|
318
|
+
drift_features = np.array(drift_features)
|
|
319
|
+
|
|
320
|
+
# Concatenate all features
|
|
321
|
+
features = np.concatenate([
|
|
322
|
+
instant_features,
|
|
323
|
+
lag_features,
|
|
324
|
+
ema_features,
|
|
325
|
+
volatility,
|
|
326
|
+
drift_features
|
|
327
|
+
])
|
|
328
|
+
|
|
329
|
+
return features
|
|
330
|
+
|
|
331
|
+
def update_transition_model(
|
|
332
|
+
self,
|
|
333
|
+
x_t: np.ndarray,
|
|
334
|
+
u_t: np.ndarray,
|
|
335
|
+
x_tp1: np.ndarray
|
|
336
|
+
) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Update transition model using Recursive Least Squares (RLS).
|
|
339
|
+
|
|
340
|
+
Model: x_{t+1} = Ax_t + Bu_t + ε, ε ~ N(0, Σ)
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
x_t: State at time t
|
|
344
|
+
u_t: Action vector at time t
|
|
345
|
+
x_tp1: State at time t+1
|
|
346
|
+
"""
|
|
347
|
+
# Construct regressor: Φ_t = [x_t; u_t]
|
|
348
|
+
Phi_t = np.concatenate([x_t, u_t])
|
|
349
|
+
n_inputs = len(Phi_t)
|
|
350
|
+
n_outputs = len(x_tp1)
|
|
351
|
+
|
|
352
|
+
# Initialize if needed - use per-output RLS
|
|
353
|
+
if not self.rls_theta:
|
|
354
|
+
# Will be initialized per output dimension below
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
# For each output dimension, maintain separate RLS
|
|
358
|
+
# We only learn to predict the instantaneous metrics (first n_metrics elements)
|
|
359
|
+
n_metrics = len(self.doctrine.metrics)
|
|
360
|
+
n_outputs_to_learn = min(n_outputs, n_metrics)
|
|
361
|
+
|
|
362
|
+
for i in range(n_outputs_to_learn):
|
|
363
|
+
# Check if we need to reinitialize due to size change
|
|
364
|
+
if i not in self.rls_theta or len(self.rls_theta[i]) != n_inputs:
|
|
365
|
+
# Initialize or reinitialize for this output dimension
|
|
366
|
+
self.rls_theta[i] = np.zeros(n_inputs)
|
|
367
|
+
self.rls_P[i] = np.eye(n_inputs) * 100.0 # Large initial covariance
|
|
368
|
+
|
|
369
|
+
# Get regressor for this output
|
|
370
|
+
phi = Phi_t
|
|
371
|
+
|
|
372
|
+
# Kalman gain for this output
|
|
373
|
+
P_phi = self.rls_P[i] @ phi
|
|
374
|
+
denom = self.rls_lambda + np.dot(phi, P_phi)
|
|
375
|
+
if denom < 1e-10:
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
K = P_phi / denom
|
|
379
|
+
|
|
380
|
+
# Prediction
|
|
381
|
+
pred = np.dot(self.rls_theta[i], phi)
|
|
382
|
+
# Target is the instantaneous metric value (first n_metrics elements of x_tp1)
|
|
383
|
+
target_idx = i if i < len(x_tp1) else 0
|
|
384
|
+
error = x_tp1[target_idx] - pred
|
|
385
|
+
|
|
386
|
+
# Update parameter
|
|
387
|
+
self.rls_theta[i] = self.rls_theta[i] + K * error
|
|
388
|
+
|
|
389
|
+
# Update covariance
|
|
390
|
+
self.rls_P[i] = (self.rls_P[i] - np.outer(K, P_phi)) / self.rls_lambda
|
|
391
|
+
|
|
392
|
+
# Initialize A and B matrices if needed (for prediction)
|
|
393
|
+
if self.rls_A is None:
|
|
394
|
+
self.rls_A = np.eye(n_outputs) * 0.9 # Slight decay
|
|
395
|
+
self.rls_B = np.zeros((n_outputs, max(1, n_inputs - n_outputs))) # Action effects
|
|
396
|
+
|
|
397
|
+
# Store updated model state
|
|
398
|
+
theta_dict = {str(k): v.tolist() for k, v in self.rls_theta.items()} if isinstance(self.rls_theta, dict) else {}
|
|
399
|
+
P_dict = {str(k): v.tolist() for k, v in self.rls_P.items()} if isinstance(self.rls_P, dict) else {}
|
|
400
|
+
|
|
401
|
+
self.model_state = ModelState.create(
|
|
402
|
+
parameters={
|
|
403
|
+
"A": self.rls_A.tolist() if self.rls_A is not None else [],
|
|
404
|
+
"B": self.rls_B.tolist() if self.rls_B is not None else [],
|
|
405
|
+
"theta": theta_dict
|
|
406
|
+
},
|
|
407
|
+
covariance={
|
|
408
|
+
"P": P_dict
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def predict_next_state(
|
|
413
|
+
self,
|
|
414
|
+
x_t: np.ndarray,
|
|
415
|
+
u_t: np.ndarray
|
|
416
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
417
|
+
"""
|
|
418
|
+
Predict next state and uncertainty.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
x_t: Current state (feature vector)
|
|
422
|
+
u_t: Action vector
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Tuple[np.ndarray, np.ndarray]: (predicted_state, uncertainty)
|
|
426
|
+
"""
|
|
427
|
+
Phi_t = np.concatenate([x_t, u_t])
|
|
428
|
+
n_inputs = len(Phi_t)
|
|
429
|
+
|
|
430
|
+
# Get number of metrics (output dimensions)
|
|
431
|
+
n_metrics = len(self.doctrine.metrics)
|
|
432
|
+
|
|
433
|
+
if not self.rls_theta:
|
|
434
|
+
# No model yet - return current state with high uncertainty
|
|
435
|
+
# Extract just the instantaneous metrics from feature vector
|
|
436
|
+
instant_features = x_t[:n_metrics] if len(x_t) >= n_metrics else x_t
|
|
437
|
+
return instant_features.copy(), np.ones(len(instant_features)) * 10.0
|
|
438
|
+
|
|
439
|
+
# Predict using learned parameters
|
|
440
|
+
# We predict the instantaneous metrics (first n_metrics elements of feature vector)
|
|
441
|
+
x_pred = np.zeros(n_metrics)
|
|
442
|
+
uncertainty = np.zeros(n_metrics)
|
|
443
|
+
|
|
444
|
+
for i in range(n_metrics):
|
|
445
|
+
if i in self.rls_theta:
|
|
446
|
+
# Check if parameter vector size matches
|
|
447
|
+
if len(self.rls_theta[i]) == n_inputs:
|
|
448
|
+
# Use learned parameters
|
|
449
|
+
x_pred[i] = np.dot(self.rls_theta[i], Phi_t)
|
|
450
|
+
# Uncertainty from covariance diagonal
|
|
451
|
+
if i in self.rls_P and self.rls_P[i].size > 0:
|
|
452
|
+
uncertainty[i] = np.sqrt(np.abs(self.rls_P[i][0, 0]))
|
|
453
|
+
else:
|
|
454
|
+
uncertainty[i] = 1.0
|
|
455
|
+
else:
|
|
456
|
+
# Size mismatch - reinitialize for this output
|
|
457
|
+
self.rls_theta[i] = np.zeros(n_inputs)
|
|
458
|
+
self.rls_P[i] = np.eye(n_inputs) * 100.0
|
|
459
|
+
x_pred[i] = x_t[i] if i < len(x_t) else 0.0
|
|
460
|
+
uncertainty[i] = 10.0
|
|
461
|
+
else:
|
|
462
|
+
# Not initialized yet
|
|
463
|
+
x_pred[i] = x_t[i] if i < len(x_t) else 0.0
|
|
464
|
+
uncertainty[i] = 10.0
|
|
465
|
+
|
|
466
|
+
return x_pred, uncertainty
|
|
467
|
+
|
|
468
|
+
def update_causal_effects(self, epoch: int) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Update causal effect estimates using Bayesian linear regression.
|
|
471
|
+
|
|
472
|
+
Model: Δm_t = β^T z_t + η_t
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
epoch: Current epoch
|
|
476
|
+
"""
|
|
477
|
+
# Get deltas from ledger
|
|
478
|
+
deltas = self.ledger.compute_deltas(epoch)
|
|
479
|
+
if not deltas:
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Get recent actions and outcomes
|
|
483
|
+
if len(self.action_history) == 0 or len(self.metric_delta_history) == 0:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
# For each metric, estimate causal effect
|
|
487
|
+
metric_names = sorted(self.doctrine.metrics.keys())
|
|
488
|
+
|
|
489
|
+
for metric_name in metric_names:
|
|
490
|
+
if metric_name not in deltas:
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
# Collect data: (action_features, delta)
|
|
494
|
+
Z = [] # Action features
|
|
495
|
+
y = [] # Metric deltas
|
|
496
|
+
|
|
497
|
+
for i, (action, delta_dict) in enumerate(zip(self.action_history, self.metric_delta_history)):
|
|
498
|
+
if metric_name in delta_dict:
|
|
499
|
+
# Encode action as feature vector
|
|
500
|
+
action_vec = self._encode_action(action)
|
|
501
|
+
Z.append(action_vec)
|
|
502
|
+
y.append(delta_dict[metric_name])
|
|
503
|
+
|
|
504
|
+
if len(Z) < 2:
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
Z = np.array(Z)
|
|
508
|
+
y = np.array(y)
|
|
509
|
+
|
|
510
|
+
# Bayesian regression: β ~ N(0, τ²I), η ~ N(0, σ²)
|
|
511
|
+
# Posterior: Σ_β = (σ⁻² Z^T Z + τ⁻² I)⁻¹, μ_β = σ⁻² Σ_β Z^T y
|
|
512
|
+
|
|
513
|
+
n_features = Z.shape[1]
|
|
514
|
+
I = np.eye(n_features)
|
|
515
|
+
|
|
516
|
+
# Compute posterior
|
|
517
|
+
ZTZ = Z.T @ Z
|
|
518
|
+
sigma_inv_sq = 1.0 / (self.bayesian_sigma ** 2)
|
|
519
|
+
tau_inv_sq = 1.0 / (self.bayesian_tau ** 2)
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
if SCIPY_AVAILABLE:
|
|
523
|
+
Sigma_beta_inv = sigma_inv_sq * ZTZ + tau_inv_sq * I
|
|
524
|
+
Sigma_beta = scipy_linalg.inv(Sigma_beta_inv)
|
|
525
|
+
else:
|
|
526
|
+
Sigma_beta_inv = sigma_inv_sq * ZTZ + tau_inv_sq * I
|
|
527
|
+
Sigma_beta = np.linalg.inv(Sigma_beta_inv)
|
|
528
|
+
|
|
529
|
+
mu_beta = sigma_inv_sq * Sigma_beta @ Z.T @ y
|
|
530
|
+
|
|
531
|
+
# Store for this metric
|
|
532
|
+
if self.bayesian_beta_mu is None:
|
|
533
|
+
self.bayesian_beta_mu = {}
|
|
534
|
+
if self.bayesian_beta_sigma is None:
|
|
535
|
+
self.bayesian_beta_sigma = {}
|
|
536
|
+
|
|
537
|
+
self.bayesian_beta_mu[metric_name] = mu_beta
|
|
538
|
+
self.bayesian_beta_sigma[metric_name] = Sigma_beta
|
|
539
|
+
|
|
540
|
+
except np.linalg.LinAlgError:
|
|
541
|
+
logger.warning(f"Singular matrix in Bayesian regression for {metric_name}")
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
def _encode_action(self, action: Dict[str, Any]) -> np.ndarray:
|
|
545
|
+
"""Encode action as feature vector for causal effect estimation."""
|
|
546
|
+
# Simple encoding: one-hot for lever_id + parameter values
|
|
547
|
+
lever_ids = sorted(self.doctrine.levers.keys())
|
|
548
|
+
n_levers = len(lever_ids)
|
|
549
|
+
|
|
550
|
+
# One-hot lever ID
|
|
551
|
+
lever_vec = np.zeros(n_levers)
|
|
552
|
+
if "lever_id" in action:
|
|
553
|
+
try:
|
|
554
|
+
idx = lever_ids.index(action["lever_id"])
|
|
555
|
+
lever_vec[idx] = 1.0
|
|
556
|
+
except ValueError:
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
# Parameter values (normalized)
|
|
560
|
+
param_vec = []
|
|
561
|
+
if "parameters" in action:
|
|
562
|
+
for lever_id in lever_ids:
|
|
563
|
+
if lever_id in self.doctrine.levers:
|
|
564
|
+
lever_spec = self.doctrine.levers[lever_id]
|
|
565
|
+
for param_name, param_value in action["parameters"].items():
|
|
566
|
+
if param_name in lever_spec.bounds:
|
|
567
|
+
bounds = lever_spec.bounds[param_name]
|
|
568
|
+
if "min" in bounds and "max" in bounds:
|
|
569
|
+
# Normalize to [0, 1]
|
|
570
|
+
norm_val = (param_value - bounds["min"]) / (bounds["max"] - bounds["min"])
|
|
571
|
+
param_vec.append(norm_val)
|
|
572
|
+
else:
|
|
573
|
+
param_vec.append(param_value)
|
|
574
|
+
else:
|
|
575
|
+
param_vec.append(param_value)
|
|
576
|
+
|
|
577
|
+
return np.concatenate([lever_vec, np.array(param_vec)])
|
|
578
|
+
|
|
579
|
+
def set_mpc_mode(self, use_mpc: bool = True, horizon: int = 5, use_robust: bool = False) -> None:
|
|
580
|
+
"""
|
|
581
|
+
Enable or disable MPC planning mode.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
use_mpc: Whether to use MPC planner
|
|
585
|
+
horizon: Prediction horizon for MPC
|
|
586
|
+
use_robust: Whether to use robust MPC
|
|
587
|
+
"""
|
|
588
|
+
self.use_mpc = use_mpc
|
|
589
|
+
if use_mpc and MPC_AVAILABLE and MPCPlanner:
|
|
590
|
+
try:
|
|
591
|
+
self.mpc_planner = MPCPlanner(self.doctrine, horizon=horizon, use_robust=use_robust)
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.warning(f"Failed to initialize MPC planner: {e}")
|
|
594
|
+
self.use_mpc = False
|
|
595
|
+
|
|
596
|
+
def plan_actions(
|
|
597
|
+
self,
|
|
598
|
+
x_t: np.ndarray,
|
|
599
|
+
current_metrics: Dict[str, float],
|
|
600
|
+
beam_width: int = 5
|
|
601
|
+
) -> Tuple[List[InterventionSpec], float, str]:
|
|
602
|
+
"""
|
|
603
|
+
Plan actions using constrained multi-objective optimization.
|
|
604
|
+
|
|
605
|
+
Uses MPC if enabled, otherwise falls back to beam search.
|
|
606
|
+
|
|
607
|
+
Score: Σ w_i ΔĴ_i(a) - λ_c C(a) - λ_r R(a)
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
x_t: Current feature vector
|
|
611
|
+
current_metrics: Current metric values
|
|
612
|
+
beam_width: Beam search width (used if MPC not available)
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Tuple[List[InterventionSpec], float, str]: (chosen_interventions, score, rationale)
|
|
616
|
+
"""
|
|
617
|
+
# Try MPC first if enabled
|
|
618
|
+
if self.use_mpc and self.mpc_planner:
|
|
619
|
+
try:
|
|
620
|
+
# Prepare objectives and constraints
|
|
621
|
+
objectives = [{"metric_name": obj.metric_name, "direction": obj.direction, "weight": self.compiled_policy.normalized_weights.get(obj.metric_name, 1.0)} for obj in self.doctrine.objectives]
|
|
622
|
+
constraints = [{"condition": inv.condition} for inv in self.doctrine.invariants]
|
|
623
|
+
lever_bounds = {lever_id: lever_spec.bounds for lever_id, lever_spec in self.doctrine.levers.items()}
|
|
624
|
+
|
|
625
|
+
# Get transition model matrices
|
|
626
|
+
A = self.rls_A
|
|
627
|
+
B = self.rls_B
|
|
628
|
+
|
|
629
|
+
interventions, score, rationale = self.mpc_planner.solve_mpc(
|
|
630
|
+
x_t, A, B, objectives, constraints, lever_bounds
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if interventions: # MPC succeeded
|
|
634
|
+
return interventions, score, f"MPC: {rationale}"
|
|
635
|
+
else:
|
|
636
|
+
# Fall through to beam search
|
|
637
|
+
logger.debug(f"MPC returned no interventions, falling back to beam search: {rationale}")
|
|
638
|
+
except Exception as e:
|
|
639
|
+
logger.warning(f"MPC planning failed, falling back to beam search: {e}")
|
|
640
|
+
|
|
641
|
+
# Fallback to beam search
|
|
642
|
+
# Generate candidate actions from allowed levers
|
|
643
|
+
candidates = []
|
|
644
|
+
|
|
645
|
+
for lever_id, lever_spec in self.doctrine.levers.items():
|
|
646
|
+
# Generate parameter combinations (simplified - grid search)
|
|
647
|
+
bounds = lever_spec.bounds
|
|
648
|
+
|
|
649
|
+
# Simple strategy: try a few parameter values
|
|
650
|
+
param_combos = [{}] # No-op action
|
|
651
|
+
|
|
652
|
+
# Add some parameter variations
|
|
653
|
+
for param_name, param_bounds in bounds.items():
|
|
654
|
+
if "min" in param_bounds and "max" in param_bounds:
|
|
655
|
+
min_val = param_bounds["min"]
|
|
656
|
+
max_val = param_bounds["max"]
|
|
657
|
+
# Try low, medium, high
|
|
658
|
+
for val in [min_val, (min_val + max_val) / 2, max_val]:
|
|
659
|
+
new_combos = []
|
|
660
|
+
for combo in param_combos:
|
|
661
|
+
new_combo = combo.copy()
|
|
662
|
+
new_combo[param_name] = val
|
|
663
|
+
new_combos.append(new_combo)
|
|
664
|
+
param_combos.extend(new_combos)
|
|
665
|
+
|
|
666
|
+
# Limit combinations
|
|
667
|
+
param_combos = param_combos[:10]
|
|
668
|
+
|
|
669
|
+
for params in param_combos:
|
|
670
|
+
try:
|
|
671
|
+
intervention = self.intervention_catalog.create_intervention(lever_id, **params)
|
|
672
|
+
candidates.append(intervention)
|
|
673
|
+
except Exception as e:
|
|
674
|
+
logger.debug(f"Invalid intervention: {e}")
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
# Score each candidate
|
|
678
|
+
scored_candidates = []
|
|
679
|
+
|
|
680
|
+
for candidate in candidates:
|
|
681
|
+
# Encode action
|
|
682
|
+
u_t = self._encode_action({"lever_id": candidate.lever_id, "parameters": candidate.parameters})
|
|
683
|
+
|
|
684
|
+
# Predict effect
|
|
685
|
+
x_pred, uncertainty = self.predict_next_state(x_t, u_t)
|
|
686
|
+
|
|
687
|
+
# Compute objective improvement
|
|
688
|
+
score = 0.0
|
|
689
|
+
rationale_parts = []
|
|
690
|
+
|
|
691
|
+
for obj in self.doctrine.objectives:
|
|
692
|
+
metric_name = obj.metric_name
|
|
693
|
+
weight = self.compiled_policy.normalized_weights.get(metric_name, 1.0)
|
|
694
|
+
|
|
695
|
+
# Get predicted metric change (simplified - use first metric dimension)
|
|
696
|
+
metric_idx = sorted(self.doctrine.metrics.keys()).index(metric_name) if metric_name in self.doctrine.metrics else 0
|
|
697
|
+
if metric_idx < len(x_pred):
|
|
698
|
+
delta_pred = x_pred[metric_idx] - x_t[metric_idx] if metric_idx < len(x_t) else 0.0
|
|
699
|
+
else:
|
|
700
|
+
delta_pred = 0.0
|
|
701
|
+
|
|
702
|
+
# Use causal effect estimate if available
|
|
703
|
+
if metric_name in (self.bayesian_beta_mu or {}):
|
|
704
|
+
beta_mu = self.bayesian_beta_mu[metric_name]
|
|
705
|
+
causal_effect = np.dot(beta_mu, u_t)
|
|
706
|
+
delta_pred = causal_effect
|
|
707
|
+
|
|
708
|
+
# Objective contribution
|
|
709
|
+
if obj.direction == "minimize":
|
|
710
|
+
improvement = -delta_pred * weight
|
|
711
|
+
else:
|
|
712
|
+
improvement = delta_pred * weight
|
|
713
|
+
|
|
714
|
+
score += improvement
|
|
715
|
+
rationale_parts.append(f"{metric_name}: {improvement:.3f}")
|
|
716
|
+
|
|
717
|
+
# Cost penalty
|
|
718
|
+
cost = candidate.cost or 0.0
|
|
719
|
+
score -= 0.1 * cost # Cost penalty weight
|
|
720
|
+
|
|
721
|
+
# Risk penalty (from uncertainty)
|
|
722
|
+
risk = np.mean(uncertainty)
|
|
723
|
+
score -= 0.2 * risk # Risk penalty weight
|
|
724
|
+
|
|
725
|
+
# Check invariants
|
|
726
|
+
violates_invariant = False
|
|
727
|
+
for inv in self.doctrine.invariants:
|
|
728
|
+
# Simple invariant check (full version would evaluate condition)
|
|
729
|
+
if "never_touch" in inv.condition.lower():
|
|
730
|
+
# Check if lever targets forbidden items
|
|
731
|
+
if any(forbidden in str(candidate.parameters) for forbidden in ["sshd", "NetworkManager"]):
|
|
732
|
+
violates_invariant = True
|
|
733
|
+
break
|
|
734
|
+
|
|
735
|
+
if violates_invariant:
|
|
736
|
+
score = -1e6 # Heavy penalty
|
|
737
|
+
|
|
738
|
+
scored_candidates.append((candidate, score, " | ".join(rationale_parts)))
|
|
739
|
+
|
|
740
|
+
# Beam search: keep top beam_width candidates
|
|
741
|
+
scored_candidates.sort(key=lambda x: x[1], reverse=True)
|
|
742
|
+
top_candidates = scored_candidates[:beam_width]
|
|
743
|
+
|
|
744
|
+
# Apply risk budget
|
|
745
|
+
total_risk = 0.0
|
|
746
|
+
selected = []
|
|
747
|
+
|
|
748
|
+
for candidate, score, rationale in top_candidates:
|
|
749
|
+
if len(selected) >= self.doctrine.risk_budget.max_actions_per_epoch:
|
|
750
|
+
break
|
|
751
|
+
|
|
752
|
+
# Estimate risk
|
|
753
|
+
u_t = self._encode_action({"lever_id": candidate.lever_id, "parameters": candidate.parameters})
|
|
754
|
+
_, uncertainty = self.predict_next_state(x_t, u_t)
|
|
755
|
+
risk = np.mean(uncertainty)
|
|
756
|
+
|
|
757
|
+
if total_risk + risk > self.doctrine.risk_budget.max_risk_per_epoch:
|
|
758
|
+
if self.doctrine.risk_budget.rollback_required:
|
|
759
|
+
break # Stop if rollback required
|
|
760
|
+
else:
|
|
761
|
+
continue # Skip this action
|
|
762
|
+
|
|
763
|
+
selected.append(candidate)
|
|
764
|
+
total_risk += risk
|
|
765
|
+
|
|
766
|
+
# Return best action (or empty if conservative mode)
|
|
767
|
+
if self.conservative_mode and len(selected) > 1:
|
|
768
|
+
selected = selected[:1] # Only one action in conservative mode
|
|
769
|
+
|
|
770
|
+
if not selected:
|
|
771
|
+
return [], 0.0, "No actions selected (risk budget or conservative mode)"
|
|
772
|
+
|
|
773
|
+
best_score = top_candidates[0][1] if top_candidates else 0.0
|
|
774
|
+
best_rationale = top_candidates[0][2] if top_candidates else "No rationale"
|
|
775
|
+
|
|
776
|
+
return selected, best_score, best_rationale
|
|
777
|
+
|
|
778
|
+
def set_drift_detection_mode(self, mode: str = "cusum") -> None:
|
|
779
|
+
"""
|
|
780
|
+
Set drift detection mode.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
mode: Detection mode ("cusum", "ruptures", "hybrid")
|
|
784
|
+
"""
|
|
785
|
+
self.drift_detection_mode = mode
|
|
786
|
+
if mode in ["ruptures", "hybrid"] and DRIFT_DETECTION_AVAILABLE and HybridDriftDetector:
|
|
787
|
+
try:
|
|
788
|
+
self.drift_detector = HybridDriftDetector(use_ruptures=(mode == "ruptures" or mode == "hybrid"))
|
|
789
|
+
except Exception as e:
|
|
790
|
+
logger.warning(f"Failed to initialize drift detector: {e}")
|
|
791
|
+
self.drift_detection_mode = "cusum"
|
|
792
|
+
|
|
793
|
+
def update_drift_detection(
|
|
794
|
+
self,
|
|
795
|
+
x_t: np.ndarray,
|
|
796
|
+
x_pred: np.ndarray
|
|
797
|
+
) -> None:
|
|
798
|
+
"""
|
|
799
|
+
Update drift detection using CUSUM or ruptures.
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
x_t: Actual state
|
|
803
|
+
x_pred: Predicted state
|
|
804
|
+
"""
|
|
805
|
+
n_metrics = len(self.doctrine.metrics)
|
|
806
|
+
metric_names = sorted(self.doctrine.metrics.keys())
|
|
807
|
+
|
|
808
|
+
# Update per-metric history for ruptures
|
|
809
|
+
for i, metric_name in enumerate(metric_names):
|
|
810
|
+
if i < len(x_t):
|
|
811
|
+
if metric_name not in self.metric_history:
|
|
812
|
+
self.metric_history[metric_name] = []
|
|
813
|
+
self.metric_history[metric_name].append(float(x_t[i]))
|
|
814
|
+
# Keep only last 100 values
|
|
815
|
+
if len(self.metric_history[metric_name]) > 100:
|
|
816
|
+
self.metric_history[metric_name].pop(0)
|
|
817
|
+
|
|
818
|
+
# Use hybrid/ruptures if enabled
|
|
819
|
+
if self.drift_detection_mode in ["ruptures", "hybrid"] and self.drift_detector:
|
|
820
|
+
drift_detected = False
|
|
821
|
+
for i, metric_name in enumerate(metric_names):
|
|
822
|
+
if i < len(x_t) and i < len(x_pred):
|
|
823
|
+
detected, drift_info = self.drift_detector.update(
|
|
824
|
+
metric_name,
|
|
825
|
+
float(x_t[i]),
|
|
826
|
+
float(x_pred[i]),
|
|
827
|
+
self.metric_history.get(metric_name, [])
|
|
828
|
+
)
|
|
829
|
+
if detected:
|
|
830
|
+
drift_detected = True
|
|
831
|
+
logger.warning(f"Drift detected in {metric_name}: {drift_info}")
|
|
832
|
+
|
|
833
|
+
# Update conservative mode
|
|
834
|
+
if drift_detected:
|
|
835
|
+
self.conservative_mode = True
|
|
836
|
+
elif self.drift_detector.cusum_stat < self.cusum_h * 0.5:
|
|
837
|
+
self.conservative_mode = False
|
|
838
|
+
|
|
839
|
+
# Update CUSUM stat from detector
|
|
840
|
+
self.cusum_stat = self.drift_detector.cusum_stat
|
|
841
|
+
else:
|
|
842
|
+
# Fallback to CUSUM
|
|
843
|
+
residual = x_t - x_pred
|
|
844
|
+
r_t = np.mean(np.abs(residual)) # Mean absolute residual
|
|
845
|
+
|
|
846
|
+
# Update CUSUM
|
|
847
|
+
self.cusum_stat = max(0.0, self.cusum_stat + r_t - self.cusum_k)
|
|
848
|
+
|
|
849
|
+
# Check alarm
|
|
850
|
+
if self.cusum_stat > self.cusum_h:
|
|
851
|
+
if not self.conservative_mode:
|
|
852
|
+
logger.warning(f"Drift detected! CUSUM={self.cusum_stat:.3f} > {self.cusum_h}")
|
|
853
|
+
self.conservative_mode = True
|
|
854
|
+
elif self.cusum_stat < self.cusum_h * 0.5:
|
|
855
|
+
# Reset if well below threshold
|
|
856
|
+
self.conservative_mode = False
|
|
857
|
+
|
|
858
|
+
def explain_decision(
|
|
859
|
+
self,
|
|
860
|
+
interventions: List[InterventionSpec],
|
|
861
|
+
score: float,
|
|
862
|
+
rationale: str,
|
|
863
|
+
epoch: int,
|
|
864
|
+
x_t: np.ndarray
|
|
865
|
+
) -> Tuple[str, str]:
|
|
866
|
+
"""
|
|
867
|
+
Generate decision explanation and compute decision hash.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
interventions: Chosen interventions
|
|
871
|
+
score: Optimization score
|
|
872
|
+
rationale: Rationale text
|
|
873
|
+
epoch: Current epoch
|
|
874
|
+
x_t: Current feature vector
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Tuple[str, str]: (explanation, decision_hash)
|
|
878
|
+
"""
|
|
879
|
+
# Build explanation
|
|
880
|
+
explanation_parts = [
|
|
881
|
+
f"Epoch {epoch}",
|
|
882
|
+
f"Score: {score:.3f}",
|
|
883
|
+
f"Rationale: {rationale}",
|
|
884
|
+
f"Interventions: {len(interventions)}"
|
|
885
|
+
]
|
|
886
|
+
|
|
887
|
+
for i, interv in enumerate(interventions):
|
|
888
|
+
explanation_parts.append(f" {i+1}. {interv.lever_id}: {interv.parameters}")
|
|
889
|
+
|
|
890
|
+
explanation = "\n".join(explanation_parts)
|
|
891
|
+
|
|
892
|
+
# Compute decision hash
|
|
893
|
+
# Extract just the instantaneous metrics for state hash (deterministic)
|
|
894
|
+
n_metrics = len(self.doctrine.metrics)
|
|
895
|
+
x_t_metrics = x_t[:n_metrics] if len(x_t) >= n_metrics else x_t
|
|
896
|
+
|
|
897
|
+
decision_data = {
|
|
898
|
+
"epoch": epoch,
|
|
899
|
+
"policy_hash": self.compiled_policy.policy_hash,
|
|
900
|
+
"state_hash": stable_hash(x_t_metrics.tolist()),
|
|
901
|
+
"interventions": [interv.model_dump() for interv in interventions],
|
|
902
|
+
"score": score,
|
|
903
|
+
"seed": self.seed
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
# Get ledger prefix hash (only observation/decision events, excluding timestamps)
|
|
907
|
+
ledger_events = self.ledger.window(epoch, k=epoch + 1)
|
|
908
|
+
# Hash only the payload content, not timestamps
|
|
909
|
+
ledger_hashes = []
|
|
910
|
+
for e in ledger_events:
|
|
911
|
+
if e["type"] in ["observation", "decision"]:
|
|
912
|
+
# Create deterministic hash from payload (excluding timestamp)
|
|
913
|
+
payload = e["payload"].copy()
|
|
914
|
+
if "timestamp" in payload:
|
|
915
|
+
del payload["timestamp"]
|
|
916
|
+
ledger_hashes.append(stable_hash(payload))
|
|
917
|
+
ledger_hash = stable_hash(sorted(ledger_hashes)) # Sort for determinism
|
|
918
|
+
decision_data["ledger_prefix_hash"] = ledger_hash
|
|
919
|
+
|
|
920
|
+
decision_hash = stable_hash(decision_data)
|
|
921
|
+
|
|
922
|
+
return explanation, decision_hash
|
|
923
|
+
|
|
924
|
+
def run_epoch(
|
|
925
|
+
self,
|
|
926
|
+
epoch: int,
|
|
927
|
+
sensor_provider: Optional[Callable] = None,
|
|
928
|
+
actuator: Optional[Callable] = None
|
|
929
|
+
) -> Dict[str, Any]:
|
|
930
|
+
"""
|
|
931
|
+
Execute single epoch: observe → plan → act → update.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
epoch: Epoch number
|
|
935
|
+
sensor_provider: Function to get current state snapshot (returns Dict[str, float])
|
|
936
|
+
actuator: Function to execute interventions (takes List[InterventionSpec])
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
Dict[str, Any]: Epoch results
|
|
940
|
+
"""
|
|
941
|
+
# 1. Observe
|
|
942
|
+
if sensor_provider:
|
|
943
|
+
snapshot = sensor_provider()
|
|
944
|
+
elif self.sensor_registry:
|
|
945
|
+
# Use sensor registry if available
|
|
946
|
+
required_metrics = list(self.doctrine.metrics.keys())
|
|
947
|
+
snapshot = self.sensor_registry.read_all(required_metrics)
|
|
948
|
+
# Fill missing metrics with 0.0
|
|
949
|
+
for metric_name in required_metrics:
|
|
950
|
+
if metric_name not in snapshot:
|
|
951
|
+
snapshot[metric_name] = 0.0
|
|
952
|
+
else:
|
|
953
|
+
# Dummy sensor for base implementation
|
|
954
|
+
snapshot = {name: 0.0 for name in self.doctrine.metrics.keys()}
|
|
955
|
+
|
|
956
|
+
# Extract features
|
|
957
|
+
x_t = self.extract_features(snapshot)
|
|
958
|
+
|
|
959
|
+
# Record observation
|
|
960
|
+
obs_event = ObservationEvent(
|
|
961
|
+
epoch=epoch,
|
|
962
|
+
metrics=snapshot,
|
|
963
|
+
timestamp=datetime.now(timezone.utc).isoformat()
|
|
964
|
+
)
|
|
965
|
+
ledger_event = LedgerEvent.from_observation(obs_event)
|
|
966
|
+
self.ledger.append(ledger_event)
|
|
967
|
+
|
|
968
|
+
# Update feature history
|
|
969
|
+
self.feature_history.append(snapshot.copy())
|
|
970
|
+
if len(self.feature_history) > self.max_history:
|
|
971
|
+
self.feature_history.pop(0)
|
|
972
|
+
|
|
973
|
+
# 2. Plan
|
|
974
|
+
interventions, score, rationale = self.plan_actions(x_t, snapshot)
|
|
975
|
+
|
|
976
|
+
# Generate explanation and decision hash
|
|
977
|
+
explanation, decision_hash = self.explain_decision(interventions, score, rationale, epoch, x_t)
|
|
978
|
+
|
|
979
|
+
# Record decision
|
|
980
|
+
decision_event = DecisionEvent(
|
|
981
|
+
epoch=epoch,
|
|
982
|
+
interventions=[interv.model_dump() for interv in interventions],
|
|
983
|
+
rationale=rationale,
|
|
984
|
+
decision_hash=decision_hash,
|
|
985
|
+
score=score
|
|
986
|
+
)
|
|
987
|
+
ledger_event = LedgerEvent.from_decision(decision_event)
|
|
988
|
+
self.ledger.append(ledger_event)
|
|
989
|
+
|
|
990
|
+
# 3. Act
|
|
991
|
+
checkpoint_id = None
|
|
992
|
+
intervention_ids = []
|
|
993
|
+
|
|
994
|
+
if interventions:
|
|
995
|
+
# Create checkpoint before interventions if rollback enabled
|
|
996
|
+
if self.rollback_manager:
|
|
997
|
+
checkpoint_id = self.rollback_manager.create_checkpoint(epoch, snapshot)
|
|
998
|
+
|
|
999
|
+
if actuator:
|
|
1000
|
+
# Use provided actuator function
|
|
1001
|
+
try:
|
|
1002
|
+
actuator(interventions)
|
|
1003
|
+
# Record interventions
|
|
1004
|
+
if self.rollback_manager:
|
|
1005
|
+
for interv in interventions:
|
|
1006
|
+
interv_id = self.rollback_manager.record_intervention(
|
|
1007
|
+
epoch, interv, result={"status": "success"}, checkpoint_id=checkpoint_id
|
|
1008
|
+
)
|
|
1009
|
+
intervention_ids.append(interv_id)
|
|
1010
|
+
except Exception as e:
|
|
1011
|
+
logger.error(f"Actuator error: {e}")
|
|
1012
|
+
# Record failed intervention
|
|
1013
|
+
if self.rollback_manager:
|
|
1014
|
+
for interv in interventions:
|
|
1015
|
+
self.rollback_manager.record_intervention(
|
|
1016
|
+
epoch, interv, result={"status": "error", "message": str(e)}, checkpoint_id=checkpoint_id
|
|
1017
|
+
)
|
|
1018
|
+
# Auto-rollback if enabled
|
|
1019
|
+
if self.auto_rollback_on_error and self.rollback_manager:
|
|
1020
|
+
logger.warning("Auto-rolling back due to actuator error")
|
|
1021
|
+
self.rollback_manager.rollback(epoch, len(interventions), self.actuator_registry)
|
|
1022
|
+
elif self.actuator_registry:
|
|
1023
|
+
# Use actuator registry
|
|
1024
|
+
try:
|
|
1025
|
+
result = self.actuator_registry.execute(interventions, transaction=True)
|
|
1026
|
+
# Record interventions
|
|
1027
|
+
if self.rollback_manager:
|
|
1028
|
+
for i, interv in enumerate(interventions):
|
|
1029
|
+
interv_result = result.get("results", [{}])[i] if i < len(result.get("results", [])) else {}
|
|
1030
|
+
interv_id = self.rollback_manager.record_intervention(
|
|
1031
|
+
epoch, interv, result=interv_result, checkpoint_id=checkpoint_id
|
|
1032
|
+
)
|
|
1033
|
+
intervention_ids.append(interv_id)
|
|
1034
|
+
|
|
1035
|
+
if result.get("status") == "error":
|
|
1036
|
+
logger.error(f"Actuator registry execution failed: {result.get('message')}")
|
|
1037
|
+
# Auto-rollback if enabled
|
|
1038
|
+
if self.auto_rollback_on_error and self.rollback_manager:
|
|
1039
|
+
logger.warning("Auto-rolling back due to actuator registry error")
|
|
1040
|
+
self.rollback_manager.rollback(epoch, len(interventions), self.actuator_registry)
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
logger.error(f"Actuator registry error: {e}")
|
|
1043
|
+
# Auto-rollback if enabled
|
|
1044
|
+
if self.auto_rollback_on_error and self.rollback_manager:
|
|
1045
|
+
logger.warning("Auto-rolling back due to actuator registry exception")
|
|
1046
|
+
self.rollback_manager.rollback(epoch, len(interventions), self.actuator_registry)
|
|
1047
|
+
|
|
1048
|
+
# Store action for causal effect estimation
|
|
1049
|
+
if interventions:
|
|
1050
|
+
for interv in interventions:
|
|
1051
|
+
self.action_history.append({
|
|
1052
|
+
"lever_id": interv.lever_id,
|
|
1053
|
+
"parameters": interv.parameters
|
|
1054
|
+
})
|
|
1055
|
+
|
|
1056
|
+
# 4. Update (after observing outcome)
|
|
1057
|
+
# For base implementation, we'll simulate outcome
|
|
1058
|
+
# In real implementation, this would come from sensor after action
|
|
1059
|
+
if sensor_provider:
|
|
1060
|
+
outcome_snapshot = sensor_provider() # Get outcome
|
|
1061
|
+
else:
|
|
1062
|
+
# Simulate outcome (simplified)
|
|
1063
|
+
outcome_snapshot = snapshot.copy()
|
|
1064
|
+
for interv in interventions:
|
|
1065
|
+
# Simple effect simulation
|
|
1066
|
+
for metric_name in outcome_snapshot:
|
|
1067
|
+
outcome_snapshot[metric_name] += 0.1 * len(interventions)
|
|
1068
|
+
|
|
1069
|
+
# Record outcome
|
|
1070
|
+
outcome_event = OutcomeEvent(
|
|
1071
|
+
epoch=epoch,
|
|
1072
|
+
metrics=outcome_snapshot,
|
|
1073
|
+
timestamp=datetime.now(timezone.utc).isoformat()
|
|
1074
|
+
)
|
|
1075
|
+
ledger_event = LedgerEvent.from_outcome(outcome_event)
|
|
1076
|
+
self.ledger.append(ledger_event)
|
|
1077
|
+
|
|
1078
|
+
# Compute deltas
|
|
1079
|
+
deltas = {name: outcome_snapshot.get(name, 0.0) - snapshot.get(name, 0.0) for name in self.doctrine.metrics.keys()}
|
|
1080
|
+
self.metric_delta_history.append(deltas)
|
|
1081
|
+
if len(self.metric_delta_history) > self.max_history:
|
|
1082
|
+
self.metric_delta_history.pop(0)
|
|
1083
|
+
|
|
1084
|
+
# Extract outcome features
|
|
1085
|
+
x_tp1 = self.extract_features(outcome_snapshot)
|
|
1086
|
+
|
|
1087
|
+
# Encode action for transition model
|
|
1088
|
+
if interventions:
|
|
1089
|
+
u_t = self._encode_action({
|
|
1090
|
+
"lever_id": interventions[0].lever_id,
|
|
1091
|
+
"parameters": interventions[0].parameters
|
|
1092
|
+
})
|
|
1093
|
+
else:
|
|
1094
|
+
u_t = np.zeros(len(x_t))
|
|
1095
|
+
|
|
1096
|
+
# Update transition model
|
|
1097
|
+
self.update_transition_model(x_t, u_t, x_tp1)
|
|
1098
|
+
|
|
1099
|
+
# Update causal effects
|
|
1100
|
+
self.update_causal_effects(epoch)
|
|
1101
|
+
|
|
1102
|
+
# Update drift detection
|
|
1103
|
+
# Extract just the instantaneous metrics for comparison
|
|
1104
|
+
n_metrics = len(self.doctrine.metrics)
|
|
1105
|
+
x_tp1_metrics = x_tp1[:n_metrics] if len(x_tp1) >= n_metrics else x_tp1
|
|
1106
|
+
x_pred_metrics, _ = self.predict_next_state(x_t, u_t)
|
|
1107
|
+
self.update_drift_detection(x_tp1_metrics, x_pred_metrics)
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
"epoch": epoch,
|
|
1111
|
+
"observation": snapshot,
|
|
1112
|
+
"interventions": [interv.model_dump() for interv in interventions],
|
|
1113
|
+
"outcome": outcome_snapshot,
|
|
1114
|
+
"deltas": deltas,
|
|
1115
|
+
"score": score,
|
|
1116
|
+
"rationale": rationale,
|
|
1117
|
+
"decision_hash": decision_hash,
|
|
1118
|
+
"explanation": explanation,
|
|
1119
|
+
"conservative_mode": self.conservative_mode,
|
|
1120
|
+
"cusum_stat": self.cusum_stat,
|
|
1121
|
+
"checkpoint_id": checkpoint_id,
|
|
1122
|
+
"intervention_ids": intervention_ids
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
def rollback_last_n(self, epoch: int, n: int) -> List[str]:
|
|
1126
|
+
"""
|
|
1127
|
+
Rollback last N interventions.
|
|
1128
|
+
|
|
1129
|
+
Args:
|
|
1130
|
+
epoch: Current epoch
|
|
1131
|
+
n: Number of interventions to rollback
|
|
1132
|
+
|
|
1133
|
+
Returns:
|
|
1134
|
+
List[str]: List of intervention IDs that were rolled back
|
|
1135
|
+
"""
|
|
1136
|
+
if not self.rollback_manager:
|
|
1137
|
+
logger.warning("Rollback manager not initialized")
|
|
1138
|
+
return []
|
|
1139
|
+
|
|
1140
|
+
return self.rollback_manager.rollback(epoch, n, self.actuator_registry)
|
|
1141
|
+
|
|
1142
|
+
def enable_rollback(
|
|
1143
|
+
self,
|
|
1144
|
+
db_path: str = ":memory:",
|
|
1145
|
+
retention_days: int = 7,
|
|
1146
|
+
auto_rollback_on_error: bool = False,
|
|
1147
|
+
auto_rollback_on_invariant: bool = False
|
|
1148
|
+
) -> None:
|
|
1149
|
+
"""
|
|
1150
|
+
Enable rollback functionality.
|
|
1151
|
+
|
|
1152
|
+
Args:
|
|
1153
|
+
db_path: Path to rollback database
|
|
1154
|
+
retention_days: Number of days to retain checkpoints
|
|
1155
|
+
auto_rollback_on_error: Auto-rollback on actuator errors
|
|
1156
|
+
auto_rollback_on_invariant: Auto-rollback on invariant violations
|
|
1157
|
+
"""
|
|
1158
|
+
if ROLLBACK_AVAILABLE and RollbackManager:
|
|
1159
|
+
try:
|
|
1160
|
+
self.rollback_manager = RollbackManager(db_path, retention_days)
|
|
1161
|
+
self.auto_rollback_on_error = auto_rollback_on_error
|
|
1162
|
+
self.auto_rollback_on_invariant = auto_rollback_on_invariant
|
|
1163
|
+
logger.info("Rollback functionality enabled")
|
|
1164
|
+
except Exception as e:
|
|
1165
|
+
logger.error(f"Failed to enable rollback: {e}")
|
|
1166
|
+
else:
|
|
1167
|
+
logger.warning("Rollback not available (utils.rollback not importable)")
|
|
1168
|
+
|