flowent 0.0.4 → 0.0.6

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 (311) hide show
  1. package/README.md +1 -1
  2. package/backend/README.md +74 -0
  3. package/backend/pyproject.toml +2 -1
  4. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/{stats_service.cpython-313.pyc → observability_service.cpython-313.pyc} +0 -0
  21. package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
  22. package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
  23. package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
  24. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  25. package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
  26. package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
  27. package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
  28. package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
  29. package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
  30. package/backend/src/flowent/agent.py +364 -52
  31. package/backend/src/flowent/assistant_commands.py +31 -22
  32. package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
  34. package/backend/src/flowent/channels/telegram.py +4 -4
  35. package/backend/src/flowent/graph_service.py +1307 -145
  36. package/backend/src/flowent/mcp_service.py +21 -7
  37. package/backend/src/flowent/model_metadata.py +4 -0
  38. package/backend/src/flowent/models/__init__.py +6 -2
  39. package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
  41. package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
  42. package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
  43. package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
  44. package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
  45. package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
  46. package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
  47. package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
  48. package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
  49. package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
  50. package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
  51. package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
  52. package/backend/src/flowent/models/agent.py +1 -0
  53. package/backend/src/flowent/models/graph.py +44 -9
  54. package/backend/src/flowent/models/history.py +73 -15
  55. package/backend/src/flowent/models/llm.py +1 -0
  56. package/backend/src/flowent/models/message.py +6 -0
  57. package/backend/src/flowent/models/tab.py +38 -1
  58. package/backend/src/flowent/{stats_service.py → observability_service.py} +4 -4
  59. package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
  61. package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
  62. package/backend/src/flowent/prompts/common.py +2 -2
  63. package/backend/src/flowent/prompts/steward.py +2 -2
  64. package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  65. package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
  66. package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
  67. package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
  68. package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
  69. package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
  70. package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
  71. package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
  72. package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
  73. package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
  74. package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
  75. package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
  76. package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
  77. package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
  78. package/backend/src/flowent/providers/configuration.py +7 -0
  79. package/backend/src/flowent/role_management.py +12 -0
  80. package/backend/src/flowent/routes/__init__.py +0 -2
  81. package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
  82. package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
  83. package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
  84. package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
  85. package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
  86. package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
  87. package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
  88. package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
  89. package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
  90. package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
  91. package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
  92. package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
  93. package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
  94. package/backend/src/flowent/routes/assistant.py +4 -4
  95. package/backend/src/flowent/routes/nodes.py +54 -6
  96. package/backend/src/flowent/routes/providers_route.py +1 -0
  97. package/backend/src/flowent/routes/roles.py +1 -1
  98. package/backend/src/flowent/routes/settings.py +4 -0
  99. package/backend/src/flowent/routes/tabs.py +29 -11
  100. package/backend/src/flowent/runtime.py +7 -30
  101. package/backend/src/flowent/security.py +23 -8
  102. package/backend/src/flowent/settings.py +56 -5
  103. package/backend/src/flowent/settings_management.py +12 -0
  104. package/backend/src/flowent/static/assets/AssistantPage-VBohhz4d.js +1 -0
  105. package/backend/src/flowent/static/assets/ChannelsPage-CIydPZA_.js +1 -0
  106. package/backend/src/flowent/static/assets/McpPage-CHPm2TPY.js +7 -0
  107. package/backend/src/flowent/static/assets/PageScaffold-DteOA8V7.js +1 -0
  108. package/backend/src/flowent/static/assets/PromptsPage-CSmJ3sZg.js +1 -0
  109. package/backend/src/flowent/static/assets/ProvidersPage-sl2jeG4e.js +3 -0
  110. package/backend/src/flowent/static/assets/RolesPage-DCe7W6Km.js +1 -0
  111. package/backend/src/flowent/static/assets/SettingsPage-Bix9e63E.js +3 -0
  112. package/backend/src/flowent/static/assets/ToolsPage-favNkj5C.js +1 -0
  113. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-DRS6wiD6.js +1 -0
  114. package/backend/src/flowent/static/assets/WorkspacePage-KuaDjt_D.js +3 -0
  115. package/backend/src/flowent/static/assets/WorkspacePanels-BZxBw8M5.js +1 -0
  116. package/backend/src/flowent/static/assets/alert-dialog-DIBUCmqM.js +1 -0
  117. package/backend/src/flowent/static/assets/{dialog-BeGSweF6.js → dialog-BOvHIBrg.js} +1 -1
  118. package/backend/src/flowent/static/assets/index-Biio-CoI.js +10 -0
  119. package/backend/src/flowent/static/assets/index-CmQvO7sl.css +1 -0
  120. package/backend/src/flowent/static/assets/modelParams-DcEhGnu0.js +1 -0
  121. package/backend/src/flowent/static/assets/roles-BbIEIMeG.js +1 -0
  122. package/backend/src/flowent/static/assets/select-D9SwnlXF.js +1 -0
  123. package/backend/src/flowent/static/assets/surface-Bzr1FRG4.js +1 -0
  124. package/backend/src/flowent/static/assets/{ui-vendor-Dg9NNnWX.js → ui-vendor-UazN8rcv.js} +15 -15
  125. package/backend/src/flowent/static/index.html +3 -4
  126. package/backend/src/flowent/tools/__init__.py +76 -2
  127. package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  128. package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
  129. package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
  130. package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
  131. package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
  132. package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
  133. package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
  134. package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
  135. package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
  136. package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
  137. package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
  138. package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
  139. package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
  140. package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
  141. package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
  142. package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
  143. package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
  144. package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
  145. package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
  146. package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
  147. package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
  148. package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
  149. package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
  150. package/backend/src/flowent/tools/connect.py +10 -66
  151. package/backend/src/flowent/tools/contacts.py +1 -1
  152. package/backend/src/flowent/tools/create_agent.py +9 -88
  153. package/backend/src/flowent/tools/create_tab.py +7 -5
  154. package/backend/src/flowent/tools/exec.py +3 -2
  155. package/backend/src/flowent/tools/list_roles.py +29 -4
  156. package/backend/src/flowent/tools/list_tabs.py +4 -0
  157. package/backend/src/flowent/tools/list_tools.py +5 -1
  158. package/backend/src/flowent/tools/manage_settings.py +18 -0
  159. package/backend/src/flowent/tools/send.py +21 -3
  160. package/backend/src/flowent/tools/set_permissions.py +21 -6
  161. package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  162. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  163. package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  164. package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
  165. package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
  166. package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
  167. package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
  168. package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
  169. package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
  170. package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
  171. package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
  172. package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
  173. package/backend/tests/integration/api/test_assistant_api.py +1 -1
  174. package/backend/tests/integration/api/test_nodes_api.py +257 -21
  175. package/backend/tests/integration/api/test_roles_api.py +3 -2
  176. package/backend/tests/integration/api/test_tabs_api.py +312 -11
  177. package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
  178. package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
  179. package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  180. package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
  181. package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
  182. package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
  183. package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
  184. package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  185. package/backend/tests/unit/agent/test_agent_public_api.py +162 -71
  186. package/backend/tests/unit/agent/test_agent_runtime.py +285 -69
  187. package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
  188. package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  189. package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
  190. package/backend/tests/unit/prompts/test_prompts.py +3 -2
  191. package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  192. package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
  193. package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
  194. package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
  195. package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
  196. package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
  197. package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
  198. package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  199. package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
  200. package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  201. package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  202. package/backend/tests/unit/routes/test_providers_route.py +2 -0
  203. package/backend/tests/unit/routes/test_roles_routes.py +109 -0
  204. package/backend/tests/unit/routes/test_settings_routes.py +4 -0
  205. package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
  206. package/backend/tests/unit/runtime/test_bootstrap_runtime.py +8 -18
  207. package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  208. package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
  209. package/backend/tests/unit/security/test_security.py +16 -2
  210. package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
  211. package/backend/tests/unit/settings/test_settings_roles.py +40 -0
  212. package/backend/tests/unit/test_state_sqlite_storage.py +67 -1
  213. package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  214. package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  215. package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  216. package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  217. package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  218. package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  219. package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  220. package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  221. package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  222. package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  223. package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  224. package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  225. package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
  226. package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
  227. package/backend/tests/unit/tools/test_connect_tool.py +2 -3
  228. package/backend/tests/unit/tools/test_create_agent_tool.py +9 -97
  229. package/backend/tests/unit/tools/test_delete_tab_tool.py +33 -0
  230. package/backend/tests/unit/tools/test_manage_providers_tool.py +2 -0
  231. package/backend/tests/unit/tools/test_manage_settings_tool.py +3 -0
  232. package/backend/tests/unit/tools/test_set_permissions_tool.py +216 -12
  233. package/backend/tests/unit/tools/test_tool_registry.py +103 -0
  234. package/backend/uv.lock +1 -1
  235. package/dist/frontend/assets/AssistantPage-VBohhz4d.js +1 -0
  236. package/dist/frontend/assets/ChannelsPage-CIydPZA_.js +1 -0
  237. package/dist/frontend/assets/McpPage-CHPm2TPY.js +7 -0
  238. package/dist/frontend/assets/PageScaffold-DteOA8V7.js +1 -0
  239. package/dist/frontend/assets/PromptsPage-CSmJ3sZg.js +1 -0
  240. package/dist/frontend/assets/ProvidersPage-sl2jeG4e.js +3 -0
  241. package/dist/frontend/assets/RolesPage-DCe7W6Km.js +1 -0
  242. package/dist/frontend/assets/SettingsPage-Bix9e63E.js +3 -0
  243. package/dist/frontend/assets/ToolsPage-favNkj5C.js +1 -0
  244. package/dist/frontend/assets/WorkspaceCommandDialog-DRS6wiD6.js +1 -0
  245. package/dist/frontend/assets/WorkspacePage-KuaDjt_D.js +3 -0
  246. package/dist/frontend/assets/WorkspacePanels-BZxBw8M5.js +1 -0
  247. package/dist/frontend/assets/alert-dialog-DIBUCmqM.js +1 -0
  248. package/dist/frontend/assets/{dialog-BeGSweF6.js → dialog-BOvHIBrg.js} +1 -1
  249. package/dist/frontend/assets/index-Biio-CoI.js +10 -0
  250. package/dist/frontend/assets/index-CmQvO7sl.css +1 -0
  251. package/dist/frontend/assets/modelParams-DcEhGnu0.js +1 -0
  252. package/dist/frontend/assets/roles-BbIEIMeG.js +1 -0
  253. package/dist/frontend/assets/select-D9SwnlXF.js +1 -0
  254. package/dist/frontend/assets/surface-Bzr1FRG4.js +1 -0
  255. package/dist/frontend/assets/{ui-vendor-Dg9NNnWX.js → ui-vendor-UazN8rcv.js} +15 -15
  256. package/dist/frontend/index.html +3 -4
  257. package/package.json +3 -3
  258. package/backend/src/flowent/routes/__pycache__/stats.cpython-313.pyc +0 -0
  259. package/backend/src/flowent/routes/stats.py +0 -229
  260. package/backend/src/flowent/static/assets/AssistantPage-B3Xc08AS.js +0 -1
  261. package/backend/src/flowent/static/assets/ChannelsPage-ByLd28xk.js +0 -1
  262. package/backend/src/flowent/static/assets/HomePage-C0hAx9_l.js +0 -3
  263. package/backend/src/flowent/static/assets/McpPage-DkrYLvBv.js +0 -7
  264. package/backend/src/flowent/static/assets/PageScaffold-D4jO9ooX.js +0 -1
  265. package/backend/src/flowent/static/assets/PromptsPage-DWA7rRJd.js +0 -1
  266. package/backend/src/flowent/static/assets/ProvidersPage-PUWT8seJ.js +0 -3
  267. package/backend/src/flowent/static/assets/RolesPage-CqcclGRw.js +0 -1
  268. package/backend/src/flowent/static/assets/SettingsPage-8tS2cJgX.js +0 -3
  269. package/backend/src/flowent/static/assets/StatsPage-BX9khYzu.js +0 -1
  270. package/backend/src/flowent/static/assets/ToolsPage-9Tl9FdeD.js +0 -1
  271. package/backend/src/flowent/static/assets/WorkspaceCommandDialog-CCXxjDL8.js +0 -1
  272. package/backend/src/flowent/static/assets/WorkspacePanels-aMdJ7ZH7.js +0 -1
  273. package/backend/src/flowent/static/assets/alert-dialog-kFYVQ7oX.js +0 -1
  274. package/backend/src/flowent/static/assets/badge-74-3jsCg.js +0 -1
  275. package/backend/src/flowent/static/assets/constants-XUzFf6i1.js +0 -1
  276. package/backend/src/flowent/static/assets/index-BHC1Vhy8.css +0 -1
  277. package/backend/src/flowent/static/assets/index-CL1ALZ3r.js +0 -10
  278. package/backend/src/flowent/static/assets/modelParams-CaHd0903.js +0 -1
  279. package/backend/src/flowent/static/assets/roles-2OLDeTc5.js +0 -1
  280. package/backend/src/flowent/static/assets/select-DL_LPeDj.js +0 -1
  281. package/backend/src/flowent/static/assets/shared-CMxbpLeQ.js +0 -1
  282. package/backend/tests/unit/routes/__pycache__/test_stats_routes.cpython-313-pytest-9.0.3.pyc +0 -0
  283. package/backend/tests/unit/routes/test_stats_routes.py +0 -149
  284. package/dist/frontend/assets/AssistantPage-B3Xc08AS.js +0 -1
  285. package/dist/frontend/assets/ChannelsPage-ByLd28xk.js +0 -1
  286. package/dist/frontend/assets/HomePage-C0hAx9_l.js +0 -3
  287. package/dist/frontend/assets/McpPage-DkrYLvBv.js +0 -7
  288. package/dist/frontend/assets/PageScaffold-D4jO9ooX.js +0 -1
  289. package/dist/frontend/assets/PromptsPage-DWA7rRJd.js +0 -1
  290. package/dist/frontend/assets/ProvidersPage-PUWT8seJ.js +0 -3
  291. package/dist/frontend/assets/RolesPage-CqcclGRw.js +0 -1
  292. package/dist/frontend/assets/SettingsPage-8tS2cJgX.js +0 -3
  293. package/dist/frontend/assets/StatsPage-BX9khYzu.js +0 -1
  294. package/dist/frontend/assets/ToolsPage-9Tl9FdeD.js +0 -1
  295. package/dist/frontend/assets/WorkspaceCommandDialog-CCXxjDL8.js +0 -1
  296. package/dist/frontend/assets/WorkspacePanels-aMdJ7ZH7.js +0 -1
  297. package/dist/frontend/assets/alert-dialog-kFYVQ7oX.js +0 -1
  298. package/dist/frontend/assets/badge-74-3jsCg.js +0 -1
  299. package/dist/frontend/assets/constants-XUzFf6i1.js +0 -1
  300. package/dist/frontend/assets/index-BHC1Vhy8.css +0 -1
  301. package/dist/frontend/assets/index-CL1ALZ3r.js +0 -10
  302. package/dist/frontend/assets/modelParams-CaHd0903.js +0 -1
  303. package/dist/frontend/assets/roles-2OLDeTc5.js +0 -1
  304. package/dist/frontend/assets/select-DL_LPeDj.js +0 -1
  305. package/dist/frontend/assets/shared-CMxbpLeQ.js +0 -1
  306. /package/backend/src/flowent/static/assets/{datetime-m6_O_Ci9.js → datetime-eJqd0V2S.js} +0 -0
  307. /package/backend/src/flowent/static/assets/{markdown-vendor-DVdy_w12.js → markdown-vendor-C9RtvaJh.js} +0 -0
  308. /package/backend/src/flowent/static/assets/{triState-DEr3NkXV.js → triState-DgLlKdRR.js} +0 -0
  309. /package/dist/frontend/assets/{datetime-m6_O_Ci9.js → datetime-eJqd0V2S.js} +0 -0
  310. /package/dist/frontend/assets/{markdown-vendor-DVdy_w12.js → markdown-vendor-C9RtvaJh.js} +0 -0
  311. /package/dist/frontend/assets/{triState-DEr3NkXV.js → triState-DgLlKdRR.js} +0 -0
