diff --git a/tests/cli/test_generated_agent_backend_codegen_extended.py b/tests/cli/test_generated_agent_backend_codegen_extended.py index 152204316..c62860872 100644 --- a/tests/cli/test_generated_agent_backend_codegen_extended.py +++ b/tests/cli/test_generated_agent_backend_codegen_extended.py @@ -252,6 +252,20 @@ def test_retired_a2ui_option_is_accepted_but_not_generated() -> None: assert "[a2ui]" not in files["requirements.txt"] +@pytest.mark.parametrize("agent_type", ["llm", "sequential", "parallel", "loop"]) +def test_codegen_studio_tools_follow_root_type(agent_type: str) -> None: + draft = AgentDraft.model_validate( + { + "name": "workflow", + "agentType": agent_type, + "subAgents": [{"name": "worker", "agentType": "llm"}], + } + ) + files = _file_map(generate_project_from_draft(draft)) + expected = agent_type == "llm" + assert f'"enable_studio_tools": {expected!r}' in files["app.py"] + + def test_codegen_preserves_agent_display_names_for_topology() -> None: project = generate_project_from_draft( AgentDraft( diff --git a/tests/integrations/agentkit/test_app.py b/tests/integrations/agentkit/test_app.py index 0e9b75def..570b37b86 100644 --- a/tests/integrations/agentkit/test_app.py +++ b/tests/integrations/agentkit/test_app.py @@ -21,7 +21,9 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from google.adk.agents import Agent as AdkAgent +from google.adk.agents import LoopAgent, ParallelAgent, SequentialAgent from google.adk.agents.base_agent import BaseAgent +from google.adk.apps.app import App from google.adk.plugins.base_plugin import BasePlugin import veadk @@ -245,6 +247,46 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: assert bool(studio_toolsets) is enabled +@pytest.mark.parametrize("workflow_type", [SequentialAgent, ParallelAgent, LoopAgent]) +@pytest.mark.parametrize("use_app", [False, True]) +@pytest.mark.parametrize("enabled", [False, True]) +def test_workflow_roots_disable_studio_tools( + monkeypatch: pytest.MonkeyPatch, + workflow_type: type[BaseAgent], + use_app: bool, + enabled: bool, +) -> None: + class SessionAgentServer(_FakeAgentServer): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.session_service = object() + + monkeypatch.setattr(agentkit_app, "AgentkitAgentServerApp", SessionAgentServer) + + def existing_tool() -> str: + return "ok" + + child = AdkAgent(name="worker", tools=[existing_tool]) + root = workflow_type(name="workflow", sub_agents=[child]) + kwargs = ( + {"app": App(name="workflow_app", root_agent=root)} + if use_app + else {"root_agent": root} + ) + app = agentkit_app.create_agentkit_app(**kwargs, enable_studio_tools=enabled) + client = TestClient(app) + + assert client.get("/harness/studio-channel/v1/capabilities").json() == { + "enabled": False, + "protocol": "studio-tool-channel/1", + "transports": [], + } + paths = {getattr(route, "path", "") for route in app.routes} + assert "/harness/studio-channel/v1/http-runs" not in paths + assert child.tools == [existing_tool] + assert existing_tool() == "ok" + + @pytest.mark.parametrize("enabled", [False, True]) def test_create_agentkit_app_uses_runtime_bff_route_opt_in( monkeypatch: pytest.MonkeyPatch, diff --git a/veadk/cli/generated_agent_codegen.py b/veadk/cli/generated_agent_codegen.py index a45ef4be2..a6004ee6d 100644 --- a/veadk/cli/generated_agent_codegen.py +++ b/veadk/cli/generated_agent_codegen.py @@ -1131,6 +1131,7 @@ def _render_app_py( pkg: str, feishu_channel_enabled: bool, harness_sidecar_enabled: bool, + studio_tools_enabled: bool, ) -> str: lines = [ _PYTHON_LICENSE_HEADER.rstrip(), @@ -1155,7 +1156,7 @@ def _render_app_py( "", "_app_options = {", f' "enable_feishu": {feishu_channel_enabled!r},', - ' "enable_studio_tools": True,', + f' "enable_studio_tools": {studio_tools_enabled!r},', "}", 'if "agent_draft" in signature(create_agentkit_app).parameters:', ' _app_options["agent_draft"] = AGENT_DRAFT', @@ -2443,6 +2444,7 @@ def generate_project_from_draft(draft: AgentDraft) -> GeneratedProject: pkg, feishu_channel_enabled, harness_sidecar_enabled, + studio_tools_enabled=draft.agentType not in {"sequential", "parallel", "loop"}, ) files = [ GeneratedFile(path="app.py", content=app_py), diff --git a/veadk/integrations/agentkit/app.py b/veadk/integrations/agentkit/app.py index 766e1e790..49a33cfa1 100644 --- a/veadk/integrations/agentkit/app.py +++ b/veadk/integrations/agentkit/app.py @@ -49,6 +49,7 @@ from veadk.agent_search import search_agent_component from veadk.cli.frontend_invocation import FrontendInvocationPlugin from veadk.memory.short_term_memory import ShortTermMemory +from veadk.utils.logger import get_logger if TYPE_CHECKING: from agentkit.identity import RuntimeIdentity # pyright: ignore[reportMissingImports] @@ -56,6 +57,8 @@ from veadk.runner import Runner +logger = get_logger(__name__) + _MAX_AGENT_GRAPH_DEPTH = 8 _SERVER_STATE_KEY = "_veadk_agentkit_server" _ADK_SERVER_STATE_KEY = "_veadk_adk_server" @@ -915,6 +918,14 @@ def _configure_studio_tool_routes( mount_studio_channel_routes, ) + if enabled and isinstance(root_agent, (SequentialAgent, ParallelAgent, LoopAgent)): + logger.warning( + "Disabling Studio BFF tools for workflow root agent " + f"{root_agent.name!r} ({type(root_agent).__name__}); " + "workflow roots do not support tool calls." + ) + enabled = False + if not enabled: mount_studio_channel_routes(app=app, enabled=False) return @@ -1001,7 +1012,8 @@ def create_agentkit_app( enable_studio_tools: Whether to mount the generic Runtime host for Studio BFF-owned dynamic tools. Tool manifests and executors remain in the Studio BFF and are exposed only during explicitly selected - Studio-channel runs. + Studio-channel runs. Automatically disabled for SequentialAgent, + ParallelAgent, and LoopAgent roots. enable_studio_routes: Whether to mount the generic Runtime host for Studio BFF-owned dynamic HTTP routes. Route handlers remain in the Studio BFF and are never loaded into the Runtime process. Enabled