machinaos 0.0.78 → 0.0.80
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/.env.template +74 -5
- package/{workflows/AI Assistant_workflow-1778504793388-ou1m1tz2x.json → .machina/workflows/AI Assistant_example_workflow-1779017037684-e2e5da7a.json } +164 -105
- package/{workflows/AI Employee_example_workflow-1777720598005-u4cm858dv.json → .machina/workflows/AI Employee_example_workflow-1779102911870-cbc76c82.json } +582 -328
- package/{workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json → .machina/workflows/Claude Assistant_example_workflow-1779095939967-2369cff4.json } +152 -83
- package/README.md +5 -2
- package/bin/cli.js +2 -2
- package/{machina → cli}/__main__.py +11 -7
- package/cli/_common.py +122 -0
- package/cli/buildenv.py +40 -0
- package/cli/cli.py +204 -0
- package/{machina → cli}/colors.py +10 -2
- package/cli/commands/__init__.py +1 -0
- package/cli/commands/_temporal_specs.py +59 -0
- package/{machina → cli}/commands/build.py +35 -45
- package/cli/commands/clean.py +141 -0
- package/cli/commands/daemon/__init__.py +47 -0
- package/cli/commands/daemon/_state.py +97 -0
- package/cli/commands/daemon/restart.py +14 -0
- package/cli/commands/daemon/start.py +49 -0
- package/cli/commands/daemon/status.py +20 -0
- package/cli/commands/daemon/stop.py +22 -0
- package/{machina → cli}/commands/dev.py +32 -42
- package/{machina → cli}/commands/docs.py +13 -11
- package/{machina → cli}/commands/start.py +69 -62
- package/{machina → cli}/commands/stop.py +7 -10
- package/{machina → cli}/commands/version.py +12 -6
- package/cli/config.py +170 -0
- package/cli/platform_.py +169 -0
- package/{machina → cli}/ports.py +42 -3
- package/{machina → cli}/run.py +29 -2
- package/{machina → cli}/supervisor.py +29 -12
- package/{machina → cli}/tcp.py +6 -2
- package/{machina → cli}/tree.py +38 -11
- package/client/dist/assets/{ActionBar-Du2MSFSz.js → ActionBar-Cjr3TF7g.js} +1 -1
- package/client/dist/assets/{ApiKeyInput-k2LBmBjb.js → ApiKeyInput-DIJE2PVA.js} +1 -1
- package/client/dist/assets/{ApiKeyPanel-C_bV9U0X.js → ApiKeyPanel-CPmye7uh.js} +1 -1
- package/client/dist/assets/{ApiUsageSection-CmVfwZzL.js → ApiUsageSection-TF_7gH2D.js} +1 -1
- package/client/dist/assets/{EmailPanel-CeKIMGu-.js → EmailPanel-Bs-xvbKR.js} +1 -1
- package/client/dist/assets/{OAuthPanel-KA3t3Q2K.js → OAuthPanel-BDtVJhAV.js} +1 -1
- package/client/dist/assets/{QrPairingPanel-NgNpJNuk.js → QrPairingPanel-BwJehTuZ.js} +1 -1
- package/client/dist/assets/{RateLimitSection-Du5YNVIA.js → RateLimitSection-CfNOoPIS.js} +1 -1
- package/client/dist/assets/{StatusCard-DNLyayXc.js → StatusCard-DkwIrgdP.js} +1 -1
- package/client/dist/assets/index-P2FzntoL.js +165 -0
- package/client/dist/index.html +1 -1
- package/client/package.json +1 -1
- package/client/src/Dashboard.tsx +128 -76
- package/client/src/adapters/nodeSpecToDescription.ts +7 -0
- package/client/src/assets/icons/index.test.ts +10 -0
- package/client/src/assets/icons/index.ts +16 -3
- package/client/src/components/AIAgentNode.tsx +8 -8
- package/client/src/components/ParameterRenderer.tsx +6 -3
- package/client/src/components/SkillEditorModal.tsx +1 -0
- package/client/src/components/credentials/panels/EmailPanel.tsx +2 -0
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +2 -0
- package/client/src/components/credentials/sections/RateLimitSection.tsx +1 -0
- package/client/src/components/icons/AIProviderIcons.tsx +1 -0
- package/client/src/components/maps/GoogleMapsPicker.tsx +1 -0
- package/client/src/components/parameterPanel/InputSection.tsx +1 -0
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +1 -0
- package/client/src/components/parameterPanel/OutputSection.tsx +1 -0
- package/client/src/components/ui/ComponentPalette.tsx +1 -0
- package/client/src/components/ui/MapSelector.tsx +1 -0
- package/client/src/components/ui/NodeContextMenu.tsx +3 -3
- package/client/src/components/ui/SettingsPanel.tsx +1 -0
- package/client/src/components/ui/action-button.tsx +1 -0
- package/client/src/components/ui/badge.tsx +1 -0
- package/client/src/components/ui/button.tsx +1 -0
- package/client/src/components/ui/form.tsx +1 -0
- package/client/src/components/ui/tabs.tsx +1 -0
- package/client/src/contexts/AuthContext.tsx +1 -0
- package/client/src/contexts/ThemeContext.tsx +1 -0
- package/client/src/contexts/WebSocketContext.tsx +104 -34
- package/client/src/hooks/__tests__/useApiKeys.test.ts +2 -2
- package/client/src/hooks/useReactFlowNodes.ts +1 -0
- package/client/src/hooks/useWorkflowValidation.ts +142 -0
- package/client/src/lib/nodeSpec.ts +1 -0
- package/client/src/test/providers.tsx +1 -0
- package/client/src/types/__tests__/cloudEvents.test.ts +5 -2
- package/client/src/types/cloudEvents.ts +19 -7
- package/client/src/utils/nodeUtils.ts +1 -1
- package/client/src/utils/workflow.ts +8 -2
- package/client/src/utils/workflowExport.ts +60 -3
- package/package.json +24 -23
- package/scripts/install.js +16 -27
- package/scripts/migrate_icons.py +3 -1
- package/scripts/migrate_skill_icons.py +6 -7
- package/scripts/postinstall.js +11 -9
- package/server/config/ai_cli_providers.json +2 -3
- package/server/config/credential_providers.json +15 -15
- package/server/config/llm_defaults.json +1 -1
- package/server/config/model_registry.json +416 -611
- package/server/constants.py +285 -223
- package/server/core/__init__.py +1 -1
- package/server/core/cache.py +9 -29
- package/server/core/cleanup.py +12 -24
- package/server/core/config.py +148 -24
- package/server/core/container.py +68 -59
- package/server/core/credential_backends.py +5 -13
- package/server/core/credentials_database.py +13 -43
- package/server/core/database.py +292 -353
- package/server/core/health.py +4 -5
- package/server/core/logging.py +241 -87
- package/server/core/paths.py +285 -0
- package/server/core/tracing.py +2 -8
- package/server/gunicorn.conf.py +1 -0
- package/server/main.py +150 -74
- package/server/middleware/auth.py +18 -24
- package/server/models/__init__.py +1 -1
- package/server/models/auth.py +5 -12
- package/server/models/database.py +36 -68
- package/server/models/node_metadata.py +25 -18
- package/server/nodejs/dist/index.js +107 -0
- package/server/nodes/README.md +11 -5
- package/server/nodes/__init__.py +1 -1
- package/server/nodes/_visuals.py +146 -14
- package/server/nodes/agent/_events.py +124 -0
- package/server/nodes/agent/_handles.py +15 -29
- package/server/nodes/agent/_inline.py +28 -25
- package/server/nodes/agent/_specialized.py +30 -15
- package/server/nodes/agent/{ai_agent.py → ai_agent/__init__.py} +33 -17
- package/server/nodes/agent/ai_agent/meta.json +3 -0
- package/server/nodes/agent/{ai_employee.py → ai_employee/__init__.py} +5 -2
- package/server/nodes/agent/ai_employee/meta.json +3 -0
- package/server/nodes/agent/{android_agent.py → android_agent/__init__.py} +1 -1
- package/server/nodes/agent/android_agent/meta.json +3 -0
- package/server/nodes/agent/{autonomous_agent.py → autonomous_agent/__init__.py} +2 -1
- package/server/nodes/agent/autonomous_agent/meta.json +3 -0
- package/server/nodes/agent/{chat_agent.py → chat_agent/__init__.py} +29 -12
- package/server/nodes/agent/chat_agent/meta.json +3 -0
- package/server/nodes/agent/{claude_code_agent.py → claude_code_agent/__init__.py} +192 -95
- package/server/nodes/agent/claude_code_agent/_handlers.py +169 -0
- package/server/{services/claude_oauth.py → nodes/agent/claude_code_agent/_oauth.py} +26 -13
- package/server/nodes/agent/claude_code_agent/_pool.py +1020 -0
- package/server/nodes/agent/claude_code_agent/_provider.py +513 -0
- package/server/nodes/agent/claude_code_agent/_skills.py +245 -0
- package/server/nodes/agent/claude_code_agent/meta.json +3 -0
- package/server/nodes/agent/{codex_agent.py → codex_agent/__init__.py} +26 -35
- package/server/nodes/agent/codex_agent/meta.json +3 -0
- package/server/nodes/agent/{coding_agent.py → coding_agent/__init__.py} +1 -1
- package/server/nodes/agent/coding_agent/meta.json +3 -0
- package/server/nodes/agent/{consumer_agent.py → consumer_agent/__init__.py} +1 -1
- package/server/nodes/agent/consumer_agent/meta.json +3 -0
- package/server/nodes/agent/{orchestrator_agent.py → orchestrator_agent/__init__.py} +5 -2
- package/server/nodes/agent/orchestrator_agent/meta.json +3 -0
- package/server/nodes/agent/{payments_agent.py → payments_agent/__init__.py} +1 -1
- package/server/nodes/agent/payments_agent/meta.json +3 -0
- package/server/nodes/agent/{productivity_agent.py → productivity_agent/__init__.py} +1 -1
- package/server/nodes/agent/productivity_agent/meta.json +3 -0
- package/server/nodes/agent/{rlm_agent.py → rlm_agent/__init__.py} +18 -17
- package/server/nodes/agent/rlm_agent/meta.json +3 -0
- package/server/nodes/agent/{social_agent.py → social_agent/__init__.py} +1 -1
- package/server/nodes/agent/social_agent/meta.json +3 -0
- package/server/nodes/agent/{task_agent.py → task_agent/__init__.py} +1 -1
- package/server/nodes/agent/task_agent/meta.json +3 -0
- package/server/nodes/agent/{tool_agent.py → tool_agent/__init__.py} +1 -1
- package/server/nodes/agent/tool_agent/meta.json +3 -0
- package/server/nodes/agent/{travel_agent.py → travel_agent/__init__.py} +1 -1
- package/server/nodes/agent/travel_agent/meta.json +3 -0
- package/server/nodes/agent/{web_agent.py → web_agent/__init__.py} +1 -1
- package/server/nodes/agent/web_agent/meta.json +3 -0
- package/server/nodes/android/__init__.py +24 -0
- package/server/nodes/android/_base.py +93 -76
- package/server/nodes/android/_dispatcher.py +140 -223
- package/server/nodes/android/_events.py +154 -0
- package/server/nodes/android/_handlers.py +13 -7
- package/server/nodes/android/_option_loaders.py +1 -4
- package/server/nodes/android/_refresh.py +27 -37
- package/server/nodes/android/_relay/broadcaster.py +25 -41
- package/server/nodes/android/_relay/client.py +23 -42
- package/server/nodes/android/_relay/manager.py +1 -0
- package/server/nodes/android/_relay/protocol.py +6 -0
- package/server/nodes/android/_router.py +48 -133
- package/server/nodes/android/{airplane_mode_control.py → airplane_mode_control/__init__.py} +2 -1
- package/server/nodes/android/airplane_mode_control/meta.json +3 -0
- package/server/nodes/android/{app_launcher.py → app_launcher/__init__.py} +2 -1
- package/server/nodes/android/app_launcher/meta.json +3 -0
- package/server/nodes/android/{app_list.py → app_list/__init__.py} +2 -1
- package/server/nodes/android/app_list/meta.json +3 -0
- package/server/nodes/android/{audio_automation.py → audio_automation/__init__.py} +2 -1
- package/server/nodes/android/audio_automation/meta.json +3 -0
- package/server/nodes/android/{battery_monitor.py → battery_monitor/__init__.py} +2 -1
- package/server/nodes/android/battery_monitor/meta.json +3 -0
- package/server/nodes/android/{bluetooth_automation.py → bluetooth_automation/__init__.py} +2 -1
- package/server/nodes/android/bluetooth_automation/meta.json +3 -0
- package/server/nodes/android/{camera_control.py → camera_control/__init__.py} +2 -1
- package/server/nodes/android/camera_control/meta.json +3 -0
- package/server/nodes/android/{device_state_automation.py → device_state_automation/__init__.py} +2 -1
- package/server/nodes/android/device_state_automation/meta.json +3 -0
- package/server/nodes/android/{environmental_sensors.py → environmental_sensors/__init__.py} +2 -1
- package/server/nodes/android/environmental_sensors/meta.json +3 -0
- package/server/nodes/android/{location.py → location/__init__.py} +2 -1
- package/server/nodes/android/location/meta.json +3 -0
- package/server/nodes/android/{media_control.py → media_control/__init__.py} +2 -1
- package/server/nodes/android/media_control/meta.json +3 -0
- package/server/nodes/android/{motion_detection.py → motion_detection/__init__.py} +2 -1
- package/server/nodes/android/motion_detection/meta.json +3 -0
- package/server/nodes/android/{network_monitor.py → network_monitor/__init__.py} +2 -1
- package/server/nodes/android/network_monitor/meta.json +3 -0
- package/server/nodes/android/{screen_control_automation.py → screen_control_automation/__init__.py} +2 -1
- package/server/nodes/android/screen_control_automation/meta.json +3 -0
- package/server/nodes/android/{system_info.py → system_info/__init__.py} +2 -1
- package/server/nodes/android/system_info/meta.json +3 -0
- package/server/nodes/android/{wifi_automation.py → wifi_automation/__init__.py} +2 -1
- package/server/nodes/android/wifi_automation/meta.json +3 -0
- package/server/nodes/browser/__init__.py +22 -1
- package/server/nodes/browser/_install.py +63 -0
- package/server/nodes/browser/_service.py +21 -25
- package/server/nodes/browser/{browser.py → browser/__init__.py} +58 -25
- package/server/nodes/browser/browser/meta.json +3 -0
- package/server/nodes/chat/{chat_history.py → chat_history/__init__.py} +2 -4
- package/server/nodes/chat/chat_history/meta.json +3 -0
- package/server/nodes/chat/{chat_send.py → chat_send/__init__.py} +2 -4
- package/server/nodes/chat/chat_send/icon.svg +1 -0
- package/server/nodes/chat/chat_send/meta.json +3 -0
- package/server/nodes/code/_base.py +1 -1
- package/server/nodes/code/{javascript_executor.py → javascript_executor/__init__.py} +5 -5
- package/server/nodes/code/javascript_executor/meta.json +3 -0
- package/server/nodes/code/{python_executor.py → python_executor/__init__.py} +32 -14
- package/server/nodes/code/python_executor/meta.json +3 -0
- package/server/nodes/code/{typescript_executor.py → typescript_executor/__init__.py} +5 -5
- package/server/nodes/code/typescript_executor/meta.json +3 -0
- package/server/nodes/document/{document_parser.py → document_parser/__init__.py} +26 -15
- package/server/nodes/document/document_parser/meta.json +3 -0
- package/server/nodes/document/{embedding_generator.py → embedding_generator/__init__.py} +16 -9
- package/server/nodes/document/embedding_generator/meta.json +3 -0
- package/server/nodes/document/{file_downloader.py → file_downloader/__init__.py} +30 -20
- package/server/nodes/document/file_downloader/meta.json +3 -0
- package/server/nodes/document/{http_scraper.py → http_scraper/__init__.py} +31 -21
- package/server/nodes/document/http_scraper/meta.json +3 -0
- package/server/nodes/document/{text_chunker.py → text_chunker/__init__.py} +17 -12
- package/server/nodes/document/text_chunker/meta.json +3 -0
- package/server/nodes/document/{vector_store.py → vector_store/__init__.py} +88 -72
- package/server/nodes/document/vector_store/meta.json +3 -0
- package/server/nodes/email/__init__.py +9 -2
- package/server/nodes/email/_events.py +54 -0
- package/server/nodes/email/_filters.py +3 -3
- package/server/nodes/email/_himalaya.py +95 -50
- package/server/nodes/email/_service.py +23 -13
- package/server/nodes/email/{email_read.py → email_read/__init__.py} +23 -11
- package/server/nodes/email/email_read/icon.svg +6 -0
- package/server/nodes/email/email_read/meta.json +3 -0
- package/server/nodes/email/{email_receive.py → email_receive/__init__.py} +45 -23
- package/server/nodes/email/email_receive/meta.json +3 -0
- package/server/nodes/email/{email_send.py → email_send/__init__.py} +13 -7
- package/server/nodes/email/email_send/meta.json +3 -0
- package/server/nodes/filesystem/_backend.py +1 -5
- package/server/nodes/filesystem/{file_modify.py → file_modify/__init__.py} +10 -5
- package/server/nodes/filesystem/file_modify/meta.json +3 -0
- package/server/nodes/filesystem/{file_read.py → file_read/__init__.py} +7 -3
- package/server/nodes/filesystem/file_read/meta.json +3 -0
- package/server/nodes/filesystem/{fs_search.py → fs_search/__init__.py} +11 -3
- package/server/nodes/filesystem/fs_search/meta.json +3 -0
- package/server/nodes/filesystem/{shell.py → shell/__init__.py} +12 -5
- package/server/nodes/filesystem/shell/meta.json +3 -0
- package/server/nodes/google/__init__.py +12 -0
- package/server/nodes/google/_auth_helper.py +7 -13
- package/server/nodes/google/_base.py +14 -11
- package/server/nodes/google/_credentials.py +2 -1
- package/server/nodes/google/_events.py +47 -0
- package/server/nodes/google/_filters.py +3 -3
- package/server/nodes/google/_gmail.py +70 -47
- package/server/nodes/google/_handlers.py +3 -1
- package/server/nodes/google/_oauth.py +25 -11
- package/server/nodes/google/_option_loaders.py +9 -30
- package/server/nodes/google/_refresh.py +8 -12
- package/server/nodes/google/_router.py +4 -5
- package/server/nodes/google/{calendar.py → calendar/__init__.py} +87 -64
- package/server/nodes/google/calendar/meta.json +3 -0
- package/server/nodes/google/{contacts.py → contacts/__init__.py} +84 -72
- package/server/nodes/google/contacts/meta.json +3 -0
- package/server/nodes/google/{drive.py → drive/__init__.py} +87 -72
- package/server/nodes/google/drive/meta.json +3 -0
- package/server/nodes/google/{gmail.py → gmail/__init__.py} +73 -39
- package/server/nodes/google/gmail/meta.json +3 -0
- package/server/nodes/google/{gmail_receive.py → gmail_receive/__init__.py} +31 -24
- package/server/nodes/google/gmail_receive/icon.svg +7 -0
- package/server/nodes/google/gmail_receive/meta.json +3 -0
- package/server/nodes/google/google.svg +7 -0
- package/server/nodes/google/{sheets.py → sheets/__init__.py} +54 -42
- package/server/nodes/google/sheets/meta.json +3 -0
- package/server/nodes/google/{tasks.py → tasks/__init__.py} +56 -43
- package/server/nodes/google/tasks/meta.json +3 -0
- package/server/nodes/groups.py +28 -28
- package/server/nodes/location/__init__.py +31 -1
- package/server/nodes/location/_credentials.py +1 -6
- package/server/nodes/location/_service.py +88 -107
- package/server/nodes/location/{gmaps_create.py → gmaps_create/__init__.py} +6 -6
- package/server/nodes/location/gmaps_create/meta.json +3 -0
- package/server/nodes/location/{gmaps_locations.py → gmaps_locations/__init__.py} +8 -6
- package/server/nodes/location/gmaps_locations/meta.json +3 -0
- package/server/nodes/location/{gmaps_nearby_places.py → gmaps_nearby_places/__init__.py} +8 -6
- package/server/nodes/location/gmaps_nearby_places/meta.json +3 -0
- package/server/nodes/model/_base.py +10 -7
- package/server/nodes/model/_credentials.py +10 -10
- package/server/nodes/model/_local_validator.py +28 -24
- package/server/nodes/model/{anthropic_chat_model.py → anthropic_chat_model/__init__.py} +5 -3
- package/server/nodes/model/anthropic_chat_model/meta.json +3 -0
- package/server/nodes/model/{cerebras_chat_model.py → cerebras_chat_model/__init__.py} +5 -3
- package/server/nodes/model/cerebras_chat_model/meta.json +3 -0
- package/server/nodes/model/{deepseek_chat_model.py → deepseek_chat_model/__init__.py} +8 -4
- package/server/nodes/model/deepseek_chat_model/meta.json +3 -0
- package/server/nodes/model/{gemini_chat_model.py → gemini_chat_model/__init__.py} +5 -3
- package/server/nodes/model/gemini_chat_model/meta.json +3 -0
- package/server/nodes/model/{groq_chat_model.py → groq_chat_model/__init__.py} +2 -2
- package/server/nodes/model/groq_chat_model/meta.json +3 -0
- package/server/nodes/model/{kimi_chat_model.py → kimi_chat_model/__init__.py} +2 -2
- package/server/nodes/model/kimi_chat_model/meta.json +3 -0
- package/server/nodes/model/{lmstudio_chat_model.py → lmstudio_chat_model/__init__.py} +2 -2
- package/server/nodes/model/lmstudio_chat_model/meta.json +3 -0
- package/server/nodes/model/{mistral_chat_model.py → mistral_chat_model/__init__.py} +2 -2
- package/server/nodes/model/mistral_chat_model/meta.json +3 -0
- package/server/nodes/model/{ollama_chat_model.py → ollama_chat_model/__init__.py} +2 -2
- package/server/nodes/model/ollama_chat_model/meta.json +3 -0
- package/server/nodes/model/{openai_chat_model.py → openai_chat_model/__init__.py} +8 -4
- package/server/nodes/model/openai_chat_model/meta.json +3 -0
- package/server/nodes/model/{openrouter_chat_model.py → openrouter_chat_model/__init__.py} +8 -4
- package/server/nodes/model/openrouter_chat_model/meta.json +3 -0
- package/server/nodes/proxy/_usage.py +14 -15
- package/server/nodes/proxy/{proxy_config.py → proxy_config/__init__.py} +39 -30
- package/server/nodes/proxy/proxy_config/meta.json +3 -0
- package/server/nodes/proxy/{proxy_request.py → proxy_request/__init__.py} +30 -16
- package/server/nodes/proxy/proxy_request/meta.json +3 -0
- package/server/nodes/proxy/{proxy_status.py → proxy_status/__init__.py} +2 -0
- package/server/nodes/proxy/proxy_status/meta.json +3 -0
- package/server/nodes/scheduler/{cron_scheduler.py → cron_scheduler/__init__.py} +96 -23
- package/server/nodes/scheduler/cron_scheduler/_workflow.py +155 -0
- package/server/nodes/scheduler/cron_scheduler/meta.json +3 -0
- package/server/nodes/scheduler/{timer.py → timer/__init__.py} +6 -5
- package/server/nodes/scheduler/timer/meta.json +3 -0
- package/server/nodes/scraper/_credentials.py +0 -1
- package/server/nodes/scraper/{apify_actor.py → apify_actor/__init__.py} +44 -35
- package/server/nodes/scraper/apify_actor/icon.svg +5 -0
- package/server/nodes/scraper/apify_actor/meta.json +3 -0
- package/server/nodes/scraper/{crawlee_scraper.py → crawlee_scraper/__init__.py} +96 -57
- package/server/nodes/scraper/crawlee_scraper/meta.json +3 -0
- package/server/nodes/search/{brave_search.py → brave_search/__init__.py} +6 -5
- package/server/nodes/search/brave_search/icon.svg +3 -0
- package/server/nodes/search/brave_search/meta.json +3 -0
- package/server/nodes/search/{duckduckgo_search.py → duckduckgo_search/__init__.py} +17 -6
- package/server/nodes/search/duckduckgo_search/meta.json +3 -0
- package/server/nodes/search/{perplexity_search.py → perplexity_search/__init__.py} +4 -5
- package/server/nodes/search/perplexity_search/icon.svg +3 -0
- package/server/nodes/search/perplexity_search/meta.json +3 -0
- package/server/nodes/search/{serper_search.py → serper_search/__init__.py} +32 -25
- package/server/nodes/search/serper_search/icon.svg +3 -0
- package/server/nodes/search/serper_search/meta.json +3 -0
- package/server/nodes/skill/__init__.py +21 -1
- package/server/nodes/skill/_expander.py +75 -0
- package/server/nodes/skill/{master_skill.py → master_skill/__init__.py} +2 -8
- package/server/nodes/skill/master_skill/_events.py +84 -0
- package/server/nodes/skill/master_skill/meta.json +3 -0
- package/server/nodes/skill/{simple_memory.py → simple_memory/__init__.py} +8 -16
- package/server/nodes/skill/simple_memory/meta.json +3 -0
- package/server/nodes/social/_base.py +223 -231
- package/server/nodes/social/{social_receive.py → social_receive/__init__.py} +38 -13
- package/server/nodes/social/social_receive/meta.json +3 -0
- package/server/nodes/social/{social_send.py → social_send/__init__.py} +71 -29
- package/server/nodes/social/social_send/icon.svg +1 -0
- package/server/nodes/social/social_send/meta.json +3 -0
- package/server/nodes/stripe/__init__.py +7 -3
- package/server/nodes/stripe/_credentials.py +0 -1
- package/server/nodes/stripe/_handlers.py +18 -7
- package/server/nodes/stripe/_install.py +14 -15
- package/server/nodes/stripe/_source.py +5 -5
- package/server/nodes/stripe/icon.svg +1 -0
- package/server/nodes/stripe/meta.json +3 -0
- package/server/nodes/stripe/stripe_action.py +4 -4
- package/server/nodes/stripe/stripe_receive.py +6 -9
- package/server/nodes/telegram/__init__.py +13 -0
- package/server/nodes/telegram/_credentials.py +2 -7
- package/server/nodes/telegram/_events.py +167 -0
- package/server/nodes/telegram/_filters.py +3 -11
- package/server/nodes/telegram/_handlers.py +17 -7
- package/server/nodes/telegram/_refresh.py +24 -34
- package/server/nodes/telegram/_service.py +29 -45
- package/server/nodes/telegram/meta.json +3 -0
- package/server/nodes/telegram/telegram.svg +3 -0
- package/server/nodes/telegram/telegram_receive.py +38 -18
- package/server/nodes/telegram/telegram_send.py +21 -19
- package/server/nodes/text/{file_handler.py → file_handler/__init__.py} +7 -1
- package/server/nodes/text/file_handler/meta.json +3 -0
- package/server/nodes/text/{text_generator.py → text_generator/__init__.py} +2 -1
- package/server/nodes/text/text_generator/meta.json +3 -0
- package/server/nodes/tool/{agent_builder.py → agent_builder/__init__.py} +105 -100
- package/server/nodes/tool/agent_builder/_events.py +91 -0
- package/server/nodes/tool/agent_builder/meta.json +3 -0
- package/server/nodes/tool/{calculator_tool.py → calculator_tool/__init__.py} +19 -7
- package/server/nodes/tool/calculator_tool/meta.json +3 -0
- package/server/nodes/tool/{current_time_tool.py → current_time_tool/__init__.py} +6 -4
- package/server/nodes/tool/current_time_tool/meta.json +3 -0
- package/server/nodes/tool/{task_manager.py → task_manager/__init__.py} +17 -18
- package/server/nodes/tool/task_manager/meta.json +3 -0
- package/server/nodes/tool/{write_todos.py → write_todos/__init__.py} +20 -6
- package/server/nodes/tool/write_todos/meta.json +3 -0
- package/server/nodes/trigger/{chat_trigger.py → chat_trigger/__init__.py} +11 -7
- package/server/nodes/trigger/chat_trigger/_events.py +53 -0
- package/server/nodes/trigger/chat_trigger/meta.json +3 -0
- package/server/nodes/trigger/{task_trigger.py → task_trigger/__init__.py} +10 -7
- package/server/nodes/trigger/task_trigger/meta.json +3 -0
- package/server/nodes/trigger/{webhook_trigger.py → webhook_trigger/__init__.py} +10 -7
- package/server/nodes/trigger/webhook_trigger/_events.py +54 -0
- package/server/nodes/trigger/webhook_trigger/meta.json +3 -0
- package/server/nodes/twitter/__init__.py +7 -1
- package/server/nodes/twitter/_base.py +86 -61
- package/server/nodes/twitter/_credentials.py +7 -5
- package/server/nodes/twitter/_events.py +101 -0
- package/server/nodes/twitter/_filters.py +9 -9
- package/server/nodes/twitter/_handlers.py +3 -1
- package/server/nodes/twitter/_oauth.py +1 -2
- package/server/nodes/twitter/_refresh.py +8 -12
- package/server/nodes/twitter/{twitter_receive.py → twitter_receive/__init__.py} +7 -7
- package/server/nodes/twitter/twitter_receive/icon.svg +1 -0
- package/server/nodes/twitter/twitter_receive/meta.json +3 -0
- package/server/nodes/twitter/{twitter_search.py → twitter_search/__init__.py} +16 -11
- package/server/nodes/twitter/twitter_search/icon.svg +1 -0
- package/server/nodes/twitter/twitter_search/meta.json +3 -0
- package/server/nodes/twitter/{twitter_send.py → twitter_send/__init__.py} +60 -27
- package/server/nodes/twitter/twitter_send/icon.svg +1 -0
- package/server/nodes/twitter/twitter_send/meta.json +3 -0
- package/server/nodes/twitter/{twitter_user.py → twitter_user/__init__.py} +34 -19
- package/server/nodes/twitter/twitter_user/icon.svg +1 -0
- package/server/nodes/twitter/twitter_user/meta.json +3 -0
- package/server/nodes/utility/{console.py → console/__init__.py} +17 -22
- package/server/nodes/utility/console/meta.json +3 -0
- package/server/nodes/utility/{http_request.py → http_request/__init__.py} +9 -6
- package/server/nodes/utility/http_request/meta.json +3 -0
- package/server/nodes/utility/{process_manager.py → process_manager/__init__.py} +10 -6
- package/server/nodes/utility/process_manager/meta.json +3 -0
- package/server/nodes/utility/team_monitor/meta.json +3 -0
- package/server/nodes/utility/{webhook_response.py → webhook_response/__init__.py} +12 -7
- package/server/nodes/utility/webhook_response/meta.json +3 -0
- package/server/nodes/visuals.json +69 -251
- package/server/nodes/whatsapp/__init__.py +24 -0
- package/server/nodes/whatsapp/_base.py +283 -338
- package/server/nodes/whatsapp/_credentials.py +44 -0
- package/server/nodes/whatsapp/_events.py +277 -0
- package/server/nodes/whatsapp/_filters.py +36 -37
- package/server/nodes/whatsapp/_handlers.py +2 -0
- package/server/nodes/whatsapp/_option_loaders.py +1 -3
- package/server/nodes/whatsapp/_refresh.py +13 -18
- package/server/nodes/whatsapp/_runtime.py +9 -6
- package/server/nodes/whatsapp/_service.py +89 -152
- package/server/nodes/whatsapp/meta.json +3 -0
- package/server/nodes/whatsapp/whatsapp_db.py +116 -54
- package/server/nodes/whatsapp/whatsapp_receive.py +30 -13
- package/server/nodes/whatsapp/whatsapp_send.py +60 -37
- package/server/nodes/workflow/{start.py → start/__init__.py} +1 -4
- package/server/nodes/workflow/start/meta.json +3 -0
- package/server/package-lock.json +3 -3
- package/server/package.json +3 -0
- package/server/pyproject.toml +39 -10
- package/server/requirements.txt +3 -5
- package/server/routers/__init__.py +1 -1
- package/server/routers/auth.py +16 -56
- package/server/routers/database.py +27 -50
- package/server/routers/nodejs_compat.py +25 -87
- package/server/routers/schemas.py +66 -2
- package/server/routers/webhook.py +12 -12
- package/server/routers/websocket.py +312 -1716
- package/server/routers/workflow.py +28 -53
- package/server/scripts/smoke_test_skills.py +178 -0
- package/server/services/__init__.py +1 -1
- package/server/services/_supervisor/process.py +9 -3
- package/server/services/_supervisor/registry.py +3 -3
- package/server/services/_supervisor/util.py +1 -1
- package/server/services/agent_team.py +15 -43
- package/server/services/agent_teams/__init__.py +17 -0
- package/server/services/agent_teams/handlers.py +195 -0
- package/server/services/ai.py +853 -1108
- package/server/services/auth.py +10 -34
- package/server/services/chat_client.py +5 -34
- package/server/services/circuit_breaker.py +2 -6
- package/server/services/cli_agent/__init__.py +28 -4
- package/server/services/cli_agent/_cli_auth.py +61 -0
- package/server/services/cli_agent/_handlers.py +24 -183
- package/server/services/cli_agent/config.py +5 -8
- package/server/services/cli_agent/factory.py +168 -22
- package/server/services/cli_agent/jsonl_watcher.py +380 -0
- package/server/services/cli_agent/lockfile.py +9 -2
- package/server/services/cli_agent/mcp_server.py +110 -34
- package/server/services/cli_agent/protocol.py +37 -19
- package/server/services/cli_agent/providers/__init__.py +8 -4
- package/server/services/cli_agent/providers/google_gemini.py +11 -5
- package/server/services/cli_agent/providers/openai_codex.py +34 -34
- package/server/services/cli_agent/service.py +245 -83
- package/server/services/cli_agent/session.py +409 -229
- package/server/services/cli_agent/transports/__init__.py +47 -0
- package/server/services/cli_agent/transports/base.py +111 -0
- package/server/services/cli_agent/transports/posix.py +196 -0
- package/server/services/cli_agent/transports/windows.py +189 -0
- package/server/services/cli_agent/types.py +45 -18
- package/server/services/cli_agent/workflow_tools.py +28 -15
- package/server/services/compaction.py +68 -52
- package/server/services/credential_registry.py +6 -20
- package/server/services/credentials/__init__.py +18 -0
- package/server/services/credentials/handlers.py +196 -0
- package/server/services/deployment/__init__.py +12 -1
- package/server/services/deployment/canary_registry.py +137 -0
- package/server/services/deployment/handlers.py +382 -0
- package/server/services/deployment/manager.py +653 -163
- package/server/services/deployment/poll_registry.py +2 -6
- package/server/services/deployment/state.py +2 -0
- package/server/services/deployment/triggers.py +87 -93
- package/server/services/event_waiter.py +47 -54
- package/server/services/events/__init__.py +11 -0
- package/server/services/events/admin_handlers.py +368 -0
- package/server/services/events/daemon.py +3 -1
- package/server/services/events/dispatch.py +188 -0
- package/server/services/events/envelope.py +264 -45
- package/server/services/events/oauth_lifecycle.py +98 -42
- package/server/services/events/triggers.py +3 -13
- package/server/services/events/verifiers/hmac_basic.py +1 -1
- package/server/services/events/verifiers/standard_webhooks.py +2 -4
- package/server/services/events/webhook.py +2 -3
- package/server/services/example_loader.py +73 -15
- package/server/services/execution/cache.py +36 -76
- package/server/services/execution/conditions.py +7 -20
- package/server/services/execution/dlq.py +20 -24
- package/server/services/execution/executor.py +234 -265
- package/server/services/execution/models.py +40 -46
- package/server/services/execution/recovery.py +23 -46
- package/server/services/handlers/__init__.py +12 -16
- package/server/services/handlers/todo.py +3 -6
- package/server/services/handlers/tools.py +143 -194
- package/server/services/handlers/triggers.py +24 -23
- package/server/services/llm/config.py +10 -1
- package/server/services/llm/factory.py +16 -4
- package/server/services/llm/messages.py +1 -5
- package/server/services/llm/protocol.py +9 -1
- package/server/services/llm/providers/anthropic.py +23 -12
- package/server/services/llm/providers/gemini.py +43 -22
- package/server/services/llm/providers/openai.py +14 -6
- package/server/services/llm/providers/openrouter.py +6 -1
- package/server/services/markdown_formatter.py +1 -2
- package/server/services/memory/__init__.py +2 -2
- package/server/services/memory/jsonl.py +6 -2
- package/server/services/memory/markdown.py +6 -6
- package/server/services/memory/state.py +6 -5
- package/server/services/memory_store.py +8 -12
- package/server/services/model_registry.py +22 -20
- package/server/services/node_executor.py +85 -80
- package/server/services/node_output_schemas.py +4 -7
- package/server/services/node_registry.py +40 -4
- package/server/services/node_spec.py +3 -7
- package/server/services/nodejs_client.py +4 -14
- package/server/services/oauth_utils.py +11 -7
- package/server/services/parameter_resolver.py +30 -36
- package/server/services/plugin/base.py +321 -38
- package/server/services/plugin/connection.py +12 -7
- package/server/services/plugin/credential.py +80 -22
- package/server/services/plugin/edge_walker.py +128 -105
- package/server/services/plugin/identifiers.py +48 -0
- package/server/services/plugin/interceptor.py +1 -1
- package/server/services/plugin/oauth.py +25 -21
- package/server/services/plugin/operation.py +1 -1
- package/server/services/plugin/polling.py +151 -26
- package/server/services/plugin/registry.py +52 -4
- package/server/services/plugin/routing.py +6 -9
- package/server/services/plugin/scaling.py +36 -18
- package/server/services/plugin/service_factories.py +95 -0
- package/server/services/plugin/shutdown_hooks.py +103 -0
- package/server/services/plugin/social_provider_registry.py +80 -0
- package/server/services/plugin/ws.py +2 -1
- package/server/services/pricing.py +26 -40
- package/server/services/pricing_handlers.py +90 -0
- package/server/services/process_service.py +33 -32
- package/server/services/proxy/models.py +15 -9
- package/server/services/proxy/service.py +26 -40
- package/server/services/rlm/adapters.py +43 -40
- package/server/services/rlm/constants.py +9 -9
- package/server/services/rlm/service.py +57 -45
- package/server/services/scheduler.py +8 -39
- package/server/services/settings/__init__.py +16 -0
- package/server/services/settings/handlers.py +275 -0
- package/server/services/skill_loader.py +53 -45
- package/server/services/skill_prompt.py +8 -6
- package/server/services/skills/__init__.py +23 -0
- package/server/services/skills/handlers.py +479 -0
- package/server/services/status_broadcaster.py +314 -291
- package/server/services/temporal/__init__.py +22 -1
- package/server/services/temporal/_handlers.py +65 -0
- package/server/services/temporal/_install.py +158 -0
- package/server/services/temporal/_refresh.py +57 -0
- package/server/services/temporal/_retry_policies.py +85 -0
- package/server/services/temporal/_runtime.py +181 -0
- package/server/services/temporal/_supervised_runtime.py +102 -0
- package/server/services/temporal/activities.py +168 -11
- package/server/services/temporal/agent_activities.py +683 -0
- package/server/services/temporal/agent_workflow.py +601 -0
- package/server/services/temporal/client.py +58 -13
- package/server/services/temporal/executor.py +2 -3
- package/server/services/temporal/plugin_activities.py +37 -2
- package/server/services/temporal/plugin_registry.py +82 -0
- package/server/services/temporal/polling_trigger_workflow.py +267 -0
- package/server/services/temporal/schedules.py +220 -0
- package/server/services/temporal/search_attributes.py +177 -0
- package/server/services/temporal/trigger_listener_workflow.py +378 -0
- package/server/services/temporal/worker.py +111 -18
- package/server/services/temporal/workflow.py +259 -40
- package/server/services/temporal/ws_client.py +22 -11
- package/server/services/text.py +14 -28
- package/server/services/tracked_http.py +29 -49
- package/server/services/user_auth.py +7 -21
- package/server/services/workflow.py +28 -20
- package/server/services/workflow_import.py +351 -0
- package/server/services/workflow_ops.py +4 -0
- package/server/services/workflow_storage/__init__.py +18 -0
- package/server/services/workflow_storage/handlers.py +132 -0
- package/server/services/workflow_validator.py +209 -0
- package/server/services/ws_handler_registry.py +80 -9
- package/server/skills/assistant/agent-builder-skill/SKILL.md +6 -6
- package/server/tests/conftest.py +54 -3
- package/server/tests/credentials/test_auth_service.py +9 -21
- package/server/tests/credentials/test_credential_broadcasts.py +116 -22
- package/server/tests/credentials/test_credentials_database.py +12 -38
- package/server/tests/credentials/test_encryption.py +3 -9
- package/server/tests/credentials/test_google_oauth.py +1 -3
- package/server/tests/credentials/test_oauth_utils.py +31 -38
- package/server/tests/credentials/test_twitter_oauth.py +1 -3
- package/server/tests/credentials/test_websocket_handlers.py +37 -72
- package/server/tests/fixtures/tool_names_snapshot.json +78 -0
- package/server/tests/llm/test_factory.py +12 -4
- package/server/tests/llm/test_providers.py +25 -32
- package/server/tests/llm/test_wiring.py +27 -22
- package/server/tests/nodes/_compat.py +4 -5
- package/server/tests/nodes/_harness.py +31 -24
- package/server/tests/nodes/_mocks.py +2 -6
- package/server/tests/nodes/test_agent_builder.py +43 -35
- package/server/tests/nodes/test_ai_agents.py +29 -24
- package/server/tests/nodes/test_ai_chat_models.py +3 -9
- package/server/tests/nodes/test_ai_tools.py +29 -24
- package/server/tests/nodes/test_android.py +34 -64
- package/server/tests/nodes/test_chat_utility.py +2 -2
- package/server/tests/nodes/test_code_fs_process.py +26 -84
- package/server/tests/nodes/test_document.py +23 -47
- package/server/tests/nodes/test_email.py +88 -51
- package/server/tests/nodes/test_google_workspace.py +26 -20
- package/server/tests/nodes/test_http_proxy.py +43 -89
- package/server/tests/nodes/test_search.py +3 -9
- package/server/tests/nodes/test_specialized_agents.py +58 -162
- package/server/tests/nodes/test_stripe_plugin.py +25 -5
- package/server/tests/nodes/test_telegram_social.py +33 -37
- package/server/tests/nodes/test_twitter.py +59 -150
- package/server/tests/nodes/test_web_automation.py +21 -51
- package/server/tests/nodes/test_whatsapp.py +13 -19
- package/server/tests/nodes/test_workflow_triggers.py +16 -45
- package/server/tests/services/cli_agent/test_claude_session_events.py +201 -0
- package/server/tests/services/cli_agent/test_jsonl_watcher.py +190 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +67 -29
- package/server/tests/services/cli_agent/test_providers.py +236 -47
- package/server/tests/services/cli_agent/test_service.py +9 -7
- package/server/tests/services/memory/test_jsonl.py +30 -25
- package/server/tests/services/test_events.py +26 -7
- package/server/tests/services/test_identifiers.py +122 -0
- package/server/tests/services/test_process_lifecycle.py +129 -0
- package/server/tests/services/test_supervisor.py +0 -1
- package/server/tests/temporal/__init__.py +0 -0
- package/server/tests/temporal/test_agent_workflow.py +215 -0
- package/server/tests/temporal/test_dispatch.py +231 -0
- package/server/tests/test_admin_handlers.py +394 -0
- package/server/tests/test_auto_skill.py +4 -2
- package/server/tests/test_canary_registry.py +310 -0
- package/server/tests/test_chat_trigger_canary_producer.py +101 -0
- package/server/tests/test_cloudevents_node_parameters.py +129 -0
- package/server/tests/test_credential_icon.py +115 -0
- package/server/tests/test_cron_canary.py +511 -0
- package/server/tests/test_deployment_canary_listener.py +692 -0
- package/server/tests/test_event_framework_phase_a.py +537 -0
- package/server/tests/test_no_raw_prints.py +131 -0
- package/server/tests/test_node_spec.py +196 -103
- package/server/tests/test_parameter_resolver.py +20 -20
- package/server/tests/test_plugin_contract.py +76 -49
- package/server/tests/test_plugin_helpers.py +0 -1
- package/server/tests/test_plugin_self_containment.py +40 -47
- package/server/tests/test_polling_trigger_workflow.py +572 -0
- package/server/tests/test_retry_policies.py +146 -0
- package/server/tests/test_service_factories.py +168 -0
- package/server/tests/test_shutdown_hooks.py +199 -0
- package/server/tests/test_social_provider_registry.py +177 -0
- package/server/tests/test_status_broadcasts.py +214 -63
- package/server/tests/test_task_trigger_canary_producer.py +131 -0
- package/server/tests/test_telegram_trigger_canary_producer.py +113 -0
- package/server/tests/test_tool_registry.py +110 -0
- package/server/tests/test_trigger_listener_workflow.py +365 -0
- package/server/tests/test_whatsapp_trigger_canary_producer.py +164 -0
- package/server/tests/test_workflow_ops.py +1 -3
- package/server/tests/test_workflow_validator.py +791 -0
- package/server/uv.lock +3539 -0
- package/client/dist/assets/index-DQ0nwhec.js +0 -257
- package/client/src/assets/icons/apify/index.ts +0 -19
- package/client/src/assets/icons/browser/index.ts +0 -17
- package/client/src/assets/icons/email/index.ts +0 -22
- package/client/src/assets/icons/google/index.ts +0 -34
- package/client/src/assets/icons/llm/deepseek.svg +0 -1
- package/client/src/assets/icons/llm/index.ts +0 -18
- package/client/src/assets/icons/llm/kimi.svg +0 -1
- package/client/src/assets/icons/llm/mistral.svg +0 -1
- package/client/src/assets/icons/search/index.ts +0 -28
- package/client/src/assets/icons/telegram/index.ts +0 -19
- package/machina/buildenv.py +0 -44
- package/machina/cli.py +0 -55
- package/machina/commands/__init__.py +0 -1
- package/machina/commands/clean.py +0 -80
- package/machina/commands/daemon.py +0 -150
- package/machina/config.py +0 -93
- package/machina/platform_.py +0 -37
- package/machina/pyproject.toml +0 -33
- package/server/nodes/agent/deep_agent.py +0 -103
- package/server/services/agents/__init__.py +0 -9
- package/server/services/agents/adapters.py +0 -199
- package/server/services/agents/constants.py +0 -10
- package/server/services/agents/service.py +0 -297
- package/server/services/cli_agent/providers/anthropic_claude.py +0 -419
- /package/{machina → cli}/README.md +0 -0
- /package/{machina → cli}/__init__.py +0 -0
- /package/{client/src/assets/icons/apify → server/credentials/icons}/apify.svg +0 -0
- /package/{client/src/assets/icons/search/brave.svg → server/credentials/icons/brave_search.svg} +0 -0
- /package/{client/src/assets/icons/email/read.svg → server/credentials/icons/email_himalaya.svg} +0 -0
- /package/{client/src/assets/icons/search → server/credentials/icons}/perplexity.svg +0 -0
- /package/{client/src/assets/icons/search/google.svg → server/credentials/icons/serper.svg} +0 -0
- /package/{client/src/assets → server/credentials}/icons/stripe.svg +0 -0
- /package/{client/src/assets/icons/twitter/x.svg → server/credentials/icons/twitter.svg} +0 -0
- /package/{client/src/assets/icons/browser/chrome.svg → server/nodes/browser/browser/icon.svg} +0 -0
- /package/{client/src/assets/icons/chat/chat.svg → server/nodes/chat/chat_history/icon.svg} +0 -0
- /package/{client/src/assets/icons/code/javascript.svg → server/nodes/code/javascript_executor/icon.svg} +0 -0
- /package/{client/src/assets/icons/code/python.svg → server/nodes/code/python_executor/icon.svg} +0 -0
- /package/{client/src/assets/icons/code/typescript.svg → server/nodes/code/typescript_executor/icon.svg} +0 -0
- /package/{client/src/assets/icons/email/receive.svg → server/nodes/email/email_receive/icon.svg} +0 -0
- /package/{client/src/assets/icons/email/send.svg → server/nodes/email/email_send/icon.svg} +0 -0
- /package/{client/src/assets/icons/google/calendar.svg → server/nodes/google/calendar/icon.svg} +0 -0
- /package/{client/src/assets/icons/google/contacts.svg → server/nodes/google/contacts/icon.svg} +0 -0
- /package/{client/src/assets/icons/google/drive.svg → server/nodes/google/drive/icon.svg} +0 -0
- /package/{client/src/assets/icons/google/gmail.svg → server/nodes/google/gmail/icon.svg} +0 -0
- /package/{client/src/assets/icons/google/sheets.svg → server/nodes/google/sheets/icon.svg} +0 -0
- /package/{client/src/assets/icons/google/tasks.svg → server/nodes/google/tasks/icon.svg} +0 -0
- /package/{client/src/assets/icons/search/duckduckgo.svg → server/nodes/search/duckduckgo_search/icon.svg} +0 -0
- /package/{client/src/assets/icons/social/social.svg → server/nodes/social/social_receive/icon.svg} +0 -0
- /package/{client/src/assets/icons/telegram/telegram.svg → server/nodes/telegram/icon.svg} +0 -0
- /package/server/nodes/utility/{team_monitor.py → team_monitor/__init__.py} +0 -0
- /package/{client/src/assets/icons/whatsapp/whatsapp-db.svg → server/nodes/whatsapp/icon_whatsappDb.svg} +0 -0
- /package/{client/src/assets/icons/whatsapp/whatsapp-receive.svg → server/nodes/whatsapp/icon_whatsappReceive.svg} +0 -0
- /package/{client/src/assets/icons/whatsapp/whatsapp-send.svg → server/nodes/whatsapp/icon_whatsappSend.svg} +0 -0
- /package/{client/src/assets/icons → server/nodes}/whatsapp/whatsapp.svg +0 -0
|
@@ -24,6 +24,33 @@ if TYPE_CHECKING:
|
|
|
24
24
|
logger = get_logger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
# Listener Temporal workflow type-names. Used both for ``start_workflow``
|
|
28
|
+
# and as the Visibility filter for cancellation discovery. Push-based
|
|
29
|
+
# triggers (webhook, chat, task, telegram, whatsapp) use the
|
|
30
|
+
# signal-driven listener; polling triggers (gmail, twitter) use the
|
|
31
|
+
# workflow.sleep-driven listener (Wave 12 C2). Cancel queries both via
|
|
32
|
+
# an OR clause so deployment cancel reaches every canary listener
|
|
33
|
+
# without per-type code.
|
|
34
|
+
_PUSH_LISTENER_WORKFLOW_TYPE = "TriggerListenerWorkflow"
|
|
35
|
+
_POLLING_LISTENER_WORKFLOW_TYPE = "PollingTriggerWorkflow"
|
|
36
|
+
_LISTENER_WORKFLOW_TYPES = (
|
|
37
|
+
_PUSH_LISTENER_WORKFLOW_TYPE,
|
|
38
|
+
_POLLING_LISTENER_WORKFLOW_TYPE,
|
|
39
|
+
)
|
|
40
|
+
# Kept for back-compat with any reader that still imports the old
|
|
41
|
+
# single-type constant. Equals the push workflow name.
|
|
42
|
+
_LISTENER_WORKFLOW_TYPE = _PUSH_LISTENER_WORKFLOW_TYPE
|
|
43
|
+
|
|
44
|
+
# Wave 12 C1 canary: which trigger types route to TriggerListenerWorkflow
|
|
45
|
+
# instead of the legacy in-process collector/processor is owned by the
|
|
46
|
+
# plugins themselves via ``services.deployment.canary_registry``. Each
|
|
47
|
+
# canary-enabled plugin's ``__init__.py`` calls
|
|
48
|
+
# ``register_canary_trigger_type("<node_type>")`` and the deployment
|
|
49
|
+
# manager queries ``is_canary_trigger_type`` here — no framework-side
|
|
50
|
+
# allowlist to drift. Producer side (the plugin's ``_events.py`` calling
|
|
51
|
+
# ``services.events.dispatch.emit``) is the second half of opt-in.
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
class DeploymentManager:
|
|
28
55
|
"""Manages event-driven workflow deployment.
|
|
29
56
|
|
|
@@ -54,11 +81,7 @@ class DeploymentManager:
|
|
|
54
81
|
self._cron_iterations: Dict[str, int] = {} # node_id -> iteration count
|
|
55
82
|
self._main_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
56
83
|
|
|
57
|
-
self._settings = {
|
|
58
|
-
"stop_on_error": False,
|
|
59
|
-
"max_concurrent_runs": 100,
|
|
60
|
-
"use_parallel_executor": True
|
|
61
|
-
}
|
|
84
|
+
self._settings = {"stop_on_error": False, "max_concurrent_runs": 100, "use_parallel_executor": True}
|
|
62
85
|
|
|
63
86
|
@property
|
|
64
87
|
def is_running(self) -> bool:
|
|
@@ -105,7 +128,7 @@ class DeploymentManager:
|
|
|
105
128
|
"success": False,
|
|
106
129
|
"error": f"Workflow {workflow_id} is already deployed",
|
|
107
130
|
"workflow_id": workflow_id,
|
|
108
|
-
"deployment_id": self._deployments[workflow_id].deployment_id
|
|
131
|
+
"deployment_id": self._deployments[workflow_id].deployment_id,
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
# Setup
|
|
@@ -136,7 +159,7 @@ class DeploymentManager:
|
|
|
136
159
|
nodes=nodes,
|
|
137
160
|
edges=edges,
|
|
138
161
|
session_id=session_id,
|
|
139
|
-
settings=self._settings.copy()
|
|
162
|
+
settings=self._settings.copy(),
|
|
140
163
|
)
|
|
141
164
|
|
|
142
165
|
logger.info("Deployment starting", deployment_id=deployment_id, workflow_id=workflow_id, nodes=len(nodes))
|
|
@@ -163,18 +186,16 @@ class DeploymentManager:
|
|
|
163
186
|
triggers_setup.append(info.to_dict())
|
|
164
187
|
|
|
165
188
|
# Notify started
|
|
166
|
-
await self._notify(
|
|
167
|
-
"deployment_id": deployment_id,
|
|
168
|
-
|
|
169
|
-
"triggers": triggers_setup
|
|
170
|
-
}, workflow_id)
|
|
189
|
+
await self._notify(
|
|
190
|
+
"started", {"deployment_id": deployment_id, "workflow_id": workflow_id, "triggers": triggers_setup}, workflow_id
|
|
191
|
+
)
|
|
171
192
|
|
|
172
193
|
return {
|
|
173
194
|
"success": True,
|
|
174
195
|
"deployment_id": deployment_id,
|
|
175
196
|
"workflow_id": workflow_id,
|
|
176
197
|
"message": "Workflow deployed",
|
|
177
|
-
"triggers_setup": triggers_setup
|
|
198
|
+
"triggers_setup": triggers_setup,
|
|
178
199
|
}
|
|
179
200
|
|
|
180
201
|
except Exception as e:
|
|
@@ -244,7 +265,44 @@ class DeploymentManager:
|
|
|
244
265
|
# Cancel event waiters for nodes in this workflow
|
|
245
266
|
waiter_count = 0
|
|
246
267
|
for node in state.nodes:
|
|
247
|
-
waiter_count += event_waiter.cancel_for_node(node[
|
|
268
|
+
waiter_count += event_waiter.cancel_for_node(node["id"])
|
|
269
|
+
|
|
270
|
+
# Wave 12 C1 canary: cancel Temporal-durable listeners for this
|
|
271
|
+
# deployment. Visibility-query-based; no local handle dict.
|
|
272
|
+
canary_count = await self._cancel_canary_listeners(workflow_id)
|
|
273
|
+
|
|
274
|
+
# Wave 12 C3 canary: delete Temporal Schedules created by this
|
|
275
|
+
# deployment's cron triggers. Different Temporal resource type
|
|
276
|
+
# from listener workflows so it needs its own sweep
|
|
277
|
+
# (client.list_schedules vs client.list_workflows).
|
|
278
|
+
cron_schedule_count = await self._cancel_canary_cron_schedules(workflow_id)
|
|
279
|
+
|
|
280
|
+
# Sweep any downstream nodes still glowing on the canvas. The
|
|
281
|
+
# cron + listener resets above only cover the trigger nodes the
|
|
282
|
+
# manager owns directly; if a child run was mid-execution when
|
|
283
|
+
# cancellation hit, downstream agents / tools / actions may have
|
|
284
|
+
# been broadcast ``executing`` and would otherwise stay glowing
|
|
285
|
+
# forever. ``include_waiting=True`` because explicit user-cancel
|
|
286
|
+
# is the "every indicator goes quiet" signal — matches the
|
|
287
|
+
# behaviour of the ``handle_cancel_execution`` WS path. The
|
|
288
|
+
# delegation guard inside ``_clear_stuck_node_statuses`` still
|
|
289
|
+
# protects in-flight fire-and-forget child agents.
|
|
290
|
+
await self._broadcaster._clear_stuck_node_statuses(
|
|
291
|
+
workflow_id,
|
|
292
|
+
include_waiting=True,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# And broadcast a final ``executing=False`` for the deployment
|
|
296
|
+
# so the toolbar Start/Stop indicator reflects the cancel. The
|
|
297
|
+
# legacy path's run-counter eviction (``workflow_run_ended``)
|
|
298
|
+
# doesn't fire here because deployment-level cancel can race
|
|
299
|
+
# ahead of in-flight ``workflow_run_started`` callers — emit
|
|
300
|
+
# the terminal state directly to avoid a stuck ``executing=True``
|
|
301
|
+
# on the FE toolbar.
|
|
302
|
+
await self._broadcaster.update_workflow_status(
|
|
303
|
+
executing=False,
|
|
304
|
+
workflow_id=workflow_id,
|
|
305
|
+
)
|
|
248
306
|
|
|
249
307
|
# Clear cron iteration counters for this workflow's cron nodes
|
|
250
308
|
for node_id in cron_node_ids:
|
|
@@ -265,7 +323,9 @@ class DeploymentManager:
|
|
|
265
323
|
"listeners_cancelled": listener_count,
|
|
266
324
|
"crons_cancelled": cron_count,
|
|
267
325
|
"waiters_cancelled": waiter_count,
|
|
268
|
-
"
|
|
326
|
+
"canary_listeners_cancelled": canary_count,
|
|
327
|
+
"canary_cron_schedules_cancelled": cron_schedule_count,
|
|
328
|
+
"cancelled_listener_node_ids": listener_nodes,
|
|
269
329
|
}
|
|
270
330
|
|
|
271
331
|
def get_status(self, workflow_id: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -289,7 +349,7 @@ class DeploymentManager:
|
|
|
289
349
|
"active_runs": len(execution_runs),
|
|
290
350
|
"active_listeners": len(workflow_runs) - len(execution_runs),
|
|
291
351
|
"run_counter": self._run_counters.get(workflow_id, 0),
|
|
292
|
-
"deployed_at": state.deployed_at
|
|
352
|
+
"deployed_at": state.deployed_at,
|
|
293
353
|
}
|
|
294
354
|
|
|
295
355
|
# Global status (backward compatibility)
|
|
@@ -316,7 +376,7 @@ class DeploymentManager:
|
|
|
316
376
|
"deployed_workflows": deployed_workflows,
|
|
317
377
|
"active_runs": total_runs,
|
|
318
378
|
"active_listeners": total_listeners,
|
|
319
|
-
"run_counter": total_run_counter
|
|
379
|
+
"run_counter": total_run_counter,
|
|
320
380
|
}
|
|
321
381
|
|
|
322
382
|
# =========================================================================
|
|
@@ -324,19 +384,57 @@ class DeploymentManager:
|
|
|
324
384
|
# =========================================================================
|
|
325
385
|
|
|
326
386
|
async def _setup_cron_trigger(self, node: Dict, workflow_id: str) -> TriggerInfo:
|
|
327
|
-
"""Setup cron trigger for a node.
|
|
328
|
-
|
|
387
|
+
"""Setup cron trigger for a node.
|
|
388
|
+
|
|
389
|
+
Two paths:
|
|
390
|
+
|
|
391
|
+
1. **Wave 12 C3 canary**: when ``Settings.event_framework_enabled``
|
|
392
|
+
is on AND ``cronScheduler`` is in the canary registry, create
|
|
393
|
+
a Temporal :class:`Schedule` whose action is the plugin's
|
|
394
|
+
:class:`CronTriggerWorkflow`. Survives FastAPI process
|
|
395
|
+
restart via the Temporal Schedule service.
|
|
396
|
+
2. **Legacy**: APScheduler ``register_cron_job`` runs an
|
|
397
|
+
in-process tick callback. Dies on process restart — kept as
|
|
398
|
+
the default while the canary stabilises.
|
|
399
|
+
"""
|
|
400
|
+
node_id = node["id"]
|
|
401
|
+
node_type = node.get("type", "cronScheduler")
|
|
329
402
|
params = await self.database.get_node_parameters(node_id) or {}
|
|
330
403
|
|
|
331
404
|
cron_expr = TriggerManager.build_cron_expression(params)
|
|
332
|
-
timezone = params.get(
|
|
333
|
-
frequency = params.get(
|
|
405
|
+
timezone = params.get("timezone", "UTC")
|
|
406
|
+
frequency = params.get("frequency", "minutes")
|
|
407
|
+
schedule_desc = self._get_schedule_description(params)
|
|
334
408
|
|
|
335
|
-
#
|
|
336
|
-
self.
|
|
409
|
+
# Path 1: Wave 12 C3 canary — Temporal Schedule.
|
|
410
|
+
if await self._canary_listener_enabled_for(node_type):
|
|
411
|
+
schedule_id = await self._start_canary_cron_schedule(
|
|
412
|
+
node,
|
|
413
|
+
workflow_id,
|
|
414
|
+
params,
|
|
415
|
+
cron_expr=cron_expr,
|
|
416
|
+
timezone=timezone,
|
|
417
|
+
frequency=frequency,
|
|
418
|
+
schedule_desc=schedule_desc,
|
|
419
|
+
)
|
|
420
|
+
if schedule_id is not None:
|
|
421
|
+
await self._broadcaster.update_node_status(
|
|
422
|
+
node_id,
|
|
423
|
+
"waiting",
|
|
424
|
+
{
|
|
425
|
+
"message": f"Waiting for schedule: {cron_expr} (Temporal-durable)",
|
|
426
|
+
"cron_expression": cron_expr,
|
|
427
|
+
"timezone": timezone,
|
|
428
|
+
"schedule_id": schedule_id,
|
|
429
|
+
},
|
|
430
|
+
workflow_id=workflow_id,
|
|
431
|
+
)
|
|
432
|
+
return TriggerInfo(node_id, "cron", job_id=schedule_id)
|
|
433
|
+
# Fall through to legacy path if Temporal unavailable
|
|
434
|
+
# (already logged inside _start_canary_cron_schedule).
|
|
337
435
|
|
|
338
|
-
#
|
|
339
|
-
|
|
436
|
+
# Initialize iteration counter for this cron node (legacy path).
|
|
437
|
+
self._cron_iterations[node_id] = 0
|
|
340
438
|
|
|
341
439
|
def on_tick():
|
|
342
440
|
if self._main_loop and self._main_loop.is_running():
|
|
@@ -345,22 +443,19 @@ class DeploymentManager:
|
|
|
345
443
|
iteration = self._cron_iterations[node_id]
|
|
346
444
|
|
|
347
445
|
trigger_data = {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
446
|
+
"node_id": node_id,
|
|
447
|
+
"timestamp": datetime.now().isoformat(),
|
|
448
|
+
"trigger_type": "cron",
|
|
449
|
+
"event_data": {
|
|
450
|
+
"timestamp": datetime.now().isoformat(),
|
|
451
|
+
"iteration": iteration,
|
|
452
|
+
"frequency": frequency,
|
|
453
|
+
"timezone": timezone,
|
|
454
|
+
"schedule": schedule_desc,
|
|
455
|
+
"cron_expression": cron_expr,
|
|
456
|
+
},
|
|
359
457
|
}
|
|
360
|
-
asyncio.run_coroutine_threadsafe(
|
|
361
|
-
self._spawn_run(node_id, trigger_data, workflow_id=workflow_id),
|
|
362
|
-
self._main_loop
|
|
363
|
-
)
|
|
458
|
+
asyncio.run_coroutine_threadsafe(self._spawn_run(node_id, trigger_data, workflow_id=workflow_id), self._main_loop)
|
|
364
459
|
|
|
365
460
|
trigger_manager = self._trigger_managers.get(workflow_id)
|
|
366
461
|
if not trigger_manager:
|
|
@@ -369,48 +464,69 @@ class DeploymentManager:
|
|
|
369
464
|
job_id = trigger_manager.setup_cron(node_id, cron_expr, timezone, on_tick)
|
|
370
465
|
|
|
371
466
|
# Broadcast waiting status for cron trigger (like event triggers do)
|
|
372
|
-
await self._broadcaster.update_node_status(
|
|
373
|
-
|
|
374
|
-
"
|
|
375
|
-
"timezone": timezone,
|
|
376
|
-
|
|
377
|
-
|
|
467
|
+
await self._broadcaster.update_node_status(
|
|
468
|
+
node_id,
|
|
469
|
+
"waiting",
|
|
470
|
+
{"message": f"Waiting for schedule: {cron_expr}", "cron_expression": cron_expr, "timezone": timezone, "job_id": job_id},
|
|
471
|
+
workflow_id=workflow_id,
|
|
472
|
+
)
|
|
378
473
|
|
|
379
474
|
return TriggerInfo(node_id, "cron", job_id=job_id)
|
|
380
475
|
|
|
381
476
|
async def _fire_start_trigger(self, node: Dict, workflow_id: str) -> TriggerInfo:
|
|
382
477
|
"""Fire a start trigger immediately."""
|
|
383
|
-
node_id = node[
|
|
478
|
+
node_id = node["id"]
|
|
384
479
|
params = await self.database.get_node_parameters(node_id) or {}
|
|
385
480
|
|
|
386
|
-
initial_data_str = params.get(
|
|
481
|
+
initial_data_str = params.get("initial_data", "{}")
|
|
387
482
|
try:
|
|
388
483
|
initial_data = json.loads(initial_data_str) if initial_data_str else {}
|
|
389
484
|
except json.JSONDecodeError:
|
|
390
485
|
initial_data = {}
|
|
391
486
|
|
|
392
|
-
trigger_data = {
|
|
393
|
-
'node_id': node_id,
|
|
394
|
-
'timestamp': datetime.now().isoformat(),
|
|
395
|
-
'trigger_type': 'start',
|
|
396
|
-
'event_data': initial_data
|
|
397
|
-
}
|
|
487
|
+
trigger_data = {"node_id": node_id, "timestamp": datetime.now().isoformat(), "trigger_type": "start", "event_data": initial_data}
|
|
398
488
|
|
|
399
489
|
await self._spawn_run(node_id, trigger_data, workflow_id=workflow_id)
|
|
400
490
|
return TriggerInfo(node_id, "start", fired=True)
|
|
401
491
|
|
|
402
492
|
async def _setup_event_trigger(self, node: Dict, workflow_id: str) -> TriggerInfo:
|
|
403
|
-
"""Setup event-based trigger.
|
|
404
|
-
|
|
405
|
-
|
|
493
|
+
"""Setup event-based trigger.
|
|
494
|
+
|
|
495
|
+
Three dispatch paths (in priority order):
|
|
496
|
+
|
|
497
|
+
1. **Wave 12 C1 canary**: when ``Settings.event_framework_enabled``
|
|
498
|
+
is on AND the plugin has opted into the canary via
|
|
499
|
+
:func:`services.deployment.canary_registry.register_canary_trigger_type`
|
|
500
|
+
(called from the plugin's ``__init__.py``), start a
|
|
501
|
+
Temporal-durable :class:`TriggerListenerWorkflow`. Survives
|
|
502
|
+
FastAPI process restart via Temporal Event-History replay.
|
|
503
|
+
|
|
504
|
+
2. **Polling triggers** (Gmail, Twitter): API-polling factory
|
|
505
|
+
registered by the plugin's ``PollingTriggerNode`` subclass.
|
|
506
|
+
|
|
507
|
+
3. **Legacy**: in-process collector/processor task pair via
|
|
508
|
+
``trigger_manager.setup_event_trigger``. Dies on process
|
|
509
|
+
restart — pre-Wave-12 behaviour, kept for triggers not yet
|
|
510
|
+
on the canary list.
|
|
511
|
+
"""
|
|
512
|
+
node_id = node["id"]
|
|
513
|
+
node_type = node.get("type", "")
|
|
406
514
|
params = await self.database.get_node_parameters(node_id) or {}
|
|
407
515
|
|
|
516
|
+
# Path 1: Wave 12 C1 canary — Temporal-durable listener.
|
|
517
|
+
if await self._canary_listener_enabled_for(node_type):
|
|
518
|
+
listener_id = await self._start_canary_listener(node, workflow_id, params)
|
|
519
|
+
if listener_id is not None:
|
|
520
|
+
return TriggerInfo(node_id, node_type, job_id=listener_id)
|
|
521
|
+
# Fall through to legacy path if Temporal client unavailable
|
|
522
|
+
# (already logged inside _start_canary_listener).
|
|
523
|
+
|
|
408
524
|
async def on_event(event_data: Dict):
|
|
409
525
|
trigger_data = {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
526
|
+
"node_id": node_id,
|
|
527
|
+
"timestamp": datetime.now().isoformat(),
|
|
528
|
+
"trigger_type": node_type,
|
|
529
|
+
"event_data": event_data,
|
|
414
530
|
}
|
|
415
531
|
await self._spawn_run(node_id, trigger_data, wait=True, workflow_id=workflow_id)
|
|
416
532
|
|
|
@@ -431,35 +547,425 @@ class DeploymentManager:
|
|
|
431
547
|
if factory is not None:
|
|
432
548
|
poll_coroutine = factory(node_id, params)
|
|
433
549
|
await trigger_manager.setup_polling_trigger(
|
|
434
|
-
node_id, node_type, params, poll_coroutine, on_event,
|
|
435
|
-
self._broadcaster, workflow_id=workflow_id
|
|
550
|
+
node_id, node_type, params, poll_coroutine, on_event, self._broadcaster, workflow_id=workflow_id
|
|
436
551
|
)
|
|
437
552
|
return TriggerInfo(node_id, node_type)
|
|
438
553
|
# Fall through to event_waiter if no polling factory registered
|
|
439
554
|
logger.warning("No polling factory registered for trigger", node_type=node_type)
|
|
440
555
|
|
|
441
|
-
await trigger_manager.setup_event_trigger(
|
|
442
|
-
node_id, node_type, params, on_event, self._broadcaster,
|
|
443
|
-
workflow_id=workflow_id
|
|
444
|
-
)
|
|
556
|
+
await trigger_manager.setup_event_trigger(node_id, node_type, params, on_event, self._broadcaster, workflow_id=workflow_id)
|
|
445
557
|
return TriggerInfo(node_id, node_type)
|
|
446
558
|
|
|
559
|
+
# =========================================================================
|
|
560
|
+
# WAVE 12 C1 CANARY: TEMPORAL-DURABLE LISTENERS
|
|
561
|
+
# =========================================================================
|
|
562
|
+
#
|
|
563
|
+
# Pattern (cross-confirmed across Temporal docs, samples-python,
|
|
564
|
+
# Inngest, Prefect, n8n): deterministic workflow_id mapped to
|
|
565
|
+
# business entity (deployment + node), WorkflowIDConflictPolicy.
|
|
566
|
+
# USE_EXISTING for idempotent re-deploy, Search Attributes set at
|
|
567
|
+
# start, Visibility API queries for cross-workflow discovery on
|
|
568
|
+
# cancel. The Temporal server's Visibility store IS the registry —
|
|
569
|
+
# no Python dict of handles, no instance state to drift on
|
|
570
|
+
# FastAPI restart. Cancellation uses ``cancel()`` (graceful) over
|
|
571
|
+
# ``terminate()`` per docs.temporal.io/develop/python/cancellation.
|
|
572
|
+
#
|
|
573
|
+
# Refs:
|
|
574
|
+
# - https://docs.temporal.io/develop/python/temporal-clients
|
|
575
|
+
# ("Workflow ID mapped to business entities")
|
|
576
|
+
# - https://docs.temporal.io/visibility + /list-filter
|
|
577
|
+
# - https://docs.temporal.io/develop/python/cancellation
|
|
578
|
+
# - https://github.com/temporalio/samples-python/blob/main/hello/hello_search_attributes.py
|
|
579
|
+
# =========================================================================
|
|
580
|
+
|
|
581
|
+
@staticmethod
|
|
582
|
+
def _listener_workflow_id(workflow_id: str, node_id: str) -> str:
|
|
583
|
+
"""Deterministic Temporal workflow_id for a canary listener.
|
|
584
|
+
|
|
585
|
+
Mapped to the business entity (deployment workflow_id + trigger
|
|
586
|
+
node_id) so re-deploy of the same workflow produces the same
|
|
587
|
+
listener id. Pairs with ``WorkflowIDConflictPolicy.USE_EXISTING``
|
|
588
|
+
for idempotent start.
|
|
589
|
+
"""
|
|
590
|
+
return f"trigger-listener-{workflow_id}-{node_id}"
|
|
591
|
+
|
|
592
|
+
@staticmethod
|
|
593
|
+
def _trigger_kind_for(node_type: str) -> str:
|
|
594
|
+
"""Coarse-grained classification for the ``EventTriggerKind``
|
|
595
|
+
Search Attribute. Derived from the node_type by stripping the
|
|
596
|
+
``Trigger`` / ``Receive`` suffix — same mapping the frontend
|
|
597
|
+
uses for filter chips on the ops dashboard.
|
|
598
|
+
|
|
599
|
+
Examples: ``webhookTrigger`` → ``webhook``, ``chatTrigger`` →
|
|
600
|
+
``chat``, ``telegramReceive`` → ``telegram``.
|
|
601
|
+
"""
|
|
602
|
+
for suffix in ("Trigger", "Receive"):
|
|
603
|
+
if node_type.endswith(suffix):
|
|
604
|
+
return node_type[: -len(suffix)]
|
|
605
|
+
return node_type
|
|
606
|
+
|
|
607
|
+
@staticmethod
|
|
608
|
+
async def _canary_listener_enabled_for(node_type: str) -> bool:
|
|
609
|
+
"""Whether the C1 canary applies to this trigger type.
|
|
610
|
+
|
|
611
|
+
Two conditions, both required:
|
|
612
|
+
|
|
613
|
+
1. Plugin has opted in via
|
|
614
|
+
:func:`services.deployment.canary_registry.register_canary_trigger_type`
|
|
615
|
+
— registry lookup, no framework-side allowlist.
|
|
616
|
+
2. ``Settings.event_framework_enabled`` is on — lazy ``Settings()``
|
|
617
|
+
call so a runtime flag flip takes effect on the next deploy
|
|
618
|
+
without process restart.
|
|
619
|
+
"""
|
|
620
|
+
from services.deployment.canary_registry import is_canary_trigger_type
|
|
621
|
+
|
|
622
|
+
if not is_canary_trigger_type(node_type):
|
|
623
|
+
return False
|
|
624
|
+
from core.config import Settings
|
|
625
|
+
|
|
626
|
+
return bool(Settings().event_framework_enabled)
|
|
627
|
+
|
|
628
|
+
@staticmethod
|
|
629
|
+
def _is_polling_trigger_class(node_type: str) -> bool:
|
|
630
|
+
"""True iff ``node_type`` resolves to a
|
|
631
|
+
:class:`services.plugin.PollingTriggerNode` subclass.
|
|
632
|
+
|
|
633
|
+
Used by :meth:`_start_canary_listener` to dispatch between the
|
|
634
|
+
push-driven :class:`TriggerListenerWorkflow` and the poll-driven
|
|
635
|
+
:class:`PollingTriggerWorkflow`. Lazy imports the registry +
|
|
636
|
+
base class to keep the manager free of top-level cycles.
|
|
637
|
+
"""
|
|
638
|
+
try:
|
|
639
|
+
from services.node_registry import get_node_class
|
|
640
|
+
from services.plugin import PollingTriggerNode
|
|
641
|
+
except Exception: # noqa: BLE001
|
|
642
|
+
return False
|
|
643
|
+
cls = get_node_class(node_type)
|
|
644
|
+
return isinstance(cls, type) and issubclass(cls, PollingTriggerNode)
|
|
645
|
+
|
|
646
|
+
async def _start_canary_listener(
|
|
647
|
+
self,
|
|
648
|
+
node: Dict,
|
|
649
|
+
workflow_id: str,
|
|
650
|
+
params: Dict,
|
|
651
|
+
) -> Optional[str]:
|
|
652
|
+
"""Start the canary listener workflow for a trigger.
|
|
653
|
+
|
|
654
|
+
Dispatches by plugin class:
|
|
655
|
+
- :class:`PollingTriggerNode` subclasses → :class:`PollingTriggerWorkflow`
|
|
656
|
+
(Wave 12 C2 — workflow.sleep loop + per-cycle activity).
|
|
657
|
+
- Everything else → :class:`TriggerListenerWorkflow`
|
|
658
|
+
(Wave 12 C1 — signal-driven wait_condition).
|
|
659
|
+
|
|
660
|
+
Returns the Temporal listener workflow_id on success, or
|
|
661
|
+
``None`` if the Temporal client isn't connected (caller falls
|
|
662
|
+
through to the legacy collector/processor path).
|
|
663
|
+
|
|
664
|
+
Idempotent: re-deploying the same MachinaOs workflow re-runs
|
|
665
|
+
this with the same deterministic id; Temporal returns the
|
|
666
|
+
existing handle via ``WorkflowIDConflictPolicy.USE_EXISTING``
|
|
667
|
+
instead of erroring. Search Attributes provide the registry
|
|
668
|
+
used by :meth:`_cancel_canary_listeners`.
|
|
669
|
+
"""
|
|
670
|
+
from core.container import container
|
|
671
|
+
from temporalio.common import (
|
|
672
|
+
SearchAttributeKey,
|
|
673
|
+
SearchAttributePair,
|
|
674
|
+
TypedSearchAttributes,
|
|
675
|
+
WorkflowIDConflictPolicy,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
node_id = node["id"]
|
|
679
|
+
node_type = node.get("type", "")
|
|
680
|
+
|
|
681
|
+
wrapper = container.temporal_client()
|
|
682
|
+
if wrapper is None or wrapper.client is None:
|
|
683
|
+
logger.warning(
|
|
684
|
+
"Canary listener requested but Temporal not connected; " "falling back to legacy collector/processor path",
|
|
685
|
+
node_id=node_id,
|
|
686
|
+
workflow_id=workflow_id,
|
|
687
|
+
node_type=node_type,
|
|
688
|
+
)
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
state = self._deployments.get(workflow_id)
|
|
692
|
+
if state is None:
|
|
693
|
+
raise RuntimeError(f"No deployment state for workflow {workflow_id}")
|
|
694
|
+
|
|
695
|
+
from services.deployment.canary_registry import cloudevent_type_for
|
|
696
|
+
|
|
697
|
+
config = event_waiter.get_trigger_config(node_type)
|
|
698
|
+
# Legacy snake_case event_type — only used in the listener_args
|
|
699
|
+
# payload + node-status display strings. NOT the Search Attribute
|
|
700
|
+
# value; that has to be the CloudEvents reverse-DNS string the
|
|
701
|
+
# producer puts on outgoing envelopes (see EventType SA pair
|
|
702
|
+
# below).
|
|
703
|
+
event_type = config.event_type if config else f"unknown_{node_type}"
|
|
704
|
+
|
|
705
|
+
# CloudEvents type the producer's _events.py factory emits on its
|
|
706
|
+
# outgoing envelope. Used as the EventType Search Attribute value
|
|
707
|
+
# so services.events.dispatch.emit's Visibility query
|
|
708
|
+
# ``ListWorkflows(query="EventType='<event.type>'")`` actually
|
|
709
|
+
# matches this listener.
|
|
710
|
+
#
|
|
711
|
+
# Pre-fix (2026-05-15) the EventType SA was set to ``event_type``
|
|
712
|
+
# (the legacy snake_case), but dispatch.emit queries with the
|
|
713
|
+
# producer's CloudEvents reverse-DNS string. The mismatch silently
|
|
714
|
+
# zeroed the signal fan-out — listener started OK, never reacted.
|
|
715
|
+
cloudevent_type = cloudevent_type_for(node_type)
|
|
716
|
+
if cloudevent_type is None:
|
|
717
|
+
logger.warning(
|
|
718
|
+
"Canary listener: no cloudevent_type registered for node_type; "
|
|
719
|
+
"falling back to legacy event_type for EventType SA. This will "
|
|
720
|
+
"silently fail dispatch.emit fan-out — register cloudevent_type "
|
|
721
|
+
"in the plugin's __init__.py.",
|
|
722
|
+
node_id=node_id,
|
|
723
|
+
workflow_id=workflow_id,
|
|
724
|
+
node_type=node_type,
|
|
725
|
+
)
|
|
726
|
+
cloudevent_type = event_type
|
|
727
|
+
|
|
728
|
+
# Pick workflow type by plugin class. EventTriggerKind picks up
|
|
729
|
+
# the "polling" classification for ops dashboards independent
|
|
730
|
+
# of the per-plugin kind (gmail / twitter / ...).
|
|
731
|
+
is_polling = self._is_polling_trigger_class(node_type)
|
|
732
|
+
workflow_type_name = _POLLING_LISTENER_WORKFLOW_TYPE if is_polling else _PUSH_LISTENER_WORKFLOW_TYPE
|
|
733
|
+
trigger_kind = "polling" if is_polling else self._trigger_kind_for(node_type)
|
|
734
|
+
|
|
735
|
+
listener_id = self._listener_workflow_id(workflow_id, node_id)
|
|
736
|
+
|
|
737
|
+
# Common payload shape for both workflow types (signal-driven
|
|
738
|
+
# TriggerListenerWorkflow ignores poll-specific fields like
|
|
739
|
+
# ``version`` / ``seen_ids``; polling workflow reads them).
|
|
740
|
+
listener_args: Dict[str, Any] = {
|
|
741
|
+
"workflow_id": workflow_id,
|
|
742
|
+
"trigger_node_id": node_id,
|
|
743
|
+
"node_type": node_type,
|
|
744
|
+
"event_type": event_type,
|
|
745
|
+
"filter_params": params,
|
|
746
|
+
"nodes": state.nodes,
|
|
747
|
+
"edges": state.edges,
|
|
748
|
+
"session_id": state.session_id,
|
|
749
|
+
}
|
|
750
|
+
if is_polling:
|
|
751
|
+
# ``version`` feeds the polling workflow's activity-name
|
|
752
|
+
# construction (``poll.{type}.v{version}``). Pulled from the
|
|
753
|
+
# plugin class — single source of truth.
|
|
754
|
+
from services.node_registry import get_node_class
|
|
755
|
+
|
|
756
|
+
cls = get_node_class(node_type)
|
|
757
|
+
listener_args["version"] = getattr(cls, "version", 1) if cls else 1
|
|
758
|
+
# First start: empty seen_ids; the workflow runs a
|
|
759
|
+
# baseline-only cycle to establish the seen set without
|
|
760
|
+
# emitting events.
|
|
761
|
+
listener_args["seen_ids"] = []
|
|
762
|
+
|
|
763
|
+
# Search Attribute keys mirror services/temporal/search_attributes.py
|
|
764
|
+
# (the A4 registration spec). All Keyword-typed; values are scalars.
|
|
765
|
+
event_type_key = SearchAttributeKey.for_keyword("EventType")
|
|
766
|
+
trigger_node_id_key = SearchAttributeKey.for_keyword("TriggerNodeId")
|
|
767
|
+
event_workflow_id_key = SearchAttributeKey.for_keyword("EventWorkflowId")
|
|
768
|
+
event_trigger_kind_key = SearchAttributeKey.for_keyword("EventTriggerKind")
|
|
769
|
+
|
|
770
|
+
await wrapper.client.start_workflow(
|
|
771
|
+
workflow_type_name,
|
|
772
|
+
args=[listener_args],
|
|
773
|
+
id=listener_id,
|
|
774
|
+
task_queue="machina-tasks",
|
|
775
|
+
id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING,
|
|
776
|
+
search_attributes=TypedSearchAttributes(
|
|
777
|
+
[
|
|
778
|
+
# MUST be the CloudEvents type the producer emits on
|
|
779
|
+
# outgoing envelopes — dispatch.emit's Visibility query
|
|
780
|
+
# substitutes ``event.type`` into the EventType filter.
|
|
781
|
+
SearchAttributePair(event_type_key, cloudevent_type),
|
|
782
|
+
SearchAttributePair(trigger_node_id_key, node_id),
|
|
783
|
+
SearchAttributePair(event_workflow_id_key, workflow_id),
|
|
784
|
+
SearchAttributePair(event_trigger_kind_key, trigger_kind),
|
|
785
|
+
]
|
|
786
|
+
),
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# Broadcast waiting status (parity with legacy path).
|
|
790
|
+
await self._broadcaster.update_node_status(
|
|
791
|
+
node_id,
|
|
792
|
+
"waiting",
|
|
793
|
+
{
|
|
794
|
+
"message": f"Waiting for {config.display_name if config else node_type} (Temporal-durable)...",
|
|
795
|
+
"event_type": event_type,
|
|
796
|
+
"listener_id": listener_id,
|
|
797
|
+
},
|
|
798
|
+
workflow_id=workflow_id,
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
logger.info(
|
|
802
|
+
"Canary listener started",
|
|
803
|
+
listener_id=listener_id,
|
|
804
|
+
workflow_id=workflow_id,
|
|
805
|
+
node_id=node_id,
|
|
806
|
+
event_type=event_type,
|
|
807
|
+
)
|
|
808
|
+
return listener_id
|
|
809
|
+
|
|
810
|
+
async def _cancel_canary_cron_schedules(self, workflow_id: str) -> int:
|
|
811
|
+
"""Wave 12 C3: delete every Temporal Schedule created for this
|
|
812
|
+
deployment's cron triggers.
|
|
813
|
+
|
|
814
|
+
Uses ``services.temporal.schedules.delete_cron_schedules_for_deployment``
|
|
815
|
+
which queries ``client.list_schedules`` with the ``EventWorkflowId``
|
|
816
|
+
Search Attribute filter. Same no-local-dict contract as
|
|
817
|
+
:meth:`_cancel_canary_listeners` but against the Schedule
|
|
818
|
+
resource type (Temporal Schedules and Workflows have separate
|
|
819
|
+
Visibility lists).
|
|
820
|
+
"""
|
|
821
|
+
from core.container import container
|
|
822
|
+
from services.temporal.schedules import delete_cron_schedules_for_deployment
|
|
823
|
+
|
|
824
|
+
wrapper = container.temporal_client()
|
|
825
|
+
if wrapper is None or wrapper.client is None:
|
|
826
|
+
return 0
|
|
827
|
+
|
|
828
|
+
return await delete_cron_schedules_for_deployment(
|
|
829
|
+
wrapper.client,
|
|
830
|
+
workflow_id,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
async def _start_canary_cron_schedule(
|
|
834
|
+
self,
|
|
835
|
+
node: Dict,
|
|
836
|
+
workflow_id: str,
|
|
837
|
+
params: Dict,
|
|
838
|
+
*,
|
|
839
|
+
cron_expr: str,
|
|
840
|
+
timezone: str,
|
|
841
|
+
frequency: str,
|
|
842
|
+
schedule_desc: str,
|
|
843
|
+
) -> Optional[str]:
|
|
844
|
+
"""Wave 12 C3 canary: create a Temporal Schedule for a cron trigger.
|
|
845
|
+
|
|
846
|
+
Schedule action targets the plugin-owned ``CronTriggerWorkflow``
|
|
847
|
+
(registered as a :class:`temporalio.plugin.SimplePlugin` from
|
|
848
|
+
``nodes/scheduler/cron_scheduler/__init__.py``). Each firing
|
|
849
|
+
spawns a child :class:`MachinaWorkflow`.
|
|
850
|
+
|
|
851
|
+
Returns the Schedule id on success, ``None`` when Temporal isn't
|
|
852
|
+
reachable so the caller falls back to APScheduler.
|
|
853
|
+
"""
|
|
854
|
+
from core.container import container
|
|
855
|
+
from services.temporal.schedules import create_cron_schedule
|
|
856
|
+
|
|
857
|
+
node_id = node["id"]
|
|
858
|
+
|
|
859
|
+
wrapper = container.temporal_client()
|
|
860
|
+
if wrapper is None or wrapper.client is None:
|
|
861
|
+
logger.warning(
|
|
862
|
+
"Canary cron Schedule requested but Temporal not connected; " "falling back to APScheduler",
|
|
863
|
+
node_id=node_id,
|
|
864
|
+
workflow_id=workflow_id,
|
|
865
|
+
)
|
|
866
|
+
return None
|
|
867
|
+
|
|
868
|
+
state = self._deployments.get(workflow_id)
|
|
869
|
+
if state is None:
|
|
870
|
+
raise RuntimeError(f"No deployment state for workflow {workflow_id}")
|
|
871
|
+
|
|
872
|
+
listener_data: Dict[str, Any] = {
|
|
873
|
+
"workflow_id": workflow_id,
|
|
874
|
+
"trigger_node_id": node_id,
|
|
875
|
+
"node_type": node.get("type", "cronScheduler"),
|
|
876
|
+
"cron_expression": cron_expr,
|
|
877
|
+
"frequency": frequency,
|
|
878
|
+
"timezone": timezone,
|
|
879
|
+
"schedule": schedule_desc,
|
|
880
|
+
"filter_params": params,
|
|
881
|
+
"nodes": state.nodes,
|
|
882
|
+
"edges": state.edges,
|
|
883
|
+
"session_id": state.session_id,
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
schedule_id = await create_cron_schedule(
|
|
887
|
+
wrapper.client,
|
|
888
|
+
deployment_workflow_id=workflow_id,
|
|
889
|
+
node_id=node_id,
|
|
890
|
+
cron_expression=cron_expr,
|
|
891
|
+
timezone=timezone,
|
|
892
|
+
listener_data=listener_data,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
logger.info(
|
|
896
|
+
"Canary cron Schedule created",
|
|
897
|
+
schedule_id=schedule_id,
|
|
898
|
+
workflow_id=workflow_id,
|
|
899
|
+
node_id=node_id,
|
|
900
|
+
cron_expression=cron_expr,
|
|
901
|
+
)
|
|
902
|
+
return schedule_id
|
|
903
|
+
|
|
904
|
+
async def _cancel_canary_listeners(self, workflow_id: str) -> int:
|
|
905
|
+
"""Cancel all canary listeners for this deployment.
|
|
906
|
+
|
|
907
|
+
Uses Visibility API query — the Temporal server's Visibility
|
|
908
|
+
store IS the registry, no local dict. ``cancel()`` is graceful;
|
|
909
|
+
listeners drain in-flight child spawns before exiting.
|
|
910
|
+
|
|
911
|
+
Visibility is eventually consistent
|
|
912
|
+
(https://docs.temporal.io/visibility) — a freshly-started listener
|
|
913
|
+
might not appear in the query result for a few seconds. Acceptable
|
|
914
|
+
here: the listener will eventually surface in a subsequent cancel
|
|
915
|
+
sweep, AND its parent_close_policy=ABANDON means in-flight runs
|
|
916
|
+
complete regardless.
|
|
917
|
+
"""
|
|
918
|
+
from core.container import container
|
|
919
|
+
|
|
920
|
+
wrapper = container.temporal_client()
|
|
921
|
+
if wrapper is None or wrapper.client is None:
|
|
922
|
+
return 0
|
|
923
|
+
|
|
924
|
+
# Visibility List Filter query — operators per docs.temporal.io/list-filter.
|
|
925
|
+
# Match BOTH workflow types via ``WorkflowType IN (...)`` so push
|
|
926
|
+
# (TriggerListenerWorkflow) and polling (PollingTriggerWorkflow)
|
|
927
|
+
# listeners drain in one sweep.
|
|
928
|
+
wf_types_in = ", ".join(f"'{t}'" for t in _LISTENER_WORKFLOW_TYPES)
|
|
929
|
+
query = f"EventWorkflowId='{workflow_id}' " f"AND WorkflowType IN ({wf_types_in}) " f"AND ExecutionStatus='Running'"
|
|
930
|
+
|
|
931
|
+
cancelled = 0
|
|
932
|
+
try:
|
|
933
|
+
async for wf in wrapper.client.list_workflows(query=query):
|
|
934
|
+
try:
|
|
935
|
+
await wrapper.client.get_workflow_handle(wf.id).cancel()
|
|
936
|
+
cancelled += 1
|
|
937
|
+
except Exception as exc: # noqa: BLE001
|
|
938
|
+
# Per-listener failures don't block sweep of the rest.
|
|
939
|
+
logger.warning(
|
|
940
|
+
f"Failed to cancel canary listener {wf.id}: {exc}",
|
|
941
|
+
workflow_id=workflow_id,
|
|
942
|
+
)
|
|
943
|
+
except Exception as exc: # noqa: BLE001
|
|
944
|
+
logger.warning(
|
|
945
|
+
f"Visibility query for canary listeners failed: {exc} " f"(query={query!r})",
|
|
946
|
+
workflow_id=workflow_id,
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
if cancelled:
|
|
950
|
+
logger.info(
|
|
951
|
+
"Canary listeners cancelled",
|
|
952
|
+
workflow_id=workflow_id,
|
|
953
|
+
count=cancelled,
|
|
954
|
+
)
|
|
955
|
+
return cancelled
|
|
956
|
+
|
|
447
957
|
# =========================================================================
|
|
448
958
|
# EXECUTION RUNS
|
|
449
959
|
# =========================================================================
|
|
450
960
|
|
|
451
961
|
async def _spawn_run(
|
|
452
|
-
self,
|
|
453
|
-
trigger_node_id: str,
|
|
454
|
-
trigger_data: Dict[str, Any],
|
|
455
|
-
wait: bool = False,
|
|
456
|
-
workflow_id: Optional[str] = None
|
|
962
|
+
self, trigger_node_id: str, trigger_data: Dict[str, Any], wait: bool = False, workflow_id: Optional[str] = None
|
|
457
963
|
) -> Optional[asyncio.Task]:
|
|
458
964
|
"""Spawn a new execution run for a specific workflow."""
|
|
459
965
|
if not workflow_id:
|
|
460
966
|
# Backward compatibility: find workflow for this trigger node
|
|
461
967
|
for wid, state in self._deployments.items():
|
|
462
|
-
if state.is_running and any(n[
|
|
968
|
+
if state.is_running and any(n["id"] == trigger_node_id for n in state.nodes):
|
|
463
969
|
workflow_id = wid
|
|
464
970
|
break
|
|
465
971
|
|
|
@@ -480,24 +986,25 @@ class DeploymentManager:
|
|
|
480
986
|
self._run_counters[workflow_id] = self._run_counters.get(workflow_id, 0) + 1
|
|
481
987
|
run_id = f"run_{state.deployment_id}_{self._run_counters[workflow_id]}"
|
|
482
988
|
|
|
483
|
-
await self._notify(
|
|
484
|
-
"
|
|
485
|
-
"workflow_id": workflow_id,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}, workflow_id)
|
|
989
|
+
await self._notify(
|
|
990
|
+
"run_started",
|
|
991
|
+
{"run_id": run_id, "workflow_id": workflow_id, "trigger_node_id": trigger_node_id, "active_runs": active_count + 1},
|
|
992
|
+
workflow_id,
|
|
993
|
+
)
|
|
489
994
|
|
|
490
995
|
async def execute():
|
|
491
996
|
try:
|
|
492
|
-
result = await self._execute_from_trigger(
|
|
493
|
-
|
|
997
|
+
result = await self._execute_from_trigger(run_id, trigger_node_id, trigger_data, workflow_id)
|
|
998
|
+
await self._notify(
|
|
999
|
+
"run_completed",
|
|
1000
|
+
{
|
|
1001
|
+
"run_id": run_id,
|
|
1002
|
+
"workflow_id": workflow_id,
|
|
1003
|
+
"success": result.get("success", False),
|
|
1004
|
+
"execution_time": result.get("execution_time"),
|
|
1005
|
+
},
|
|
1006
|
+
workflow_id,
|
|
494
1007
|
)
|
|
495
|
-
await self._notify("run_completed", {
|
|
496
|
-
"run_id": run_id,
|
|
497
|
-
"workflow_id": workflow_id,
|
|
498
|
-
"success": result.get("success", False),
|
|
499
|
-
"execution_time": result.get("execution_time")
|
|
500
|
-
}, workflow_id)
|
|
501
1008
|
except asyncio.CancelledError:
|
|
502
1009
|
logger.debug("Run cancelled", run_id=run_id, workflow_id=workflow_id)
|
|
503
1010
|
except Exception as e:
|
|
@@ -522,11 +1029,7 @@ class DeploymentManager:
|
|
|
522
1029
|
return task
|
|
523
1030
|
|
|
524
1031
|
async def _execute_from_trigger(
|
|
525
|
-
self,
|
|
526
|
-
run_id: str,
|
|
527
|
-
trigger_node_id: str,
|
|
528
|
-
trigger_data: Dict[str, Any],
|
|
529
|
-
workflow_id: str
|
|
1032
|
+
self, run_id: str, trigger_node_id: str, trigger_data: Dict[str, Any], workflow_id: str
|
|
530
1033
|
) -> Dict[str, Any]:
|
|
531
1034
|
"""Execute workflow from a trigger node."""
|
|
532
1035
|
state = self._deployments.get(workflow_id)
|
|
@@ -537,15 +1040,11 @@ class DeploymentManager:
|
|
|
537
1040
|
run_session_id = f"{state.session_id}_{run_id}"
|
|
538
1041
|
|
|
539
1042
|
# Store trigger output
|
|
540
|
-
trigger_output = trigger_data.get(
|
|
1043
|
+
trigger_output = trigger_data.get("event_data", trigger_data)
|
|
541
1044
|
await self._store_output(run_session_id, trigger_node_id, "output_0", trigger_output)
|
|
542
1045
|
|
|
543
1046
|
# Get downstream nodes
|
|
544
|
-
downstream = self._get_downstream_nodes(
|
|
545
|
-
trigger_node_id,
|
|
546
|
-
state.nodes,
|
|
547
|
-
state.edges
|
|
548
|
-
)
|
|
1047
|
+
downstream = self._get_downstream_nodes(trigger_node_id, state.nodes, state.edges)
|
|
549
1048
|
|
|
550
1049
|
if not downstream:
|
|
551
1050
|
return {
|
|
@@ -554,33 +1053,30 @@ class DeploymentManager:
|
|
|
554
1053
|
"workflow_id": workflow_id,
|
|
555
1054
|
"nodes_executed": [trigger_node_id],
|
|
556
1055
|
"execution_time": time.time() - start_time,
|
|
557
|
-
"message": "No downstream nodes"
|
|
1056
|
+
"message": "No downstream nodes",
|
|
558
1057
|
}
|
|
559
1058
|
|
|
560
1059
|
# Build filtered graph
|
|
561
|
-
run_filter = {trigger_node_id} | {n[
|
|
1060
|
+
run_filter = {trigger_node_id} | {n["id"] for n in downstream}
|
|
562
1061
|
logger.debug(f"[Run] run_filter has {len(run_filter)} nodes")
|
|
563
1062
|
|
|
564
1063
|
filtered_nodes = []
|
|
565
1064
|
for node in state.nodes:
|
|
566
|
-
if node[
|
|
1065
|
+
if node["id"] not in run_filter:
|
|
567
1066
|
continue
|
|
568
1067
|
node_copy = node.copy()
|
|
569
|
-
node_type = node.get(
|
|
570
|
-
if node[
|
|
571
|
-
node_copy[
|
|
572
|
-
node_copy[
|
|
1068
|
+
node_type = node.get("type", "")
|
|
1069
|
+
if node["id"] == trigger_node_id:
|
|
1070
|
+
node_copy["_pre_executed"] = True
|
|
1071
|
+
node_copy["_trigger_output"] = trigger_output
|
|
573
1072
|
elif node_type in WORKFLOW_TRIGGER_TYPES:
|
|
574
1073
|
# Non-firing triggers: pre-execute to prevent blocking as event waiters
|
|
575
|
-
node_copy[
|
|
576
|
-
node_copy[
|
|
1074
|
+
node_copy["_pre_executed"] = True
|
|
1075
|
+
node_copy["_trigger_output"] = {"not_triggered": True}
|
|
577
1076
|
logger.debug(f"[Run] Marking non-firing trigger as pre-executed: {node['id']} ({node_type})")
|
|
578
1077
|
filtered_nodes.append(node_copy)
|
|
579
1078
|
|
|
580
|
-
filtered_edges = [
|
|
581
|
-
e for e in state.edges
|
|
582
|
-
if e.get('source') in run_filter and e.get('target') in run_filter
|
|
583
|
-
]
|
|
1079
|
+
filtered_edges = [e for e in state.edges if e.get("source") in run_filter and e.get("target") in run_filter]
|
|
584
1080
|
logger.debug(f"[Run] filtered_edges: {len(filtered_edges)} edges")
|
|
585
1081
|
|
|
586
1082
|
# Execute filtered graph with deployment's workflow_id for scoped status
|
|
@@ -601,26 +1097,21 @@ class DeploymentManager:
|
|
|
601
1097
|
result["trigger_node_id"] = trigger_node_id
|
|
602
1098
|
return result
|
|
603
1099
|
|
|
604
|
-
def _get_downstream_nodes(
|
|
605
|
-
self,
|
|
606
|
-
node_id: str,
|
|
607
|
-
nodes: List[Dict],
|
|
608
|
-
edges: List[Dict]
|
|
609
|
-
) -> List[Dict]:
|
|
1100
|
+
def _get_downstream_nodes(self, node_id: str, nodes: List[Dict], edges: List[Dict]) -> List[Dict]:
|
|
610
1101
|
"""Get all downstream nodes from a trigger."""
|
|
611
1102
|
downstream_ids = set()
|
|
612
|
-
node_types = {n[
|
|
613
|
-
nodes_with_inputs = {e.get(
|
|
1103
|
+
node_types = {n["id"]: n.get("type", "") for n in nodes}
|
|
1104
|
+
nodes_with_inputs = {e.get("target") for e in edges if e.get("target")}
|
|
614
1105
|
|
|
615
1106
|
def collect(current_id: str):
|
|
616
1107
|
for edge in edges:
|
|
617
|
-
if edge.get(
|
|
1108
|
+
if edge.get("source") != current_id:
|
|
618
1109
|
continue
|
|
619
|
-
target_id = edge.get(
|
|
1110
|
+
target_id = edge.get("target")
|
|
620
1111
|
if not target_id or target_id in downstream_ids:
|
|
621
1112
|
continue
|
|
622
1113
|
|
|
623
|
-
target_type = node_types.get(target_id,
|
|
1114
|
+
target_type = node_types.get(target_id, "")
|
|
624
1115
|
is_trigger = target_type in WORKFLOW_TRIGGER_TYPES
|
|
625
1116
|
|
|
626
1117
|
# Stop at trigger nodes — they are independent event listeners,
|
|
@@ -636,15 +1127,15 @@ class DeploymentManager:
|
|
|
636
1127
|
|
|
637
1128
|
# Include config nodes connected to downstream nodes
|
|
638
1129
|
for edge in edges:
|
|
639
|
-
target = edge.get(
|
|
640
|
-
source = edge.get(
|
|
641
|
-
handle = edge.get(
|
|
1130
|
+
target = edge.get("target")
|
|
1131
|
+
source = edge.get("source")
|
|
1132
|
+
handle = edge.get("targetHandle", "")
|
|
642
1133
|
|
|
643
|
-
is_config = handle and handle.startswith(
|
|
1134
|
+
is_config = handle and handle.startswith("input-") and handle != "input-main"
|
|
644
1135
|
if is_config and target in downstream_ids and source not in downstream_ids:
|
|
645
1136
|
# Never include trigger nodes as config dependencies -
|
|
646
1137
|
# they are event listeners, not configuration providers
|
|
647
|
-
source_type = node_types.get(source,
|
|
1138
|
+
source_type = node_types.get(source, "")
|
|
648
1139
|
if source_type in WORKFLOW_TRIGGER_TYPES:
|
|
649
1140
|
continue
|
|
650
1141
|
downstream_ids.add(source)
|
|
@@ -654,10 +1145,10 @@ class DeploymentManager:
|
|
|
654
1145
|
# handle) and need to be included so the toolkit can discover
|
|
655
1146
|
# them. ``TOOLKIT_NODE_TYPES`` is the canonical set; today only
|
|
656
1147
|
# ``androidTool`` is in it.
|
|
657
|
-
toolkit_node_ids = {n[
|
|
1148
|
+
toolkit_node_ids = {n["id"] for n in nodes if n.get("type") in TOOLKIT_NODE_TYPES and n["id"] in downstream_ids}
|
|
658
1149
|
for edge in edges:
|
|
659
|
-
target = edge.get(
|
|
660
|
-
source = edge.get(
|
|
1150
|
+
target = edge.get("target")
|
|
1151
|
+
source = edge.get("source")
|
|
661
1152
|
# Include nodes that connect to toolkit nodes
|
|
662
1153
|
if target in toolkit_node_ids and source not in downstream_ids:
|
|
663
1154
|
downstream_ids.add(source)
|
|
@@ -667,17 +1158,18 @@ class DeploymentManager:
|
|
|
667
1158
|
# When a child agent is included, we need its connected tools so the parent
|
|
668
1159
|
# can discover what capabilities the child has
|
|
669
1160
|
from constants import AI_AGENT_TYPES
|
|
670
|
-
|
|
1161
|
+
|
|
1162
|
+
agent_node_ids = {n["id"] for n in nodes if n.get("type") in AI_AGENT_TYPES and n["id"] in downstream_ids}
|
|
671
1163
|
for edge in edges:
|
|
672
|
-
target = edge.get(
|
|
673
|
-
source = edge.get(
|
|
674
|
-
target_handle = edge.get(
|
|
1164
|
+
target = edge.get("target")
|
|
1165
|
+
source = edge.get("source")
|
|
1166
|
+
target_handle = edge.get("targetHandle", "")
|
|
675
1167
|
# Include tool nodes connected to agent's input-tools handle
|
|
676
|
-
if target in agent_node_ids and target_handle ==
|
|
1168
|
+
if target in agent_node_ids and target_handle == "input-tools" and source not in downstream_ids:
|
|
677
1169
|
downstream_ids.add(source)
|
|
678
1170
|
logger.debug(f"[Deployment] Including tool node {source} connected to agent {target}")
|
|
679
1171
|
|
|
680
|
-
return [n for n in nodes if n[
|
|
1172
|
+
return [n for n in nodes if n["id"] in downstream_ids]
|
|
681
1173
|
|
|
682
1174
|
# =========================================================================
|
|
683
1175
|
# HELPERS
|
|
@@ -699,11 +1191,13 @@ class DeploymentManager:
|
|
|
699
1191
|
try:
|
|
700
1192
|
db_settings = await self.database.get_deployment_settings()
|
|
701
1193
|
if db_settings:
|
|
702
|
-
self._settings.update(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1194
|
+
self._settings.update(
|
|
1195
|
+
{
|
|
1196
|
+
"stop_on_error": db_settings.get("stop_on_error", False),
|
|
1197
|
+
"max_concurrent_runs": db_settings.get("max_concurrent_runs", 100),
|
|
1198
|
+
"use_parallel_executor": db_settings.get("use_parallel_executor", True),
|
|
1199
|
+
}
|
|
1200
|
+
)
|
|
707
1201
|
except Exception:
|
|
708
1202
|
pass
|
|
709
1203
|
|
|
@@ -723,43 +1217,39 @@ class DeploymentManager:
|
|
|
723
1217
|
return
|
|
724
1218
|
|
|
725
1219
|
try:
|
|
726
|
-
await status_callback("__deployment__", event, {
|
|
727
|
-
**data,
|
|
728
|
-
"workflow_id": workflow_id,
|
|
729
|
-
"timestamp": datetime.now().isoformat()
|
|
730
|
-
})
|
|
1220
|
+
await status_callback("__deployment__", event, {**data, "workflow_id": workflow_id, "timestamp": datetime.now().isoformat()})
|
|
731
1221
|
except Exception as e:
|
|
732
1222
|
logger.warning("Status callback failed", workflow_id=workflow_id, error=str(e))
|
|
733
1223
|
|
|
734
1224
|
@staticmethod
|
|
735
1225
|
def _get_schedule_description(params: Dict[str, Any]) -> str:
|
|
736
1226
|
"""Get human-readable schedule description from parameters."""
|
|
737
|
-
frequency = params.get(
|
|
1227
|
+
frequency = params.get("frequency", "minutes")
|
|
738
1228
|
|
|
739
1229
|
match frequency:
|
|
740
|
-
case
|
|
741
|
-
interval = params.get(
|
|
1230
|
+
case "seconds":
|
|
1231
|
+
interval = params.get("interval", 30)
|
|
742
1232
|
return f"Every {interval} seconds"
|
|
743
|
-
case
|
|
744
|
-
interval = params.get(
|
|
1233
|
+
case "minutes":
|
|
1234
|
+
interval = params.get("interval_minutes", 5)
|
|
745
1235
|
return f"Every {interval} minutes"
|
|
746
|
-
case
|
|
747
|
-
interval = params.get(
|
|
1236
|
+
case "hours":
|
|
1237
|
+
interval = params.get("interval_hours", 1)
|
|
748
1238
|
return f"Every {interval} hours"
|
|
749
|
-
case
|
|
750
|
-
time_str = params.get(
|
|
1239
|
+
case "days":
|
|
1240
|
+
time_str = params.get("daily_time", "09:00")
|
|
751
1241
|
return f"Daily at {time_str}"
|
|
752
|
-
case
|
|
753
|
-
weekday = params.get(
|
|
754
|
-
time_str = params.get(
|
|
755
|
-
days = [
|
|
1242
|
+
case "weeks":
|
|
1243
|
+
weekday = params.get("weekday", "1")
|
|
1244
|
+
time_str = params.get("weekly_time", "09:00")
|
|
1245
|
+
days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
756
1246
|
day_name = days[int(weekday)] if str(weekday).isdigit() else weekday
|
|
757
1247
|
return f"Weekly on {day_name} at {time_str}"
|
|
758
|
-
case
|
|
759
|
-
day = params.get(
|
|
760
|
-
time_str = params.get(
|
|
1248
|
+
case "months":
|
|
1249
|
+
day = params.get("month_day", "1")
|
|
1250
|
+
time_str = params.get("monthly_time", "09:00")
|
|
761
1251
|
return f"Monthly on day {day} at {time_str}"
|
|
762
|
-
case
|
|
1252
|
+
case "once":
|
|
763
1253
|
return "Once (no repeat)"
|
|
764
1254
|
case _:
|
|
765
1255
|
return "Unknown schedule"
|