Skip to content

Adding agents

This guide covers how to create a new specialist agent, register it with LiteLLM, and wire it into Docker Compose.

Creating a new agent

1. Subclass PrometheusAgent

Every agent extends the PrometheusAgent base class from prometheus_api:

# agent.py
from prometheus_api.agent import PrometheusAgent

class MyAnalysisAgent(PrometheusAgent):
    async def process_analysis(self, params, context, event_queue):
        """Main entry point for agent logic.

        Args:
            params: Dict extracted from the A2A request message
            context: RequestContext with task_id, context_id, etc.
            event_queue: EventQueue for status updates and artifacts

        Returns:
            Dict with analysis results (auto-wrapped as JSON artifact)
        """
        material_id = params.get("material_id")
        temperature = params.get("temperature")

        # Send progress update
        await self.send_status_update(
            context, event_queue,
            state="working",
            message=f"Analyzing {material_id} at {temperature}C...",
            final=False,
        )

        # Your analysis logic
        result = run_analysis(material_id, temperature)

        return {"status": "success", "analysis": result}

The base class handles: - HTTP server setup with Bearer token auth - Agent card loading and serving at /.well-known/agent.json - LiteLLM agent registration on startup - A2A JSON-RPC message parsing - Task lifecycle (working/completed/failed status) - Automatic JSON artifact emission for the return value

2. Create agent_card.json

The agent card defines the agent's identity, skills, and capabilities:

{
  "name": "My Analysis Agent",
  "description": "Performs custom analysis for nuclear materials",
  "version": "1.0.0",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false,
    "stateTransitionHistory": false
  },
  "skills": [
    {
      "id": "analyze_custom",
      "name": "Custom Analysis",
      "description": "Runs custom analysis on a given material under specified conditions",
      "tags": ["analysis", "materials"],
      "inputSchema": {
        "type": "object",
        "properties": {
          "material_id": {
            "type": "string",
            "description": "Material identifier (e.g., SS316, Inconel625)"
          },
          "temperature": {
            "type": "number",
            "description": "Temperature in Celsius"
          },
          "duration_hours": {
            "type": "number",
            "description": "Exposure duration in hours"
          }
        },
        "required": ["material_id", "temperature"]
      },
      "expected_duration_seconds": 30,
      "requires_approval": false
    }
  ],
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "authentication": {
    "schemes": ["bearer"]
  }
}

Key fields for discovery and routing: - skills[].id: Used by the client-agent to invoke specific skills via skillId in A2A context - skills[].inputSchema: The client-agent uses this to determine what parameters to collect from the user - skills[].tags: Used by the client-agent when matching capabilities to a request - skills[].requires_approval: If true, the client-agent asks the user before invoking

Do not put a url field in the card. PrometheusAgent sets the registered URL at startup: EXTERNAL_AGENT_URL if set, else http://{container hostname}:{AGENT_PORT}. Always set EXTERNAL_AGENT_URL to http://<service-name>:8000 in the agent's docker-compose.yml. The hostname fallback only works under Docker Compose, where the hostname happens to be the service name; on Kubernetes the hostname is the pod name, which other pods can't resolve and which changes on every restart. make test-static fails if an agent doesn't set it.

3. Create environment and dependencies

.env.example:

AGENT_PORT=8000
AGENT_HOST=localhost
AGENT_SECRET=your-shared-secret
AGENT_SECRET_REQUIRED=true
REGISTRY_URL=http://localhost:4000
LITELLM_MASTER_KEY=your-litellm-master-key
LLM_CONFIG_PROFILE=openai
LLM_CONFIG_FILE=
LLM_API_KEY=your-openai-key
REDIS_URL=redis://localhost:6379

Use registry, inl_hpc, inl_litellm, amsc, anthropic, openai, or anl_asksage for LLM_CONFIG_PROFILE; every profile reads its provider key from LLM_API_KEY. If LLM_CONFIG_FILE is set, its path must exist inside the agent container and it overrides the installed profile. Export the provider keys referenced by the selected configuration.

requirements.txt:

prometheus-api[agent]
pydantic>=2.0
httpx
uvicorn

4. Run the agent

# Local
uv run agent.py

# Docker
docker build -t my-analysis-agent .
docker run -p 8000:8000 --env-file .env my-analysis-agent

The agent starts a uvicorn server and registers with LiteLLM (if REGISTRY_URL is set). Keep AGENT_SECRET_REQUIRED=true for local runs as well.

5. Add to Docker Compose

Add a service block to docker-compose.yml:

