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.
- package/README.md +1 -1
- package/backend/README.md +74 -0
- package/backend/pyproject.toml +2 -1
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/{stats_service.cpython-313.pyc → observability_service.cpython-313.pyc} +0 -0
- package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
- package/backend/src/flowent/agent.py +364 -52
- package/backend/src/flowent/assistant_commands.py +31 -22
- package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
- package/backend/src/flowent/channels/telegram.py +4 -4
- package/backend/src/flowent/graph_service.py +1307 -145
- package/backend/src/flowent/mcp_service.py +21 -7
- package/backend/src/flowent/model_metadata.py +4 -0
- package/backend/src/flowent/models/__init__.py +6 -2
- package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/agent.py +1 -0
- package/backend/src/flowent/models/graph.py +44 -9
- package/backend/src/flowent/models/history.py +73 -15
- package/backend/src/flowent/models/llm.py +1 -0
- package/backend/src/flowent/models/message.py +6 -0
- package/backend/src/flowent/models/tab.py +38 -1
- package/backend/src/flowent/{stats_service.py → observability_service.py} +4 -4
- package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/common.py +2 -2
- package/backend/src/flowent/prompts/steward.py +2 -2
- package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/configuration.py +7 -0
- package/backend/src/flowent/role_management.py +12 -0
- package/backend/src/flowent/routes/__init__.py +0 -2
- package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/assistant.py +4 -4
- package/backend/src/flowent/routes/nodes.py +54 -6
- package/backend/src/flowent/routes/providers_route.py +1 -0
- package/backend/src/flowent/routes/roles.py +1 -1
- package/backend/src/flowent/routes/settings.py +4 -0
- package/backend/src/flowent/routes/tabs.py +29 -11
- package/backend/src/flowent/runtime.py +7 -30
- package/backend/src/flowent/security.py +23 -8
- package/backend/src/flowent/settings.py +56 -5
- package/backend/src/flowent/settings_management.py +12 -0
- package/backend/src/flowent/static/assets/AssistantPage-VBohhz4d.js +1 -0
- package/backend/src/flowent/static/assets/ChannelsPage-CIydPZA_.js +1 -0
- package/backend/src/flowent/static/assets/McpPage-CHPm2TPY.js +7 -0
- package/backend/src/flowent/static/assets/PageScaffold-DteOA8V7.js +1 -0
- package/backend/src/flowent/static/assets/PromptsPage-CSmJ3sZg.js +1 -0
- package/backend/src/flowent/static/assets/ProvidersPage-sl2jeG4e.js +3 -0
- package/backend/src/flowent/static/assets/RolesPage-DCe7W6Km.js +1 -0
- package/backend/src/flowent/static/assets/SettingsPage-Bix9e63E.js +3 -0
- package/backend/src/flowent/static/assets/ToolsPage-favNkj5C.js +1 -0
- package/backend/src/flowent/static/assets/WorkspaceCommandDialog-DRS6wiD6.js +1 -0
- package/backend/src/flowent/static/assets/WorkspacePage-KuaDjt_D.js +3 -0
- package/backend/src/flowent/static/assets/WorkspacePanels-BZxBw8M5.js +1 -0
- package/backend/src/flowent/static/assets/alert-dialog-DIBUCmqM.js +1 -0
- package/backend/src/flowent/static/assets/{dialog-BeGSweF6.js → dialog-BOvHIBrg.js} +1 -1
- package/backend/src/flowent/static/assets/index-Biio-CoI.js +10 -0
- package/backend/src/flowent/static/assets/index-CmQvO7sl.css +1 -0
- package/backend/src/flowent/static/assets/modelParams-DcEhGnu0.js +1 -0
- package/backend/src/flowent/static/assets/roles-BbIEIMeG.js +1 -0
- package/backend/src/flowent/static/assets/select-D9SwnlXF.js +1 -0
- package/backend/src/flowent/static/assets/surface-Bzr1FRG4.js +1 -0
- package/backend/src/flowent/static/assets/{ui-vendor-Dg9NNnWX.js → ui-vendor-UazN8rcv.js} +15 -15
- package/backend/src/flowent/static/index.html +3 -4
- package/backend/src/flowent/tools/__init__.py +76 -2
- package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/connect.py +10 -66
- package/backend/src/flowent/tools/contacts.py +1 -1
- package/backend/src/flowent/tools/create_agent.py +9 -88
- package/backend/src/flowent/tools/create_tab.py +7 -5
- package/backend/src/flowent/tools/exec.py +3 -2
- package/backend/src/flowent/tools/list_roles.py +29 -4
- package/backend/src/flowent/tools/list_tabs.py +4 -0
- package/backend/src/flowent/tools/list_tools.py +5 -1
- package/backend/src/flowent/tools/manage_settings.py +18 -0
- package/backend/src/flowent/tools/send.py +21 -3
- package/backend/src/flowent/tools/set_permissions.py +21 -6
- package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/test_assistant_api.py +1 -1
- package/backend/tests/integration/api/test_nodes_api.py +257 -21
- package/backend/tests/integration/api/test_roles_api.py +3 -2
- package/backend/tests/integration/api/test_tabs_api.py +312 -11
- package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/test_agent_public_api.py +162 -71
- package/backend/tests/unit/agent/test_agent_runtime.py +285 -69
- package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/prompts/test_prompts.py +3 -2
- package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/test_providers_route.py +2 -0
- package/backend/tests/unit/routes/test_roles_routes.py +109 -0
- package/backend/tests/unit/routes/test_settings_routes.py +4 -0
- package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/runtime/test_bootstrap_runtime.py +8 -18
- package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/security/test_security.py +16 -2
- package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/settings/test_settings_roles.py +40 -0
- package/backend/tests/unit/test_state_sqlite_storage.py +67 -1
- package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/test_connect_tool.py +2 -3
- package/backend/tests/unit/tools/test_create_agent_tool.py +9 -97
- package/backend/tests/unit/tools/test_delete_tab_tool.py +33 -0
- package/backend/tests/unit/tools/test_manage_providers_tool.py +2 -0
- package/backend/tests/unit/tools/test_manage_settings_tool.py +3 -0
- package/backend/tests/unit/tools/test_set_permissions_tool.py +216 -12
- package/backend/tests/unit/tools/test_tool_registry.py +103 -0
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/AssistantPage-VBohhz4d.js +1 -0
- package/dist/frontend/assets/ChannelsPage-CIydPZA_.js +1 -0
- package/dist/frontend/assets/McpPage-CHPm2TPY.js +7 -0
- package/dist/frontend/assets/PageScaffold-DteOA8V7.js +1 -0
- package/dist/frontend/assets/PromptsPage-CSmJ3sZg.js +1 -0
- package/dist/frontend/assets/ProvidersPage-sl2jeG4e.js +3 -0
- package/dist/frontend/assets/RolesPage-DCe7W6Km.js +1 -0
- package/dist/frontend/assets/SettingsPage-Bix9e63E.js +3 -0
- package/dist/frontend/assets/ToolsPage-favNkj5C.js +1 -0
- package/dist/frontend/assets/WorkspaceCommandDialog-DRS6wiD6.js +1 -0
- package/dist/frontend/assets/WorkspacePage-KuaDjt_D.js +3 -0
- package/dist/frontend/assets/WorkspacePanels-BZxBw8M5.js +1 -0
- package/dist/frontend/assets/alert-dialog-DIBUCmqM.js +1 -0
- package/dist/frontend/assets/{dialog-BeGSweF6.js → dialog-BOvHIBrg.js} +1 -1
- package/dist/frontend/assets/index-Biio-CoI.js +10 -0
- package/dist/frontend/assets/index-CmQvO7sl.css +1 -0
- package/dist/frontend/assets/modelParams-DcEhGnu0.js +1 -0
- package/dist/frontend/assets/roles-BbIEIMeG.js +1 -0
- package/dist/frontend/assets/select-D9SwnlXF.js +1 -0
- package/dist/frontend/assets/surface-Bzr1FRG4.js +1 -0
- package/dist/frontend/assets/{ui-vendor-Dg9NNnWX.js → ui-vendor-UazN8rcv.js} +15 -15
- package/dist/frontend/index.html +3 -4
- package/package.json +3 -3
- package/backend/src/flowent/routes/__pycache__/stats.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/stats.py +0 -229
- package/backend/src/flowent/static/assets/AssistantPage-B3Xc08AS.js +0 -1
- package/backend/src/flowent/static/assets/ChannelsPage-ByLd28xk.js +0 -1
- package/backend/src/flowent/static/assets/HomePage-C0hAx9_l.js +0 -3
- package/backend/src/flowent/static/assets/McpPage-DkrYLvBv.js +0 -7
- package/backend/src/flowent/static/assets/PageScaffold-D4jO9ooX.js +0 -1
- package/backend/src/flowent/static/assets/PromptsPage-DWA7rRJd.js +0 -1
- package/backend/src/flowent/static/assets/ProvidersPage-PUWT8seJ.js +0 -3
- package/backend/src/flowent/static/assets/RolesPage-CqcclGRw.js +0 -1
- package/backend/src/flowent/static/assets/SettingsPage-8tS2cJgX.js +0 -3
- package/backend/src/flowent/static/assets/StatsPage-BX9khYzu.js +0 -1
- package/backend/src/flowent/static/assets/ToolsPage-9Tl9FdeD.js +0 -1
- package/backend/src/flowent/static/assets/WorkspaceCommandDialog-CCXxjDL8.js +0 -1
- package/backend/src/flowent/static/assets/WorkspacePanels-aMdJ7ZH7.js +0 -1
- package/backend/src/flowent/static/assets/alert-dialog-kFYVQ7oX.js +0 -1
- package/backend/src/flowent/static/assets/badge-74-3jsCg.js +0 -1
- package/backend/src/flowent/static/assets/constants-XUzFf6i1.js +0 -1
- package/backend/src/flowent/static/assets/index-BHC1Vhy8.css +0 -1
- package/backend/src/flowent/static/assets/index-CL1ALZ3r.js +0 -10
- package/backend/src/flowent/static/assets/modelParams-CaHd0903.js +0 -1
- package/backend/src/flowent/static/assets/roles-2OLDeTc5.js +0 -1
- package/backend/src/flowent/static/assets/select-DL_LPeDj.js +0 -1
- package/backend/src/flowent/static/assets/shared-CMxbpLeQ.js +0 -1
- package/backend/tests/unit/routes/__pycache__/test_stats_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/test_stats_routes.py +0 -149
- package/dist/frontend/assets/AssistantPage-B3Xc08AS.js +0 -1
- package/dist/frontend/assets/ChannelsPage-ByLd28xk.js +0 -1
- package/dist/frontend/assets/HomePage-C0hAx9_l.js +0 -3
- package/dist/frontend/assets/McpPage-DkrYLvBv.js +0 -7
- package/dist/frontend/assets/PageScaffold-D4jO9ooX.js +0 -1
- package/dist/frontend/assets/PromptsPage-DWA7rRJd.js +0 -1
- package/dist/frontend/assets/ProvidersPage-PUWT8seJ.js +0 -3
- package/dist/frontend/assets/RolesPage-CqcclGRw.js +0 -1
- package/dist/frontend/assets/SettingsPage-8tS2cJgX.js +0 -3
- package/dist/frontend/assets/StatsPage-BX9khYzu.js +0 -1
- package/dist/frontend/assets/ToolsPage-9Tl9FdeD.js +0 -1
- package/dist/frontend/assets/WorkspaceCommandDialog-CCXxjDL8.js +0 -1
- package/dist/frontend/assets/WorkspacePanels-aMdJ7ZH7.js +0 -1
- package/dist/frontend/assets/alert-dialog-kFYVQ7oX.js +0 -1
- package/dist/frontend/assets/badge-74-3jsCg.js +0 -1
- package/dist/frontend/assets/constants-XUzFf6i1.js +0 -1
- package/dist/frontend/assets/index-BHC1Vhy8.css +0 -1
- package/dist/frontend/assets/index-CL1ALZ3r.js +0 -10
- package/dist/frontend/assets/modelParams-CaHd0903.js +0 -1
- package/dist/frontend/assets/roles-2OLDeTc5.js +0 -1
- package/dist/frontend/assets/select-DL_LPeDj.js +0 -1
- package/dist/frontend/assets/shared-CMxbpLeQ.js +0 -1
- /package/backend/src/flowent/static/assets/{datetime-m6_O_Ci9.js → datetime-eJqd0V2S.js} +0 -0
- /package/backend/src/flowent/static/assets/{markdown-vendor-DVdy_w12.js → markdown-vendor-C9RtvaJh.js} +0 -0
- /package/backend/src/flowent/static/assets/{triState-DEr3NkXV.js → triState-DgLlKdRR.js} +0 -0
- /package/dist/frontend/assets/{datetime-m6_O_Ci9.js → datetime-eJqd0V2S.js} +0 -0
- /package/dist/frontend/assets/{markdown-vendor-DVdy_w12.js → markdown-vendor-C9RtvaJh.js} +0 -0
- /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
|
-
|
|
58
|
+
observability_store.reset()
|
|
56
59
|
yield
|
|
57
60
|
registry.reset()
|
|
58
61
|
workspace_store.reset_cache()
|
|
59
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
383
|
-
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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 "
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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="
|
|
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
|
|
1749
|
+
for entry in peer.get_history_snapshot()
|
|
1633
1750
|
if isinstance(entry, ReceivedMessage)
|
|
1634
1751
|
)
|
|
1635
|
-
signal =
|
|
1752
|
+
signal = peer._wake_queue.get_nowait()
|
|
1636
1753
|
|
|
1637
|
-
assert result == {
|
|
1638
|
-
|
|
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": "
|
|
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
|
|
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
|
|
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 `
|
|
1857
|
+
match=r"Send failed: target `peer` does not support `input_image`\.",
|
|
1713
1858
|
):
|
|
1714
1859
|
child.send_message(
|
|
1715
|
-
target_ref="
|
|
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": "
|
|
1760
|
-
"
|
|
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) == {
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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": "
|
|
1846
|
-
"
|
|
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
|
-
"
|
|
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": "
|
|
1862
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|