diff --git a/tests/cloud/test_harness_app_contract.py b/tests/cloud/test_harness_app_contract.py index 6719c5cd..7d70109f 100644 --- a/tests/cloud/test_harness_app_contract.py +++ b/tests/cloud/test_harness_app_contract.py @@ -34,7 +34,11 @@ RunAgentRequest, ) from veadk.cloud.harness_app.env_mapping import to_runtime_env -from veadk.cloud.harness_app.utils import config_from_env, split_csv +from veadk.cloud.harness_app.utils import ( + agent_name_from_harness, + config_from_env, + split_csv, +) from veadk.consts import DEFAULT_MODEL_AGENT_NAME from veadk.prompts.agent_default_prompt import DEFAULT_INSTRUCTION @@ -91,6 +95,7 @@ def test_extends_overrides(self): def test_adds_creation_time_fields(self): assert set(_fields(HarnessConfig)) == set(_fields(HarnessOverrides)) | { "app_name", + "description", "knowledgebase_type", "longterm_memory_type", "shortterm_memory_type", @@ -232,3 +237,26 @@ def test_empty_string_is_empty_list(self): def test_drops_blank_segments(self): assert split_csv("a,, ,b") == ["a", "b"] + + +class TestAgentNameFromHarness: + """The agent name (and thus the A2A card name) is derived from the harness + name, normalized to a valid ADK identifier.""" + + def test_identifier_passes_through(self): + assert agent_name_from_harness("harness_app") == "harness_app" + + def test_hyphens_become_underscores(self): + assert agent_name_from_harness("oauth-test") == "oauth_test" + + def test_leading_digit_is_prefixed(self): + assert agent_name_from_harness("2048-bot") == "_2048_bot" + + def test_reserved_user_is_escaped(self): + assert agent_name_from_harness("user") == "user_" + + def test_result_is_always_a_valid_identifier(self): + for raw in ["oauth-test", "2048-bot", "user", "a.b c", ""]: + name = agent_name_from_harness(raw) + assert name.isidentifier(), raw + assert name != "user" diff --git a/veadk/cloud/harness_app/agent.py b/veadk/cloud/harness_app/agent.py index 7d4ed9f7..669976de 100644 --- a/veadk/cloud/harness_app/agent.py +++ b/veadk/cloud/harness_app/agent.py @@ -22,6 +22,7 @@ Environment variables: MODEL_NAME Reasoning model name. Default: VeADK default model. SYSTEM_PROMPT Agent instruction. Default: VeADK default instruction. + DESCRIPTION Agent description (e.g. for A2A discovery). Default: VeADK default description. TOOLS Comma-separated built-in tool names, e.g. "web_search,link_reader". SKILLS Comma-separated skill names, e.g. "data-visualization-cloud,...". RUNTIME Agent runtime backend: "adk" (default) or "codex". diff --git a/veadk/cloud/harness_app/types.py b/veadk/cloud/harness_app/types.py index a310aec5..dd36cec0 100644 --- a/veadk/cloud/harness_app/types.py +++ b/veadk/cloud/harness_app/types.py @@ -30,7 +30,7 @@ from pydantic import BaseModel, Field from veadk.consts import DEFAULT_MODEL_AGENT_NAME -from veadk.prompts.agent_default_prompt import DEFAULT_INSTRUCTION +from veadk.prompts.agent_default_prompt import DEFAULT_DESCRIPTION, DEFAULT_INSTRUCTION class HarnessOverrides(BaseModel): @@ -82,6 +82,7 @@ class HarnessConfig(HarnessOverrides): app_name: str = Field(default="harness_app", alias="name") system_prompt: str = Field(default=DEFAULT_INSTRUCTION) + description: str = Field(default=DEFAULT_DESCRIPTION) knowledgebase_type: str = Field(default="") longterm_memory_type: str = Field(default="") shortterm_memory_type: str = Field(default="local") diff --git a/veadk/cloud/harness_app/utils.py b/veadk/cloud/harness_app/utils.py index e4674e30..7fb42e5e 100644 --- a/veadk/cloud/harness_app/utils.py +++ b/veadk/cloud/harness_app/utils.py @@ -27,6 +27,7 @@ import io import os +import re import shutil import tempfile import zipfile @@ -66,6 +67,7 @@ "HarnessConfig", "HarnessOverrides", "split_csv", + "agent_name_from_harness", "build_skill_toolset", "SkillLoadError", "ToolLoadError", @@ -111,6 +113,7 @@ def _load_builtin_tool(name: str) -> Any: "tools": "TOOLS", "skills": "SKILLS", "system_prompt": "SYSTEM_PROMPT", + "description": "DESCRIPTION", "runtime": "RUNTIME", "structured_tool_calls": "STRUCTURED_TOOL_CALLS", "include_tools_every_turn": "INCLUDE_TOOLS_EVERY_TURN", @@ -139,6 +142,24 @@ def split_csv(value: str) -> list[str]: return [item.strip() for item in value.split(",") if item.strip()] +def agent_name_from_harness(harness_name: str) -> str: + """Derive a valid ADK agent name from the harness name. + + The agent name becomes the A2A agent card's ``name``, so it should reflect + the harness rather than a shared constant. ADK requires the agent ``name`` + to be a Python identifier (letters, digits, underscores; not starting with a + digit) and forbids ``"user"``, while harness names also allow ``-`` and may + start with a digit. Normalize: map every non-identifier char to ``_`` and + prefix a digit-leading or empty name with ``_``. + + ``"oauth-test"`` -> ``"oauth_test"``; ``"2048-bot"`` -> ``"_2048_bot"``. + """ + name = re.sub(r"[^0-9A-Za-z_]", "_", harness_name or "") + if not name or name[0].isdigit(): + name = f"_{name}" + return f"{name}_" if name == "user" else name + + def _download_and_extract_skill(skill: str, dest_dir: Path) -> Path: """Download a skill zip from the skill hub and extract it. @@ -315,9 +336,10 @@ def _assemble_agent(config: HarnessConfig) -> tuple[Agent, ShortTermMemory]: ) agent = Agent( - name="harness_agent", + name=agent_name_from_harness(config.app_name), model_name=config.model_name, instruction=config.system_prompt, + description=config.description, tools=tools, runtime=config.runtime, enable_responses=config.structured_tool_calls,