Skip to content

feat: add AgentCard for self-describing agent capabilities#296

Open
prassanna-ravishankar wants to merge 6 commits intomainfrom
feature/agent-card
Open

feat: add AgentCard for self-describing agent capabilities#296
prassanna-ravishankar wants to merge 6 commits intomainfrom
feature/agent-card

Conversation

@prassanna-ravishankar
Copy link
Copy Markdown
Member

@prassanna-ravishankar prassanna-ravishankar commented Mar 28, 2026

Summary

  • Adds AgentCard model to agentex-sdk, enabling agents to describe their capabilities, lifecycle, and communication contract during registration
  • Card is derived from existing code (StateWorkflow annotations + Pydantic models), not manually maintained config
  • Flows through existing registration_metadata JSONB field — no server changes required

What changed

  • New lib/types/agent_card.pyAgentCard, AgentLifecycle, LifecycleState models with from_state_machine() constructor and extract_literal_values() utility
  • StateWorkflow — Added optional description, waits_for_input, accepts, transitions class attrs (all default to empty, no impact on existing agents)
  • StateMachine — Added get_lifecycle() method to introspect registered workflows
  • FastACP.create() — Accepts optional agent_card param
  • register_agent() — Merges agent card into registration_metadata["agent_card"]

Backward compatibility

  • All new fields are optional with safe defaults
  • Verified 14 existing StateWorkflow subclasses across enterprise-emu, sgp-solutions, and tutorials — no conflicts
  • registration_metadata preserves None when no agent_card or build info is present (existing behavior unchanged)
  • Class hierarchy verified: SyncACP, AsyncBaseACP, TemporalACP all inherit _agent_card from BaseACPServer

Usage

Stateful agent:

from agentex.lib.sdk.state_machine import AgentCard

card = AgentCard.from_state_machine(
    state_machine=my_state_machine,
    output_event_model=MyOutputEventPayload,
    queries=["get_current_state"],
)
acp = FastACP.create(acp_type="agentic", agent_card=card, config=...)

Simple agent:

acp = FastACP.create(
    acp_type="sync",
    agent_card=AgentCard(input_types=["text"], data_events=["result"]),
)

Motivation

See design doc: agent-to-agent communication (OneEdge orchestrator ↔ specialized agents) requires a machine-readable contract per agent. Currently encoded as prose in LLM system prompts. Also enables TypeScript type codegen for frontend apps via output_schema (JSON Schema from Pydantic models).

🤖 Generated with Claude Code

Greptile Summary

Adds AgentCard — a Pydantic model that lets agents describe their capabilities, lifecycle states, and communication contract during registration. The card is derived from existing StateWorkflow annotations and Pydantic models (not manually maintained config), and flows through the existing registration_metadata JSONB field with no server-side changes required.

  • New AgentCard, AgentLifecycle, LifecycleState models in agent_card.py with two constructors: from_states() (from a raw states list) and from_state_machine() (from a StateMachine instance)
  • StateWorkflow gains optional class-level attributes (description, waits_for_input, accepts, transitions) with safe defaults
  • StateMachine.get_lifecycle() introspects registered workflows for card construction
  • FastACP.create() accepts optional agent_card param, threaded through to register_agent() which merges it into registration_metadata
  • All changes are backward-compatible — new fields are optional with safe defaults
  • Comprehensive test suite (396 lines) covering all new functionality

Confidence Score: 4/5

This PR is safe to merge — all new fields are optional with defaults, backward compatibility is preserved, and tests are thorough.

Well-structured additive feature with no breaking changes. All new attributes are optional. Comprehensive test coverage. One minor style concern about mutable class-level defaults in StateWorkflow (safe today but structurally fragile). Code duplication between from_states and from_state_machine is a minor concern but not a blocker.

src/agentex/lib/sdk/state_machine/state_workflow.py has mutable class-level list defaults that are safe today but could become a shared-state bug if future code mutates them in-place.

Important Files Changed

Filename Overview
src/agentex/lib/types/agent_card.py New file: AgentCard, AgentLifecycle, LifecycleState Pydantic models with two constructors (from_states, from_state_machine) and extract_literal_values utility. Well-structured with proper PEP 604 union handling. Some code duplication between from_states and from_state_machine.
src/agentex/lib/sdk/state_machine/state_workflow.py Adds optional class-level attributes (description, waits_for_input, accepts, transitions) with defaults. Mutable list defaults are a latent concern but safe with current usage patterns.
src/agentex/lib/sdk/state_machine/state_machine.py Adds get_lifecycle() method that introspects registered workflows and serializes state/transition info. Correctly resolves Enum values to strings.
src/agentex/lib/sdk/fastacp/fastacp.py Factory method now accepts optional agent_card, assigns it to the instance after creation. Also adds an explicit else-raise for unknown acp_type (good improvement).
src/agentex/lib/sdk/fastacp/base/base_acp_server.py Adds _agent_card attribute and passes it through to register_agent during lifespan. Minimal, clean change.
src/agentex/lib/utils/registration.py Merges agent_card into registration_metadata, correctly handling None build_info. Preserves None when neither card nor build info is present.
src/agentex/lib/sdk/state_machine/init.py Re-exports AgentCard, AgentLifecycle, LifecycleState from types module for convenient import access.
tests/lib/test_agent_card.py Comprehensive test suite covering extract_literal_values, StateWorkflow defaults, get_lifecycle, AgentCard construction, from_states, from_state_machine, and registration merging. Good coverage with 396 lines.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["FastACP.create(agent_card=card)"] --> B["Instance created<br/>(SyncACP / AsyncBaseACP / TemporalACP)"]
    B --> C["instance._agent_card = card"]
    C --> D["Lifespan startup"]
    D --> E["register_agent(env_vars, agent_card)"]
    E --> F{"agent_card is not None?"}
    F -- Yes --> G["card_data = agent_card.model_dump()"]
    G --> H["registration_metadata['agent_card'] = card_data"]
    F -- No --> I["registration_metadata = get_build_info()"]
    H --> J["POST /agents/register"]
    I --> J

    subgraph "AgentCard Construction"
        K["StateWorkflow subclasses<br/>(description, accepts, transitions)"] --> L["StateMachine.get_lifecycle()"]
        L --> M["AgentCard.from_state_machine()"]
        K --> N["State list + initial_state"]
        N --> O["AgentCard.from_states()"]
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agentex/lib/sdk/state_machine/state_workflow.py
Line: 16-17

Comment:
**Mutable class-level list defaults are shared across subclasses**

`accepts` and `transitions` are mutable `list` objects defined at the class level. Any `StateWorkflow` subclass that does **not** override these attributes will share the exact same list object from the base class. If any code mutates these lists in-place at runtime (e.g., `workflow.accepts.append("video")`), it will silently corrupt all subclasses that inherit the default.

Today the code is safe because: (1) subclasses that care always override with a literal list, (2) `get_lifecycle` and `from_states` defensively copy with `list(...)`, and (3) no code mutates these in-place. However, using immutable defaults like tuples would make this structurally safe and prevent a future contributor from accidentally introducing a shared-state bug.

```suggestion
    accepts: tuple[str, ...] | list[str] = ()
    transitions: tuple[str, ...] | list[str] = ()
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "Add AgentCard.from_states() classmethod ..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant