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.
Files changed (743) hide show
  1. package/.env.template +74 -5
  2. package/{workflows/AI Assistant_workflow-1778504793388-ou1m1tz2x.json → .machina/workflows/AI Assistant_example_workflow-1779017037684-e2e5da7a.json } +164 -105
  3. package/{workflows/AI Employee_example_workflow-1777720598005-u4cm858dv.json → .machina/workflows/AI Employee_example_workflow-1779102911870-cbc76c82.json } +582 -328
  4. package/{workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json → .machina/workflows/Claude Assistant_example_workflow-1779095939967-2369cff4.json } +152 -83
  5. package/README.md +5 -2
  6. package/bin/cli.js +2 -2
  7. package/{machina → cli}/__main__.py +11 -7
  8. package/cli/_common.py +122 -0
  9. package/cli/buildenv.py +40 -0
  10. package/cli/cli.py +204 -0
  11. package/{machina → cli}/colors.py +10 -2
  12. package/cli/commands/__init__.py +1 -0
  13. package/cli/commands/_temporal_specs.py +59 -0
  14. package/{machina → cli}/commands/build.py +35 -45
  15. package/cli/commands/clean.py +141 -0
  16. package/cli/commands/daemon/__init__.py +47 -0
  17. package/cli/commands/daemon/_state.py +97 -0
  18. package/cli/commands/daemon/restart.py +14 -0
  19. package/cli/commands/daemon/start.py +49 -0
  20. package/cli/commands/daemon/status.py +20 -0
  21. package/cli/commands/daemon/stop.py +22 -0
  22. package/{machina → cli}/commands/dev.py +32 -42
  23. package/{machina → cli}/commands/docs.py +13 -11
  24. package/{machina → cli}/commands/start.py +69 -62
  25. package/{machina → cli}/commands/stop.py +7 -10
  26. package/{machina → cli}/commands/version.py +12 -6
  27. package/cli/config.py +170 -0
  28. package/cli/platform_.py +169 -0
  29. package/{machina → cli}/ports.py +42 -3
  30. package/{machina → cli}/run.py +29 -2
  31. package/{machina → cli}/supervisor.py +29 -12
  32. package/{machina → cli}/tcp.py +6 -2
  33. package/{machina → cli}/tree.py +38 -11
  34. package/client/dist/assets/{ActionBar-Du2MSFSz.js → ActionBar-Cjr3TF7g.js} +1 -1
  35. package/client/dist/assets/{ApiKeyInput-k2LBmBjb.js → ApiKeyInput-DIJE2PVA.js} +1 -1
  36. package/client/dist/assets/{ApiKeyPanel-C_bV9U0X.js → ApiKeyPanel-CPmye7uh.js} +1 -1
  37. package/client/dist/assets/{ApiUsageSection-CmVfwZzL.js → ApiUsageSection-TF_7gH2D.js} +1 -1
  38. package/client/dist/assets/{EmailPanel-CeKIMGu-.js → EmailPanel-Bs-xvbKR.js} +1 -1
  39. package/client/dist/assets/{OAuthPanel-KA3t3Q2K.js → OAuthPanel-BDtVJhAV.js} +1 -1
  40. package/client/dist/assets/{QrPairingPanel-NgNpJNuk.js → QrPairingPanel-BwJehTuZ.js} +1 -1
  41. package/client/dist/assets/{RateLimitSection-Du5YNVIA.js → RateLimitSection-CfNOoPIS.js} +1 -1
  42. package/client/dist/assets/{StatusCard-DNLyayXc.js → StatusCard-DkwIrgdP.js} +1 -1
  43. package/client/dist/assets/index-P2FzntoL.js +165 -0
  44. package/client/dist/index.html +1 -1
  45. package/client/package.json +1 -1
  46. package/client/src/Dashboard.tsx +128 -76
  47. package/client/src/adapters/nodeSpecToDescription.ts +7 -0
  48. package/client/src/assets/icons/index.test.ts +10 -0
  49. package/client/src/assets/icons/index.ts +16 -3
  50. package/client/src/components/AIAgentNode.tsx +8 -8
  51. package/client/src/components/ParameterRenderer.tsx +6 -3
  52. package/client/src/components/SkillEditorModal.tsx +1 -0
  53. package/client/src/components/credentials/panels/EmailPanel.tsx +2 -0
  54. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +2 -0
  55. package/client/src/components/credentials/sections/RateLimitSection.tsx +1 -0
  56. package/client/src/components/icons/AIProviderIcons.tsx +1 -0
  57. package/client/src/components/maps/GoogleMapsPicker.tsx +1 -0
  58. package/client/src/components/parameterPanel/InputSection.tsx +1 -0
  59. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +1 -0
  60. package/client/src/components/parameterPanel/OutputSection.tsx +1 -0
  61. package/client/src/components/ui/ComponentPalette.tsx +1 -0
  62. package/client/src/components/ui/MapSelector.tsx +1 -0
  63. package/client/src/components/ui/NodeContextMenu.tsx +3 -3
  64. package/client/src/components/ui/SettingsPanel.tsx +1 -0
  65. package/client/src/components/ui/action-button.tsx +1 -0
  66. package/client/src/components/ui/badge.tsx +1 -0
  67. package/client/src/components/ui/button.tsx +1 -0
  68. package/client/src/components/ui/form.tsx +1 -0
  69. package/client/src/components/ui/tabs.tsx +1 -0
  70. package/client/src/contexts/AuthContext.tsx +1 -0
  71. package/client/src/contexts/ThemeContext.tsx +1 -0
  72. package/client/src/contexts/WebSocketContext.tsx +104 -34
  73. package/client/src/hooks/__tests__/useApiKeys.test.ts +2 -2
  74. package/client/src/hooks/useReactFlowNodes.ts +1 -0
  75. package/client/src/hooks/useWorkflowValidation.ts +142 -0
  76. package/client/src/lib/nodeSpec.ts +1 -0
  77. package/client/src/test/providers.tsx +1 -0
  78. package/client/src/types/__tests__/cloudEvents.test.ts +5 -2
  79. package/client/src/types/cloudEvents.ts +19 -7
  80. package/client/src/utils/nodeUtils.ts +1 -1
  81. package/client/src/utils/workflow.ts +8 -2
  82. package/client/src/utils/workflowExport.ts +60 -3
  83. package/package.json +24 -23
  84. package/scripts/install.js +16 -27
  85. package/scripts/migrate_icons.py +3 -1
  86. package/scripts/migrate_skill_icons.py +6 -7
  87. package/scripts/postinstall.js +11 -9
  88. package/server/config/ai_cli_providers.json +2 -3
  89. package/server/config/credential_providers.json +15 -15
  90. package/server/config/llm_defaults.json +1 -1
  91. package/server/config/model_registry.json +416 -611
  92. package/server/constants.py +285 -223
  93. package/server/core/__init__.py +1 -1
  94. package/server/core/cache.py +9 -29
  95. package/server/core/cleanup.py +12 -24
  96. package/server/core/config.py +148 -24
  97. package/server/core/container.py +68 -59
  98. package/server/core/credential_backends.py +5 -13
  99. package/server/core/credentials_database.py +13 -43
  100. package/server/core/database.py +292 -353
  101. package/server/core/health.py +4 -5
  102. package/server/core/logging.py +241 -87
  103. package/server/core/paths.py +285 -0
  104. package/server/core/tracing.py +2 -8
  105. package/server/gunicorn.conf.py +1 -0
  106. package/server/main.py +150 -74
  107. package/server/middleware/auth.py +18 -24
  108. package/server/models/__init__.py +1 -1
  109. package/server/models/auth.py +5 -12
  110. package/server/models/database.py +36 -68
  111. package/server/models/node_metadata.py +25 -18
  112. package/server/nodejs/dist/index.js +107 -0
  113. package/server/nodes/README.md +11 -5
  114. package/server/nodes/__init__.py +1 -1
  115. package/server/nodes/_visuals.py +146 -14
  116. package/server/nodes/agent/_events.py +124 -0
  117. package/server/nodes/agent/_handles.py +15 -29
  118. package/server/nodes/agent/_inline.py +28 -25
  119. package/server/nodes/agent/_specialized.py +30 -15
  120. package/server/nodes/agent/{ai_agent.py → ai_agent/__init__.py} +33 -17
  121. package/server/nodes/agent/ai_agent/meta.json +3 -0
  122. package/server/nodes/agent/{ai_employee.py → ai_employee/__init__.py} +5 -2
  123. package/server/nodes/agent/ai_employee/meta.json +3 -0
  124. package/server/nodes/agent/{android_agent.py → android_agent/__init__.py} +1 -1
  125. package/server/nodes/agent/android_agent/meta.json +3 -0
  126. package/server/nodes/agent/{autonomous_agent.py → autonomous_agent/__init__.py} +2 -1
  127. package/server/nodes/agent/autonomous_agent/meta.json +3 -0
  128. package/server/nodes/agent/{chat_agent.py → chat_agent/__init__.py} +29 -12
  129. package/server/nodes/agent/chat_agent/meta.json +3 -0
  130. package/server/nodes/agent/{claude_code_agent.py → claude_code_agent/__init__.py} +192 -95
  131. package/server/nodes/agent/claude_code_agent/_handlers.py +169 -0
  132. package/server/{services/claude_oauth.py → nodes/agent/claude_code_agent/_oauth.py} +26 -13
  133. package/server/nodes/agent/claude_code_agent/_pool.py +1020 -0
  134. package/server/nodes/agent/claude_code_agent/_provider.py +513 -0
  135. package/server/nodes/agent/claude_code_agent/_skills.py +245 -0
  136. package/server/nodes/agent/claude_code_agent/meta.json +3 -0
  137. package/server/nodes/agent/{codex_agent.py → codex_agent/__init__.py} +26 -35
  138. package/server/nodes/agent/codex_agent/meta.json +3 -0
  139. package/server/nodes/agent/{coding_agent.py → coding_agent/__init__.py} +1 -1
  140. package/server/nodes/agent/coding_agent/meta.json +3 -0
  141. package/server/nodes/agent/{consumer_agent.py → consumer_agent/__init__.py} +1 -1
  142. package/server/nodes/agent/consumer_agent/meta.json +3 -0
  143. package/server/nodes/agent/{orchestrator_agent.py → orchestrator_agent/__init__.py} +5 -2
  144. package/server/nodes/agent/orchestrator_agent/meta.json +3 -0
  145. package/server/nodes/agent/{payments_agent.py → payments_agent/__init__.py} +1 -1
  146. package/server/nodes/agent/payments_agent/meta.json +3 -0
  147. package/server/nodes/agent/{productivity_agent.py → productivity_agent/__init__.py} +1 -1
  148. package/server/nodes/agent/productivity_agent/meta.json +3 -0
  149. package/server/nodes/agent/{rlm_agent.py → rlm_agent/__init__.py} +18 -17
  150. package/server/nodes/agent/rlm_agent/meta.json +3 -0
  151. package/server/nodes/agent/{social_agent.py → social_agent/__init__.py} +1 -1
  152. package/server/nodes/agent/social_agent/meta.json +3 -0
  153. package/server/nodes/agent/{task_agent.py → task_agent/__init__.py} +1 -1
  154. package/server/nodes/agent/task_agent/meta.json +3 -0
  155. package/server/nodes/agent/{tool_agent.py → tool_agent/__init__.py} +1 -1
  156. package/server/nodes/agent/tool_agent/meta.json +3 -0
  157. package/server/nodes/agent/{travel_agent.py → travel_agent/__init__.py} +1 -1
  158. package/server/nodes/agent/travel_agent/meta.json +3 -0
  159. package/server/nodes/agent/{web_agent.py → web_agent/__init__.py} +1 -1
  160. package/server/nodes/agent/web_agent/meta.json +3 -0
  161. package/server/nodes/android/__init__.py +24 -0
  162. package/server/nodes/android/_base.py +93 -76
  163. package/server/nodes/android/_dispatcher.py +140 -223
  164. package/server/nodes/android/_events.py +154 -0
  165. package/server/nodes/android/_handlers.py +13 -7
  166. package/server/nodes/android/_option_loaders.py +1 -4
  167. package/server/nodes/android/_refresh.py +27 -37
  168. package/server/nodes/android/_relay/broadcaster.py +25 -41
  169. package/server/nodes/android/_relay/client.py +23 -42
  170. package/server/nodes/android/_relay/manager.py +1 -0
  171. package/server/nodes/android/_relay/protocol.py +6 -0
  172. package/server/nodes/android/_router.py +48 -133
  173. package/server/nodes/android/{airplane_mode_control.py → airplane_mode_control/__init__.py} +2 -1
  174. package/server/nodes/android/airplane_mode_control/meta.json +3 -0
  175. package/server/nodes/android/{app_launcher.py → app_launcher/__init__.py} +2 -1
  176. package/server/nodes/android/app_launcher/meta.json +3 -0
  177. package/server/nodes/android/{app_list.py → app_list/__init__.py} +2 -1
  178. package/server/nodes/android/app_list/meta.json +3 -0
  179. package/server/nodes/android/{audio_automation.py → audio_automation/__init__.py} +2 -1
  180. package/server/nodes/android/audio_automation/meta.json +3 -0
  181. package/server/nodes/android/{battery_monitor.py → battery_monitor/__init__.py} +2 -1
  182. package/server/nodes/android/battery_monitor/meta.json +3 -0
  183. package/server/nodes/android/{bluetooth_automation.py → bluetooth_automation/__init__.py} +2 -1
  184. package/server/nodes/android/bluetooth_automation/meta.json +3 -0
  185. package/server/nodes/android/{camera_control.py → camera_control/__init__.py} +2 -1
  186. package/server/nodes/android/camera_control/meta.json +3 -0
  187. package/server/nodes/android/{device_state_automation.py → device_state_automation/__init__.py} +2 -1
  188. package/server/nodes/android/device_state_automation/meta.json +3 -0
  189. package/server/nodes/android/{environmental_sensors.py → environmental_sensors/__init__.py} +2 -1
  190. package/server/nodes/android/environmental_sensors/meta.json +3 -0
  191. package/server/nodes/android/{location.py → location/__init__.py} +2 -1
  192. package/server/nodes/android/location/meta.json +3 -0
  193. package/server/nodes/android/{media_control.py → media_control/__init__.py} +2 -1
  194. package/server/nodes/android/media_control/meta.json +3 -0
  195. package/server/nodes/android/{motion_detection.py → motion_detection/__init__.py} +2 -1
  196. package/server/nodes/android/motion_detection/meta.json +3 -0
  197. package/server/nodes/android/{network_monitor.py → network_monitor/__init__.py} +2 -1
  198. package/server/nodes/android/network_monitor/meta.json +3 -0
  199. package/server/nodes/android/{screen_control_automation.py → screen_control_automation/__init__.py} +2 -1
  200. package/server/nodes/android/screen_control_automation/meta.json +3 -0
  201. package/server/nodes/android/{system_info.py → system_info/__init__.py} +2 -1
  202. package/server/nodes/android/system_info/meta.json +3 -0
  203. package/server/nodes/android/{wifi_automation.py → wifi_automation/__init__.py} +2 -1
  204. package/server/nodes/android/wifi_automation/meta.json +3 -0
  205. package/server/nodes/browser/__init__.py +22 -1
  206. package/server/nodes/browser/_install.py +63 -0
  207. package/server/nodes/browser/_service.py +21 -25
  208. package/server/nodes/browser/{browser.py → browser/__init__.py} +58 -25
  209. package/server/nodes/browser/browser/meta.json +3 -0
  210. package/server/nodes/chat/{chat_history.py → chat_history/__init__.py} +2 -4
  211. package/server/nodes/chat/chat_history/meta.json +3 -0
  212. package/server/nodes/chat/{chat_send.py → chat_send/__init__.py} +2 -4
  213. package/server/nodes/chat/chat_send/icon.svg +1 -0
  214. package/server/nodes/chat/chat_send/meta.json +3 -0
  215. package/server/nodes/code/_base.py +1 -1
  216. package/server/nodes/code/{javascript_executor.py → javascript_executor/__init__.py} +5 -5
  217. package/server/nodes/code/javascript_executor/meta.json +3 -0
  218. package/server/nodes/code/{python_executor.py → python_executor/__init__.py} +32 -14
  219. package/server/nodes/code/python_executor/meta.json +3 -0
  220. package/server/nodes/code/{typescript_executor.py → typescript_executor/__init__.py} +5 -5
  221. package/server/nodes/code/typescript_executor/meta.json +3 -0
  222. package/server/nodes/document/{document_parser.py → document_parser/__init__.py} +26 -15
  223. package/server/nodes/document/document_parser/meta.json +3 -0
  224. package/server/nodes/document/{embedding_generator.py → embedding_generator/__init__.py} +16 -9
  225. package/server/nodes/document/embedding_generator/meta.json +3 -0
  226. package/server/nodes/document/{file_downloader.py → file_downloader/__init__.py} +30 -20
  227. package/server/nodes/document/file_downloader/meta.json +3 -0
  228. package/server/nodes/document/{http_scraper.py → http_scraper/__init__.py} +31 -21
  229. package/server/nodes/document/http_scraper/meta.json +3 -0
  230. package/server/nodes/document/{text_chunker.py → text_chunker/__init__.py} +17 -12
  231. package/server/nodes/document/text_chunker/meta.json +3 -0
  232. package/server/nodes/document/{vector_store.py → vector_store/__init__.py} +88 -72
  233. package/server/nodes/document/vector_store/meta.json +3 -0
  234. package/server/nodes/email/__init__.py +9 -2
  235. package/server/nodes/email/_events.py +54 -0
  236. package/server/nodes/email/_filters.py +3 -3
  237. package/server/nodes/email/_himalaya.py +95 -50
  238. package/server/nodes/email/_service.py +23 -13
  239. package/server/nodes/email/{email_read.py → email_read/__init__.py} +23 -11
  240. package/server/nodes/email/email_read/icon.svg +6 -0
  241. package/server/nodes/email/email_read/meta.json +3 -0
  242. package/server/nodes/email/{email_receive.py → email_receive/__init__.py} +45 -23
  243. package/server/nodes/email/email_receive/meta.json +3 -0
  244. package/server/nodes/email/{email_send.py → email_send/__init__.py} +13 -7
  245. package/server/nodes/email/email_send/meta.json +3 -0
  246. package/server/nodes/filesystem/_backend.py +1 -5
  247. package/server/nodes/filesystem/{file_modify.py → file_modify/__init__.py} +10 -5
  248. package/server/nodes/filesystem/file_modify/meta.json +3 -0
  249. package/server/nodes/filesystem/{file_read.py → file_read/__init__.py} +7 -3
  250. package/server/nodes/filesystem/file_read/meta.json +3 -0
  251. package/server/nodes/filesystem/{fs_search.py → fs_search/__init__.py} +11 -3
  252. package/server/nodes/filesystem/fs_search/meta.json +3 -0
  253. package/server/nodes/filesystem/{shell.py → shell/__init__.py} +12 -5
  254. package/server/nodes/filesystem/shell/meta.json +3 -0
  255. package/server/nodes/google/__init__.py +12 -0
  256. package/server/nodes/google/_auth_helper.py +7 -13
  257. package/server/nodes/google/_base.py +14 -11
  258. package/server/nodes/google/_credentials.py +2 -1
  259. package/server/nodes/google/_events.py +47 -0
  260. package/server/nodes/google/_filters.py +3 -3
  261. package/server/nodes/google/_gmail.py +70 -47
  262. package/server/nodes/google/_handlers.py +3 -1
  263. package/server/nodes/google/_oauth.py +25 -11
  264. package/server/nodes/google/_option_loaders.py +9 -30
  265. package/server/nodes/google/_refresh.py +8 -12
  266. package/server/nodes/google/_router.py +4 -5
  267. package/server/nodes/google/{calendar.py → calendar/__init__.py} +87 -64
  268. package/server/nodes/google/calendar/meta.json +3 -0
  269. package/server/nodes/google/{contacts.py → contacts/__init__.py} +84 -72
  270. package/server/nodes/google/contacts/meta.json +3 -0
  271. package/server/nodes/google/{drive.py → drive/__init__.py} +87 -72
  272. package/server/nodes/google/drive/meta.json +3 -0
  273. package/server/nodes/google/{gmail.py → gmail/__init__.py} +73 -39
  274. package/server/nodes/google/gmail/meta.json +3 -0
  275. package/server/nodes/google/{gmail_receive.py → gmail_receive/__init__.py} +31 -24
  276. package/server/nodes/google/gmail_receive/icon.svg +7 -0
  277. package/server/nodes/google/gmail_receive/meta.json +3 -0
  278. package/server/nodes/google/google.svg +7 -0
  279. package/server/nodes/google/{sheets.py → sheets/__init__.py} +54 -42
  280. package/server/nodes/google/sheets/meta.json +3 -0
  281. package/server/nodes/google/{tasks.py → tasks/__init__.py} +56 -43
  282. package/server/nodes/google/tasks/meta.json +3 -0
  283. package/server/nodes/groups.py +28 -28
  284. package/server/nodes/location/__init__.py +31 -1
  285. package/server/nodes/location/_credentials.py +1 -6
  286. package/server/nodes/location/_service.py +88 -107
  287. package/server/nodes/location/{gmaps_create.py → gmaps_create/__init__.py} +6 -6
  288. package/server/nodes/location/gmaps_create/meta.json +3 -0
  289. package/server/nodes/location/{gmaps_locations.py → gmaps_locations/__init__.py} +8 -6
  290. package/server/nodes/location/gmaps_locations/meta.json +3 -0
  291. package/server/nodes/location/{gmaps_nearby_places.py → gmaps_nearby_places/__init__.py} +8 -6
  292. package/server/nodes/location/gmaps_nearby_places/meta.json +3 -0
  293. package/server/nodes/model/_base.py +10 -7
  294. package/server/nodes/model/_credentials.py +10 -10
  295. package/server/nodes/model/_local_validator.py +28 -24
  296. package/server/nodes/model/{anthropic_chat_model.py → anthropic_chat_model/__init__.py} +5 -3
  297. package/server/nodes/model/anthropic_chat_model/meta.json +3 -0
  298. package/server/nodes/model/{cerebras_chat_model.py → cerebras_chat_model/__init__.py} +5 -3
  299. package/server/nodes/model/cerebras_chat_model/meta.json +3 -0
  300. package/server/nodes/model/{deepseek_chat_model.py → deepseek_chat_model/__init__.py} +8 -4
  301. package/server/nodes/model/deepseek_chat_model/meta.json +3 -0
  302. package/server/nodes/model/{gemini_chat_model.py → gemini_chat_model/__init__.py} +5 -3
  303. package/server/nodes/model/gemini_chat_model/meta.json +3 -0
  304. package/server/nodes/model/{groq_chat_model.py → groq_chat_model/__init__.py} +2 -2
  305. package/server/nodes/model/groq_chat_model/meta.json +3 -0
  306. package/server/nodes/model/{kimi_chat_model.py → kimi_chat_model/__init__.py} +2 -2
  307. package/server/nodes/model/kimi_chat_model/meta.json +3 -0
  308. package/server/nodes/model/{lmstudio_chat_model.py → lmstudio_chat_model/__init__.py} +2 -2
  309. package/server/nodes/model/lmstudio_chat_model/meta.json +3 -0
  310. package/server/nodes/model/{mistral_chat_model.py → mistral_chat_model/__init__.py} +2 -2
  311. package/server/nodes/model/mistral_chat_model/meta.json +3 -0
  312. package/server/nodes/model/{ollama_chat_model.py → ollama_chat_model/__init__.py} +2 -2
  313. package/server/nodes/model/ollama_chat_model/meta.json +3 -0
  314. package/server/nodes/model/{openai_chat_model.py → openai_chat_model/__init__.py} +8 -4
  315. package/server/nodes/model/openai_chat_model/meta.json +3 -0
  316. package/server/nodes/model/{openrouter_chat_model.py → openrouter_chat_model/__init__.py} +8 -4
  317. package/server/nodes/model/openrouter_chat_model/meta.json +3 -0
  318. package/server/nodes/proxy/_usage.py +14 -15
  319. package/server/nodes/proxy/{proxy_config.py → proxy_config/__init__.py} +39 -30
  320. package/server/nodes/proxy/proxy_config/meta.json +3 -0
  321. package/server/nodes/proxy/{proxy_request.py → proxy_request/__init__.py} +30 -16
  322. package/server/nodes/proxy/proxy_request/meta.json +3 -0
  323. package/server/nodes/proxy/{proxy_status.py → proxy_status/__init__.py} +2 -0
  324. package/server/nodes/proxy/proxy_status/meta.json +3 -0
  325. package/server/nodes/scheduler/{cron_scheduler.py → cron_scheduler/__init__.py} +96 -23
  326. package/server/nodes/scheduler/cron_scheduler/_workflow.py +155 -0
  327. package/server/nodes/scheduler/cron_scheduler/meta.json +3 -0
  328. package/server/nodes/scheduler/{timer.py → timer/__init__.py} +6 -5
  329. package/server/nodes/scheduler/timer/meta.json +3 -0
  330. package/server/nodes/scraper/_credentials.py +0 -1
  331. package/server/nodes/scraper/{apify_actor.py → apify_actor/__init__.py} +44 -35
  332. package/server/nodes/scraper/apify_actor/icon.svg +5 -0
  333. package/server/nodes/scraper/apify_actor/meta.json +3 -0
  334. package/server/nodes/scraper/{crawlee_scraper.py → crawlee_scraper/__init__.py} +96 -57
  335. package/server/nodes/scraper/crawlee_scraper/meta.json +3 -0
  336. package/server/nodes/search/{brave_search.py → brave_search/__init__.py} +6 -5
  337. package/server/nodes/search/brave_search/icon.svg +3 -0
  338. package/server/nodes/search/brave_search/meta.json +3 -0
  339. package/server/nodes/search/{duckduckgo_search.py → duckduckgo_search/__init__.py} +17 -6
  340. package/server/nodes/search/duckduckgo_search/meta.json +3 -0
  341. package/server/nodes/search/{perplexity_search.py → perplexity_search/__init__.py} +4 -5
  342. package/server/nodes/search/perplexity_search/icon.svg +3 -0
  343. package/server/nodes/search/perplexity_search/meta.json +3 -0
  344. package/server/nodes/search/{serper_search.py → serper_search/__init__.py} +32 -25
  345. package/server/nodes/search/serper_search/icon.svg +3 -0
  346. package/server/nodes/search/serper_search/meta.json +3 -0
  347. package/server/nodes/skill/__init__.py +21 -1
  348. package/server/nodes/skill/_expander.py +75 -0
  349. package/server/nodes/skill/{master_skill.py → master_skill/__init__.py} +2 -8
  350. package/server/nodes/skill/master_skill/_events.py +84 -0
  351. package/server/nodes/skill/master_skill/meta.json +3 -0
  352. package/server/nodes/skill/{simple_memory.py → simple_memory/__init__.py} +8 -16
  353. package/server/nodes/skill/simple_memory/meta.json +3 -0
  354. package/server/nodes/social/_base.py +223 -231
  355. package/server/nodes/social/{social_receive.py → social_receive/__init__.py} +38 -13
  356. package/server/nodes/social/social_receive/meta.json +3 -0
  357. package/server/nodes/social/{social_send.py → social_send/__init__.py} +71 -29
  358. package/server/nodes/social/social_send/icon.svg +1 -0
  359. package/server/nodes/social/social_send/meta.json +3 -0
  360. package/server/nodes/stripe/__init__.py +7 -3
  361. package/server/nodes/stripe/_credentials.py +0 -1
  362. package/server/nodes/stripe/_handlers.py +18 -7
  363. package/server/nodes/stripe/_install.py +14 -15
  364. package/server/nodes/stripe/_source.py +5 -5
  365. package/server/nodes/stripe/icon.svg +1 -0
  366. package/server/nodes/stripe/meta.json +3 -0
  367. package/server/nodes/stripe/stripe_action.py +4 -4
  368. package/server/nodes/stripe/stripe_receive.py +6 -9
  369. package/server/nodes/telegram/__init__.py +13 -0
  370. package/server/nodes/telegram/_credentials.py +2 -7
  371. package/server/nodes/telegram/_events.py +167 -0
  372. package/server/nodes/telegram/_filters.py +3 -11
  373. package/server/nodes/telegram/_handlers.py +17 -7
  374. package/server/nodes/telegram/_refresh.py +24 -34
  375. package/server/nodes/telegram/_service.py +29 -45
  376. package/server/nodes/telegram/meta.json +3 -0
  377. package/server/nodes/telegram/telegram.svg +3 -0
  378. package/server/nodes/telegram/telegram_receive.py +38 -18
  379. package/server/nodes/telegram/telegram_send.py +21 -19
  380. package/server/nodes/text/{file_handler.py → file_handler/__init__.py} +7 -1
  381. package/server/nodes/text/file_handler/meta.json +3 -0
  382. package/server/nodes/text/{text_generator.py → text_generator/__init__.py} +2 -1
  383. package/server/nodes/text/text_generator/meta.json +3 -0
  384. package/server/nodes/tool/{agent_builder.py → agent_builder/__init__.py} +105 -100
  385. package/server/nodes/tool/agent_builder/_events.py +91 -0
  386. package/server/nodes/tool/agent_builder/meta.json +3 -0
  387. package/server/nodes/tool/{calculator_tool.py → calculator_tool/__init__.py} +19 -7
  388. package/server/nodes/tool/calculator_tool/meta.json +3 -0
  389. package/server/nodes/tool/{current_time_tool.py → current_time_tool/__init__.py} +6 -4
  390. package/server/nodes/tool/current_time_tool/meta.json +3 -0
  391. package/server/nodes/tool/{task_manager.py → task_manager/__init__.py} +17 -18
  392. package/server/nodes/tool/task_manager/meta.json +3 -0
  393. package/server/nodes/tool/{write_todos.py → write_todos/__init__.py} +20 -6
  394. package/server/nodes/tool/write_todos/meta.json +3 -0
  395. package/server/nodes/trigger/{chat_trigger.py → chat_trigger/__init__.py} +11 -7
  396. package/server/nodes/trigger/chat_trigger/_events.py +53 -0
  397. package/server/nodes/trigger/chat_trigger/meta.json +3 -0
  398. package/server/nodes/trigger/{task_trigger.py → task_trigger/__init__.py} +10 -7
  399. package/server/nodes/trigger/task_trigger/meta.json +3 -0
  400. package/server/nodes/trigger/{webhook_trigger.py → webhook_trigger/__init__.py} +10 -7
  401. package/server/nodes/trigger/webhook_trigger/_events.py +54 -0
  402. package/server/nodes/trigger/webhook_trigger/meta.json +3 -0
  403. package/server/nodes/twitter/__init__.py +7 -1
  404. package/server/nodes/twitter/_base.py +86 -61
  405. package/server/nodes/twitter/_credentials.py +7 -5
  406. package/server/nodes/twitter/_events.py +101 -0
  407. package/server/nodes/twitter/_filters.py +9 -9
  408. package/server/nodes/twitter/_handlers.py +3 -1
  409. package/server/nodes/twitter/_oauth.py +1 -2
  410. package/server/nodes/twitter/_refresh.py +8 -12
  411. package/server/nodes/twitter/{twitter_receive.py → twitter_receive/__init__.py} +7 -7
  412. package/server/nodes/twitter/twitter_receive/icon.svg +1 -0
  413. package/server/nodes/twitter/twitter_receive/meta.json +3 -0
  414. package/server/nodes/twitter/{twitter_search.py → twitter_search/__init__.py} +16 -11
  415. package/server/nodes/twitter/twitter_search/icon.svg +1 -0
  416. package/server/nodes/twitter/twitter_search/meta.json +3 -0
  417. package/server/nodes/twitter/{twitter_send.py → twitter_send/__init__.py} +60 -27
  418. package/server/nodes/twitter/twitter_send/icon.svg +1 -0
  419. package/server/nodes/twitter/twitter_send/meta.json +3 -0
  420. package/server/nodes/twitter/{twitter_user.py → twitter_user/__init__.py} +34 -19
  421. package/server/nodes/twitter/twitter_user/icon.svg +1 -0
  422. package/server/nodes/twitter/twitter_user/meta.json +3 -0
  423. package/server/nodes/utility/{console.py → console/__init__.py} +17 -22
  424. package/server/nodes/utility/console/meta.json +3 -0
  425. package/server/nodes/utility/{http_request.py → http_request/__init__.py} +9 -6
  426. package/server/nodes/utility/http_request/meta.json +3 -0
  427. package/server/nodes/utility/{process_manager.py → process_manager/__init__.py} +10 -6
  428. package/server/nodes/utility/process_manager/meta.json +3 -0
  429. package/server/nodes/utility/team_monitor/meta.json +3 -0
  430. package/server/nodes/utility/{webhook_response.py → webhook_response/__init__.py} +12 -7
  431. package/server/nodes/utility/webhook_response/meta.json +3 -0
  432. package/server/nodes/visuals.json +69 -251
  433. package/server/nodes/whatsapp/__init__.py +24 -0
  434. package/server/nodes/whatsapp/_base.py +283 -338
  435. package/server/nodes/whatsapp/_credentials.py +44 -0
  436. package/server/nodes/whatsapp/_events.py +277 -0
  437. package/server/nodes/whatsapp/_filters.py +36 -37
  438. package/server/nodes/whatsapp/_handlers.py +2 -0
  439. package/server/nodes/whatsapp/_option_loaders.py +1 -3
  440. package/server/nodes/whatsapp/_refresh.py +13 -18
  441. package/server/nodes/whatsapp/_runtime.py +9 -6
  442. package/server/nodes/whatsapp/_service.py +89 -152
  443. package/server/nodes/whatsapp/meta.json +3 -0
  444. package/server/nodes/whatsapp/whatsapp_db.py +116 -54
  445. package/server/nodes/whatsapp/whatsapp_receive.py +30 -13
  446. package/server/nodes/whatsapp/whatsapp_send.py +60 -37
  447. package/server/nodes/workflow/{start.py → start/__init__.py} +1 -4
  448. package/server/nodes/workflow/start/meta.json +3 -0
  449. package/server/package-lock.json +3 -3
  450. package/server/package.json +3 -0
  451. package/server/pyproject.toml +39 -10
  452. package/server/requirements.txt +3 -5
  453. package/server/routers/__init__.py +1 -1
  454. package/server/routers/auth.py +16 -56
  455. package/server/routers/database.py +27 -50
  456. package/server/routers/nodejs_compat.py +25 -87
  457. package/server/routers/schemas.py +66 -2
  458. package/server/routers/webhook.py +12 -12
  459. package/server/routers/websocket.py +312 -1716
  460. package/server/routers/workflow.py +28 -53
  461. package/server/scripts/smoke_test_skills.py +178 -0
  462. package/server/services/__init__.py +1 -1
  463. package/server/services/_supervisor/process.py +9 -3
  464. package/server/services/_supervisor/registry.py +3 -3
  465. package/server/services/_supervisor/util.py +1 -1
  466. package/server/services/agent_team.py +15 -43
  467. package/server/services/agent_teams/__init__.py +17 -0
  468. package/server/services/agent_teams/handlers.py +195 -0
  469. package/server/services/ai.py +853 -1108
  470. package/server/services/auth.py +10 -34
  471. package/server/services/chat_client.py +5 -34
  472. package/server/services/circuit_breaker.py +2 -6
  473. package/server/services/cli_agent/__init__.py +28 -4
  474. package/server/services/cli_agent/_cli_auth.py +61 -0
  475. package/server/services/cli_agent/_handlers.py +24 -183
  476. package/server/services/cli_agent/config.py +5 -8
  477. package/server/services/cli_agent/factory.py +168 -22
  478. package/server/services/cli_agent/jsonl_watcher.py +380 -0
  479. package/server/services/cli_agent/lockfile.py +9 -2
  480. package/server/services/cli_agent/mcp_server.py +110 -34
  481. package/server/services/cli_agent/protocol.py +37 -19
  482. package/server/services/cli_agent/providers/__init__.py +8 -4
  483. package/server/services/cli_agent/providers/google_gemini.py +11 -5
  484. package/server/services/cli_agent/providers/openai_codex.py +34 -34
  485. package/server/services/cli_agent/service.py +245 -83
  486. package/server/services/cli_agent/session.py +409 -229
  487. package/server/services/cli_agent/transports/__init__.py +47 -0
  488. package/server/services/cli_agent/transports/base.py +111 -0
  489. package/server/services/cli_agent/transports/posix.py +196 -0
  490. package/server/services/cli_agent/transports/windows.py +189 -0
  491. package/server/services/cli_agent/types.py +45 -18
  492. package/server/services/cli_agent/workflow_tools.py +28 -15
  493. package/server/services/compaction.py +68 -52
  494. package/server/services/credential_registry.py +6 -20
  495. package/server/services/credentials/__init__.py +18 -0
  496. package/server/services/credentials/handlers.py +196 -0
  497. package/server/services/deployment/__init__.py +12 -1
  498. package/server/services/deployment/canary_registry.py +137 -0
  499. package/server/services/deployment/handlers.py +382 -0
  500. package/server/services/deployment/manager.py +653 -163
  501. package/server/services/deployment/poll_registry.py +2 -6
  502. package/server/services/deployment/state.py +2 -0
  503. package/server/services/deployment/triggers.py +87 -93
  504. package/server/services/event_waiter.py +47 -54
  505. package/server/services/events/__init__.py +11 -0
  506. package/server/services/events/admin_handlers.py +368 -0
  507. package/server/services/events/daemon.py +3 -1
  508. package/server/services/events/dispatch.py +188 -0
  509. package/server/services/events/envelope.py +264 -45
  510. package/server/services/events/oauth_lifecycle.py +98 -42
  511. package/server/services/events/triggers.py +3 -13
  512. package/server/services/events/verifiers/hmac_basic.py +1 -1
  513. package/server/services/events/verifiers/standard_webhooks.py +2 -4
  514. package/server/services/events/webhook.py +2 -3
  515. package/server/services/example_loader.py +73 -15
  516. package/server/services/execution/cache.py +36 -76
  517. package/server/services/execution/conditions.py +7 -20
  518. package/server/services/execution/dlq.py +20 -24
  519. package/server/services/execution/executor.py +234 -265
  520. package/server/services/execution/models.py +40 -46
  521. package/server/services/execution/recovery.py +23 -46
  522. package/server/services/handlers/__init__.py +12 -16
  523. package/server/services/handlers/todo.py +3 -6
  524. package/server/services/handlers/tools.py +143 -194
  525. package/server/services/handlers/triggers.py +24 -23
  526. package/server/services/llm/config.py +10 -1
  527. package/server/services/llm/factory.py +16 -4
  528. package/server/services/llm/messages.py +1 -5
  529. package/server/services/llm/protocol.py +9 -1
  530. package/server/services/llm/providers/anthropic.py +23 -12
  531. package/server/services/llm/providers/gemini.py +43 -22
  532. package/server/services/llm/providers/openai.py +14 -6
  533. package/server/services/llm/providers/openrouter.py +6 -1
  534. package/server/services/markdown_formatter.py +1 -2
  535. package/server/services/memory/__init__.py +2 -2
  536. package/server/services/memory/jsonl.py +6 -2
  537. package/server/services/memory/markdown.py +6 -6
  538. package/server/services/memory/state.py +6 -5
  539. package/server/services/memory_store.py +8 -12
  540. package/server/services/model_registry.py +22 -20
  541. package/server/services/node_executor.py +85 -80
  542. package/server/services/node_output_schemas.py +4 -7
  543. package/server/services/node_registry.py +40 -4
  544. package/server/services/node_spec.py +3 -7
  545. package/server/services/nodejs_client.py +4 -14
  546. package/server/services/oauth_utils.py +11 -7
  547. package/server/services/parameter_resolver.py +30 -36
  548. package/server/services/plugin/base.py +321 -38
  549. package/server/services/plugin/connection.py +12 -7
  550. package/server/services/plugin/credential.py +80 -22
  551. package/server/services/plugin/edge_walker.py +128 -105
  552. package/server/services/plugin/identifiers.py +48 -0
  553. package/server/services/plugin/interceptor.py +1 -1
  554. package/server/services/plugin/oauth.py +25 -21
  555. package/server/services/plugin/operation.py +1 -1
  556. package/server/services/plugin/polling.py +151 -26
  557. package/server/services/plugin/registry.py +52 -4
  558. package/server/services/plugin/routing.py +6 -9
  559. package/server/services/plugin/scaling.py +36 -18
  560. package/server/services/plugin/service_factories.py +95 -0
  561. package/server/services/plugin/shutdown_hooks.py +103 -0
  562. package/server/services/plugin/social_provider_registry.py +80 -0
  563. package/server/services/plugin/ws.py +2 -1
  564. package/server/services/pricing.py +26 -40
  565. package/server/services/pricing_handlers.py +90 -0
  566. package/server/services/process_service.py +33 -32
  567. package/server/services/proxy/models.py +15 -9
  568. package/server/services/proxy/service.py +26 -40
  569. package/server/services/rlm/adapters.py +43 -40
  570. package/server/services/rlm/constants.py +9 -9
  571. package/server/services/rlm/service.py +57 -45
  572. package/server/services/scheduler.py +8 -39
  573. package/server/services/settings/__init__.py +16 -0
  574. package/server/services/settings/handlers.py +275 -0
  575. package/server/services/skill_loader.py +53 -45
  576. package/server/services/skill_prompt.py +8 -6
  577. package/server/services/skills/__init__.py +23 -0
  578. package/server/services/skills/handlers.py +479 -0
  579. package/server/services/status_broadcaster.py +314 -291
  580. package/server/services/temporal/__init__.py +22 -1
  581. package/server/services/temporal/_handlers.py +65 -0
  582. package/server/services/temporal/_install.py +158 -0
  583. package/server/services/temporal/_refresh.py +57 -0
  584. package/server/services/temporal/_retry_policies.py +85 -0
  585. package/server/services/temporal/_runtime.py +181 -0
  586. package/server/services/temporal/_supervised_runtime.py +102 -0
  587. package/server/services/temporal/activities.py +168 -11
  588. package/server/services/temporal/agent_activities.py +683 -0
  589. package/server/services/temporal/agent_workflow.py +601 -0
  590. package/server/services/temporal/client.py +58 -13
  591. package/server/services/temporal/executor.py +2 -3
  592. package/server/services/temporal/plugin_activities.py +37 -2
  593. package/server/services/temporal/plugin_registry.py +82 -0
  594. package/server/services/temporal/polling_trigger_workflow.py +267 -0
  595. package/server/services/temporal/schedules.py +220 -0
  596. package/server/services/temporal/search_attributes.py +177 -0
  597. package/server/services/temporal/trigger_listener_workflow.py +378 -0
  598. package/server/services/temporal/worker.py +111 -18
  599. package/server/services/temporal/workflow.py +259 -40
  600. package/server/services/temporal/ws_client.py +22 -11
  601. package/server/services/text.py +14 -28
  602. package/server/services/tracked_http.py +29 -49
  603. package/server/services/user_auth.py +7 -21
  604. package/server/services/workflow.py +28 -20
  605. package/server/services/workflow_import.py +351 -0
  606. package/server/services/workflow_ops.py +4 -0
  607. package/server/services/workflow_storage/__init__.py +18 -0
  608. package/server/services/workflow_storage/handlers.py +132 -0
  609. package/server/services/workflow_validator.py +209 -0
  610. package/server/services/ws_handler_registry.py +80 -9
  611. package/server/skills/assistant/agent-builder-skill/SKILL.md +6 -6
  612. package/server/tests/conftest.py +54 -3
  613. package/server/tests/credentials/test_auth_service.py +9 -21
  614. package/server/tests/credentials/test_credential_broadcasts.py +116 -22
  615. package/server/tests/credentials/test_credentials_database.py +12 -38
  616. package/server/tests/credentials/test_encryption.py +3 -9
  617. package/server/tests/credentials/test_google_oauth.py +1 -3
  618. package/server/tests/credentials/test_oauth_utils.py +31 -38
  619. package/server/tests/credentials/test_twitter_oauth.py +1 -3
  620. package/server/tests/credentials/test_websocket_handlers.py +37 -72
  621. package/server/tests/fixtures/tool_names_snapshot.json +78 -0
  622. package/server/tests/llm/test_factory.py +12 -4
  623. package/server/tests/llm/test_providers.py +25 -32
  624. package/server/tests/llm/test_wiring.py +27 -22
  625. package/server/tests/nodes/_compat.py +4 -5
  626. package/server/tests/nodes/_harness.py +31 -24
  627. package/server/tests/nodes/_mocks.py +2 -6
  628. package/server/tests/nodes/test_agent_builder.py +43 -35
  629. package/server/tests/nodes/test_ai_agents.py +29 -24
  630. package/server/tests/nodes/test_ai_chat_models.py +3 -9
  631. package/server/tests/nodes/test_ai_tools.py +29 -24
  632. package/server/tests/nodes/test_android.py +34 -64
  633. package/server/tests/nodes/test_chat_utility.py +2 -2
  634. package/server/tests/nodes/test_code_fs_process.py +26 -84
  635. package/server/tests/nodes/test_document.py +23 -47
  636. package/server/tests/nodes/test_email.py +88 -51
  637. package/server/tests/nodes/test_google_workspace.py +26 -20
  638. package/server/tests/nodes/test_http_proxy.py +43 -89
  639. package/server/tests/nodes/test_search.py +3 -9
  640. package/server/tests/nodes/test_specialized_agents.py +58 -162
  641. package/server/tests/nodes/test_stripe_plugin.py +25 -5
  642. package/server/tests/nodes/test_telegram_social.py +33 -37
  643. package/server/tests/nodes/test_twitter.py +59 -150
  644. package/server/tests/nodes/test_web_automation.py +21 -51
  645. package/server/tests/nodes/test_whatsapp.py +13 -19
  646. package/server/tests/nodes/test_workflow_triggers.py +16 -45
  647. package/server/tests/services/cli_agent/test_claude_session_events.py +201 -0
  648. package/server/tests/services/cli_agent/test_jsonl_watcher.py +190 -0
  649. package/server/tests/services/cli_agent/test_mcp_server.py +67 -29
  650. package/server/tests/services/cli_agent/test_providers.py +236 -47
  651. package/server/tests/services/cli_agent/test_service.py +9 -7
  652. package/server/tests/services/memory/test_jsonl.py +30 -25
  653. package/server/tests/services/test_events.py +26 -7
  654. package/server/tests/services/test_identifiers.py +122 -0
  655. package/server/tests/services/test_process_lifecycle.py +129 -0
  656. package/server/tests/services/test_supervisor.py +0 -1
  657. package/server/tests/temporal/__init__.py +0 -0
  658. package/server/tests/temporal/test_agent_workflow.py +215 -0
  659. package/server/tests/temporal/test_dispatch.py +231 -0
  660. package/server/tests/test_admin_handlers.py +394 -0
  661. package/server/tests/test_auto_skill.py +4 -2
  662. package/server/tests/test_canary_registry.py +310 -0
  663. package/server/tests/test_chat_trigger_canary_producer.py +101 -0
  664. package/server/tests/test_cloudevents_node_parameters.py +129 -0
  665. package/server/tests/test_credential_icon.py +115 -0
  666. package/server/tests/test_cron_canary.py +511 -0
  667. package/server/tests/test_deployment_canary_listener.py +692 -0
  668. package/server/tests/test_event_framework_phase_a.py +537 -0
  669. package/server/tests/test_no_raw_prints.py +131 -0
  670. package/server/tests/test_node_spec.py +196 -103
  671. package/server/tests/test_parameter_resolver.py +20 -20
  672. package/server/tests/test_plugin_contract.py +76 -49
  673. package/server/tests/test_plugin_helpers.py +0 -1
  674. package/server/tests/test_plugin_self_containment.py +40 -47
  675. package/server/tests/test_polling_trigger_workflow.py +572 -0
  676. package/server/tests/test_retry_policies.py +146 -0
  677. package/server/tests/test_service_factories.py +168 -0
  678. package/server/tests/test_shutdown_hooks.py +199 -0
  679. package/server/tests/test_social_provider_registry.py +177 -0
  680. package/server/tests/test_status_broadcasts.py +214 -63
  681. package/server/tests/test_task_trigger_canary_producer.py +131 -0
  682. package/server/tests/test_telegram_trigger_canary_producer.py +113 -0
  683. package/server/tests/test_tool_registry.py +110 -0
  684. package/server/tests/test_trigger_listener_workflow.py +365 -0
  685. package/server/tests/test_whatsapp_trigger_canary_producer.py +164 -0
  686. package/server/tests/test_workflow_ops.py +1 -3
  687. package/server/tests/test_workflow_validator.py +791 -0
  688. package/server/uv.lock +3539 -0
  689. package/client/dist/assets/index-DQ0nwhec.js +0 -257
  690. package/client/src/assets/icons/apify/index.ts +0 -19
  691. package/client/src/assets/icons/browser/index.ts +0 -17
  692. package/client/src/assets/icons/email/index.ts +0 -22
  693. package/client/src/assets/icons/google/index.ts +0 -34
  694. package/client/src/assets/icons/llm/deepseek.svg +0 -1
  695. package/client/src/assets/icons/llm/index.ts +0 -18
  696. package/client/src/assets/icons/llm/kimi.svg +0 -1
  697. package/client/src/assets/icons/llm/mistral.svg +0 -1
  698. package/client/src/assets/icons/search/index.ts +0 -28
  699. package/client/src/assets/icons/telegram/index.ts +0 -19
  700. package/machina/buildenv.py +0 -44
  701. package/machina/cli.py +0 -55
  702. package/machina/commands/__init__.py +0 -1
  703. package/machina/commands/clean.py +0 -80
  704. package/machina/commands/daemon.py +0 -150
  705. package/machina/config.py +0 -93
  706. package/machina/platform_.py +0 -37
  707. package/machina/pyproject.toml +0 -33
  708. package/server/nodes/agent/deep_agent.py +0 -103
  709. package/server/services/agents/__init__.py +0 -9
  710. package/server/services/agents/adapters.py +0 -199
  711. package/server/services/agents/constants.py +0 -10
  712. package/server/services/agents/service.py +0 -297
  713. package/server/services/cli_agent/providers/anthropic_claude.py +0 -419
  714. /package/{machina → cli}/README.md +0 -0
  715. /package/{machina → cli}/__init__.py +0 -0
  716. /package/{client/src/assets/icons/apify → server/credentials/icons}/apify.svg +0 -0
  717. /package/{client/src/assets/icons/search/brave.svg → server/credentials/icons/brave_search.svg} +0 -0
  718. /package/{client/src/assets/icons/email/read.svg → server/credentials/icons/email_himalaya.svg} +0 -0
  719. /package/{client/src/assets/icons/search → server/credentials/icons}/perplexity.svg +0 -0
  720. /package/{client/src/assets/icons/search/google.svg → server/credentials/icons/serper.svg} +0 -0
  721. /package/{client/src/assets → server/credentials}/icons/stripe.svg +0 -0
  722. /package/{client/src/assets/icons/twitter/x.svg → server/credentials/icons/twitter.svg} +0 -0
  723. /package/{client/src/assets/icons/browser/chrome.svg → server/nodes/browser/browser/icon.svg} +0 -0
  724. /package/{client/src/assets/icons/chat/chat.svg → server/nodes/chat/chat_history/icon.svg} +0 -0
  725. /package/{client/src/assets/icons/code/javascript.svg → server/nodes/code/javascript_executor/icon.svg} +0 -0
  726. /package/{client/src/assets/icons/code/python.svg → server/nodes/code/python_executor/icon.svg} +0 -0
  727. /package/{client/src/assets/icons/code/typescript.svg → server/nodes/code/typescript_executor/icon.svg} +0 -0
  728. /package/{client/src/assets/icons/email/receive.svg → server/nodes/email/email_receive/icon.svg} +0 -0
  729. /package/{client/src/assets/icons/email/send.svg → server/nodes/email/email_send/icon.svg} +0 -0
  730. /package/{client/src/assets/icons/google/calendar.svg → server/nodes/google/calendar/icon.svg} +0 -0
  731. /package/{client/src/assets/icons/google/contacts.svg → server/nodes/google/contacts/icon.svg} +0 -0
  732. /package/{client/src/assets/icons/google/drive.svg → server/nodes/google/drive/icon.svg} +0 -0
  733. /package/{client/src/assets/icons/google/gmail.svg → server/nodes/google/gmail/icon.svg} +0 -0
  734. /package/{client/src/assets/icons/google/sheets.svg → server/nodes/google/sheets/icon.svg} +0 -0
  735. /package/{client/src/assets/icons/google/tasks.svg → server/nodes/google/tasks/icon.svg} +0 -0
  736. /package/{client/src/assets/icons/search/duckduckgo.svg → server/nodes/search/duckduckgo_search/icon.svg} +0 -0
  737. /package/{client/src/assets/icons/social/social.svg → server/nodes/social/social_receive/icon.svg} +0 -0
  738. /package/{client/src/assets/icons/telegram/telegram.svg → server/nodes/telegram/icon.svg} +0 -0
  739. /package/server/nodes/utility/{team_monitor.py → team_monitor/__init__.py} +0 -0
  740. /package/{client/src/assets/icons/whatsapp/whatsapp-db.svg → server/nodes/whatsapp/icon_whatsappDb.svg} +0 -0
  741. /package/{client/src/assets/icons/whatsapp/whatsapp-receive.svg → server/nodes/whatsapp/icon_whatsappReceive.svg} +0 -0
  742. /package/{client/src/assets/icons/whatsapp/whatsapp-send.svg → server/nodes/whatsapp/icon_whatsappSend.svg} +0 -0
  743. /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("started", {
167
- "deployment_id": deployment_id,
168
- "workflow_id": workflow_id,
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['id'])
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
- "cancelled_listener_node_ids": listener_nodes
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
- node_id = node['id']
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('timezone', 'UTC')
333
- frequency = params.get('frequency', 'minutes')
405
+ timezone = params.get("timezone", "UTC")
406
+ frequency = params.get("frequency", "minutes")
407
+ schedule_desc = self._get_schedule_description(params)
334
408
 
335
- # Initialize iteration counter for this cron node
336
- self._cron_iterations[node_id] = 0
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
- # Build schedule description for output
339
- schedule_desc = self._get_schedule_description(params)
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
- 'node_id': node_id,
349
- 'timestamp': datetime.now().isoformat(),
350
- 'trigger_type': 'cron',
351
- 'event_data': {
352
- 'timestamp': datetime.now().isoformat(),
353
- 'iteration': iteration,
354
- 'frequency': frequency,
355
- 'timezone': timezone,
356
- 'schedule': schedule_desc,
357
- 'cron_expression': cron_expr
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(node_id, "waiting", {
373
- "message": f"Waiting for schedule: {cron_expr}",
374
- "cron_expression": cron_expr,
375
- "timezone": timezone,
376
- "job_id": job_id
377
- }, workflow_id=workflow_id)
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['id']
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('initial_data', '{}')
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
- node_id = node['id']
405
- node_type = node.get('type', '')
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
- 'node_id': node_id,
411
- 'timestamp': datetime.now().isoformat(),
412
- 'trigger_type': node_type,
413
- 'event_data': event_data
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['id'] == trigger_node_id for n in state.nodes):
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("run_started", {
484
- "run_id": run_id,
485
- "workflow_id": workflow_id,
486
- "trigger_node_id": trigger_node_id,
487
- "active_runs": active_count + 1
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
- run_id, trigger_node_id, trigger_data, workflow_id
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('event_data', trigger_data)
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['id'] for n in downstream}
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['id'] not in run_filter:
1065
+ if node["id"] not in run_filter:
567
1066
  continue
568
1067
  node_copy = node.copy()
569
- node_type = node.get('type', '')
570
- if node['id'] == trigger_node_id:
571
- node_copy['_pre_executed'] = True
572
- node_copy['_trigger_output'] = trigger_output
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['_pre_executed'] = True
576
- node_copy['_trigger_output'] = {'not_triggered': True}
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['id']: n.get('type', '') for n in nodes}
613
- nodes_with_inputs = {e.get('target') for e in edges if e.get('target')}
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('source') != current_id:
1108
+ if edge.get("source") != current_id:
618
1109
  continue
619
- target_id = edge.get('target')
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('target')
640
- source = edge.get('source')
641
- handle = edge.get('targetHandle', '')
1130
+ target = edge.get("target")
1131
+ source = edge.get("source")
1132
+ handle = edge.get("targetHandle", "")
642
1133
 
643
- is_config = handle and handle.startswith('input-') and handle != 'input-main'
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['id'] for n in nodes if n.get('type') in TOOLKIT_NODE_TYPES and n['id'] in downstream_ids}
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('target')
660
- source = edge.get('source')
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
- agent_node_ids = {n['id'] for n in nodes if n.get('type') in AI_AGENT_TYPES and n['id'] in downstream_ids}
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('target')
673
- source = edge.get('source')
674
- target_handle = edge.get('targetHandle', '')
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 == 'input-tools' and source not in downstream_ids:
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['id'] in downstream_ids]
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
- "stop_on_error": db_settings.get("stop_on_error", False),
704
- "max_concurrent_runs": db_settings.get("max_concurrent_runs", 100),
705
- "use_parallel_executor": db_settings.get("use_parallel_executor", True)
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('frequency', 'minutes')
1227
+ frequency = params.get("frequency", "minutes")
738
1228
 
739
1229
  match frequency:
740
- case 'seconds':
741
- interval = params.get('interval', 30)
1230
+ case "seconds":
1231
+ interval = params.get("interval", 30)
742
1232
  return f"Every {interval} seconds"
743
- case 'minutes':
744
- interval = params.get('interval_minutes', 5)
1233
+ case "minutes":
1234
+ interval = params.get("interval_minutes", 5)
745
1235
  return f"Every {interval} minutes"
746
- case 'hours':
747
- interval = params.get('interval_hours', 1)
1236
+ case "hours":
1237
+ interval = params.get("interval_hours", 1)
748
1238
  return f"Every {interval} hours"
749
- case 'days':
750
- time_str = params.get('daily_time', '09:00')
1239
+ case "days":
1240
+ time_str = params.get("daily_time", "09:00")
751
1241
  return f"Daily at {time_str}"
752
- case 'weeks':
753
- weekday = params.get('weekday', '1')
754
- time_str = params.get('weekly_time', '09:00')
755
- days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
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 'months':
759
- day = params.get('month_day', '1')
760
- time_str = params.get('monthly_time', '09:00')
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 'once':
1252
+ case "once":
763
1253
  return "Once (no repeat)"
764
1254
  case _:
765
1255
  return "Unknown schedule"