@@ -13,6 +13,7 @@ from flowent.agent import (
13
13
  WakeSignal,
14
14
  )
15
15
  from flowent.events import event_bus
16
+ from flowent.graph_service import build_workflow_node_definition, create_edge
16
17
  from flowent.models import (
17
18
  AgentState,
18
19
  AssistantText,
@@ -28,17 +29,19 @@ from flowent.models import (
28
29
  NodeType,
29
30
  ReceivedMessage,
30
31
  SentMessage,
31
- StateEntry,
32
32
  SystemEntry,
33
33
  Tab,
34
34
  TodoItem,
35
35
  ToolCall,
36
36
  ToolCallResult,
37
+ WorkflowActivationState,
38
+ WorkflowDefinition,
39
+ WorkflowNodeKind,
37
40
  )
41
+ from flowent.observability_service import observability_store
38
42
  from flowent.providers.errors import LLMProviderError
39
43
  from flowent.registry import registry
40
44
  from flowent.settings import ModelSettings, ProviderConfig, Settings
41
- from flowent.stats_service import stats_store
42
45
  from flowent.workspace_store import workspace_store
43
46
 
44
47
 
@@ -52,16 +55,24 @@ def reset_runtime_state(monkeypatch, tmp_path):
52
55
  monkeypatch.setattr(settings_module, "_cached_settings", None)
53
56
  registry.reset()
54
57
  workspace_store.reset_cache()
55
- stats_store.reset()
58
+ observability_store.reset()
56
59
  yield
57
60
  registry.reset()
58
61
  workspace_store.reset_cache()
59
- stats_store.reset()
62
+ observability_store.reset()
60
63
  monkeypatch.setattr(settings_module, "_cached_settings", None)
61
64
 
62
65
 
63
66
  def _register_tab_leader(*, tab_id: str = "tab-1", leader_id: str = "leader") -> Agent:
64
- workspace_store.upsert_tab(Tab(id=tab_id, title="Task", leader_id=leader_id))
67
+ workspace_store.upsert_tab(
68
+ Tab(
69
+ id=tab_id,
70
+ title="Task",
71
+ leader_id=leader_id,
72
+ allow_network=True,
73
+ permissions_initialized=True,
74
+ )
75
+ )
65
76
  leader = Agent(
66
77
  NodeConfig(
67
78
  node_type=NodeType.AGENT,
@@ -75,8 +86,41 @@ def _register_tab_leader(*, tab_id: str = "tab-1", leader_id: str = "leader") ->
75
86
  return leader
76
87
 
77
88
 
89
+ def _add_agent_path(
90
+ *,
91
+ tab_id: str = "tab-1",
92
+ source_id: str,
93
+ target_id: str,
94
+ ) -> None:
95
+ tab = workspace_store.get_tab(tab_id)
96
+ assert tab is not None
97
+ existing = {node.id for node in tab.definition.nodes}
98
+ next_nodes = list(tab.definition.nodes)
99
+ for node_id in (source_id, target_id):
100
+ if node_id not in existing:
101
+ next_nodes.append(
102
+ build_workflow_node_definition(
103
+ node_id=node_id,
104
+ node_kind=WorkflowNodeKind.AGENT,
105
+ )
106
+ )
107
+ tab.definition = WorkflowDefinition(
108
+ version=tab.definition.version,
109
+ nodes=next_nodes,
110
+ edges=list(tab.definition.edges),
111
+ view=tab.definition.view,
112
+ )
113
+ workspace_store.upsert_tab(tab)
114
+ edge, error = create_edge(
115
+ tab_id=tab_id,
116
+ from_node_id=source_id,
117
+ to_node_id=target_id,
118
+ )
119
+ assert error is None and edge is not None
120
+
121
+
78
122
  def test_agent_keeps_running_after_pure_text_response(monkeypatch):
79
- agent = Agent(NodeConfig(node_type=NodeType.AGENT))
123
+ agent = Agent(NodeConfig(node_type=NodeType.AGENT, allow_network=True))
80
124
  wait_calls = 0
81
125
  llm_messages: list[list[dict]] = []
82
126
  responses = iter([LLMResponse(content="working through the task"), LLMResponse()])
@@ -124,7 +168,7 @@ def test_agent_keeps_running_after_pure_text_response(monkeypatch):
124
168
 
125
169
 
126
170
  def test_agent_retries_transient_llm_errors_before_succeeding(monkeypatch):
127
- agent = Agent(NodeConfig(node_type=NodeType.AGENT))
171
+ agent = Agent(NodeConfig(node_type=NodeType.AGENT, allow_network=True))
128
172
  wait_calls = 0
129
173
  llm_calls = 0
130
174
 
@@ -184,7 +228,15 @@ def test_agent_retries_transient_llm_errors_before_succeeding(monkeypatch):
184
228
 
185
229
 
186
230
  def test_chat_with_retries_records_single_request_stat(monkeypatch):
187
- workspace_store.upsert_tab(Tab(id="tab-1", title="Task", leader_id="leader-1"))
231
+ workspace_store.upsert_tab(
232
+ Tab(
233
+ id="tab-1",
234
+ title="Task",
235
+ leader_id="leader-1",
236
+ allow_network=True,
237
+ permissions_initialized=True,
238
+ )
239
+ )
188
240
  agent = Agent(
189
241
  NodeConfig(
190
242
  node_type=NodeType.AGENT,
@@ -254,7 +306,7 @@ def test_chat_with_retries_records_single_request_stat(monkeypatch):
254
306
  tools_schema=None,
255
307
  )
256
308
 
257
- records = stats_store.list_requests(since=0)
309
+ records = observability_store.list_requests(since=0)
258
310
 
259
311
  assert response.content == "Done"
260
312
  assert len(records) == 1
@@ -269,10 +321,67 @@ def test_chat_with_retries_records_single_request_stat(monkeypatch):
269
321
  assert records[0]["raw_usage"] == {"total_tokens": 120, "input_tokens": 90}
270
322
 
271
323
 
324
+ def test_chat_with_retries_blocks_remote_model_when_network_is_disabled(monkeypatch):
325
+ workspace_store.upsert_tab(
326
+ Tab(
327
+ id="tab-1",
328
+ title="Task",
329
+ leader_id="leader-1",
330
+ allow_network=False,
331
+ permissions_initialized=True,
332
+ )
333
+ )
334
+ agent = Agent(
335
+ NodeConfig(
336
+ node_type=NodeType.AGENT,
337
+ role_name="Worker",
338
+ name="Planner",
339
+ tab_id="tab-1",
340
+ ),
341
+ uuid="agent-1",
342
+ )
343
+ settings = Settings(
344
+ model=ModelSettings(
345
+ active_provider_id="provider-1",
346
+ active_model="gpt-5.2",
347
+ ),
348
+ providers=[
349
+ ProviderConfig(
350
+ id="provider-1",
351
+ name="Primary",
352
+ type="openai_responses",
353
+ base_url="https://api.example.com/v1",
354
+ api_key="secret",
355
+ )
356
+ ],
357
+ )
358
+ monkeypatch.setattr("flowent.agent.get_settings", lambda: settings)
359
+ monkeypatch.setattr(
360
+ "flowent.agent.gateway.chat",
361
+ lambda **kwargs: (_ for _ in ()).throw(
362
+ AssertionError("gateway should not be called")
363
+ ),
364
+ )
365
+
366
+ with pytest.raises(
367
+ LLMProviderError,
368
+ match="Network access is disabled for this workflow",
369
+ ):
370
+ agent._chat_with_retries(
371
+ prepared_context=PreparedLLMContext(
372
+ messages=[{"role": "user", "content": "hello"}],
373
+ system_messages=[],
374
+ execution_context_messages=[],
375
+ runtime_tail_messages=[],
376
+ ),
377
+ tools_schema=None,
378
+ )
379
+
380
+
272
381
  def test_agent_does_not_retry_transient_llm_errors_when_retry_policy_is_no_retry(
273
382
  monkeypatch,
274
383
  ):
275
- agent = Agent(NodeConfig(node_type=NodeType.AGENT))
384
+ agent = Agent(NodeConfig(node_type=NodeType.AGENT, allow_network=True))
276
385
  wait_calls = 0
277
386
  llm_calls = 0
278
387
 
@@ -325,7 +434,7 @@ def test_agent_does_not_retry_transient_llm_errors_when_retry_policy_is_no_retry
325
434
 
326
435
 
327
436
  def test_agent_does_not_retry_non_transient_llm_errors(monkeypatch):
328
- agent = Agent(NodeConfig(node_type=NodeType.AGENT))
437
+ agent = Agent(NodeConfig(node_type=NodeType.AGENT, allow_network=True))
329
438
  wait_calls = 0
330
439
  llm_calls = 0
331
440
  error_summary = (
@@ -379,10 +488,8 @@ def test_agent_does_not_retry_non_transient_llm_errors(monkeypatch):
379
488
  isinstance(entry, ErrorEntry) and entry.content == error_summary
380
489
  for entry in agent.get_history_snapshot()
381
490
  )
382
- assert any(
383
- isinstance(entry, StateEntry)
384
- and entry.state == AgentState.ERROR.value
385
- and entry.reason == error_summary
491
+ assert all(
492
+ entry.__class__.__name__ != "StateEntry"
386
493
  for entry in agent.get_history_snapshot()
387
494
  )
388
495
  assert not any(
@@ -395,7 +502,7 @@ def test_agent_does_not_retry_non_transient_llm_errors(monkeypatch):
395
502
 
396
503
 
397
504
  def test_agent_interrupt_stops_retry_backoff(monkeypatch):
398
- agent = Agent(NodeConfig(node_type=NodeType.AGENT))
505
+ agent = Agent(NodeConfig(node_type=NodeType.AGENT, allow_network=True))
399
506
  wait_calls = 0
400
507
  llm_calls = 0
401
508
  interrupter: threading.Thread | None = None
@@ -452,7 +559,7 @@ def test_agent_interrupt_stops_retry_backoff(monkeypatch):
452
559
 
453
560
 
454
561
  def test_agent_retries_transient_errors_when_retry_policy_is_unlimited(monkeypatch):
455
- agent = Agent(NodeConfig(node_type=NodeType.AGENT))
562
+ agent = Agent(NodeConfig(node_type=NodeType.AGENT, allow_network=True))
456
563
  wait_calls = 0
457
564
  llm_calls = 0
458
565
 
@@ -562,7 +669,15 @@ def test_get_llm_retry_429_delay_uses_active_provider_only_for_429(monkeypatch):
562
669
 
563
670
 
564
671
  def test_prepare_messages_records_auto_compact_stat(monkeypatch):
565
- workspace_store.upsert_tab(Tab(id="tab-1", title="Task", leader_id="leader-1"))
672
+ workspace_store.upsert_tab(
673
+ Tab(
674
+ id="tab-1",
675
+ title="Task",
676
+ leader_id="leader-1",
677
+ allow_network=True,
678
+ permissions_initialized=True,
679
+ )
680
+ )
566
681
  agent = Agent(
567
682
  NodeConfig(
568
683
  node_type=NodeType.AGENT,
@@ -622,7 +737,7 @@ def test_prepare_messages_records_auto_compact_stat(monkeypatch):
622
737
  )
623
738
 
624
739
  result = agent._prepare_messages_for_llm()
625
- records = stats_store.list_compacts(since=0)
740
+ records = observability_store.list_compacts(since=0)
626
741
 
627
742
  assert result == prepared_context
628
743
  assert compact_calls == [None]
@@ -654,8 +769,7 @@ def test_clear_assistant_chat_history_drops_conversation_entries():
654
769
  assistant.clear_chat_history()
655
770
 
656
771
  assert all(
657
- isinstance(entry, (SystemEntry, StateEntry))
658
- for entry in assistant.get_history_snapshot()
772
+ isinstance(entry, SystemEntry) for entry in assistant.get_history_snapshot()
659
773
  )
660
774
 
661
775
 
@@ -1001,7 +1115,7 @@ def test_execute_compact_command_replaces_history_with_summary(monkeypatch):
1001
1115
  assert "Summarize the rollout" not in serialized
1002
1116
  assert "Compacted execution context" in serialized
1003
1117
  assert "Ship the command layer." in serialized
1004
- assert "Compacted the current Assistant execution context." not in serialized
1118
+ assert "Compacted this chat for future replies." not in serialized
1005
1119
 
1006
1120
 
1007
1121
  def test_compact_command_excludes_queued_messages_from_summary(monkeypatch):
@@ -1070,7 +1184,7 @@ def test_help_command_result_does_not_reenter_model_context():
1070
1184
  assert isinstance(entry, CommandResultEntry)
1071
1185
  assert entry.include_in_context is False
1072
1186
  assert "/compact" in entry.content
1073
- assert "Built-in Assistant commands" not in serialized
1187
+ assert "Available commands" not in serialized
1074
1188
 
1075
1189
 
1076
1190
  def test_agent_normalizes_think_tags_in_final_content(monkeypatch):
@@ -1173,7 +1287,10 @@ def test_agent_dedupes_structured_thinking_and_raw_think_tags(monkeypatch):
1173
1287
 
1174
1288
  def test_agent_unregisters_from_registry_after_termination_request(monkeypatch):
1175
1289
  registry.reset()
1176
- agent = Agent(NodeConfig(node_type=NodeType.AGENT), uuid="agent-x")
1290
+ agent = Agent(
1291
+ NodeConfig(node_type=NodeType.AGENT, allow_network=True),
1292
+ uuid="agent-x",
1293
+ )
1177
1294
  registry.register(agent)
1178
1295
  events = []
1179
1296
 
@@ -1435,11 +1552,7 @@ def test_request_sleep_wakes_early_when_new_message_arrives():
1435
1552
  ]
1436
1553
  assert len(received_entries) == 1
1437
1554
  assert received_entries[0].content == "wake up"
1438
- assert [
1439
- entry.state
1440
- for entry in agent.get_history_snapshot()
1441
- if isinstance(entry, StateEntry)
1442
- ][-2:] == ["sleeping", "running"]
1555
+ assert all(entry.__class__.__name__ != "StateEntry" for entry in agent.history)
1443
1556
 
1444
1557
 
1445
1558
  def test_request_sleep_timeout_queues_deadline_notice():
@@ -1451,11 +1564,7 @@ def test_request_sleep_timeout_queues_deadline_notice():
1451
1564
  assert result.startswith("slept ")
1452
1565
  assert agent.state == AgentState.RUNNING
1453
1566
  assert agent._consume_runtime_notices() == [agent._build_sleep_deadline_notice()]
1454
- assert [
1455
- entry.state
1456
- for entry in agent.get_history_snapshot()
1457
- if isinstance(entry, StateEntry)
1458
- ][-2:] == ["sleeping", "running"]
1567
+ assert all(entry.__class__.__name__ != "StateEntry" for entry in agent.history)
1459
1568
 
1460
1569
 
1461
1570
  def test_agent_interrupts_blocked_provider_without_streaming_output(monkeypatch):
@@ -1519,7 +1628,10 @@ def test_agent_interrupts_blocked_provider_without_streaming_output(monkeypatch)
1519
1628
 
1520
1629
 
1521
1630
  def test_provider_resolution_error_is_recorded_in_history(monkeypatch):
1522
- agent = Agent(NodeConfig(node_type=NodeType.AGENT), uuid="agent-y")
1631
+ agent = Agent(
1632
+ NodeConfig(node_type=NodeType.AGENT, allow_network=True),
1633
+ uuid="agent-y",
1634
+ )
1523
1635
  wait_calls = 0
1524
1636
 
1525
1637
  def fake_wait_for_input() -> None:
@@ -1605,9 +1717,12 @@ def test_assistant_content_streams_even_when_response_has_tool_calls(monkeypatch
1605
1717
 
1606
1718
  def test_send_message_delivers_to_single_contact_and_records_histories(monkeypatch):
1607
1719
  registry.reset()
1608
- leader = _register_tab_leader()
1720
+ _register_tab_leader()
1609
1721
  child = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="child")
1722
+ peer = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="peer")
1610
1723
  registry.register(child)
1724
+ registry.register(peer)
1725
+ _add_agent_path(source_id="child", target_id="peer")
1611
1726
  events = []
1612
1727
 
1613
1728
  monkeypatch.setattr(event_bus, "emit", lambda event: events.append(event))
@@ -1615,7 +1730,9 @@ def test_send_message_delivers_to_single_contact_and_records_histories(monkeypat
1615
1730
  try:
1616
1731
  result = json.loads(
1617
1732
  child.send_message(
1618
- target_ref="leader",
1733
+ target_ref="peer",
1734
+ from_output_port_key="out",
1735
+ to_input_port_key="in",
1619
1736
  raw_parts=[{"type": "text", "text": "investigate the error"}],
1620
1737
  )
1621
1738
  )
@@ -1629,16 +1746,29 @@ def test_send_message_delivers_to_single_contact_and_records_histories(monkeypat
1629
1746
  )
1630
1747
  received_entry = next(
1631
1748
  entry
1632
- for entry in leader.get_history_snapshot()
1749
+ for entry in peer.get_history_snapshot()
1633
1750
  if isinstance(entry, ReceivedMessage)
1634
1751
  )
1635
- signal = leader._wake_queue.get_nowait()
1752
+ signal = peer._wake_queue.get_nowait()
1636
1753
 
1637
- assert result == {"status": "sent", "target_id": "leader"}
1638
- assert sent_entry.to_id == "leader"
1754
+ assert result == {
1755
+ "status": "sent",
1756
+ "target_id": "peer",
1757
+ "from_output_port_key": "out",
1758
+ "to_input_port_key": "in",
1759
+ "port_type": "parts",
1760
+ "value_summary": "investigate the error",
1761
+ }
1762
+ assert sent_entry.to_id == "peer"
1639
1763
  assert sent_entry.content == "investigate the error"
1764
+ assert sent_entry.from_output_port_key == "out"
1765
+ assert sent_entry.to_input_port_key == "in"
1766
+ assert sent_entry.value_summary == "investigate the error"
1640
1767
  assert received_entry.from_id == "child"
1641
1768
  assert received_entry.content == "investigate the error"
1769
+ assert received_entry.from_output_port_key == "out"
1770
+ assert received_entry.to_input_port_key == "in"
1771
+ assert received_entry.value_summary == "investigate the error"
1642
1772
  assert sent_entry.message_id == received_entry.message_id
1643
1773
  assert signal.payload == {
1644
1774
  "message": {
@@ -1647,13 +1777,21 @@ def test_send_message_delivers_to_single_contact_and_records_histories(monkeypat
1647
1777
  "parts": [{"type": "text", "text": "investigate the error"}],
1648
1778
  "history_recorded": True,
1649
1779
  "message_id": sent_entry.message_id,
1780
+ "from_output_port_key": "out",
1781
+ "to_input_port_key": "in",
1782
+ "port_type": "parts",
1783
+ "value": [{"type": "text", "text": "investigate the error"}],
1784
+ "value_summary": "investigate the error",
1650
1785
  }
1651
1786
  }
1652
1787
  assert [event.data for event in events if event.type == EventType.NODE_MESSAGE] == [
1653
1788
  {
1654
- "to_id": "leader",
1789
+ "to_id": "peer",
1655
1790
  "content": "investigate the error",
1656
1791
  "message_id": sent_entry.message_id,
1792
+ "from_output_port_key": "out",
1793
+ "to_input_port_key": "in",
1794
+ "port_type": "parts",
1657
1795
  }
1658
1796
  ]
1659
1797
 
@@ -1669,10 +1807,12 @@ def test_send_message_reports_error_when_target_is_not_in_contacts():
1669
1807
  try:
1670
1808
  with pytest.raises(
1671
1809
  ValueError,
1672
- match=r"Send failed: target `peer` is not in contacts\.",
1810
+ match=r"Send failed: target `peer` is not connected from `out` to `in`\.",
1673
1811
  ):
1674
1812
  child.send_message(
1675
1813
  target_ref="peer",
1814
+ from_output_port_key="out",
1815
+ to_input_port_key="in",
1676
1816
  raw_parts=[{"type": "text", "text": "reply with the findings"}],
1677
1817
  )
1678
1818
  finally:
@@ -1690,10 +1830,12 @@ def test_send_message_validates_target_before_image_capability():
1690
1830
  try:
1691
1831
  with pytest.raises(
1692
1832
  ValueError,
1693
- match=r"Send failed: target `peer` is not in contacts\.",
1833
+ match=r"Send failed: target `peer` is not connected from `out` to `in`\.",
1694
1834
  ):
1695
1835
  child.send_message(
1696
1836
  target_ref="peer",
1837
+ from_output_port_key="out",
1838
+ to_input_port_key="in",
1697
1839
  raw_parts=[{"type": "image", "asset_id": "asset-1"}],
1698
1840
  )
1699
1841
  finally:
@@ -1704,21 +1846,65 @@ def test_send_message_reports_error_when_target_lacks_input_image_support():
1704
1846
  registry.reset()
1705
1847
  _register_tab_leader()
1706
1848
  child = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="child")
1849
+ peer = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="peer")
1707
1850
  registry.register(child)
1851
+ registry.register(peer)
1852
+ _add_agent_path(source_id="child", target_id="peer")
1708
1853
 
1709
1854
  try:
1710
1855
  with pytest.raises(
1711
1856
  ValueError,
1712
- match=r"Send failed: target `leader` does not support `input_image`\.",
1857
+ match=r"Send failed: target `peer` does not support `input_image`\.",
1713
1858
  ):
1714
1859
  child.send_message(
1715
- target_ref="leader",
1860
+ target_ref="peer",
1861
+ from_output_port_key="out",
1862
+ to_input_port_key="in",
1716
1863
  raw_parts=[{"type": "image", "asset_id": "asset-1"}],
1717
1864
  )
1718
1865
  finally:
1719
1866
  registry.reset()
1720
1867
 
1721
1868
 
1869
+ def test_inactive_leader_cannot_send_work_to_agent_nodes():
1870
+ registry.reset()
1871
+ leader = _register_tab_leader()
1872
+ worker = Agent(
1873
+ NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1", name="Worker"),
1874
+ uuid="worker",
1875
+ )
1876
+ registry.register(worker)
1877
+
1878
+ try:
1879
+ with pytest.raises(
1880
+ ValueError,
1881
+ match=r"Activate this workflow before sending work to agent nodes\.",
1882
+ ):
1883
+ leader.send_message(
1884
+ target_ref="worker",
1885
+ raw_parts=[{"type": "text", "text": "start the task"}],
1886
+ )
1887
+ assert not any(
1888
+ isinstance(entry, ReceivedMessage)
1889
+ for entry in worker.get_history_snapshot()
1890
+ )
1891
+
1892
+ tab = workspace_store.get_tab("tab-1")
1893
+ assert tab is not None
1894
+ tab.activation_state = WorkflowActivationState.ACTIVE
1895
+ workspace_store.upsert_tab(tab)
1896
+
1897
+ result = json.loads(
1898
+ leader.send_message(
1899
+ target_ref="worker",
1900
+ raw_parts=[{"type": "text", "text": "start the task"}],
1901
+ )
1902
+ )
1903
+ assert result == {"status": "sent", "target_id": "worker"}
1904
+ finally:
1905
+ registry.reset()
1906
+
1907
+
1722
1908
  def test_record_content_output_treats_target_like_text_as_plain_output(monkeypatch):
1723
1909
  registry.reset()
1724
1910
  assistant = Agent(NodeConfig(node_type=NodeType.ASSISTANT), uuid="assistant")
@@ -1750,21 +1936,33 @@ def test_handle_tool_call_send_success_omits_toolcall_history(monkeypatch):
1750
1936
  registry.reset()
1751
1937
  _register_tab_leader()
1752
1938
  child = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="child")
1939
+ peer = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="peer")
1753
1940
  registry.register(child)
1941
+ registry.register(peer)
1942
+ _add_agent_path(source_id="child", target_id="peer")
1754
1943
 
1755
1944
  try:
1756
1945
  result = child._handle_tool_call(
1757
1946
  "send",
1758
1947
  {
1759
- "target": "leader",
1760
- "parts": [{"type": "text", "text": "reply with the findings"}],
1948
+ "target": "peer",
1949
+ "from_output_port_key": "out",
1950
+ "to_input_port_key": "in",
1951
+ "value": [{"type": "text", "text": "reply with the findings"}],
1761
1952
  },
1762
1953
  "call-send",
1763
1954
  )
1764
1955
  finally:
1765
1956
  registry.reset()
1766
1957
 
1767
- assert json.loads(result) == {"status": "sent", "target_id": "leader"}
1958
+ assert json.loads(result) == {
1959
+ "status": "sent",
1960
+ "target_id": "peer",
1961
+ "from_output_port_key": "out",
1962
+ "to_input_port_key": "in",
1963
+ "port_type": "parts",
1964
+ "value_summary": "reply with the findings",
1965
+ }
1768
1966
  assert not any(
1769
1967
  isinstance(entry, ToolCall) and entry.tool_call_id == "call-send"
1770
1968
  for entry in child.get_history_snapshot()
@@ -1785,7 +1983,9 @@ def test_handle_tool_call_send_failure_records_error_without_toolcall():
1785
1983
  "send",
1786
1984
  {
1787
1985
  "target": "peer",
1788
- "parts": [{"type": "text", "text": "reply with the findings"}],
1986
+ "from_output_port_key": "out",
1987
+ "to_input_port_key": "in",
1988
+ "value": [{"type": "text", "text": "reply with the findings"}],
1789
1989
  },
1790
1990
  "call-send",
1791
1991
  )
@@ -1793,7 +1993,7 @@ def test_handle_tool_call_send_failure_records_error_without_toolcall():
1793
1993
  registry.reset()
1794
1994
 
1795
1995
  assert json.loads(result) == {
1796
- "error": "Send failed: target `peer` is not in contacts."
1996
+ "error": "Send failed: target `peer` is not connected from `out` to `in`."
1797
1997
  }
1798
1998
  assert not any(
1799
1999
  isinstance(entry, ToolCall) and entry.tool_call_id == "call-send"
@@ -1801,18 +2001,22 @@ def test_handle_tool_call_send_failure_records_error_without_toolcall():
1801
2001
  )
1802
2002
  assert any(
1803
2003
  isinstance(entry, ErrorEntry)
1804
- and entry.content == "Send failed: target `peer` is not in contacts."
2004
+ and entry.content
2005
+ == "Send failed: target `peer` is not connected from `out` to `in`."
1805
2006
  for entry in child.get_history_snapshot()
1806
2007
  )
1807
2008
 
1808
2009
 
1809
2010
  def test_multiple_send_tool_calls_stop_after_first_failure(monkeypatch):
1810
2011
  registry.reset()
1811
- leader = _register_tab_leader()
2012
+ _register_tab_leader()
1812
2013
  child = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="child")
2014
+ peer = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="peer")
1813
2015
  helper = Agent(NodeConfig(node_type=NodeType.AGENT, tab_id="tab-1"), uuid="helper")
1814
2016
  registry.register(child)
2017
+ registry.register(peer)
1815
2018
  registry.register(helper)
2019
+ _add_agent_path(source_id="child", target_id="peer")
1816
2020
 
1817
2021
  wait_calls = 0
1818
2022
  chat_calls = 0
@@ -1842,8 +2046,10 @@ def test_multiple_send_tool_calls_stop_after_first_failure(monkeypatch):
1842
2046
  id="call-send-1",
1843
2047
  name="send",
1844
2048
  arguments={
1845
- "target": "leader",
1846
- "parts": [{"type": "text", "text": "first"}],
2049
+ "target": "peer",
2050
+ "from_output_port_key": "out",
2051
+ "to_input_port_key": "in",
2052
+ "value": [{"type": "text", "text": "first"}],
1847
2053
  },
1848
2054
  ),
