machinaos 0.0.76 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Generalized event-source framework (Wave 12).
|
|
2
|
+
|
|
3
|
+
Three concrete EventSource subclasses cover all current MachinaOs
|
|
4
|
+
trigger integrations: ``PushEventSource`` (HTTP/RPC pushes),
|
|
5
|
+
``PollingEventSource`` (interval-based pull), ``DaemonEventSource``
|
|
6
|
+
(long-lived subprocess driver). Webhook flow is a thin specialisation
|
|
7
|
+
of PushEventSource via :class:`WebhookSource` + the path registry in
|
|
8
|
+
``server/routers/webhook.py``.
|
|
9
|
+
|
|
10
|
+
The unified payload type :class:`WorkflowEvent` mirrors CloudEvents
|
|
11
|
+
v1.0 verbatim — see ``envelope.py``. A back-compat shim in
|
|
12
|
+
``services.event_waiter`` auto-wraps legacy ``Dict`` dispatches so
|
|
13
|
+
existing plugins keep working untouched until they migrate.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .cli import run_cli_command
|
|
19
|
+
from .envelope import WorkflowEvent
|
|
20
|
+
from .lifecycle import make_lifecycle_handlers, make_status_refresh
|
|
21
|
+
from .source import EventSource
|
|
22
|
+
from .push import PushEventSource
|
|
23
|
+
from .polling import PollingEventSource
|
|
24
|
+
from .daemon import DaemonEventSource
|
|
25
|
+
from .triggers import BaseTriggerParams, WebhookTriggerNode
|
|
26
|
+
from .webhook import WebhookSource, WEBHOOK_SOURCES, register_webhook_source
|
|
27
|
+
from .verifiers import (
|
|
28
|
+
WebhookVerifier,
|
|
29
|
+
HmacVerifier,
|
|
30
|
+
StripeVerifier,
|
|
31
|
+
StandardWebhooksVerifier,
|
|
32
|
+
GitHubVerifier,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"BaseTriggerParams",
|
|
37
|
+
"DaemonEventSource",
|
|
38
|
+
"EventSource",
|
|
39
|
+
"GitHubVerifier",
|
|
40
|
+
"HmacVerifier",
|
|
41
|
+
"PollingEventSource",
|
|
42
|
+
"PushEventSource",
|
|
43
|
+
"StandardWebhooksVerifier",
|
|
44
|
+
"StripeVerifier",
|
|
45
|
+
"WEBHOOK_SOURCES",
|
|
46
|
+
"WebhookSource",
|
|
47
|
+
"WebhookTriggerNode",
|
|
48
|
+
"WebhookVerifier",
|
|
49
|
+
"WorkflowEvent",
|
|
50
|
+
"make_lifecycle_handlers",
|
|
51
|
+
"make_status_refresh",
|
|
52
|
+
"register_webhook_source",
|
|
53
|
+
"run_cli_command",
|
|
54
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Generic CLI invocation helper.
|
|
2
|
+
|
|
3
|
+
``run_cli_command`` resolves the binary on PATH, injects the
|
|
4
|
+
plugin's API key via the convention flag (``--api-key`` by default),
|
|
5
|
+
runs the subprocess with a timeout, captures stdout/stderr, and parses
|
|
6
|
+
stdout as JSON when possible. Used by any plugin that wraps a CLI
|
|
7
|
+
tool (Stripe, future GitHub-CLI / Cloudflare-Wrangler / etc.).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import shutil
|
|
15
|
+
from typing import Any, Dict, List, Optional, Type
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_cli_command(
|
|
19
|
+
*,
|
|
20
|
+
binary: str,
|
|
21
|
+
argv: List[str],
|
|
22
|
+
credential: Optional[Type] = None,
|
|
23
|
+
api_key_arg: str = "--api-key",
|
|
24
|
+
timeout: float = 30.0,
|
|
25
|
+
env: Optional[Dict[str, str]] = None,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""Run ``<binary> <argv> [api_key_arg <key>]`` once, return parsed JSON.
|
|
28
|
+
|
|
29
|
+
``env``: optional process environment override. When None, the child
|
|
30
|
+
inherits the parent's environment (asyncio default).
|
|
31
|
+
|
|
32
|
+
Returns a uniform envelope:
|
|
33
|
+
{"success": bool, "result": parsed-JSON-or-None,
|
|
34
|
+
"stdout": str, "stderr": str, "error": str or None}
|
|
35
|
+
"""
|
|
36
|
+
api_key: Optional[str] = None
|
|
37
|
+
if credential is not None:
|
|
38
|
+
try:
|
|
39
|
+
secrets = await credential.resolve()
|
|
40
|
+
except PermissionError:
|
|
41
|
+
return {"success": False, "error": f"{binary}: credential required"}
|
|
42
|
+
api_key = secrets.get("api_key")
|
|
43
|
+
if not api_key:
|
|
44
|
+
return {"success": False, "error": f"{binary}: credential required"}
|
|
45
|
+
|
|
46
|
+
resolved = shutil.which(binary)
|
|
47
|
+
if resolved is None:
|
|
48
|
+
return {"success": False, "error": f"{binary!r} not on PATH"}
|
|
49
|
+
|
|
50
|
+
full_argv = [resolved, *argv]
|
|
51
|
+
if api_key:
|
|
52
|
+
full_argv.extend([api_key_arg, api_key])
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
proc = await asyncio.create_subprocess_exec(
|
|
56
|
+
*full_argv,
|
|
57
|
+
stdout=asyncio.subprocess.PIPE,
|
|
58
|
+
stderr=asyncio.subprocess.PIPE,
|
|
59
|
+
env=env,
|
|
60
|
+
)
|
|
61
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
62
|
+
except asyncio.TimeoutError:
|
|
63
|
+
return {"success": False, "error": f"{binary} timed out ({timeout}s)"}
|
|
64
|
+
|
|
65
|
+
out = stdout.decode(errors="replace").strip()
|
|
66
|
+
err = stderr.decode(errors="replace").strip()
|
|
67
|
+
if proc.returncode != 0:
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"stdout": out,
|
|
71
|
+
"stderr": err,
|
|
72
|
+
"error": err or f"exit {proc.returncode}",
|
|
73
|
+
}
|
|
74
|
+
try:
|
|
75
|
+
parsed = json.loads(out) if out else None
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
parsed = None
|
|
78
|
+
return {"success": True, "result": parsed, "stdout": out, "stderr": err, "error": None}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""DaemonEventSource — long-lived subprocess (or SDK loop) that emits
|
|
2
|
+
events into the framework.
|
|
3
|
+
|
|
4
|
+
Used by stripe-listen, future telegram/whatsapp daemon migrations, and
|
|
5
|
+
any plugin that wraps a CLI tool. The class delegates lifecycle to
|
|
6
|
+
:class:`services.process_service.ProcessService` (already battle-tested:
|
|
7
|
+
PATHEXT-aware, ``kill_tree`` cleanup, log capture, terminal broadcast)
|
|
8
|
+
and subscribes to its standard per-line callback hook for typed event
|
|
9
|
+
emission. We do NOT re-tail the on-disk log files — that's the same
|
|
10
|
+
data ProcessService already streamed to us via ``line_handler``.
|
|
11
|
+
|
|
12
|
+
Subclasses contribute:
|
|
13
|
+
|
|
14
|
+
- :meth:`build_command(secrets) -> str` — full command string
|
|
15
|
+
- :meth:`parse_line(stream, line) -> Optional[WorkflowEvent]` — turn one
|
|
16
|
+
line into one event (or ``None`` for log-only lines)
|
|
17
|
+
- :attr:`binary_name` (optional) — pre-flight ``shutil.which`` check
|
|
18
|
+
with a clear error if the binary isn't on PATH
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import shutil
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, AsyncIterator, ClassVar, Dict, Optional
|
|
27
|
+
|
|
28
|
+
from core.logging import get_logger
|
|
29
|
+
from services.process_service import get_process_service
|
|
30
|
+
|
|
31
|
+
from .envelope import WorkflowEvent
|
|
32
|
+
from .source import EventSource
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DaemonEventSource(EventSource):
|
|
38
|
+
"""Long-lived subprocess driver. Subclasses provide:
|
|
39
|
+
|
|
40
|
+
* :attr:`process_name` — unique name for ProcessService key
|
|
41
|
+
* :attr:`binary_name` — the executable to look up via PATH
|
|
42
|
+
* :meth:`build_command(secrets)` — full command string
|
|
43
|
+
* :meth:`parse_line(stream, line)` — turn output lines into events
|
|
44
|
+
(return ``None`` to suppress; e.g. log-only lines)
|
|
45
|
+
* :meth:`install_hint` (optional) — install URL surfaced in the
|
|
46
|
+
"binary not on PATH" error
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
process_name: ClassVar[str] = ""
|
|
50
|
+
binary_name: ClassVar[str] = ""
|
|
51
|
+
workflow_namespace: ClassVar[str] = "_daemon"
|
|
52
|
+
install_hint: ClassVar[str] = ""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
super().__init__()
|
|
56
|
+
self._pid: Optional[int] = None
|
|
57
|
+
self._lock = asyncio.Lock()
|
|
58
|
+
self._queue: asyncio.Queue[WorkflowEvent] = asyncio.Queue()
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def pid(self) -> Optional[int]:
|
|
62
|
+
return self._pid
|
|
63
|
+
|
|
64
|
+
def build_command(self, secrets: Dict) -> str:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def parse_line(self, stream: str, line: str) -> Optional[WorkflowEvent]:
|
|
68
|
+
"""Override to emit events from output lines. ``stream`` is
|
|
69
|
+
``"stdout"`` or ``"stderr"``. Returning ``None`` swallows the line."""
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def workdir(self) -> Path:
|
|
73
|
+
from core.config import Settings
|
|
74
|
+
cwd = Path(Settings().workspace_base_resolved).resolve() / self.workflow_namespace
|
|
75
|
+
cwd.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
return cwd
|
|
77
|
+
|
|
78
|
+
async def _resolve_secrets(self) -> Dict[str, Any]:
|
|
79
|
+
if self.credential is None:
|
|
80
|
+
return {}
|
|
81
|
+
try:
|
|
82
|
+
return await self.credential.resolve()
|
|
83
|
+
except PermissionError:
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
async def has_credential(self) -> bool:
|
|
87
|
+
secrets = await self._resolve_secrets()
|
|
88
|
+
return bool(secrets.get("api_key"))
|
|
89
|
+
|
|
90
|
+
async def status(self) -> Dict[str, Any]:
|
|
91
|
+
return {
|
|
92
|
+
"type": self.type,
|
|
93
|
+
"running": self._started,
|
|
94
|
+
"pid": self._pid,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async def start(self) -> Dict[str, Any]:
|
|
98
|
+
async with self._lock:
|
|
99
|
+
if self._started:
|
|
100
|
+
logger.info("[%s] start() noop — already running (pid=%s)", self.type, self._pid)
|
|
101
|
+
return {"success": True, "message": "already running", "status": await self.status()}
|
|
102
|
+
|
|
103
|
+
logger.info("[%s] start() entered: resolving secrets + checking credential gate", self.type)
|
|
104
|
+
secrets = await self._resolve_secrets()
|
|
105
|
+
# Gate via :meth:`has_credential` so subclasses that authenticate
|
|
106
|
+
# without an ``api_key`` field (Stripe — auth lives in the CLI's
|
|
107
|
+
# config file, signalled by ``is_logged_in()``) can override the
|
|
108
|
+
# check. The default ``has_credential`` still tests
|
|
109
|
+
# ``secrets["api_key"]`` so api-key plugins keep working.
|
|
110
|
+
if self.credential is not None and not await self.has_credential():
|
|
111
|
+
logger.warning(
|
|
112
|
+
"[%s] start() blocked: has_credential() returned False — refusing to spawn daemon",
|
|
113
|
+
self.type,
|
|
114
|
+
)
|
|
115
|
+
return {"success": False, "error": f"{self.type}: credential required"}
|
|
116
|
+
|
|
117
|
+
if self.binary_name and shutil.which(self.binary_name) is None:
|
|
118
|
+
hint = f" Install: {self.install_hint}" if self.install_hint else ""
|
|
119
|
+
return {"success": False, "error": f"{self.binary_name!r} not on PATH.{hint}"}
|
|
120
|
+
|
|
121
|
+
cmd = self.build_command(secrets)
|
|
122
|
+
cwd = self.workdir()
|
|
123
|
+
result = await get_process_service().start(
|
|
124
|
+
name=self.process_name,
|
|
125
|
+
command=cmd,
|
|
126
|
+
workflow_id=self.workflow_namespace,
|
|
127
|
+
working_directory=str(cwd),
|
|
128
|
+
line_handler=self._on_line,
|
|
129
|
+
)
|
|
130
|
+
if not result.get("success"):
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
self._pid = (result.get("result") or {}).get("pid")
|
|
134
|
+
self._started = True
|
|
135
|
+
self._stopped = False
|
|
136
|
+
logger.info("[%s] daemon started pid=%s", self.type, self._pid)
|
|
137
|
+
return {"success": True, "message": f"started (pid {self._pid})", "status": await self.status()}
|
|
138
|
+
|
|
139
|
+
async def stop(self) -> Dict[str, Any]:
|
|
140
|
+
async with self._lock:
|
|
141
|
+
self._stopped = True
|
|
142
|
+
await get_process_service().stop(
|
|
143
|
+
name=self.process_name, workflow_id=self.workflow_namespace,
|
|
144
|
+
)
|
|
145
|
+
self._started = False
|
|
146
|
+
self._pid = None
|
|
147
|
+
return {"success": True, "message": "disconnected"}
|
|
148
|
+
|
|
149
|
+
async def restart(self) -> Dict[str, Any]:
|
|
150
|
+
await self.stop()
|
|
151
|
+
return await self.start()
|
|
152
|
+
|
|
153
|
+
async def _on_line(self, stream: str, line: str) -> None:
|
|
154
|
+
"""ProcessService callback: one decoded stdout/stderr line.
|
|
155
|
+
Subclasses' :meth:`parse_line` decides whether to emit an event."""
|
|
156
|
+
event = self.parse_line(stream, line)
|
|
157
|
+
if event is not None:
|
|
158
|
+
await self._queue.put(event)
|
|
159
|
+
|
|
160
|
+
async def emit(self) -> AsyncIterator[WorkflowEvent]:
|
|
161
|
+
while not self._stopped:
|
|
162
|
+
event = await self._queue.get()
|
|
163
|
+
yield event
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""WorkflowEvent — CloudEvents v1.0 envelope (in-house, no external dep).
|
|
2
|
+
|
|
3
|
+
Field set mirrors https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md
|
|
4
|
+
verbatim so future interop with EventBridge / Knative is a JSON-schema swap.
|
|
5
|
+
The MachinaOs extensions (workflow_id, trigger_node_id, correlation_id)
|
|
6
|
+
ride as CloudEvents extension attributes — fully compliant with the spec.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Literal, Mapping, Optional
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkflowEvent(BaseModel):
|
|
19
|
+
"""Unified event envelope used by every EventSource."""
|
|
20
|
+
|
|
21
|
+
specversion: str = "1.0"
|
|
22
|
+
id: str = Field(default_factory=lambda: uuid4().hex)
|
|
23
|
+
source: str
|
|
24
|
+
type: str
|
|
25
|
+
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
26
|
+
subject: Optional[str] = None
|
|
27
|
+
datacontenttype: str = "application/json"
|
|
28
|
+
dataschema: Optional[str] = None
|
|
29
|
+
data: Any = None
|
|
30
|
+
|
|
31
|
+
workflow_id: Optional[str] = None
|
|
32
|
+
trigger_node_id: Optional[str] = None
|
|
33
|
+
correlation_id: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(extra="allow")
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_legacy(cls, event_type: str, payload: dict) -> "WorkflowEvent":
|
|
39
|
+
"""Wrap a legacy Dict payload from the pre-framework dispatch path.
|
|
40
|
+
|
|
41
|
+
Used by the back-compat shim in event_waiter.dispatch so existing
|
|
42
|
+
plugins (telegram, whatsapp, gmail, etc.) keep working until they
|
|
43
|
+
migrate to native WorkflowEvent emission.
|
|
44
|
+
"""
|
|
45
|
+
return cls(
|
|
46
|
+
source=f"legacy://{event_type}",
|
|
47
|
+
type=event_type,
|
|
48
|
+
data=payload,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ---- Typed factory classmethods (Wave 11.I, milestone Q) -----------
|
|
52
|
+
#
|
|
53
|
+
# Mirrors the official `cloudevents` Python SDK's `from_http` /
|
|
54
|
+
# `to_http` convenience pattern. Each factory enforces the same
|
|
55
|
+
# source/type/subject conventions across the codebase so callers
|
|
56
|
+
# don't hand-construct envelopes with drifting URI shapes.
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def credential(
|
|
60
|
+
cls,
|
|
61
|
+
provider: str,
|
|
62
|
+
action: Literal[
|
|
63
|
+
"api_key.saved",
|
|
64
|
+
"api_key.deleted",
|
|
65
|
+
"api_key.validated",
|
|
66
|
+
"oauth.connected",
|
|
67
|
+
"oauth.disconnected",
|
|
68
|
+
"oauth.validated",
|
|
69
|
+
],
|
|
70
|
+
**extra: Any,
|
|
71
|
+
) -> "WorkflowEvent":
|
|
72
|
+
"""Credential mutation event (matches the existing
|
|
73
|
+
``broadcast_credential_event`` contract locked by
|
|
74
|
+
``test_credential_broadcasts.py``)."""
|
|
75
|
+
return cls(
|
|
76
|
+
source="machinaos://services/credentials",
|
|
77
|
+
type=f"credential.{action}",
|
|
78
|
+
subject=provider,
|
|
79
|
+
data={"provider": provider, **extra} if extra else {"provider": provider},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def connection_status(
|
|
84
|
+
cls,
|
|
85
|
+
plugin: str,
|
|
86
|
+
*,
|
|
87
|
+
connected: bool,
|
|
88
|
+
subject: Optional[str] = None,
|
|
89
|
+
data: Optional[Mapping[str, Any]] = None,
|
|
90
|
+
) -> "WorkflowEvent":
|
|
91
|
+
"""Plugin connection-state event (whatsapp / telegram / android-relay
|
|
92
|
+
/ twitter / google connect-disconnect)."""
|
|
93
|
+
return cls(
|
|
94
|
+
source=f"machinaos://nodes/{plugin}",
|
|
95
|
+
type=f"{plugin}.connection.{'opened' if connected else 'closed'}",
|
|
96
|
+
subject=subject,
|
|
97
|
+
data=dict(data) if data else {},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def oauth_completed(
|
|
102
|
+
cls,
|
|
103
|
+
provider: str,
|
|
104
|
+
*,
|
|
105
|
+
identifier: str,
|
|
106
|
+
data: Optional[Mapping[str, Any]] = None,
|
|
107
|
+
) -> "WorkflowEvent":
|
|
108
|
+
"""OAuth callback completion. ``identifier`` is the user-facing
|
|
109
|
+
handle (email / username) used as ``subject``."""
|
|
110
|
+
return cls(
|
|
111
|
+
source=f"machinaos://nodes/{provider}",
|
|
112
|
+
type=f"{provider}.oauth.completed",
|
|
113
|
+
subject=identifier,
|
|
114
|
+
data=dict(data) if data else {"identifier": identifier},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def message(
|
|
119
|
+
cls,
|
|
120
|
+
plugin: str,
|
|
121
|
+
direction: Literal["sent", "received"],
|
|
122
|
+
data: Mapping[str, Any],
|
|
123
|
+
) -> "WorkflowEvent":
|
|
124
|
+
"""Plugin-emitted message event (whatsapp / telegram / email
|
|
125
|
+
send + receive). ``subject`` defaults to the chat / sender id
|
|
126
|
+
in the payload (cast to str — Telegram uses numeric chat ids),
|
|
127
|
+
falling back to None."""
|
|
128
|
+
payload = dict(data)
|
|
129
|
+
raw_subject = (
|
|
130
|
+
payload.get("chat_id") or payload.get("from_id") or payload.get("sender")
|
|
131
|
+
)
|
|
132
|
+
return cls(
|
|
133
|
+
source=f"machinaos://nodes/{plugin}",
|
|
134
|
+
type=f"{plugin}.message.{direction}",
|
|
135
|
+
subject=str(raw_subject) if raw_subject is not None else None,
|
|
136
|
+
data=payload,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def team_event(
|
|
141
|
+
cls,
|
|
142
|
+
team_id: str,
|
|
143
|
+
kind: str,
|
|
144
|
+
data: Mapping[str, Any],
|
|
145
|
+
) -> "WorkflowEvent":
|
|
146
|
+
"""Agent-team lifecycle / task event (created / dissolved /
|
|
147
|
+
task.added / task.claimed / task.completed / task.failed /
|
|
148
|
+
message.sent)."""
|
|
149
|
+
return cls(
|
|
150
|
+
source="machinaos://services/agent_team",
|
|
151
|
+
type=f"team.{kind}",
|
|
152
|
+
subject=team_id,
|
|
153
|
+
data=dict(data),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def workflow_lifecycle(
|
|
158
|
+
cls,
|
|
159
|
+
stage: Literal[
|
|
160
|
+
"deployment.started",
|
|
161
|
+
"deployment.stopped",
|
|
162
|
+
"lock.acquired",
|
|
163
|
+
"lock.released",
|
|
164
|
+
"execution.started",
|
|
165
|
+
"execution.stopped",
|
|
166
|
+
],
|
|
167
|
+
*,
|
|
168
|
+
workflow_id: str,
|
|
169
|
+
data: Optional[Mapping[str, Any]] = None,
|
|
170
|
+
) -> "WorkflowEvent":
|
|
171
|
+
"""Workflow lifecycle event. Carries ``workflow_id`` both as
|
|
172
|
+
``subject`` (CloudEvents convention) and as the
|
|
173
|
+
``workflow_id`` extension attribute (existing reader
|
|
174
|
+
contract)."""
|
|
175
|
+
return cls(
|
|
176
|
+
source="machinaos://services/workflow",
|
|
177
|
+
type=f"workflow.{stage}",
|
|
178
|
+
subject=workflow_id,
|
|
179
|
+
workflow_id=workflow_id,
|
|
180
|
+
data=dict(data) if data else {},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def agent_progress(
|
|
185
|
+
cls,
|
|
186
|
+
node_id: str,
|
|
187
|
+
*,
|
|
188
|
+
workflow_id: Optional[str],
|
|
189
|
+
iteration: int,
|
|
190
|
+
max_iterations: int,
|
|
191
|
+
phase: Optional[str] = None,
|
|
192
|
+
data: Optional[Mapping[str, Any]] = None,
|
|
193
|
+
) -> "WorkflowEvent":
|
|
194
|
+
"""Live LangGraph supervised-loop progress for one agent node.
|
|
195
|
+
|
|
196
|
+
Emitted from inside the ``astream`` loop in
|
|
197
|
+
``services/ai.py:execute_agent`` and ``execute_chat_agent`` after
|
|
198
|
+
each super-step. ``iteration`` advances on every ``agent_node``
|
|
199
|
+
invocation; ``max_iterations`` mirrors the LangGraph
|
|
200
|
+
``recursion_limit`` (sourced from
|
|
201
|
+
``llm_defaults.json:agent.recursion_limit``).
|
|
202
|
+
|
|
203
|
+
``subject`` carries the executing node id so the FE routes the
|
|
204
|
+
update straight to ``nodeStatusStore`` for that node. The
|
|
205
|
+
``workflow_id`` extension attribute scopes per-workflow displays
|
|
206
|
+
the same way ``node_status`` broadcasts do.
|
|
207
|
+
"""
|
|
208
|
+
return cls(
|
|
209
|
+
source="machinaos://services/agent",
|
|
210
|
+
type="agent.progress",
|
|
211
|
+
subject=node_id,
|
|
212
|
+
workflow_id=workflow_id,
|
|
213
|
+
data={
|
|
214
|
+
"node_id": node_id,
|
|
215
|
+
"iteration": iteration,
|
|
216
|
+
"max_iterations": max_iterations,
|
|
217
|
+
**({"phase": phase} if phase else {}),
|
|
218
|
+
**(dict(data) if data else {}),
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def deployment_snapshot(
|
|
224
|
+
cls,
|
|
225
|
+
running_workflow_ids: list[str],
|
|
226
|
+
) -> "WorkflowEvent":
|
|
227
|
+
"""Push-on-connect snapshot of every currently-running deployment.
|
|
228
|
+
|
|
229
|
+
Emitted by ``broadcaster.broadcast_deployment_snapshot()`` to a
|
|
230
|
+
single WebSocket target right after ``initial_status``. Lets the
|
|
231
|
+
FE reconcile its local ``deploymentStatus`` / ``runningWorkflows``
|
|
232
|
+
cache against the backend's source of truth on every (re)connect,
|
|
233
|
+
instead of carrying stale "isRunning=true" forward through a
|
|
234
|
+
backend restart that wiped the in-memory deployment dict.
|
|
235
|
+
|
|
236
|
+
Distinct from ``workflow_lifecycle("deployment.started")`` —
|
|
237
|
+
that fires when a deployment STARTS (one-shot edge event).
|
|
238
|
+
``deployment.snapshot`` is an idempotent state dump tied to
|
|
239
|
+
client connect, not to a state transition. Empty list is
|
|
240
|
+
meaningful: "no deployments are running, drop your stale
|
|
241
|
+
local state."
|
|
242
|
+
"""
|
|
243
|
+
return cls(
|
|
244
|
+
source="machinaos://services/workflow",
|
|
245
|
+
type="workflow.deployment.snapshot",
|
|
246
|
+
data={"running_workflow_ids": list(running_workflow_ids)},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def task_completed(
|
|
251
|
+
cls,
|
|
252
|
+
task_id: str,
|
|
253
|
+
*,
|
|
254
|
+
status: Literal["completed", "error"],
|
|
255
|
+
agent: str,
|
|
256
|
+
data: Optional[Mapping[str, Any]] = None,
|
|
257
|
+
) -> "WorkflowEvent":
|
|
258
|
+
"""Delegated child-agent completion. Type discriminates on
|
|
259
|
+
succeeded vs failed; ``taskTrigger`` filters by both."""
|
|
260
|
+
return cls(
|
|
261
|
+
source="machinaos://services/agent",
|
|
262
|
+
type=f"agent.task.{'succeeded' if status == 'completed' else 'failed'}",
|
|
263
|
+
subject=task_id,
|
|
264
|
+
data=dict(data) if data else {"task_id": task_id, "agent": agent, "status": status},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def matches_type(self, pattern: str) -> bool:
|
|
268
|
+
"""Glob-style match on event type. ``"all"``/empty matches any.
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
"stripe.charge.succeeded" matches itself
|
|
272
|
+
"stripe.charge.*" matches "stripe.charge.succeeded"
|
|
273
|
+
"stripe.*" matches "stripe.charge.succeeded"
|
|
274
|
+
"all" or "" matches everything
|
|
275
|
+
"""
|
|
276
|
+
if not pattern or pattern == "all":
|
|
277
|
+
return True
|
|
278
|
+
if pattern.endswith(".*"):
|
|
279
|
+
prefix = pattern[:-2]
|
|
280
|
+
return self.type.startswith(prefix + ".") or self.type == prefix
|
|
281
|
+
return self.type == pattern
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Generic lifecycle wiring helpers.
|
|
2
|
+
|
|
3
|
+
:func:`make_lifecycle_handlers` returns the standard 4 WebSocket
|
|
4
|
+
handlers (``connect`` / ``disconnect`` / ``reconnect`` / ``status``) for
|
|
5
|
+
any :class:`EventSource`. Plugins call it once and register the dict —
|
|
6
|
+
no per-handler boilerplate.
|
|
7
|
+
|
|
8
|
+
:func:`make_status_refresh` returns a ``register_service_refresh``
|
|
9
|
+
callback that auto-reconnects the source when a credential is stored
|
|
10
|
+
and mirrors its status into the broadcaster cache.
|
|
11
|
+
|
|
12
|
+
Together these collapse ~50 LOC of identical boilerplate per plugin
|
|
13
|
+
into two function calls.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Awaitable, Callable, Dict, TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from fastapi import WebSocket
|
|
21
|
+
|
|
22
|
+
from core.logging import get_logger
|
|
23
|
+
|
|
24
|
+
from .source import EventSource
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from services.status_broadcaster import StatusBroadcaster
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_lifecycle_handlers(
|
|
33
|
+
prefix: str,
|
|
34
|
+
source: EventSource,
|
|
35
|
+
*,
|
|
36
|
+
extra: Dict[str, Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]]] | None = None,
|
|
37
|
+
) -> Dict[str, Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]]]:
|
|
38
|
+
"""Return ``{prefix}_connect / _disconnect / _reconnect / _status`` handlers.
|
|
39
|
+
|
|
40
|
+
Pass ``extra`` to merge additional plugin-specific handlers (e.g.
|
|
41
|
+
``{"stripe_trigger": handle_stripe_trigger}``).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
async def _connect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
45
|
+
return await source.start()
|
|
46
|
+
|
|
47
|
+
async def _disconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
48
|
+
return await source.stop()
|
|
49
|
+
|
|
50
|
+
async def _reconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
51
|
+
restart = getattr(source, "restart", None)
|
|
52
|
+
if restart is not None:
|
|
53
|
+
return await restart()
|
|
54
|
+
await source.stop()
|
|
55
|
+
return await source.start()
|
|
56
|
+
|
|
57
|
+
async def _status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
58
|
+
status = await source.status()
|
|
59
|
+
if hasattr(source, "has_credential"):
|
|
60
|
+
status["has_stored_key"] = await source.has_credential() # type: ignore[attr-defined]
|
|
61
|
+
return {"success": True, "status": status}
|
|
62
|
+
|
|
63
|
+
handlers: Dict[str, Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]]] = {
|
|
64
|
+
f"{prefix}_connect": _connect,
|
|
65
|
+
f"{prefix}_disconnect": _disconnect,
|
|
66
|
+
f"{prefix}_reconnect": _reconnect,
|
|
67
|
+
f"{prefix}_status": _status,
|
|
68
|
+
}
|
|
69
|
+
if extra:
|
|
70
|
+
handlers.update(extra)
|
|
71
|
+
return handlers
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_status_refresh(
|
|
75
|
+
source: EventSource,
|
|
76
|
+
*,
|
|
77
|
+
status_key: str,
|
|
78
|
+
broadcast_type: str,
|
|
79
|
+
) -> Callable[["StatusBroadcaster"], Awaitable[None]]:
|
|
80
|
+
"""Return a ``register_service_refresh`` callback.
|
|
81
|
+
|
|
82
|
+
The callback auto-starts the source if it has stored credentials,
|
|
83
|
+
mirrors its status into ``broadcaster._status[status_key]``, and
|
|
84
|
+
emits a ``broadcast_type`` broadcast.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
async def refresh(broadcaster: "StatusBroadcaster") -> None:
|
|
88
|
+
try:
|
|
89
|
+
if not getattr(source, "_started", False) and hasattr(source, "has_credential"):
|
|
90
|
+
if await source.has_credential(): # type: ignore[attr-defined]
|
|
91
|
+
logger.info("[StatusBroadcaster] auto-reconnecting %s", status_key)
|
|
92
|
+
await source.start()
|
|
93
|
+
status = await source.status()
|
|
94
|
+
broadcaster._status[status_key] = status
|
|
95
|
+
await broadcaster.broadcast({"type": broadcast_type, "data": status})
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.debug("[StatusBroadcaster] %s refresh failed: %s", status_key, e)
|
|
98
|
+
|
|
99
|
+
return refresh
|