Skip to content

/apps/{app_name}/app-info returns 500 when an agent's instruction is an InstructionProvider (callable) instead of a str #6909

Description

@itaieban

🔴 Required Information

Describe the Bug:
GET /apps/{app_name}/app-info returns 500 Internal Server Error for any app whose root or sub-agent defines instruction as an InstructionProvider (a callable) instead of a plain string. AgentInfo in google/adk/utils/agent_info.py types the field as instruction: str and assigns current_agent.instruction verbatim, so building the response raises an unhandled pydantic.ValidationError. Callable instructions are a documented, first-class feature (Callable[[ReadonlyContext], str | Awaitable[str]]) and the agent runs fine at runtime — only the app-info introspection endpoint crashes.

Steps to Reproduce:

  1. Create an agent whose instruction is a callable:
    # agents/dyn/agent.py
    from google.adk.agents import LlmAgent
    from google.adk.agents.readonly_context import ReadonlyContext
    
    def dynamic_instruction(ctx: ReadonlyContext) -> str:
        return "You are a helpful assistant."
    
    root_agent = LlmAgent(
        name="dyn",
        model="gemini-3.7-flash",          # any model; never reached
        description="Agent with a dynamic (callable) instruction.",
        instruction=dynamic_instruction,   # callable, not a str
    )
  2. Run adk web agents
  3. Request curl http://localhost:8000/apps/dyn/app-info
  4. Receive 500 Internal Server Error (stack trace below)

Expected Behavior:
app-info should succeed for agents with callable instructions — returning a serialized AgentInfo where instruction is coerced/resolved to a string (or a readable placeholder) rather than raising.

Observed Behavior:
500, with:

File ".../google/adk/cli/api_server.py", line 1416, in get_adk_app_info
    agents=await get_agents_dict(root_agent),
File ".../google/adk/utils/agent_info.py", line 71, in _traverse
    agents_dict[current_agent.name] = AgentInfo(
File ".../pydantic/main.py", line 263, in __init__
    validated_self = self.__pydantic_validator__.validate_python(...)
pydantic_core._pydantic_core.ValidationError: 1 validation error for AgentInfo
instruction
  Input should be a valid string [type=string_type,
   input_value=<function dynamic_instruction at 0x...>, input_type=function]

Environment Details:

  • ADK Library Version (pip show google-adk): 2.7.1
  • Desktop OS: Linux (Ubuntu, Docker container)
  • Python Version (python -V): 3.11

Model Information:

  • Are you using LiteLLM: N/A — model-independent; the model is never reached (validation fails while building app-info, before any inference). Reproduces regardless of provider.
  • Which model is being used: N/A (any; e.g. gemini-3.7-flash)

🟡 Optional Information

Regression:
Not a regression — broken since the app-info endpoint was introduced (commit da438fafd, 2026-04-06). AgentInfo.instruction has been typed str from that first commit and is still str on main, so no released version handles callable instructions here.

Logs:

pydantic_core._pydantic_core.ValidationError: 1 validation error for AgentInfo
instruction
  Input should be a valid string [type=string_type,
   input_value=<function dynamic_instruction at 0x...>, input_type=function]
    For further information visit https://errors.pydantic.dev/2.13/v/string_type

Screenshots / Video:
N/A

Additional Context:
Root cause in google/adk/utils/agent_info.py:

class AgentInfo(pydantic.BaseModel):
    ...
    instruction: str          # LlmAgent.instruction is Union[str, InstructionProvider]

# get_agents_dict._traverse():
agents_dict[current_agent.name] = AgentInfo(
    ...
    instruction=current_agent.instruction,   # may be a callable -> ValidationError
)

Suggested fix — coerce a callable before validation:

@pydantic.field_validator("instruction", mode="before")
@classmethod
def _coerce_callable_instruction(cls, v):
    if callable(v):
        return f"<InstructionProvider: {getattr(v, '__name__', repr(v))}>"
    return v

(Alternatively, resolve the provider when a context is available and fall back to a placeholder — note an InstructionProvider may be async and may require session state, so eager resolution isn't always safe here.)

Distinct from the invalid-app_name 500 fixed by #5374 / #5376 / #6376 — this is a valid app whose callable instruction fails validation. Separately, get_agents_dict only traverses isinstance(sub_agent, LlmAgent), so non-LlmAgent sub-agents (e.g. RemoteA2aAgent) are silently omitted from agents/sub_agents — expected, but worth noting for anyone using app-info as an inventory.

Minimal Reproduction Code:

from google.adk.agents import LlmAgent
from google.adk.agents.readonly_context import ReadonlyContext

def dynamic_instruction(ctx: ReadonlyContext) -> str:
    return "You are a helpful assistant."

root_agent = LlmAgent(
    name="dyn",
    model="gemini-3.7-flash",
    description="Agent with a dynamic (callable) instruction.",
    instruction=dynamic_instruction,
)
# adk web agents  ->  GET /apps/dyn/app-info  ->  500

How often has this issue occurred?:

  • Always (100%) — deterministic for any callable instruction.

Metadata

Metadata

Labels

agent config[Component] This issue is related to the Agent Config interface and implementationrequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions