Skip to content
Open
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
10 changes: 9 additions & 1 deletion src/agentex/lib/types/agent_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Any, get_args, get_origin

from pydantic import BaseModel
from pydantic import BaseModel, Field

if TYPE_CHECKING:
from agentex.lib.sdk.state_machine.state import State
Expand All @@ -31,6 +31,10 @@ class AgentCard(BaseModel):
data_events: list[str] = []
input_types: list[str] = []
output_schema: dict | None = None
# Free-form JSON object for opt-in self-description (e.g. protocol-specific
# capability flags) that callers can filter agents by via
# ``GET /agents?agent_card_metadata=...``. Not interpreted by the platform.
metadata: dict[str, Any] = Field(default_factory=dict)

@classmethod
def from_states(
Expand All @@ -40,6 +44,7 @@ def from_states(
output_event_model: type[BaseModel] | None = None,
extra_input_types: list[str] | None = None,
queries: list[str] | None = None,
metadata: dict[str, Any] | None = None,
) -> AgentCard:
"""Build an AgentCard directly from a list[State] + initial_state.

Expand Down Expand Up @@ -81,6 +86,7 @@ def from_states(
data_events=data_events,
input_types=sorted(derived_input_types | set(extra_input_types or [])),
output_schema=output_schema,
metadata=metadata or {},
)

@classmethod
Expand All @@ -90,6 +96,7 @@ def from_state_machine(
output_event_model: type[BaseModel] | None = None,
extra_input_types: list[str] | None = None,
queries: list[str] | None = None,
metadata: dict[str, Any] | None = None,
) -> AgentCard:
"""Build an AgentCard from a StateMachine instance. Delegates to from_states()."""
lifecycle = state_machine.get_lifecycle()
Expand Down Expand Up @@ -125,6 +132,7 @@ def from_state_machine(
data_events=data_events,
input_types=sorted(derived_input_types | set(extra_input_types or [])),
output_schema=output_schema,
metadata=metadata or {},
)


Expand Down
12 changes: 12 additions & 0 deletions src/agentex/resources/agents/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def retrieve(
def list(
self,
*,
agent_card_metadata: Optional[str] | Omit = omit,
limit: int | Omit = omit,
order_by: Optional[str] | Omit = omit,
order_direction: str | Omit = omit,
Expand All @@ -132,6 +133,10 @@ def list(
List all registered agents, optionally filtered by query parameters.

Args:
agent_card_metadata: JSON-encoded object filtered against
``registration_metadata.agent_card.metadata`` using exact key/value
containment semantics.

limit: Limit

order_by: Field to order by
Expand Down Expand Up @@ -159,6 +164,7 @@ def list(
timeout=timeout,
query=maybe_transform(
{
"agent_card_metadata": agent_card_metadata,
"limit": limit,
"order_by": order_by,
"order_direction": order_direction,
Expand Down Expand Up @@ -777,6 +783,7 @@ async def retrieve(
async def list(
self,
*,
agent_card_metadata: Optional[str] | Omit = omit,
limit: int | Omit = omit,
order_by: Optional[str] | Omit = omit,
order_direction: str | Omit = omit,
Expand All @@ -793,6 +800,10 @@ async def list(
List all registered agents, optionally filtered by query parameters.

Args:
agent_card_metadata: JSON-encoded object filtered against
``registration_metadata.agent_card.metadata`` using exact key/value
containment semantics.

limit: Limit

order_by: Field to order by
Expand Down Expand Up @@ -820,6 +831,7 @@ async def list(
timeout=timeout,
query=await async_maybe_transform(
{
"agent_card_metadata": agent_card_metadata,
"limit": limit,
"order_by": order_by,
"order_direction": order_direction,
Expand Down
4 changes: 4 additions & 0 deletions src/agentex/types/agent_list_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@


class AgentListParams(TypedDict, total=False):
agent_card_metadata: Optional[str]
"""JSON-encoded object filtered against ``registration_metadata.agent_card.metadata``
with exact key/value containment semantics."""

limit: int
"""Limit"""

Expand Down
2 changes: 2 additions & 0 deletions tests/api_resources/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def test_method_list(self, client: Agentex) -> None:
@parametrize
def test_method_list_with_all_params(self, client: Agentex) -> None:
agent = client.agents.list(
agent_card_metadata="agent_card_metadata",
limit=1,
order_by="order_by",
order_direction="order_direction",
Expand Down Expand Up @@ -469,6 +470,7 @@ async def test_method_list(self, async_client: AsyncAgentex) -> None:
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncAgentex) -> None:
agent = await async_client.agents.list(
agent_card_metadata="agent_card_metadata",
limit=1,
order_by="order_by",
order_direction="order_direction",
Expand Down
58 changes: 58 additions & 0 deletions tests/lib/test_agent_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,42 @@ def test_defaults(self):
assert card.data_events == []
assert card.input_types == []
assert card.output_schema is None
assert card.metadata == {}

def test_serialization_roundtrip(self):
card = AgentCard(input_types=["text"], data_events=["result"])
dumped = card.model_dump()
restored = AgentCard.model_validate(dumped)
assert restored == card

def test_metadata_accepts_arbitrary_json_object(self):
card = AgentCard(
metadata={
"permits_capable": True,
"supported_workflows": ["submit", "review"],
"limits": {"max_batch": 5},
}
)
assert card.metadata == {
"permits_capable": True,
"supported_workflows": ["submit", "review"],
"limits": {"max_batch": 5},
}

def test_metadata_serialization_roundtrip(self):
card = AgentCard(metadata={"permits_capable": True})
dumped = card.model_dump()
assert dumped["metadata"] == {"permits_capable": True}
restored = AgentCard.model_validate(dumped)
assert restored == card

def test_metadata_default_instances_are_independent(self):
"""Each default metadata is its own dict, not a shared class-level object."""
card_a = AgentCard()
card_b = AgentCard()
card_a.metadata["mutated"] = True
assert card_b.metadata == {}


# --- AgentCard.from_states ---

Expand Down Expand Up @@ -247,6 +276,14 @@ def test_state_fields(self, sample_states):
assert waiting.accepts == ["text", "doc_upload"]
assert waiting.transitions == ["processing"]

def test_metadata_forwarded(self, sample_states):
card = AgentCard.from_states(
initial_state=SampleState.WAITING,
states=sample_states,
metadata={"permits_capable": True},
)
assert card.metadata == {"permits_capable": True}

def test_matches_from_state_machine(self, sample_states, sample_sm):
"""from_states and from_state_machine should produce identical cards."""
card_states = AgentCard.from_states(
Expand Down Expand Up @@ -315,6 +352,13 @@ def test_no_output_model(self, sample_sm):
assert card.data_events == []
assert card.output_schema is None

def test_metadata_forwarded(self, sample_sm):
card = AgentCard.from_state_machine(
state_machine=sample_sm,
metadata={"permits_capable": True},
)
assert card.metadata == {"permits_capable": True}


# --- register_agent agent_card merging ---

Expand Down Expand Up @@ -370,6 +414,20 @@ async def test_agent_card_merged_into_metadata(self, mock_env_vars):
assert metadata["agent_card"]["input_types"] == ["text"]
assert metadata["agent_card"]["data_events"] == ["result"]

async def test_agent_card_metadata_propagates_through_registration(self, mock_env_vars):
card = AgentCard(metadata={"permits_capable": True})
mock_client = self._make_mock_client()

with patch("agentex.lib.utils.registration.httpx.AsyncClient", return_value=mock_client):
from agentex.lib.utils.registration import register_agent

await register_agent(mock_env_vars, agent_card=card)

sent_data = mock_client.post.call_args.kwargs["json"]
metadata = sent_data["registration_metadata"]

assert metadata["agent_card"]["metadata"] == {"permits_capable": True}

async def test_none_preserved_when_no_card(self, mock_env_vars):
mock_client = self._make_mock_client()

Expand Down
Loading