1849
2055
  ToolCallResult(
@@ -1851,15 +2057,19 @@ def test_multiple_send_tool_calls_stop_after_first_failure(monkeypatch):
1851
2057
  name="send",
1852
2058
  arguments={
1853
2059
  "target": "helper",
1854
- "parts": [{"type": "text", "text": "second"}],
2060
+ "from_output_port_key": "out",
2061
+ "to_input_port_key": "in",
2062
+ "value": [{"type": "text", "text": "second"}],
1855
2063
  },
1856
2064
  ),
1857
2065
  ToolCallResult(
1858
2066
  id="call-send-3",
1859
2067
  name="send",
1860
2068
  arguments={
1861
- "target": "leader",
1862
- "parts": [{"type": "text", "text": "third"}],
2069
+ "target": "peer",
2070
+ "from_output_port_key": "out",
2071
+ "to_input_port_key": "in",
2072
+ "value": [{"type": "text", "text": "third"}],
1863
2073
  },
1864
2074
  ),
1865
2075
  ]
@@ -1887,12 +2097,13 @@ def test_multiple_send_tool_calls_stop_after_first_failure(monkeypatch):
1887
2097
  assert [entry.content for entry in sent_entries] == ["first"]
1888
2098
  assert [
1889
2099
  entry.content
1890
- for entry in leader.get_history_snapshot()
2100
+ for entry in peer.get_history_snapshot()
1891
2101
  if isinstance(entry, ReceivedMessage)
1892
2102
  ] == ["first"]
