feat(agent-card): add metadata field and expose list filter - #502
feat(agent-card): add metadata field and expose list filter#502declan-scale wants to merge 3 commits into
Conversation
Adds an optional `metadata: dict[str, Any]` field to the SDK's `AgentCard` model (defaulting to an empty dict) and threads the value through `AgentCard.from_states` / `AgentCard.from_state_machine` so callers can attach opt-in capability flags without subclassing. Also plumbs the paired platform `agent_card_metadata` list filter through the Stainless-generated `agents.list` surface so consumers can enumerate agents whose card metadata contains a given JSON object with exact key/value semantics. The card continues to serialize through the existing `registration_metadata.agent_card` path — no wire-shape or database migration is required.
basselatscale
left a comment
There was a problem hiding this comment.
The AgentCard.metadata addition and registration propagation look good and provide the missing agent-authored descriptor path.
The list-filter SDK surface needs one change for the intended consumer contract. It is currently typed as Optional[str], which requires callers to know the wire encoding and manually json.dumps the metadata.
Please expose a mapping/JSON-object input and serialize it internally, so both sync and async clients support:
client.agents.list(
agent_card_metadata={"permits_capable": True},
)
Please also add an exercised request-level test for both clients that asserts the mapping becomes a JSON-encoded query parameter. The current generated resource tests pass a sentinel string and are skip-guarded, so they do not prove this behavior.
There is also a currently failing Ruff import-order check in agent_card.py that needs to be fixed.
With that SDK change, these paired PRs provide enough plumbing for the permits harness to discover registered descriptors and remove input-contracts.generated.yaml.
Review follow-ups on the agents.list card metadata filter.
The generated `agent_card_metadata` parameter stays `Optional[str]`. The
platform spec declares it the same way the already-shipped
`tasks.list(task_metadata=...)` filter is declared -- a nullable string
carrying a JSON-encoded object -- because the generator does not honour a
`content: application/json` query parameter and falls back to `type: string`
regardless. Widening the parameter here would be an edit the next
regeneration silently reverts, so the ergonomics live in the hand-written
layer instead: `agentex.lib.utils.metadata_filters.encode_metadata_filter`
turns a mapping into the exact wire value, with key order pinned so the same
filter always produces the same query string, and rejects NaN/Infinity up
front rather than letting the server return a 400.
The generated docstrings are updated to match the parameter description in
the spec, so a regeneration is now a no-op on these files.
Adds exercised request-level tests (respx, sync and async) asserting the
encoded mapping actually reaches the query string, and that an empty-object
filter is sent verbatim rather than dropped as falsy -- `{}` is meaningful
server-side. These use their own clients rather than the skip-guarded
mock-server fixtures, so they run in CI.
Also fixes the failing ruff import-order check in agent_card.py.
|
Addressed in 582600d. Ruff import-order in The mapping-typed I checked what the generator actually produces from the platform PR's spec rather than assuming it worked: # stainless-sdks/agentex-sdk-python @ preview/.../agx1-1048-agent-card-metadata-filter
class AgentListParams(TypedDict, total=False):
agent_card_metadata: strThe generator ignores Note also the API already has this exact pattern shipped: So the generated parameter stays from agentex.lib.utils.metadata_filters import encode_metadata_filter
client.agents.list(
agent_card_metadata=encode_metadata_filter({"permits_capable": True}),
)
I also updated the generated docstrings to match the spec's parameter description verbatim, so a regeneration is now a no-op on Exercised request-level tests — added.
50 passed across If you'd still rather have |
|
Re: "Generated filter is unreproducible" ( The platform PR's spec change has regenerated the Stainless preview, and both touched generated files in this PR are byte-identical to what the generator produces: So a regeneration is a no-op here — no parameter gets dropped, no recurring conflict. The finding was correct about the original revision: it hand-edited generated files to a shape the spec did not produce. That's fixed by making the spec produce this shape rather than by hand-editing, and the docstrings were realigned to the spec's parameter description so even the prose matches. The mapping ergonomics live in Related: the platform PR's Stainless build also went from |
basselatscale
left a comment
There was a problem hiding this comment.
The AgentCard model and registration serialization are suitable, but our real Temporal deployment path exposes one remaining SDK gap.
FastACP.create(..., agent_card=card) automatically publishes the card during ACP startup, while AgentexWorker always calls register_agent(env_vars) without a card. Consumers using the standard worker therefore have to subclass a protected _register_agent() method or perform a second registration solely to publish identical metadata.
Please add an optional AgentCard constructor parameter to AgentexWorker, for example:
worker = AgentexWorker(
task_queue=task_queue,
agent_card=agent_card,
)Store it on the worker and change the existing automatic path to:
await register_agent(env_vars, agent_card=self.agent_card)The default must remain None, preserving existing callers and wire behavior. Please add focused tests proving:
- the default worker registration still calls
register_agentwithout AgentCard metadata; - a supplied card is forwarded exactly once by the existing automatic registration lifecycle; and
- the worker and FastACP paths serialize the same card shape.
This would let Permits delete its _AgentCardPublishingWorker protected-method override and the extra idempotent registration call. Descriptor construction remains Permits-owned; this request only makes publication consistent with the SDK's existing automatic registration model.
Add an optional agent_card parameter to the AgentexWorker constructor. The worker stores the card and forwards it through the existing automatic register_agent call, so a Temporal worker can publish a card without a subclass override or a second registration. The default stays None and the wire behavior for existing callers does not change. Add tests that prove: - the default worker registers without card metadata; - a supplied card reaches register_agent exactly once through run(); - the worker path and the FastACP lifespan path serialize the same card shape into registration_metadata.agent_card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed in 6a80ede.
worker = AgentexWorker(task_queue=task_queue, agent_card=agent_card)Tests added (
|
🏆 Brought to you by the Golden Agent (Try it out)
Problem
The SDK's
AgentCardhas no free-form metadata field, so agents that want to advertise opt-in protocol capabilities (e.g. Permits' workflow submission protocol) have no place to attach that self-description data. The generatedagents.listclient also lacks the paired containment filter so consumers can't enumerate agents that opted in.Linear: AGX1-1048
Change
metadata: dict[str, Any] = Field(default_factory=dict)toAgentCardinsrc/agentex/lib/types/agent_card.py. Default factory keeps each instance's dict independent (no shared mutable default).metadata=kwarg throughAgentCard.from_statesandAgentCard.from_state_machineso callers building a card from a state machine can attach metadata inline.registration_metadata.agent_cardpath (agentex.lib.utils.registration.register_agent) — no wire-shape change required.agent_card_metadata: Optional[str]param on both sync and asyncagents.list(a JSON-encoded object matched with exact key/value containment on the server) and adds it toAgentListParams. The platform PR wires the server-side JSONB@>filter.Test coverage added
tests/lib/test_agent_card.pymetadatais an empty dict and instances don't share the default.metadataaccepts arbitrary nested JSON and round-trips throughmodel_dump/model_validate.from_statesandfrom_state_machineforward the value.register_agentpropagatesmetadatainsideregistration_metadata.agent_card.metadataon the outgoing HTTP request.tests/api_resources/test_agents.py: extended the `with_all_params` list tests (sync + async) to pass `agent_card_metadata`. (Still `@pytest.mark.skip`-guarded like the rest of the mock-server tests.)Test plan (for reviewer)
Notes
Fieldis imported from the pydantic already vendored by the SDK.Greptile Summary
The PR adds free-form AgentCard metadata, propagates it through Temporal worker registration, and exposes metadata containment filtering on synchronous and asynchronous agent-list APIs.
Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR Card[AgentCard metadata] --> Worker[Temporal worker] Card --> ACP[FastACP server] Worker --> Registration[Agent registration] ACP --> Registration Registration --> Stored[registration_metadata.agent_card.metadata] Filter[agents.list agent_card_metadata] --> API[Agents API] API --> StoredReviews (3): Last reviewed commit: "feat(worker): let AgentexWorker publish ..." | Re-trigger Greptile
Context used (3)