Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion tests/cloud/test_harness_app_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions veadk/cloud/harness_app/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
3 changes: 2 additions & 1 deletion veadk/cloud/harness_app/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down
24 changes: 23 additions & 1 deletion veadk/cloud/harness_app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import io
import os
import re
import shutil
import tempfile
import zipfile
Expand Down Expand Up @@ -66,6 +67,7 @@
"HarnessConfig",
"HarnessOverrides",
"split_csv",
"agent_name_from_harness",
"build_skill_toolset",
"SkillLoadError",
"ToolLoadError",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
Loading