1893
2103
  assert helper._wake_queue.empty()
1894
2104
  assert any(
1895
- entry.content == "Send failed: target `helper` is not in contacts."
2105
+ entry.content
2106
+ == "Send failed: target `helper` is not connected from `out` to `in`."
1896
2107
  for entry in error_entries
1897
2108
  )
1898
2109
 
@@ -1914,7 +2125,7 @@ def test_build_messages_replays_sent_messages_as_message_to_context(monkeypatch)
1914
2125
  {"role": "assistant", "content": "final answer"},
1915
2126
  {
1916
2127
  "role": "user",
1917
- "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.\n- Use `contacts` to inspect the node ids and names you can currently message directly.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If there is no unfinished TODO and the task is finished with no immediate next action, call `idle`.</system>",
2128
+ "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.\n- Use `contacts` to inspect the current output-port paths you can send through.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If there is no unfinished TODO and the task is finished with no immediate next action, call `idle`.</system>",
1918
2129
  },
1919
2130
  ]
1920
2131
 
@@ -2126,7 +2337,7 @@ def test_build_messages_appends_runtime_todo_context_without_history_entry(monke
2126
2337
  },
2127
2338
  {
2128
2339
  "role": "user",
2129
- "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.\n- Use `contacts` to inspect the node ids and names you can currently message directly.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If the TODO list is not complete yet, use `todo` to replace it with the latest remaining items.</system>",
2340
+ "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.\n- Use `contacts` to inspect the current output-port paths you can send through.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If the TODO list is not complete yet, use `todo` to replace it with the latest remaining items.</system>",
2130
2341
  },
