Create, build, and deploy A2A agents on Rossoctl Kind clusters using Ollama
Scanned 9/20/2026
Install to Claude Code
npx -y skills add rossoctl/rossoctl --skill rossoctl:agent --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Rossoctl:Agent?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/rossoctl-rossoctl-agent)More formats (shields.io, HTML) on the badges page.
---
name: rossoctl:agent
description: Create, build, and deploy A2A agents on Rossoctl Kind clusters using Ollama
---
# Create and Deploy A2A Agent
## Table of Contents
- [When to Use](#when-to-use)
- [Reference Agent](#reference-agent)
- [Placeholder Substitution Checklist](#placeholder-substitution-checklist)
- [Workflow](#workflow)
- [Step 1: Scaffold Agent Source](#step-1-scaffold-agent-source)
- [Step 2: Build and Load Image](#step-2-build-and-load-image)
- [Step 3: Deploy to Cluster](#step-3-deploy-to-cluster)
- [Step 4: Verify](#step-4-verify)
- [Reference: File Templates](#reference-file-templates)
- [Gotchas](#gotchas)
- [Troubleshooting](#troubleshooting)
- [Related Skills](#related-skills)
## When to Use
- User asks to create a new AI agent for Rossoctl
- User wants to deploy a custom A2A agent on a Kind cluster
- User asks "build an agent", "create a new agent", or "deploy agent to Kind"
## Reference Agent
The recipe_agent is a complete working example of a simple conversational agent:
- Source: `~/git/rossoctl/examples/a2a/recipe_agent/`
- Deployment: `rossoctl/examples/agents/recipe_service_deployment.yaml`
- Service: `rossoctl/examples/agents/recipe_service_service.yaml`
Read these files ONLY if the templates below are insufficient for your use case.
## Placeholder Substitution Checklist
Before writing files, determine these values from the user's request:
| Placeholder | Format | Example |
|-------------|--------|---------|
| `<AGENT_NAME>` | Python module name (snake_case) | `cheerup_agent` |
| `<AGENT-NAME>` | Kubernetes/Docker name (kebab-case) | `cheerup-agent` |
| `<AGENT_DISPLAY_NAME>` | Human-friendly name | `Cheerup Companion` |
| `<DESCRIPTION>` | Short description for pyproject.toml | `Conversational cheerup agent using Ollama.` |
| `<AGENT_CARD_DESCRIPTION>` | Markdown description for the A2A agent card (multi-line OK) | `This agent cheers you up...` |
| `<SYSTEM_PROMPT>` | The LLM system prompt defining agent behavior | `You are a cheerful companion...` |
| `<SKILL_ID>` | Skill identifier (snake_case) | `cheerup_assistant` |
| `<SKILL_NAME>` | Skill display name | `Cheerup Assistant` |
| `<SKILL_DESCRIPTION>` | Markdown skill description | `**Cheerup Assistant** – Cheers you up when you're down.` |
| `<TAGS>` | JSON list of tags | `["cheerup", "mood", "motivation"]` |
| `<EXAMPLES>` | JSON list of example prompts | `["I'm feeling down", "Cheer me up"]` |
| `<WORKING_STATUS>` | Status message while thinking | `Thinking of something cheerful...` |
## Workflow
```mermaid
flowchart TD
START(["/rossoctl:agent"]) --> SCAFFOLD["Scaffold source code"]:::deploy
SCAFFOLD --> UVLOCK["uv lock"]:::deploy
UVLOCK --> BUILD["docker build"]:::deploy
BUILD --> LOAD["kind load docker-image"]:::deploy
LOAD --> APPLY["kubectl apply deployment + service"]:::k8s
APPLY --> VERIFY{Pod Running?}:::k8s
VERIFY -->|Yes| TEST["Test agent card + chat"]:::k8s
VERIFY -->|No| DEBUG["Check logs"]:::k8s
DEBUG --> FIX["Fix, rebuild, reload"]:::deploy
FIX --> APPLY
TEST --> DONE([Agent Live])
classDef deploy fill:#795548,stroke:#333,color:white
classDef k8s fill:#00BCD4,stroke:#333,color:white
```
> Follow this diagram as the workflow.
## Step 1: Scaffold Agent Source
Create files under `~/git/rossoctl/examples/a2a/<AGENT_NAME>/`:
```
~/git/rossoctl/examples/a2a/<AGENT_NAME>/
├── pyproject.toml
├── Dockerfile
├── .dockerignore
└── src/<AGENT_NAME>/
├── __init__.py # OpenTelemetry tracer
├── configuration.py # Pydantic settings (LLM_MODEL, LLM_API_BASE, LLM_API_KEY)
├── agent.py # A2A server: AgentCard, Executor, health, run()
└── <AGENT_NAME>_llm.py # LLM logic (openai.AsyncOpenAI for Ollama)
```
Write ALL files directly from the templates below with placeholder substitution. Do NOT explore or read reference files.
### Critical pyproject.toml requirements
- Use `a2a-sdk[http-server]` (NOT plain `a2a-sdk`) — the `[http-server]` extra provides `starlette` and `sse-starlette`
- Do NOT include `readme = "README.md"` unless the file exists — hatchling build will fail
- Entry point: `[project.scripts] server = "<AGENT_NAME>.agent:run"`
## Step 2: Build and Load Image
Generate lockfile:
```bash
cd ~/git/rossoctl/examples/a2a/<AGENT_NAME>
```
```bash
uv lock
```
Build the Docker image:
```bash
docker build -t registry.cr-system.svc.cluster.local:5000/<AGENT-NAME>:v0.0.1 ~/git/rossoctl/examples/a2a/<AGENT_NAME>/
```
Load directly into Kind (no registry push needed):
```bash
kind load docker-image registry.cr-system.svc.cluster.local:5000/<AGENT-NAME>:v0.0.1 --name rossoctl
```
## Step 3: Deploy to Cluster
Create two manifests under `rossoctl/examples/agents/` using the deployment.yaml and service.yaml templates below.
### Apply
```bash
kubectl --context kind-rossoctl apply -f rossoctl/examples/agents/<AGENT_NAME>_deployment.yaml
```
```bash
kubectl --context kind-rossoctl apply -f rossoctl/examples/agents/<AGENT_NAME>_service.yaml
```
## Step 4: Verify
Check pod is running:
```bash
kubectl --context kind-rossoctl -n team1 get pods -l app.kubernetes.io/name=<AGENT-NAME>
```
Check logs if not running:
```bash
kubectl --context kind-rossoctl -n team1 logs deployment/<AGENT-NAME>
```
Port-forward and test agent card:
```bash
kubectl --context kind-rossoctl -n team1 port-forward svc/<AGENT-NAME> 18080:8080
```
```bash
curl -s http://localhost:18080/.well-known/agent.json
```
```bash
curl -s http://localhost:18080/health
```
Send a test message (A2A JSON-RPC):
```bash
curl -s -X POST http://localhost:18080/ -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"role":"user","parts":[{"type":"text","text":"Hello"}],"messageId":"test-1"}}}'
```
Or use the Rossoctl UI at http://rossoctl-ui.localtest.me:8080 → Agent Catalog → select agent → Chat.
## Reference: File Templates
### pyproject.toml
```toml
[project]
name = "<AGENT-NAME>"
version = "0.0.1"
description = "<DESCRIPTION>"
license = { text = "Apache" }
requires-python = ">=3.11"
dependencies = [
"a2a-sdk[http-server]>=0.2.16",
"openai>=1.0.0",
"pydantic-settings>=2.8.1",
"uvicorn>=0.30.0",
"opentelemetry-exporter-otlp",
]
[project.scripts]
server = "<AGENT_NAME>.agent:run"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
### Dockerfile
```dockerfile
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
ARG RELEASE_VERSION="main"
WORKDIR /app
COPY . .
RUN uv sync --no-cache --locked --link-mode copy
ENV PRODUCTION_MODE=True \
RELEASE_VERSION=${RELEASE_VERSION}
RUN chown -R 1001:1001 /app
USER 1001
CMD ["uv", "run", "--no-sync", "server", "--host", "0.0.0.0", "--port", "8000"]
```
### .dockerignore
```
.venv
```
### configuration.py
```python
from pydantic_settings import BaseSettings
class Configuration(BaseSettings):
llm_model: str = "qwen3:4b"
llm_api_base: str = "http://localhost:11434/v1"
llm_api_key: str = "dummy"
```
### __init__.py (OpenTelemetry)
```python
from opentelemetry.sdk.resources import Resource
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
def setup_tracer():
resource = Resource.create(attributes={
"service.name": "<AGENT-NAME>",
})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
setup_tracer()
```
### agent.py
```python
import logging
import uvicorn
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from textwrap import dedent
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.apps import A2AStarletteApplication
from a2a.server.events.event_queue import EventQueue
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, TaskState, TextPart
from a2a.utils import new_agent_text_message, new_task
from <AGENT_NAME>.<AGENT_NAME>_llm import chat
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def get_agent_card(host: str, port: int):
"""Returns the Agent Card for <AGENT_DISPLAY_NAME>."""
capabilities = AgentCapabilities(streaming=True)
skill = AgentSkill(
id="<SKILL_ID>",
name="<SKILL_NAME>",
description="<SKILL_DESCRIPTION>",
tags=<TAGS>,
examples=<EXAMPLES>,
)
return AgentCard(
name="<AGENT_DISPLAY_NAME>",
description=dedent(
"""\
<AGENT_CARD_DESCRIPTION>
""",
),
url=f"http://{host}:{port}/",
version="1.0.0",
default_input_modes=["text"],
default_output_modes=["text"],
capabilities=capabilities,
skills=[skill],
)
class <EXECUTOR_CLASS>(AgentExecutor):
"""Handles <AGENT_DISPLAY_NAME> execution for A2A."""
async def execute(self, context: RequestContext, event_queue: EventQueue):
task = context.current_task
if not task:
task = new_task(context.message)
await event_queue.enqueue_event(task)
task_updater = TaskUpdater(event_queue, task.id, task.context_id)
user_input = context.get_user_input()
logger.info("<AGENT_DISPLAY_NAME> received: %s (context=%s)", user_input, task.context_id)
await task_updater.update_status(
TaskState.working,
new_agent_text_message(
"<WORKING_STATUS>",
task_updater.context_id,
task_updater.task_id,
),
)
try:
reply = await chat(task.context_id, user_input)
parts = [TextPart(text=reply)]
await task_updater.add_artifact(parts)
await task_updater.update_status(
TaskState.input_required,
new_agent_text_message(
reply,
task_updater.context_id,
task_updater.task_id,
),
)
except Exception as e:
logger.error("<AGENT_DISPLAY_NAME> error: %s", e)
parts = [TextPart(text=f"Sorry, I encountered an error: {e}")]
await task_updater.add_artifact(parts)
await task_updater.failed()
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise Exception("cancel not supported")
async def health(request: Request) -> JSONResponse:
return JSONResponse({"status": "ok"})
async def agent_card_compat(request: Request) -> JSONResponse:
"""Serve agent card at /.well-known/agent-card.json for Rossoctl backend compatibility."""
card = get_agent_card(host="0.0.0.0", port=8000)
return JSONResponse(card.model_dump(mode="json", exclude_none=True))
def run():
"""Runs the A2A Agent application."""
agent_card = get_agent_card(host="0.0.0.0", port=8000)
request_handler = DefaultRequestHandler(
agent_executor=<EXECUTOR_CLASS>(),
task_store=InMemoryTaskStore(),
)
server = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
)
app = server.build()
# Add custom routes
app.routes.insert(0, Route("/health", health, methods=["GET"]))
app.routes.insert(0, Route("/.well-known/agent-card.json", agent_card_compat, methods=["GET"]))
uvicorn.run(app, host="0.0.0.0", port=8000)
```
> **Note**: `<EXECUTOR_CLASS>` is the PascalCase executor class name, e.g., `CheerupExecutor`. Derive it from the agent name.
### <AGENT_NAME>_llm.py
```python
import logging
from collections import defaultdict
from openai import AsyncOpenAI
from <AGENT_NAME>.configuration import Configuration
logger = logging.getLogger(__name__)
SYSTEM_PROMPT = (
"<SYSTEM_PROMPT>"
)
# Conversation memory keyed by context_id
_conversations: dict[str, list[dict[str, str]]] = defaultdict(list)
async def chat(context_id: str, user_message: str) -> str:
"""Send a message and get a response, maintaining conversation history."""
config = Configuration()
client = AsyncOpenAI(
base_url=config.llm_api_base,
api_key=config.llm_api_key,
)
history = _conversations[context_id]
history.append({"role": "user", "content": user_message})
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history
logger.info("Sending %d messages to LLM for context %s", len(messages), context_id)
response = await client.chat.completions.create(
model=config.llm_model,
messages=messages,
)
assistant_message = response.choices[0].message.content
history.append({"role": "assistant", "content": assistant_message})
logger.info("LLM response for context %s: %s", context_id, assistant_message[:200])
return assistant_message
```
### deployment.yaml (`rossoctl/examples/agents/<AGENT_NAME>_deployment.yaml`)
```yaml
# Deployment manifest for <AGENT-NAME>
# <DESCRIPTION>
apiVersion: apps/v1
kind: Deployment
metadata:
name: <AGENT-NAME>
namespace: team1
labels:
rossoctl.io/type: agent
protocol.rossoctl.io/a2a: ""
rossoctl.io/framework: OpenAI
rossoctl.io/workload-type: deployment
app.kubernetes.io/name: <AGENT-NAME>
app.kubernetes.io/managed-by: rossoctl-e2e
app.kubernetes.io/component: agent
annotations:
rossoctl.io/description: "<DESCRIPTION>"
rossoctl.io/shipwright-build: <AGENT-NAME>
spec:
replicas: 1
selector:
matchLabels:
rossoctl.io/type: agent
app.kubernetes.io/name: <AGENT-NAME>
template:
metadata:
labels:
rossoctl.io/type: agent
protocol.rossoctl.io/a2a: ""
rossoctl.io/framework: OpenAI
app.kubernetes.io/name: <AGENT-NAME>
spec:
containers:
- name: agent
image: registry.cr-system.svc.cluster.local:5000/<AGENT-NAME>:v0.0.1
imagePullPolicy: Never
env:
- name: PORT
value: "8000"
- name: HOST
value: "0.0.0.0"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector.rossoctl-system.svc.cluster.local:8335"
- name: LLM_API_BASE
value: "http://host.containers.internal:11434/v1"
- name: LLM_API_KEY
value: "dummy"
- name: LLM_MODEL
value: "qwen3:4b"
- name: UV_CACHE_DIR
value: "/app/.cache/uv"
ports:
- containerPort: 8000
name: http
protocol: TCP
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: cache
mountPath: /app/.cache
volumes:
- name: cache
emptyDir: {}
```
### service.yaml (`rossoctl/examples/agents/<AGENT_NAME>_service.yaml`)
```yaml
# Service manifest for <AGENT-NAME>
# Companion to <AGENT_NAME>_deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: <AGENT-NAME>
namespace: team1
labels:
rossoctl.io/type: agent
protocol.rossoctl.io/a2a: ""
app.kubernetes.io/name: <AGENT-NAME>
app.kubernetes.io/managed-by: rossoctl-e2e
spec:
type: ClusterIP
selector:
rossoctl.io/type: agent
app.kubernetes.io/name: <AGENT-NAME>
ports:
- name: http
port: 8080
targetPort: 8000
protocol: TCP
```
## Gotchas
These are hard-won lessons from real deployments:
| Issue | Cause | Fix |
|-------|-------|-----|
| `OSError: Readme file does not exist: README.md` | `readme = "README.md"` in pyproject.toml but no file | Remove the `readme` line from pyproject.toml |
| `ModuleNotFoundError: No module named 'starlette'` | Using plain `a2a-sdk` | Use `a2a-sdk[http-server]` |
| `ImportError: sse-starlette required` | Same — missing http-server extra | Use `a2a-sdk[http-server]` |
| `Failed to initialize cache at /.cache/uv` | UV_CACHE_DIR not set in deployment | Add `UV_CACHE_DIR=/app/.cache/uv` env var |
| `ErrImagePull` / `ImagePullBackOff` | `imagePullPolicy: Always` with `kind load` | Set `imagePullPolicy: Never` |
| `Connection error` from agent to LLM | `dockerhost` doesn't resolve in Kind | Use `host.containers.internal` (podman) or `host.docker.internal` (Docker) |
| LLM returns error | Model not installed in Ollama | Run `ollama list` to check, use an available model |
## Troubleshooting
### Problem: Pod crashes on startup
**Symptom**: Pod in `Error` or `CrashLoopBackOff`
Check logs:
```bash
kubectl --context kind-rossoctl -n team1 logs deployment/<AGENT-NAME>
```
Common causes: missing Python dependency, bad import, permission error. Fix source, rebuild image, `kind load`, then restart:
```bash
kubectl --context kind-rossoctl -n team1 rollout restart deployment/<AGENT-NAME>
```
### Problem: Agent can't reach Ollama
**Symptom**: `Connection error` or `Connection refused` in agent response
Verify Ollama is running on host:
```bash
curl -s http://localhost:11434/v1/models
```
Test connectivity from inside the pod:
```bash
kubectl --context kind-rossoctl -n team1 exec deployment/<AGENT-NAME> -- python3 -c "import urllib.request; print(urllib.request.urlopen('http://host.containers.internal:11434/v1/models', timeout=5).read().decode()[:100])"
```
### Problem: Agent not visible in Rossoctl UI
**Symptom**: Agent doesn't appear in Agent Catalog
Verify the `/.well-known/agent-card.json` endpoint returns valid JSON. The Rossoctl backend discovers agents via this path. Ensure the custom Starlette route is registered.
## Shipwright Build (for CI/production)
For deploying via Shipwright instead of `kind load`, create additional manifests. These pull source from GitHub and build in-cluster:
- `<agent>_shipwright_build.yaml` — Build CR pointing to `agent-examples` repo
- `<agent>_shipwright_buildrun.yaml` — BuildRun CR to trigger
- Change `imagePullPolicy` back to `Always` in the deployment
See `rossoctl/examples/agents/weather_agent_shipwright_build.yaml` for the template.
## Related Skills
- `rossoctl:operator` — Deploy Rossoctl platform and demo agents
- `rossoctl:deploy` — Deploy Kind cluster
- `k8s:pods` — Debug pod issues
- `k8s:logs` — Query component logs
- `k8s:health` — Check platform health
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!