my-analysis-agent:
  build:
    context: ./agents/my-analysis-agent
    additional_contexts:
      prometheus_api: ./prometheus_api
  container_name: my-analysis-agent
  ports:
    - "8010:8000"
  networks:
    - agent-network
  environment:
    - AGENT_HOST=my-analysis-agent
    - AGENT_PORT=8000
    - EXTERNAL_AGENT_URL=http://my-analysis-agent:8000
    - REGISTRY_URL=${REGISTRY_URL:-http://litellm:4000}
    - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
    - REDIS_URL=${REDIS_URL}
    - LLM_CONFIG_PROFILE=${LLM_CONFIG_PROFILE:-registry}
    - LLM_CONFIG_FILE=${LLM_CONFIG_FILE:-}
    - LLM_API_KEY=${LLM_API_KEY:-}
    - EMBEDDING_API_KEY=${EMBEDDING_API_KEY:-}
    - AGENT_SECRET=${AGENT_SECRET}
    - AGENT_SECRET_REQUIRED=true
  depends_on:
    litellm:
      condition: service_healthy
  volumes:
    - ./shared:/app/shared
  healthcheck:
    test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"]
    interval: 15s
    timeout: 10s
    retries: 5
    start_period: 20s
  restart: unless-stopped

The healthcheck is required. make test-static fails without it in any per-agent agents/<name>/docker-compose.yml, and make up waits on it to know that the agent is serving. It probes the base class's unauthenticated health endpoint on the container port with Python, since the image has no curl. It does not show that LiteLLM registration succeeded; make up then runs make check, which does.

Once the container starts, it registers with LiteLLM. The client-agent discovers it automatically through LiteLLM's agent directory and loads the full agent card from the specialist's well-known endpoint when needed. The agent selects semantic model flavors such as medium in code; its selected YAML maps those flavors to provider model IDs.

Workflow templates

For multi-step workflows that chain agents in a defined sequence, use the workflow library. See Workflow Templates for the complete schema, platform-owned and agent-owned publication paths, binding rules, and compiler behavior. Platform-owned workflows can still live in agents/client-agent/workflows/*.yaml:

# workflows/my_workflow.yml
id: material_assessment
name: Material Assessment
version: "1.0.0"
description: Full material assessment workflow
matching:
  patterns:
    - "material assessment for {material_id}"
inputs:
  required:
    - material_id
    - context.temperature_c
execution:
  mode: sequential
  on_error: fail_fast

steps:
  - id: data_retrieval
    skill_id: retrieve_material_data
    params:
      material_id:
        from: input.material_id

  - id: corrosion_check
    skill_id: analyze_corrosion
    depends_on: [data_retrieval]
    params:
      material_id:
        from: input.material_id
      material_data:
        from: step.data_retrieval.result
      context:
        temperature_c:
          from: input.context.temperature_c

  - id: synthesis
    skill_id: synthesize_results
    depends_on: [corrosion_check]
    params:
      analyses:
        from: step.corrosion_check.result

Workflow files are loaded by the client-agent's workflow library module. Parameters use from: input.* for user-provided values and from: step.<id>.* for outputs from previous steps.

Agents can also contribute workflow templates directly from their agent_card.json by adding a top-level workflows array. The client-agent reads these cards during discovery, compiles the definitions against the discovered skill schemas, and merges them into the same workflow catalog:

{
  "name": "Thermal Agent",
  "skills": [
    {
      "id": "analyze_thermal",
      "name": "Analyze Thermal",
      "description": "Run thermal analysis."
    }
  ],
  "workflows": [
    {
      "id": "thermal_agent.quick_check",
      "name": "Quick Thermal Check",
      "version": "1.0.0",
      "description": "Run a quick thermal analysis.",
      "matching": {"patterns": ["quick thermal check"]},
      "execution": {"mode": "sequential", "on_error": "fail_fast"},
      "steps": [
        {"id": "analyze", "skill_id": "analyze_thermal"}
      ],
      "ui": {
        "category": "Thermal",
        "display_name": "Quick Thermal Check",
        "summary": "Quick thermal analysis"
      }
    }
  ]
}

Workflow IDs are global. Prefer an agent-scoped prefix such as thermal_agent.quick_check. If a platform-local YAML workflow and an agent-provided workflow use the same id:version, the local workflow wins and the duplicate is reported in the workflow refresh errors. Before an agent-provided workflow starts, the client-agent calls the contributing agent's direct /healthz endpoint. If the contributing agent has been disconnected or scaled down, the workflow start is rejected instead of executing a stale catalog entry.

Agent endpoints

Every PrometheusAgent exposes:

Path Auth Description
GET /healthz No Health check
GET /.well-known/agent.json No Agent card (A2A discovery)
POST / Yes A2A JSON-RPC 2.0 message endpoint

Sending artifacts from agents

See artifacts.md for details on returning structured artifacts from process_analysis().