2131
2342
  ]
2132
2343
 
@@ -2154,7 +2365,7 @@ def test_build_messages_appends_runtime_post_prompt_and_idle_guidance(monkeypatc
2154
2365
  {"role": "user", "content": '<message from="human">begin</message>'},
2155
2366
  {
2156
2367
  "role": "user",
2157
- "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.\n- Use `contacts` to inspect the node ids and names you can currently message directly.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If there is no unfinished TODO and the task is finished with no immediate next action, call `idle`.</system>",
2368
+ "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.\n- Use `contacts` to inspect the current output-port paths you can send through.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If there is no unfinished TODO and the task is finished with no immediate next action, call `idle`.</system>",
2158
2369
  },
2159
2370
  {
2160
2371
  "role": "user",
@@ -2226,7 +2437,7 @@ def test_build_messages_warns_about_newly_created_agents_waiting_for_first_task(
2226
2437
  },
2227
2438
  {
2228
2439
  "role": "user",
2229
- "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.\n- Use `contacts` to inspect the node ids and names you can currently message directly.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- Newly created agents still waiting for their first task: Directory Worker (`12345678`).\n- `create_agent` only adds a new agent node to the current workflow. It does not start work by itself.\n- Before calling `idle`, dispatch each waiting agent a concrete first task with `send`.</system>",
2440
+ "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.\n- Use `contacts` to inspect the current output-port paths you can send through.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- Newly created agents still waiting for their first task: Directory Worker (`12345678`).\n- `create_agent` only adds a new agent node to the current workflow. It does not start work by itself.\n- Before calling `idle`, dispatch each waiting agent a concrete first task with `send`.</system>",
2230
2441
  },
2231
2442
  ]
2232
2443
 
@@ -2291,7 +2502,7 @@ def test_build_messages_uses_role_name_when_created_agent_has_no_explicit_name(
2291
2502
  },
2292
2503
  {
2293
2504
  "role": "user",
2294
- "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.\n- Use `contacts` to inspect the node ids and names you can currently message directly.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- Newly created agents still waiting for their first task: Worker (`12345678`).\n- `create_agent` only adds a new agent node to the current workflow. It does not start work by itself.\n- Before calling `idle`, dispatch each waiting agent a concrete first task with `send`.</system>",
2505
+ "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.\n- Use `contacts` to inspect the current output-port paths you can send through.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- Newly created agents still waiting for their first task: Worker (`12345678`).\n- `create_agent` only adds a new agent node to the current workflow. It does not start work by itself.\n- Before calling `idle`, dispatch each waiting agent a concrete first task with `send`.</system>",
2295
2506
  },
2296
2507
  ]
