Repository navigation
feat(agent-card): add metadata field and expose list filter - #502
Conversation
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.
|
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.
|
Addressed in 6a80ede.
worker = AgentexWorker(task_queue=task_queue, agent_card=agent_card)Tests added (
|
6a80ede to
d307717
Compare
jromualdez-scale
left a comment
There was a problem hiding this comment.
Approving. The default_factory on AgentCard.metadata, keeping the generated files byte-identical to the Stainless preview and putting the ergonomics in lib/utils, and the worker agent_card passthrough are all the right calls. The respx tests and the worker-vs-FastACP payload equality test are the tests that actually prove the wire behavior, since the api_resources ones are skip-guarded. One non-blocking comment inline.
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.
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.
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p for the card filter Adds the minimum-server note to metadata_filters (older servers ignore the unknown query parameter and return the full agent list) and syncs the generated agent_card_metadata docstrings with the updated OpenAPI description from scaleapi/scale-agentex#411. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
402d447 to
08c96f7
Compare
🏆 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.The PR appears safe to merge.
Summary
Diagram
sequenceDiagram participant App participant Card as AgentCard participant Worker as AgentexWorker participant Backend as AgentEx backend participant Client as agents.list client App->>Card: Build with metadata App->>Worker: Start with agent_card Worker->>Backend: Register agent with card metadata App->>Client: Encode metadata filter Client->>Backend: "GET /agents?agent_card_metadata=JSON" Backend-->>Client: Return matching agentsReviews (6) · Last reviewed commit: "docs(sdk): document server version requi..."