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
@@ -10,10 +10,11 @@ from dataclasses import dataclass, field
10
10
  from functools import lru_cache, partial
11
11
  from queue import Empty, Queue
12
12
  from typing import Any
13
+ from urllib.parse import urlparse
13
14
 
14
15
  from loguru import logger
15
16
 
16
- from flowent.assistant_commands import build_assistant_help_text
17
+ from flowent.assistant_commands import build_conversation_help_text
17
18
  from flowent.events import event_bus
18
19
  from flowent.image_assets import create_image_asset, require_image_asset
19
20
  from flowent.models import (
@@ -35,15 +36,17 @@ from flowent.models import (
35
36
  ModelInfo,
36
37
  NodeConfig,
37
38
  NodeType,
39
+ PortInboundEntry,
38
40
  ReceivedMessage,
39
41
  SentMessage,
40
- StateEntry,
41
42
  SystemEntry,
42
43
  TextPart,
43
44
  ThinkingDelta,
44
45
  TodoItem,
45
46
  ToolCall,
46
47
  ToolResultDelta,
48
+ WorkflowActivationState,
49
+ WorkflowNodeKind,
47
50
  content_parts_to_text,
48
51
  deserialize_content_parts,
49
52
  has_image_parts,
@@ -123,6 +126,7 @@ class ResolvedModelSource:
123
126
  provider_id: str | None
124
127
  provider_name: str | None
125
128
  provider_type: str | None
129
+ provider_base_url: str | None
126
130
  model: str | None
127
131
  model_info: ModelInfo | None
128
132
 
@@ -145,7 +149,6 @@ class Agent:
145
149
  self.todos: list[TodoItem] = []
146
150
  self.connections: list[str] = []
147
151
  self.history: list[HistoryEntry] = []
148
- self.history.append(StateEntry(state=self.state.value, reason="created"))
149
152
  self._terminate = threading.Event()
150
153
  self._interrupt_requested = threading.Event()
151
154
  self._interrupt_callback_lock = threading.Lock()
@@ -329,7 +332,7 @@ class Agent:
329
332
  return max(0.0, _time.perf_counter() - started_at)
330
333
 
331
334
  def get_contact_ids_snapshot(self) -> list[str]:
332
- from flowent.graph_service import get_tab_leader_id
335
+ from flowent.graph_service import get_tab_leader_id, list_agent_contact_paths
333
336
  from flowent.registry import registry
334
337
  from flowent.workspace_store import workspace_store
335
338
 
@@ -361,8 +364,6 @@ class Agent:
361
364
  assistant = registry.get_assistant()
362
365
  if is_leader and assistant is not None:
363
366
  append_contact(assistant.uuid)
364
- if not is_leader and leader_id is not None:
365
- append_contact(leader_id)
366
367
 
367
368
  if is_leader:
368
369
  for node in registry.get_all():
@@ -375,13 +376,34 @@ class Agent:
375
376
  append_contact(node.uuid)
376
377
  return contact_ids
377
378
 
379
+ for path in list_agent_contact_paths(
380
+ tab_id=self.config.tab_id,
381
+ node_id=self.uuid,
382
+ ):
383
+ append_contact(path.target_id)
378
384
  return contact_ids
379
385
 
380
386
  def get_contacts_info(self) -> list[dict[str, Any]]:
381
- from flowent.graph_service import is_tab_leader
387
+ from flowent.graph_service import (
388
+ is_tab_leader,
389
+ list_agent_contact_paths,
390
+ )
382
391
  from flowent.registry import registry
383
392
 
384
393
  result: list[dict[str, Any]] = []
394
+ if (
395
+ self.config.tab_id is not None
396
+ and not is_tab_leader(node_id=self.uuid, tab_id=self.config.tab_id)
397
+ and self.node_type == NodeType.AGENT
398
+ ):
399
+ return [
400
+ path.serialize()
401
+ for path in list_agent_contact_paths(
402
+ tab_id=self.config.tab_id,
403
+ node_id=self.uuid,
404
+ )
405
+ ]
406
+
385
407
  for contact_id in self.get_contact_ids_snapshot():
386
408
  node = registry.get(contact_id)
387
409
  if node is None:
@@ -504,7 +526,7 @@ class Agent:
504
526
  if self.state not in {AgentState.RUNNING, AgentState.SLEEPING}:
505
527
  return False
506
528
  if not self._command_interrupt_lock.acquire(timeout=timeout):
507
- raise TimeoutError("Assistant did not pause for the command in time")
529
+ raise TimeoutError("Current chat did not pause for the command in time")
508
530
 
509
531
  self._pause_after_interrupt_requested.set()
510
532
  self._paused_for_command.clear()
@@ -517,7 +539,7 @@ class Agent:
517
539
  self._command_interrupt_lock.release()
518
540
  return False
519
541
  if not self._paused_for_command.wait(timeout=timeout):
520
- raise TimeoutError("Assistant did not pause after interrupt")
542
+ raise TimeoutError("Current chat did not pause after interrupt")
521
543
  return True
522
544
  except Exception:
523
545
  self._pause_after_interrupt_requested.clear()
@@ -532,7 +554,10 @@ class Agent:
532
554
 
533
555
  def clear_chat_history(self, *, interrupt_timeout: float = 5.0) -> None:
534
556
  if self.node_type != NodeType.ASSISTANT:
535
- raise RuntimeError("Only assistant chat history can be cleared")
557
+ from flowent.graph_service import is_tab_leader
558
+
559
+ if not is_tab_leader(node_id=self.uuid, tab_id=self.config.tab_id):
560
+ raise RuntimeError("Only Assistant or workflow chats can be cleared")
536
561
 
537
562
  paused_for_command = self._pause_for_command_execution(
538
563
  timeout=interrupt_timeout
@@ -543,9 +568,7 @@ class Agent:
543
568
  self._pending_runtime_notices.clear()
544
569
  with self._history_lock:
545
570
  self.history = [
546
- entry
547
- for entry in self.history
548
- if isinstance(entry, (SystemEntry, StateEntry))
571
+ entry for entry in self.history if isinstance(entry, SystemEntry)
549
572
  ]
550
573
  self._pending_input_turn = False
551
574
  self._turn_started_with_pending_input = False
@@ -556,7 +579,11 @@ class Agent:
556
579
  Event(
557
580
  type=EventType.HISTORY_CLEARED,
558
581
  agent_id=self.uuid,
559
- data={"scope": "assistant_chat"},
582
+ data={
583
+ "scope": "assistant_chat"
584
+ if self.node_type == NodeType.ASSISTANT
585
+ else "workflow_chat"
586
+ },
560
587
  )
561
588
  )
562
589
  self._persist_workspace_node()
@@ -634,8 +661,7 @@ class Agent:
634
661
  self.history = [
635
662
  entry
636
663
  for index, entry in enumerate(previous_history)
637
- if index < anchor_index
638
- or isinstance(entry, (SystemEntry, StateEntry))
664
+ if index < anchor_index or isinstance(entry, SystemEntry)
639
665
  ]
640
666
  retried_message_id = str(_uuid.uuid4())
641
667
  self.history.append(
@@ -785,8 +811,7 @@ class Agent:
785
811
  self.history = [
786
812
  entry
787
813
  for index, entry in enumerate(previous_history)
788
- if index < anchor_index
789
- or isinstance(entry, (SystemEntry, StateEntry))
814
+ if index < anchor_index or isinstance(entry, SystemEntry)
790
815
  ]
791
816
  retried_message_id = str(_uuid.uuid4())
792
817
  self.history.append(
@@ -862,14 +887,20 @@ class Agent:
862
887
  interrupt_timeout: float = 5.0,
863
888
  ) -> CommandResultEntry:
864
889
  if self.node_type != NodeType.ASSISTANT:
865
- raise RuntimeError("Only assistant chat history can be compacted")
890
+ from flowent.graph_service import is_tab_leader
891
+
892
+ if not is_tab_leader(node_id=self.uuid, tab_id=self.config.tab_id):
893
+ raise RuntimeError("Only Assistant or workflow chats can be compacted")
866
894
 
867
895
  paused_for_command = self._pause_for_command_execution(
868
896
  timeout=interrupt_timeout
869
897
  )
870
898
  try:
871
- self._run_compact_with_stats(trigger_type="manual", focus=focus)
872
- content = "Compacted the current Assistant execution context."
899
+ self._run_compact_with_observability(
900
+ trigger_type="manual",
901
+ focus=focus,
902
+ )
903
+ content = "Compacted this chat for future replies."
873
904
  if focus and focus.strip():
874
905
  content += f"\n\nFocus: {focus.strip()}"
875
906
 
@@ -885,7 +916,7 @@ class Agent:
885
916
  if paused_for_command:
886
917
  self._resume_after_command_execution()
887
918
 
888
- def execute_assistant_command(
919
+ def execute_conversation_command(
889
920
  self,
890
921
  *,
891
922
  command_name: str,
@@ -897,7 +928,7 @@ class Agent:
897
928
  self.clear_chat_history(interrupt_timeout=interrupt_timeout)
898
929
  entry = CommandResultEntry(
899
930
  command_name=command_name,
900
- content="Cleared the current Assistant chat history.",
931
+ content="Cleared the current chat.",
901
932
  include_in_context=False,
902
933
  )
903
934
  append_to_history = False
@@ -909,15 +940,28 @@ class Agent:
909
940
  elif command_name == "/help":
910
941
  entry = CommandResultEntry(
911
942
  command_name=command_name,
912
- content=build_assistant_help_text(),
943
+ content=build_conversation_help_text(),
913
944
  )
914
945
  else:
915
- raise RuntimeError(f"Unsupported Assistant command: {command_name}")
946
+ raise RuntimeError(f"Unsupported conversation command: {command_name}")
916
947
 
917
948
  if append_to_history:
918
949
  self._append_history(entry)
919
950
  return entry
920
951
 
952
+ def execute_assistant_command(
953
+ self,
954
+ *,
955
+ command_name: str,
956
+ argument: str = "",
957
+ interrupt_timeout: float = 5.0,
958
+ ) -> CommandResultEntry:
959
+ return self.execute_conversation_command(
960
+ command_name=command_name,
961
+ argument=argument,
962
+ interrupt_timeout=interrupt_timeout,
963
+ )
964
+
921
965
  def _run(self) -> None:
922
966
  with logger.contextualize(
923
967
  agent_id=self.uuid[:8],
@@ -1237,11 +1281,20 @@ class Agent:
1237
1281
  has_todos: bool,
1238
1282
  pending_agent_dispatches: list[str],
1239
1283
  ) -> dict[str, str]:
1284
+ if self._is_entry_level_sender():
1285
+ send_lines = [
1286
+ "- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.",
1287
+ "- Use `contacts` to inspect the node ids and names you can currently message directly.",
1288
+ ]
1289
+ else:
1290
+ send_lines = [
1291
+ "- To send through a workflow path, use `send` with one target, source output port, target input port, and a value matching that port type.",
1292
+ "- Use `contacts` to inspect the current output-port paths you can send through.",
1293
+ ]
1240
1294
  lines = [
1241
1295
  "Runtime post prompt:",
1242
1296
  "- Plain content is never delivered to other agents.",
1243
- "- To send a formal message to another node, use `send` with a single `target` and ordered `parts`.",
1244
- "- Use `contacts` to inspect the node ids and names you can currently message directly.",
1297
+ *send_lines,
1245
1298
  "- `@target:` or any other `@name:` text inside normal content is just text. It does not send anything.",
1246
1299
  ]
1247
1300
  if pending_agent_dispatches:
@@ -1416,6 +1469,10 @@ class Agent:
1416
1469
  ),
1417
1470
  )
1418
1471
 
1472
+ @staticmethod
1473
+ def _parts_value(parts: list[TextPart | ImagePart]) -> list[dict[str, Any]]:
1474
+ return [part.serialize() for part in parts]
1475
+
1419
1476
  def _record_text_output(
1420
1477
  self,
1421
1478
  content: str,
@@ -1526,6 +1583,68 @@ class Agent:
1526
1583
  raise ValueError(f"Send failed: target `{target_ref}` is not in contacts.")
1527
1584
  return target
1528
1585
 
1586
+ def _is_entry_level_sender(self) -> bool:
1587
+ if self.node_type == NodeType.ASSISTANT:
1588
+ return True
1589
+ if self.config.tab_id is None:
1590
+ return False
1591
+ from flowent.graph_service import is_tab_leader
1592
+
1593
+ return is_tab_leader(node_id=self.uuid, tab_id=self.config.tab_id)
1594
+
1595
+ def _resolve_port_send_path(
1596
+ self,
1597
+ *,
1598
+ target_ref: str,
1599
+ from_output_port_key: str,
1600
+ to_input_port_key: str,
1601
+ ):
1602
+ if self.config.tab_id is None:
1603
+ raise ValueError("Send failed: node is not part of a workflow.")
1604
+ from flowent.graph_service import (
1605
+ resolve_agent_contact_path,
1606
+ resolve_workflow_node_ref,
1607
+ )
1608
+
1609
+ target_id = resolve_workflow_node_ref(
1610
+ tab_id=self.config.tab_id,
1611
+ node_ref=target_ref,
1612
+ )
1613
+ if target_id is None:
1614
+ raise ValueError(f"Send failed: target `{target_ref}` was not found.")
1615
+ path = resolve_agent_contact_path(
1616
+ tab_id=self.config.tab_id,
1617
+ source_node_id=self.uuid,
1618
+ target_node_id=target_id,
1619
+ from_output_port_key=from_output_port_key,
1620
+ to_input_port_key=to_input_port_key,
1621
+ )
1622
+ if path is None:
1623
+ raise ValueError(
1624
+ f"Send failed: target `{target_ref}` is not connected from `{from_output_port_key}` to `{to_input_port_key}`."
1625
+ )
1626
+ return path
1627
+
1628
+ def _ensure_can_dispatch_to_contact(self, target: Agent) -> None:
1629
+ if self.config.tab_id is None:
1630
+ return
1631
+
1632
+ from flowent.graph_service import is_tab_leader
1633
+ from flowent.workspace_store import workspace_store
1634
+
1635
+ if not is_tab_leader(node_id=self.uuid, tab_id=self.config.tab_id):
1636
+ return
1637
+ if target.node_type != NodeType.AGENT:
1638
+ return
1639
+ if is_tab_leader(node_id=target.uuid, tab_id=target.config.tab_id):
1640
+ return
1641
+
1642
+ tab = workspace_store.get_tab(self.config.tab_id)
1643
+ if tab is not None and tab.activation_state != WorkflowActivationState.ACTIVE:
1644
+ raise ValueError(
1645
+ "Activate this workflow before sending work to agent nodes."
1646
+ )
1647
+
1529
1648
  def supports_input_image(self) -> bool:
1530
1649
  _, model_info = self._get_effective_model_info()
1531
1650
  if model_info is None:
@@ -1543,9 +1662,20 @@ class Agent:
1543
1662
  *,
1544
1663
  target_ref: str,
1545
1664
  raw_parts: Any,
1665
+ from_output_port_key: str | None = None,
1666
+ to_input_port_key: str | None = None,
1667
+ raw_value: Any | None = None,
1546
1668
  ) -> str:
1669
+ if not self._is_entry_level_sender():
1670
+ return self.send_port_value(
1671
+ target_ref=target_ref,
1672
+ from_output_port_key=from_output_port_key,
1673
+ to_input_port_key=to_input_port_key,
1674
+ raw_value=raw_value if raw_value is not None else raw_parts,
1675
+ )
1547
1676
  parts = parse_content_parts_payload(raw_parts)
1548
1677
  target = self._resolve_contact_target(target_ref)
1678
+ self._ensure_can_dispatch_to_contact(target)
1549
1679
  if has_image_parts(parts) and not target.supports_input_image():
1550
1680
  raise ValueError(
1551
1681
  f"Send failed: target `{target_ref}` does not support `input_image`."
@@ -1566,6 +1696,78 @@ class Agent:
1566
1696
  )
1567
1697
  return json.dumps({"status": "sent", "target_id": target.uuid})
1568
1698
 
1699
+ def send_port_value(
1700
+ self,
1701
+ *,
1702
+ target_ref: str,
1703
+ from_output_port_key: str | None,
1704
+ to_input_port_key: str | None,
1705
+ raw_value: Any,
1706
+ ) -> str:
1707
+ if self._is_entry_level_sender():
1708
+ raise ValueError("Send failed: entry contacts use target and parts.")
1709
+ if (
1710
+ not isinstance(from_output_port_key, str)
1711
+ or not from_output_port_key.strip()
1712
+ ):
1713
+ raise ValueError("send.from_output_port_key must be a non-empty string")
1714
+ if not isinstance(to_input_port_key, str) or not to_input_port_key.strip():
1715
+ raise ValueError("send.to_input_port_key must be a non-empty string")
1716
+
1717
+ path = self._resolve_port_send_path(
1718
+ target_ref=target_ref,
1719
+ from_output_port_key=from_output_port_key.strip(),
1720
+ to_input_port_key=to_input_port_key.strip(),
1721
+ )
1722
+ value = raw_value
1723
+ if path.port_type == "parts":
1724
+ parts = parse_content_parts_payload(raw_value)
1725
+ value = self._parts_value(parts)
1726
+ if path.target_node_type == WorkflowNodeKind.AGENT.value:
1727
+ target = self._resolve_contact_target(path.target_id)
1728
+ if has_image_parts(parts) and not target.supports_input_image():
1729
+ raise ValueError(
1730
+ f"Send failed: target `{target_ref}` does not support `input_image`."
1731
+ )
1732
+ for part in parts:
1733
+ asset_id = getattr(part, "asset_id", None)
1734
+ if isinstance(asset_id, str):
1735
+ require_image_asset(asset_id)
1736
+
1737
+ message_id = str(_uuid.uuid4())
1738
+ from flowent.graph_service import dispatch_port_value
1739
+
1740
+ payload, error = dispatch_port_value(
1741
+ tab_id=self.config.tab_id or "",
1742
+ source_node_id=self.uuid,
1743
+ source_output_port_key=path.from_output_port_key,
1744
+ target_node_id=path.target_id,
1745
+ target_input_port_key=path.to_input_port_key,
1746
+ value=value,
1747
+ source_is_agent_send=True,
1748
+ message_id=message_id,
1749
+ )
1750
+ if error is not None or payload is None:
1751
+ raise ValueError(f"Send failed: {error or 'path unavailable'}.")
1752
+
1753
+ self._mark_turn_progress()
1754
+ sent_parts = (
1755
+ parse_content_parts_payload(value) if path.port_type == "parts" else []
1756
+ )
1757
+ value_summary = str(payload.get("value_summary", ""))
1758
+ self._append_history(
1759
+ SentMessage(
1760
+ to_id=path.target_id,
1761
+ parts=sent_parts,
1762
+ content=value_summary if path.port_type != "parts" else "",
1763
+ message_id=message_id,
1764
+ from_output_port_key=path.from_output_port_key,
1765
+ to_input_port_key=path.to_input_port_key,
1766
+ value_summary=value_summary,
1767
+ )
1768
+ )
1769
+ return json.dumps(payload)
1770
+
1569
1771
  def _mark_turn_progress(self) -> None:
1570
1772
  self._turn_made_progress = True
1571
1773
  self._pending_input_turn = False
@@ -1662,6 +1864,7 @@ class Agent:
1662
1864
  retry_limit = self._get_llm_max_retries()
1663
1865
  retry_count = 0
1664
1866
  started_at = _time.time()
1867
+ self._raise_if_llm_network_disallowed()
1665
1868
 
1666
1869
  while True:
1667
1870
  stream_state = StreamingContentState()
@@ -1682,7 +1885,7 @@ class Agent:
1682
1885
  finally:
1683
1886
  self.set_interrupt_callback(None)
1684
1887
  self._raise_if_interrupt_requested()
1685
- self._record_request_stats(
1888
+ self._record_request_observability(
1686
1889
  started_at=started_at,
1687
1890
  ended_at=_time.time(),
1688
1891
  retry_count=retry_count,
@@ -1705,7 +1908,7 @@ class Agent:
1705
1908
  elif retry_policy == "unlimited":
1706
1909
  should_retry = True
1707
1910
  if not should_retry:
1708
- self._record_request_stats(
1911
+ self._record_request_observability(
1709
1912
  started_at=started_at,
1710
1913
  ended_at=_time.time(),
1711
1914
  retry_count=retry_count,
@@ -1726,7 +1929,7 @@ class Agent:
1726
1929
  )
1727
1930
  self._wait_for_llm_retry_delay(delay_seconds)
1728
1931
  except Exception as exc:
1729
- self._record_request_stats(
1932
+ self._record_request_observability(
1730
1933
  started_at=started_at,
1731
1934
  ended_at=_time.time(),
1732
1935
  retry_count=retry_count,
@@ -1767,6 +1970,8 @@ class Agent:
1767
1970
  if not context_messages:
1768
1971
  return "- No prior execution context was available to compact."
1769
1972
 
1973
+ self._raise_if_llm_network_disallowed()
1974
+
1770
1975
  focus_text = focus.strip() if focus else ""
1771
1976
  request_lines = [
1772
1977
  "Compact this agent execution context into a durable markdown summary.",
@@ -1889,12 +2094,20 @@ class Agent:
1889
2094
 
1890
2095
  elif isinstance(entry, ReceivedMessage):
1891
2096
  self._flush_tool_calls(messages, pending_tool_calls)
2097
+ if entry.from_output_port_key and entry.to_input_port_key:
2098
+ prefix = (
2099
+ f'<message from="{entry.from_id}" '
2100
+ f'from_output="{entry.from_output_port_key}" '
2101
+ f'to_input="{entry.to_input_port_key}">'
2102
+ )
2103
+ else:
2104
+ prefix = f'<message from="{entry.from_id}">'
1892
2105
  messages.append(
1893
2106
  {
1894
2107
  "role": "user",
1895
2108
  "content": self._wrap_context_parts(
1896
2109
  entry.parts,
1897
- prefix=f'<message from="{entry.from_id}">',
2110
+ prefix=prefix,
1898
2111
  suffix="</message>",
1899
2112
  ),
1900
2113
  }
@@ -1911,12 +2124,20 @@ class Agent:
1911
2124
 
1912
2125
  elif isinstance(entry, SentMessage):
1913
2126
  self._flush_tool_calls(messages, pending_tool_calls)
2127
+ if entry.from_output_port_key and entry.to_input_port_key:
2128
+ prefix = (
2129
+ f'<message to="{entry.to_id}" '
2130
+ f'from_output="{entry.from_output_port_key}" '
2131
+ f'to_input="{entry.to_input_port_key}">'
2132
+ )
2133
+ else:
2134
+ prefix = f'<message to="{entry.to_id}">'
1914
2135
  messages.append(
1915
2136
  {
1916
2137
  "role": "assistant",
1917
2138
  "content": self._wrap_context_parts(
1918
2139
  entry.parts,
1919
- prefix=f'<message to="{entry.to_id}">',
2140
+ prefix=prefix,
1920
2141
  suffix="</message>",
1921
2142
  ),
1922
2143
  }
@@ -1967,6 +2188,18 @@ class Agent:
1967
2188
  self._flush_tool_calls(messages, pending_tool_calls)
1968
2189
  messages.append(self._build_runtime_system_message(entry.content))
1969
2190
 
2191
+ elif isinstance(entry, PortInboundEntry):
2192
+ self._flush_tool_calls(messages, pending_tool_calls)
2193
+ messages.append(
2194
+ self._build_runtime_system_message(
2195
+ "Port input received: "
2196
+ f"from {entry.from_id}.{entry.from_output_port_key} "
2197
+ f"to {entry.to_input_port_key} "
2198
+ f"({entry.port_type})\n"
2199
+ f"{json.dumps(entry.value, ensure_ascii=False, sort_keys=True)}"
2200
+ )
2201
+ )
2202
+
1970
2203
  self._flush_tool_calls(messages, pending_tool_calls)
1971
2204
  return messages
1972
2205
 
@@ -2047,7 +2280,7 @@ class Agent:
2047
2280
  runtime_tail_messages=list(prepared_context.runtime_tail_messages),
2048
2281
  )
2049
2282
 
2050
- def _get_stats_node_label(self) -> str:
2283
+ def _get_observability_node_label(self) -> str:
2051
2284
  if self.config.name:
2052
2285
  return self.config.name
2053
2286
  if self.config.role_name:
@@ -2060,7 +2293,7 @@ class Agent:
2060
2293
  return "Leader"
2061
2294
  return "Agent"
2062
2295
 
2063
- def _get_stats_tab_title(self) -> str | None:
2296
+ def _get_observability_tab_title(self) -> str | None:
2064
2297
  if not self.config.tab_id:
2065
2298
  return None
2066
2299
  from flowent.workspace_store import workspace_store
@@ -2096,6 +2329,7 @@ class Agent:
2096
2329
  provider_id=None,
2097
2330
  provider_name=None,
2098
2331
  provider_type=None,
2332
+ provider_base_url=None,
2099
2333
  model=None,
2100
2334
  model_info=None,
2101
2335
  )
@@ -2105,6 +2339,7 @@ class Agent:
2105
2339
  provider_id=None,
2106
2340
  provider_name=None,
2107
2341
  provider_type=None,
2342
+ provider_base_url=None,
2108
2343
  model=None,
2109
2344
  model_info=None,
2110
2345
  )
@@ -2112,6 +2347,7 @@ class Agent:
2112
2347
  provider_id=provider.id,
2113
2348
  provider_name=provider.name,
2114
2349
  provider_type=provider.type,
2350
+ provider_base_url=provider.base_url,
2115
2351
  model=model_id,
2116
2352
  model_info=resolve_model_info(
2117
2353
  provider=provider,
@@ -2130,11 +2366,34 @@ class Agent:
2130
2366
  ),
2131
2367
  )
2132
2368
 
2369
+ def _raise_if_llm_network_disallowed(self) -> None:
2370
+ from flowent.graph_service import resolve_effective_permissions_for_agent
2371
+
2372
+ allow_network, _ = resolve_effective_permissions_for_agent(self)
2373
+ if allow_network:
2374
+ return
2375
+ source = self._get_effective_model_source()
2376
+ if not self._model_source_requires_network(source):
2377
+ return
2378
+ raise LLMProviderError(
2379
+ "Network access is disabled for this workflow",
2380
+ transient=False,
2381
+ )
2382
+
2383
+ @staticmethod
2384
+ def _model_source_requires_network(source: ResolvedModelSource) -> bool:
2385
+ base_url = source.provider_base_url
2386
+ if not base_url:
2387
+ return False
2388
+ parsed = urlparse(base_url)
2389
+ host = (parsed.hostname or "").lower()
2390
+ return host not in {"localhost", "127.0.0.1", "::1"}
2391
+
2133
2392
  def _get_effective_model_info(self) -> tuple[str | None, ModelInfo | None]:
2134
2393
  resolved_source = self._get_effective_model_source()
2135
2394
  return resolved_source.provider_type, resolved_source.model_info
2136
2395
 
2137
- def _record_request_stats(
2396
+ def _record_request_observability(
2138
2397
  self,
2139
2398
  *,
2140
2399
  started_at: float,
@@ -2145,16 +2404,19 @@ class Agent:
2145
2404
  raw_usage: dict[str, Any] | None = None,
2146
2405
  error_summary: str | None = None,
2147
2406
  ) -> None:
2148
- from flowent.stats_service import RequestRecordInput, stats_store
2407
+ from flowent.observability_service import (
2408
+ RequestRecordInput,
2409
+ observability_store,
2410
+ )
2149
2411
 
2150
2412
  resolved_source = self._get_effective_model_source()
2151
- stats_store.record_request(
2413
+ observability_store.record_request(
2152
2414
  RequestRecordInput(
2153
2415
  node_id=self.uuid,
2154
- node_label=self._get_stats_node_label(),
2416
+ node_label=self._get_observability_node_label(),
2155
2417
  role_name=self.config.role_name,
2156
2418
  tab_id=self.config.tab_id,
2157
- tab_title=self._get_stats_tab_title(),
2419
+ tab_title=self._get_observability_tab_title(),
2158
2420
  provider_id=resolved_source.provider_id,
2159
2421
  provider_name=resolved_source.provider_name,
2160
2422
  provider_type=resolved_source.provider_type,
@@ -2169,26 +2431,29 @@ class Agent:
2169
2431
  )
2170
2432
  )
2171
2433
 
2172
- def _run_compact_with_stats(
2434
+ def _run_compact_with_observability(
2173
2435
  self,
2174
2436
  *,
2175
2437
  trigger_type: str,
2176
2438
  focus: str | None = None,
2177
2439
  ) -> str:
2178
- from flowent.stats_service import CompactRecordInput, stats_store
2440
+ from flowent.observability_service import (
2441
+ CompactRecordInput,
2442
+ observability_store,
2443
+ )
2179
2444
 
2180
2445
  started_at = _time.time()
2181
2446
  resolved_source = self._get_effective_model_source()
2182
2447
  try:
2183
2448
  result = self._compact_execution_context(focus=focus)
2184
2449
  except Exception as exc:
2185
- stats_store.record_compact(
2450
+ observability_store.record_compact(
2186
2451
  CompactRecordInput(
2187
2452
  node_id=self.uuid,
2188
- node_label=self._get_stats_node_label(),
2453
+ node_label=self._get_observability_node_label(),
2189
2454
  role_name=self.config.role_name,
2190
2455
  tab_id=self.config.tab_id,
2191
- tab_title=self._get_stats_tab_title(),
2456
+ tab_title=self._get_observability_tab_title(),
2192
2457
  provider_id=resolved_source.provider_id,
2193
2458
  provider_name=resolved_source.provider_name,
2194
2459
  provider_type=resolved_source.provider_type,
@@ -2201,13 +2466,13 @@ class Agent:
2201
2466
  )
2202
2467
  )
2203
2468
  raise
2204
- stats_store.record_compact(
2469
+ observability_store.record_compact(
2205
2470
  CompactRecordInput(
2206
2471
  node_id=self.uuid,
2207
- node_label=self._get_stats_node_label(),
2472
+ node_label=self._get_observability_node_label(),
2208
2473
  role_name=self.config.role_name,
2209
2474
  tab_id=self.config.tab_id,
2210
- tab_title=self._get_stats_tab_title(),
2475
+ tab_title=self._get_observability_tab_title(),
2211
2476
  provider_id=resolved_source.provider_id,
2212
2477
  provider_name=resolved_source.provider_name,
2213
2478
  provider_type=resolved_source.provider_type,
@@ -2311,7 +2576,7 @@ class Agent:
2311
2576
  preflight.context_window_tokens,
2312
2577
  )
2313
2578
  try:
2314
- self._run_compact_with_stats(trigger_type="auto")
2579
+ self._run_compact_with_observability(trigger_type="auto")
2315
2580
  except Exception as exc:
2316
2581
  if (
2317
2582
  preflight.safe_input_tokens is not None
@@ -2385,6 +2650,7 @@ class Agent:
2385
2650
  from_id = message.get("from", "")
2386
2651
  message_id = message.get("message_id")
2387
2652
  history_recorded = bool(message.get("history_recorded", False))
2653
+ port_inbound_recorded = bool(message.get("port_inbound_recorded", False))
2388
2654
  if (
2389
2655
  not isinstance(content, str)
2390
2656
  or not isinstance(from_id, str)
@@ -2405,6 +2671,23 @@ class Agent:
2405
2671
  message_id=message_id,
2406
2672
  ),
2407
2673
  )
2674
+ elif not port_inbound_recorded and message.get("port_type") in {
2675
+ "string",
2676
+ "json",
2677
+ }:
2678
+ value = message.get("value")
2679
+ self._append_history(
2680
+ PortInboundEntry(
2681
+ from_id=from_id,
2682
+ from_output_port_key=str(
2683
+ message.get("from_output_port_key", "")
2684
+ ),
2685
+ to_input_port_key=str(message.get("to_input_port_key", "")),
2686
+ port_type=str(message.get("port_type", "")),
2687
+ value=value,
2688
+ value_summary=str(message.get("value_summary", "")),
2689
+ )
2690
+ )
2408
2691
 
2409
2692
  def _wait_for_input(self) -> None:
2410
2693
  signal = self._wait_for_wakeup()
@@ -2422,6 +2705,9 @@ class Agent:
2422
2705
  from_id = message.get("from")
2423
2706
  message_id = message.get("message_id")
2424
2707
  history_recorded = bool(message.get("history_recorded", False))
2708
+ port_inbound_recorded = bool(
2709
+ message.get("port_inbound_recorded", False)
2710
+ )
2425
2711
  if (
2426
2712
  isinstance(content, str)
2427
2713
  and isinstance(from_id, str)
@@ -2436,6 +2722,24 @@ class Agent:
2436
2722
  message_id=message_id,
2437
2723
  )
2438
2724
  )
2725
+ elif (
2726
+ isinstance(from_id, str)
2727
+ and history_recorded
2728
+ and not port_inbound_recorded
2729
+ and message.get("port_type") in {"string", "json"}
2730
+ ):
2731
+ self._append_history(
2732
+ PortInboundEntry(
2733
+ from_id=from_id,
2734
+ from_output_port_key=str(
2735
+ message.get("from_output_port_key", "")
2736
+ ),
2737
+ to_input_port_key=str(message.get("to_input_port_key", "")),
2738
+ port_type=str(message.get("port_type", "")),
2739
+ value=message.get("value"),
2740
+ value_summary=str(message.get("value_summary", "")),
2741
+ )
2742
+ )
2439
2743
 
2440
2744
  if signal.reason != "termination":
2441
2745
  self.set_state(
@@ -2657,6 +2961,18 @@ class Agent:
2657
2961
  "parts": [part.serialize() for part in msg.parts],
2658
2962
  "history_recorded": msg.history_recorded,
2659
2963
  }
2964
+ if msg.from_output_port_key is not None:
2965
+ payload["from_output_port_key"] = msg.from_output_port_key
2966
+ if msg.to_input_port_key is not None:
2967
+ payload["to_input_port_key"] = msg.to_input_port_key
2968
+ if msg.port_type is not None:
2969
+ payload["port_type"] = msg.port_type
2970
+ if msg.value is not None:
2971
+ payload["value"] = msg.value
2972
+ if msg.value_summary is not None:
2973
+ payload["value_summary"] = msg.value_summary
2974
+ if msg.port_inbound_recorded:
2975
+ payload["port_inbound_recorded"] = msg.port_inbound_recorded
2660
2976
  if msg.message_id is not None:
2661
2977
  payload["message_id"] = msg.message_id
2662
2978
  self._wake_queue.put(
@@ -2688,10 +3004,6 @@ class Agent:
2688
3004
  self._idle_started_by_tool_call_id = None
2689
3005
  self._idle_state_event.clear()
2690
3006
  if old != state or force_emit:
2691
- if old != state:
2692
- self._append_history(
2693
- StateEntry(state=state.value, reason=reason),
2694
- )
2695
3007
  self._log.debug(
2696
3008
  "State: {} -> {}{}",
2697
3009
  old.value,