2297
2508
 
@@ -2367,7 +2578,7 @@ def test_build_messages_clears_new_agent_warning_after_first_sent_message(monkey
2367
2578
  },
2368
2579
  {
2369
2580
  "role": "user",
2370
- "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.\n- Use `contacts` to inspect the node ids and names you can currently message directly.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If there is no unfinished TODO and the task is finished with no immediate next action, call `idle`.</system>",
2581
+ "content": "<system>Runtime post prompt:\n- Plain content is never delivered to other agents.\n- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.\n- Use `contacts` to inspect the current output-port paths you can send through.\n- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.\n- If there is no unfinished TODO and the task is finished with no immediate next action, call `idle`.</system>",
2371
2582
  },
2372
2583
  ]
2373
2584
 
@@ -2525,7 +2736,9 @@ def test_assistant_emits_human_content_for_plain_text_with_target_like_prefix(
2525
2736
 
2526
2737
 
2527
2738
  def test_idle_tool_records_wakeup_message_as_new_input_block(monkeypatch):
2528
- agent = Agent(NodeConfig(node_type=NodeType.AGENT, tools=["idle"]))
2739
+ agent = Agent(
2740
+ NodeConfig(node_type=NodeType.AGENT, tools=["idle"], allow_network=True)
2741
+ )
2529
2742
  wait_calls = 0
2530
2743
  llm_messages: list[list[dict]] = []
2531
2744
  responses = iter(
@@ -2620,7 +2833,10 @@ def test_idle_tool_records_wakeup_message_as_new_input_block(monkeypatch):
2620
2833
 
2621
2834
 
2622
2835
  def test_agent_contextualizes_plain_loguru_calls(monkeypatch):
2623
- agent = Agent(NodeConfig(node_type=NodeType.AGENT), uuid="agent-z")
2836
+ agent = Agent(
2837
+ NodeConfig(node_type=NodeType.AGENT, allow_network=True),
2838
+ uuid="agent-z",
2839
+ )
2624
2840
  captured: list[tuple[str, str | None]] = []
2625
2841
  sink_id = logger.add(
2626
2842
  lambda message: captured.append(
@@ -2680,7 +2896,7 @@ def test_agent_denies_tool_call_before_edit_execute(monkeypatch, tmp_path):
2680
2896
  "call-edit",
2681
2897
  )
2682
2898
 
2683
- assert result == json.dumps({"error": "Write access is disabled for this agent"})
2899
+ assert result == json.dumps({"error": "Write access is disabled for this workflow"})
2684
2900
  assert isinstance(agent.history[-1], ToolCall)
2685
2901
  assert agent.history[-1].result == result
2686
2902