This repository contains the files utilized during the tutorial presented in the dedicated IsItObservable episode related to Sympozium
- A Kubernetes workload cluster — either on Proxmox (Cluster API + the CAPMOX provider) or on GKE — with a CNI, a LoadBalancer (MetalLB on Proxmox; built-in on GKE) and a default StorageClass.
- Sympozium installed on it (controller, apiserver, webhook, NATS). Telemetry is sent to the episode's own collectors (below), not Sympozium's built-in collector.
- Two local models on a Mac Studio (or any Ollama host):
qwen3.5:122b(the heavyweight — architect, dev/review, testing, devops) andqwen3.6:latest(the fast model — manager, analysis, planning, stories, challenger, observability). - The
bmad-ensemble— the full BMAD Crew (a Crew Manager + nine phase leads) as one Ensemble, with a custombmadSkillPack, persistent memory, and a delegation/handoff flow ported from the Paperclip‑Bmad‑Crew. - Guardrails — a
SympoziumPolicy(tool gating, sub‑agent caps, sandbox bounds, feature gates) referenced by the Ensemble, plus an optional Agent Sandbox (gVisor) so every tool call runs with kernel‑level isolation. - A Slack channel so you can task the team and watch the code → review → merge loop live.
- End‑to‑end OpenTelemetry into Dynatrace — every agent run, the controller, apiserver and
mcp‑bridge emit OTLP to two collectors in the
defaultnamespace (anotelddaemonset for logs + node metrics, and anotelgateway statefulset for OTLP + cluster scraping), deployed via the OpenTelemetry Operator. Both ship to Dynatrace, alongside the Dynatrace Operator/DynaKube for infrastructure monitoring and a Dynatrace MCP the O11y Engineer queries directly.
┌───────────────────────┐
kickoff ───────▶ │ CTO / Crew Manager │ ──── escalate ───▶ CEO
└───────────┬───────────┘
routes (delegate_to_persona) to the right phase lead
┌──────────────┬──────────────┬───────────────┬───────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼
Mary ──▶ John (PM) ──▶ Winston ──▶ Story Writer ──▶ Amelia ──▶ Testing
(analysis) (planning) (architect) (stories) (dev+review) Architect
Phase 1 Phase 2 Phase 3 Phase 3/4 Phase 4 Phase 4
│ PR + HANDOFF ──▶ CTO merges
Challenger ── adversarial quality gate (supervises all phases) ──────────────┐
O11y Engineer ── observability (supervises architecture + devops) ────────────┘
DevOps Engineer ── CI/CD, K8s, IaC (Phase 3/4)
Every handoff is a GitHub PR/issue comment, mirrored to Slack, and recorded in MEMORY.md.
- Concepts in 60 seconds
- Repository layout
- Prerequisites
- Step 1 — Create your cluster
- Step 2 — Serve the local models
- Step 3 — Deploy everything with
deployment.sh - Step 4 — Inside the
bmad-ensemble - Step 4½ — Guardrails & sandboxed execution
- Step 5 — Showcase agent memory
- Step 6 — Connect Slack
- Step 7 — Run the team
- Observability
- Gotchas
- Cleanup
- References
Sympozium is a Kubernetes‑native multi‑agent platform from the creator of k8sgpt and llmfit.
| Idea | What it means |
|---|---|
| Ephemeral agent Pod | One pod per agent run, created and torn down per task. |
| CRD‑driven policy | Agents, skills, schedules, channels and packs are all Custom Resources. |
| Job‑backed execution | Each run is a Kubernetes Job — lifecycle, retries and TTL managed by the platform. |
| Isolated skill sidecars | Every skill runs in its own sidecar with ephemeral, least‑privilege RBAC, GC'd at run end. "Give the agent tools, not trust." |
| Channels | Bridges to Slack/Telegram/Discord/WhatsApp, routed through NATS JetStream. |
An Ensemble is Sympozium's top‑level unit of teamwork: a single Ensemble CRD
(sympozium.ai/v1alpha1) that bundles several pre‑configured agent personas (spec.agentConfigs)
— each with its own system prompt, model, skills, tool policy, schedule and memory seeds — and
activates them together. Activating it stamps out one Agent per persona
(bmad-ensemble-cto, -architect, -code-reviewer, … — one per agentConfigs[] entry) — plus the
matching Schedules, secrets and memory ConfigMaps — all carrying ownerReferences back to the
Ensemble, so deleting the Ensemble garbage‑collects the whole crew.
Beyond personas, an Ensemble can also declare typed relationships between them — delegation
(request a peer and await its result), sequential (one finishes before the next starts),
supervision (one observes another) and stimulus (kick off the team once every persona is
serving) — together with a workflow type: autonomous (the default — each persona runs on its
own schedule), pipeline, or delegation (runtime hand‑offs via the built‑in
delegate_to_persona tool). See the
Ensembles concept doc.
This episode uses workflowType: delegation. The Crew Manager (CTO) routes each incoming task to
the right phase lead through the built‑in delegate_to_persona tool, the BMAD phases chain through
sequential edges, and the Challenger and O11y Engineer attach as supervision edges across phases —
the crew's reporting hierarchy expressed as first‑class relationships. On top of that runtime routing,
every persona still emits a structured BMAD HANDOFF block at the end of a run and mirrors it to
GitHub (issues, PRs, review comments), a shared Slack thread, and its own MEMORY.md — so the
coordination is observable, auditable, and survives pod restarts, exactly what we want to watch in an
IsItObservable episode.
BMAD ("Breakthrough Method for Agile AI‑Driven
Development") gives the team a repeatable lifecycle, delivered here as a custom bmad SkillPack
(sympozium/skills/bmad.yaml) mounted on every agent:
| Phase | Owner(s) | What happens |
|---|---|---|
| 1 Analysis | Mary (brainstormer) |
Research, market/domain analysis, product briefs. |
| 2 Planning | John (product-manager) |
PRD lifecycle + epics/stories with acceptance criteria. |
| 3 Solutioning | Winston (architect) |
Architecture decisions + implementation‑readiness. |
| 3/4 Stories | Story Writer | Decompose epics into implementation‑ready stories (GWT). |
| 4 Implementation + Review | Amelia (code-reviewer) |
Implement one story/PR, then adversarial review (Blind Hunter, Edge Case Hunter, Acceptance Auditor). |
| 4 Test | Testing Architect | Generate + run unit/API/E2E tests; 100% pass before done. |
| Cross‑cutting | Challenger / O11y Engineer / DevOps | Adversarial quality gate, observability, CI/CD + infra. |
| Routing + merge | Crew Manager (cto) |
Route work to phase leads; merge approved PRs. |
The reviewer's three layers are Blind Hunter, Edge Case Hunter and Acceptance Auditor.
📖 Deep dive: Ensembles, Skills & Sidecars, Channels.
- A Kubernetes cluster — see Step 1 for Proxmox or GKE.
- An Ollama host (a Mac Studio in this episode, but any machine with enough memory) reachable from the cluster network — it serves the two Qwen models.
- For Proxmox: a management/bootstrap cluster with Cluster API + the CAPMOX provider, and a Proxmox VM template (cloud‑init image with kubeadm/kubelet/containerd installed).
- Optional: an NFS server (for
csi-driver-nfspersistent volumes) and a registry mirror (Harbor or similar) to cache container images. - A GitHub repo for the team to work on, plus a token with
reposcope. - A Slack workspace where you can install a bot app.
- Local tools:
kubectl,helm,envsubst, and thesympoziumCLI:curl -fsSL https://deploy.sympozium.ai/install.sh | sh # or: brew tap AlexsJones/sympozium && brew install sympozium
Pick the path that fits your environment. Both end with a KUBECONFIG pointing at a Ready cluster.
Provision a workload cluster on your own Proxmox using Cluster API and the CAPMOX provider. You supply every environment‑specific value; nothing is hard‑coded.
1. Prepare the management cluster (one‑time): install Cluster API with the Proxmox provider.
clusterctl init --infrastructure proxmox2. Store your Proxmox API credentials as the secret the manifest references:
kubectl create secret generic proxmox-credentials \
--from-literal=url="https://YOUR-PROXMOX:8006/api2/json" \
--from-literal=token="USER@pam!TOKENID" \
--from-literal=secret="YOUR-TOKEN-SECRET"
# (field names follow your CAPMOX version's ProxmoxCluster credentialsRef schema)3. Fill in your details in infrastructure/cluster-api/workload-cluster/values.env
— Proxmox node(s), VM template ID, the control‑plane VIP, the node IP range, gateway, DNS,
your registry mirror host and your SSH public key:
# excerpt — see values.env for the full list
CLUSTER_NAME=observable-llm
K8S_VERSION=v1.31.0
PROXMOX_SOURCE_NODE=pve
PROXMOX_TEMPLATE_ID=9000
CONTROL_PLANE_VIP=192.0.2.10
NODE_IP_RANGE=192.0.2.11-192.0.2.20
NODE_GATEWAY=192.0.2.1
DNS_SERVER=192.0.2.2
HARBOR_HOST=harbor.example.internal # or your own registry mirror
SSH_PUBLIC_KEY="ssh-ed25519 AAAA... you@example.com"4. Render and apply the cluster (the manifest uses ${VAR} placeholders that envsubst fills from
values.env):
set -a; . infrastructure/cluster-api/workload-cluster/values.env; set +a
envsubst < infrastructure/cluster-api/workload-cluster/capi_llm_cluster.yaml | kubectl apply -f -
kubectl get cluster "$CLUSTER_NAME" -w # wait for Provisioned5. Get the kubeconfig and install a CNI so the nodes go Ready:
clusterctl get kubeconfig "$CLUSTER_NAME" > observable-llm.kubeconfig
export KUBECONFIG=$PWD/observable-llm.kubeconfig
# install any CNI — Cilium is a good fit (Istio-ambient + kgateway compatible):
helm repo add cilium https://helm.cilium.io && helm repo update
helm install cilium cilium/cilium -n kube-system
kubectl get nodes -w # all nodes ReadyMetalLB, csi-driver-nfs and cert‑manager are installed in the next step by deployment.sh.
The manifest pulls images through an optional registry mirror (
${HARBOR_HOST}). To pull straight from the public registries, pointHARBOR_HOSTat a host you don't use and remove thecerts.d/imageRepositoryblocks from the manifest.
No Proxmox? Spin up a managed cluster on GKE. On GKE you get a CNI, a LoadBalancer and a default
StorageClass out of the box, so you can skip MetalLB and csi-driver-nfs.
gcloud container clusters create sympozium \
--region=europe-west1 \
--num-nodes=3 \
--machine-type=e2-standard-4
gcloud container clusters get-credentials sympozium --region=europe-west1Then run deployment.sh --skip-platform (Step 3) — it still installs cert‑manager and the Sympozium
control plane. The NATS PVC binds against GKE's default standard StorageClass automatically.
The agents run on local Ollama models, so even on GKE the Mac Studio (Step 2) must be reachable from the cluster's egress — over a VPN/tunnel, or via a publicly reachable Ollama endpoint.
Both models run locally on a Mac Studio (or any Ollama host) — no cloud API keys. Ollama exposes an
OpenAI‑compatible API, so Sympozium talks to it with the provider set to ollama.
| Model | Crew personas | Why |
|---|---|---|
qwen3.5:122b |
architect, code-reviewer (Amelia, dev+review), testing-architect, devops-engineer | The heavyweight — multi‑file implementation, architecture and deep reasoning. |
qwen3.6:latest |
cto, brainstormer, product-manager, story-writer, challenger, o11y-engineer | Faster turn‑around for routing, research, planning, review and observability. |
On the Ollama host, bind it to all interfaces (so cluster pods can reach it over the network) and pull both models:
OLLAMA_HOST=0.0.0.0 ollama serve # bind to all interfaces
ollama pull qwen3.6:latest
ollama pull qwen3.5:122b
ollama list # confirm both are presentThe cluster reaches Ollama at http://$MACSTUDIO_IP:11434/v1.
⚠️ Pick an address outside your LoadBalancer pool. SetMACSTUDIO_IPto the Ollama host's address on your LAN, and make sure it is not inside the MetalLB range you choose in Step 3 — or a LoadBalancer Service could be handed that IP and hijack the agents' egress to Ollama.
How the local endpoint reaches the agents. The Ensemble carries the Ollama endpoint directly in
spec.baseURL, so the controller propagates it (asspec.agents.default.baseURL) to every generated Agent (everybmad-ensemble-<persona>) — no post‑apply patch needed. The manifest ships a<MACSTUDIO_IP>placeholder;deployment.shsubstitutes$MACSTUDIO_IPat apply time. The Ollama host is external (not a cluster node); egress to port11434is already allowed by the default network policies.
deployment.sh is the one‑shot orchestrator. It runs preflight checks, then: platform pieces (MetalLB,
csi‑driver‑nfs + StorageClass, cert‑manager, optional kgateway) → observability (OpenTelemetry
Operator + the two default‑namespace collectors + Dynatrace Operator/DynaKube + Dynatrace MCP) →
Sympozium control plane via Helm (the published deploy.sympozium.ai/charts repo, pointed at the otel
gateway collector) → the bmad SkillPack and the bmad-guardrails SympoziumPolicy → the Ollama +
Slack secrets → the bmad-ensemble Ensemble (with the macstudio baseURL substituted in, and — with
--with-sandbox — the gVisor Agent Sandbox enabled) → verification.
It needs kubectl, helm and envsubst. Pinned versions: cert‑manager v1.17.1, MetalLB v0.14.8,
csi‑driver‑nfs v4.9.0, Gateway API v1.2.1, kgateway v2.0.0, Dynatrace Operator 1.9.0,
OpenTelemetry Operator latest.
Dynatrace credentials (optional — the collectors deploy regardless but only export with real creds):
export DT_TENANT_URL=https://abc12345.live.dynatrace.com # OTLP/API base, NO trailing /api
export DT_API_TOKEN=dt0c01.XXXXXXXX... # data-ingest + API token (collectors + DynaKube)
# optional:
export DT_DATA_INGEST_TOKEN=dt0c01.YYYY... # DynaKube metric ingest (defaults to DT_API_TOKEN)
export DT_MCP_TOKEN=dt0s16.ZZZZ... # platform token for the Dynatrace MCP
export DT_MCP_ENVIRONMENT=https://abc12345.apps.dynatrace.com # MCP platform URL (.apps. domain)
export DT_CLUSTER_NAME=observable-llm # replaces CLUSTER_NAME_TO_REPLACE in the manifestsSkip the whole observability stack with --skip-observability (or SKIP_OBSERVABILITY=true).
Skip the platform step — but install cert‑manager first (the Sympozium admission webhook needs it, and
--skip-platform skips it too):
export KUBECONFIG=/path/to/your.kubeconfig
export MACSTUDIO_IP=<ollama-host-ip> # REQUIRED
export SLACK_BOT_TOKEN=xoxb-... # optional, needed to demo Slack
export SLACK_APP_TOKEN=xapp-... # optional, enables Socket Mode
export DT_TENANT_URL=https://abc12345.live.dynatrace.com # optional, Dynatrace backend
export DT_API_TOKEN=dt0c01.... # optional, Dynatrace data-ingest + API token
# cert-manager is required by BOTH the Sympozium webhook AND the OpenTelemetry Operator,
# and --skip-platform skips it, so install it first:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.yaml
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s
./deployment.sh --skip-platformexport KUBECONFIG=/path/to/observable-llm.kubeconfig
export MACSTUDIO_IP=<ollama-host-ip>
export METALLB_IP_RANGE=192.0.2.240-192.0.2.250 # a free block on your LAN, outside MACSTUDIO_IP
export NFS_SERVER=192.0.2.5 # your NFS server
export NFS_SHARE=/export/sympozium
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_APP_TOKEN=xapp-...
export DT_TENANT_URL=https://abc12345.live.dynatrace.com # Dynatrace backend (see above)
export DT_API_TOKEN=dt0c01....
export DT_CLUSTER_NAME=observable-llm
./deployment.sh # full stack
# ./deployment.sh --with-kgateway # also install kgateway + Gateway APIFlags: --skip-platform (cluster pieces already provisioned), --skip-observability (no OTel
operator/collectors or Dynatrace), --skip-ensemble (control plane only), --with-kgateway (install
kgateway + Gateway API), --with-sandbox (enable the gVisor Agent Sandbox — see
Step 4½). Run ./deployment.sh --help for the full header.
The Ensemble (sympozium/ensemble/ensemble-bmad-ensemble.yaml) is the full BMAD Crew — a Crew
Manager plus nine phase leads, ported from the
Paperclip‑Bmad‑Crew. Activating it stamps out one
Agent per agentConfigs[] entry.
| Persona (slug) | BMAD role / phase | Model | Key Sympozium skills |
|---|---|---|---|
Crew Manager (cto) |
Routing + merge — spans all phases | qwen3.6:latest |
bmad, github-gitops, memory |
Mary (brainstormer) |
Phase 1 Analysis — research, briefs | qwen3.6:latest |
bmad, memory |
John (product-manager) |
Phase 2 Planning — PRD, epics/stories | qwen3.6:latest |
bmad, github-gitops, memory |
Winston (architect) |
Phase 3 Solutioning — architecture | qwen3.5:122b |
bmad, software-dev, github-gitops, memory |
Story Writer (story-writer) |
Phase 3/4 — story decomposition | qwen3.6:latest |
bmad, github-gitops, memory |
Amelia (code-reviewer) |
Phase 4 — implement + adversarial review | qwen3.5:122b |
bmad, software-dev, code-review, github-gitops, memory |
Testing Architect (testing-architect) |
Phase 4 — test generation | qwen3.5:122b |
bmad, software-dev, code-review, github-gitops, memory |
Challenger (challenger) |
Cross-cutting quality gate | qwen3.6:latest |
bmad, code-review, memory |
DevOps Engineer (devops-engineer) |
Phase 3/4 — CI/CD, K8s, IaC | qwen3.5:122b |
bmad, software-dev, github-gitops, memory |
O11y Engineer (o11y-engineer) |
Cross-cutting observability (OTel/Dynatrace) | qwen3.6:latest |
bmad, software-dev, memory |
Shape (abridged — see the manifest for full system prompts and seeds):
apiVersion: sympozium.ai/v1alpha1
kind: Ensemble
metadata: { name: bmad-ensemble, namespace: default }
spec:
enabled: true
workflowType: delegation # the CTO routes work to phase leads at runtime
policyRef: bmad-guardrails # cluster-wide guardrails (Step 4½)
baseURL: "http://<MACSTUDIO_IP>:11434/v1" # local Ollama endpoint for every Agent
authRefs: [{ provider: ollama, secret: bmad-ollama-key }]
channelConfigs: { slack: bmad-slack-tokens }
skillParams: { github-gitops: { repo: "isItObservable/Sympozium" } }
stimulus: # injected into the CTO once every pod is Serving
name: kickoff
prompt: "Review the repo, route work to the right phase lead, keep the workflow moving."
relationships: # routing + phase flow + supervision (the crew hierarchy)
- { source: kickoff, target: cto, type: stimulus }
- { source: cto, target: architect, type: delegation, condition: "architecture / solutioning" }
- { source: product-manager, target: architect, type: sequential }
- { source: challenger, target: code-reviewer, type: supervision }
# …one delegation edge per phase lead, sequential edges for the BMAD flow, and
# challenger/o11y supervision edges — see the manifest.
agentConfigs:
- name: code-reviewer # "Amelia" — implement + adversarial review
displayName: "Code Reviewer / Developer (Amelia)"
model: qwen3.5:122b # the heavyweight local model
systemPrompt: |
You are Amelia. In dev mode you implement the next ready story (one PR, tests
for every AC); in review mode you review with Blind Hunter / Edge Case Hunter /
Acceptance Auditor. You never merge — the CTO does.
skills: [bmad, software-dev, code-review, github-gitops, memory]
toolPolicy: { deny: [delete_branch, merge_pull_request] }
schedule: { type: sweep, interval: "1h", task: "Implement the next ready story, then review open PRs…" }
memory: { enabled: true, seeds: ["Implement one story per PR; tests for every AC.", "Review layers: Blind Hunter, Edge Case Hunter, Acceptance Auditor."] }
channels: [slack]
# … cto, brainstormer, product-manager, architect, story-writer, testing-architect,
# challenger, devops-engineer, o11y-engineer (see the manifest for each).The Crew Manager (CTO) routes each incoming task to the right phase lead via delegate_to_persona,
and work chains through the BMAD phases; the Challenger and O11y Engineer supervise across phases:
kickoff ▶ CTO ─routes▶ Mary(analysis) ▶ John(PRD) ▶ Winston(arch) ▶ Story Writer
▶ Amelia(dev+review) ▶ Testing Architect ▶ CTO merges
Challenger ── adversarial gate (supervises planning / architecture / code)
O11y Engineer ── observability (supervises architecture + devops)
DevOps Engineer ── CI/CD, K8s, IaC
Note the tool policies that enforce roles: only the CTO can merge_pull_request; every other
persona denies merge_pull_request and delete_branch. Every handoff is posted as a PR/issue comment
and mirrored to Slack so a human can watch the crew live, and the platform's workflowType: delegation + typed relationships make the routing and supervision first‑class (and observable).
deployment.sh applies the SympoziumPolicy, then this Ensemble (with the Ollama baseURL substituted
in), then creates bmad-ollama-key (placeholder) and bmad-slack-tokens. To watch the crew come up:
sympozium # TUI → Personas tab shows bmad-ensemble
kubectl -n default get ensemble bmad-ensemble # Personas / Installed / Workflow / Phase
kubectl -n default get agent | grep bmad-ensemble # the 10 generated AgentsNo. You don't author individual Agent resources for a team — the Ensemble pre‑packages the whole
team and the controller stamps out the Agents for you. Applying one Ensemble produces one Agent
per agentConfigs[] entry (bmad-ensemble-cto, -architect, -code-reviewer, … — ten in all), each
with its own SympoziumSchedule and memory ConfigMap, all owned by the Ensemble (so kubectl delete ensemble
cleans the lot up). A standalone Agent CR is for a single one‑off agent; the moment you want a
coordinated group with shared providers, policy, channels and memory seeds, you use an Ensemble.
Edit spec.agentConfigs[] and re‑apply to add or change a teammate — never touch the generated Agents
directly (the controller owns them and will reconcile your changes away).
A bmad-ensemble writes code and runs commands, so it ships with two layers of containment — both
referenced by the Ensemble, both observable.
sympozium/ensemble/sympoziumpolicy-bmad.yaml defines a SympoziumPolicy named bmad-guardrails
that the Ensemble references via spec.policyRef; the controller applies it to every generated
Agent. It is the cluster‑side backstop on top of each persona's per‑agent toolPolicy:
apiVersion: sympozium.ai/v1alpha1
kind: SympoziumPolicy
metadata: { name: bmad-guardrails, namespace: default }
spec:
toolGating: # allow the dev-team tools; deny/ask the rest
defaultAction: allow
rules:
- { tool: execute_command, action: allow } # build + test
- { tool: write_file, action: allow }
- { tool: fetch_url, action: allow }
subagentPolicy: { maxDepth: 2, maxConcurrent: 3 } # cap sub-agent fan-out
sandboxPolicy: # bounds when the sandbox runs
required: false
maxCPU: "4"
maxMemory: 8Gi
agentSandboxPolicy: { required: false, defaultRuntimeClass: gvisor, allowedRuntimeClasses: [gvisor, kata] }
featureGates: { code-execution: true, file-access: true, sub-agents: true, browser-automation: false }
imagePolicy: { allowedRegistries: ["ghcr.io/sympozium-ai/", "docker.io/library/"] }It's observable. Every tool call the policy blocks is emitted as the OTel metric
sympozium.access.denied(tagged with agent/instance/tool), so you can graph "denied tool calls per agent" right next to the run metrics — the guardrail proves itself in your backend.
kubectl -n default get sympoziumpolicy bmad-guardrails
kubectl -n default get agent bmad-ensemble-code-reviewer -o jsonpath='{.spec.policyRef}{"\n"}' # -> bmad-guardrailsFor kernel‑level isolation, run each agent's tool calls inside a Kubernetes Agent Sandbox (gVisor or
Kata). The Ensemble carries this in spec.agentSandbox, propagated to every generated Agent:
./deployment.sh --with-sandbox # patches spec.agentSandbox onto the Ensemble
# equivalently:
kubectl -n default patch ensemble bmad-ensemble --type=merge \
-p '{"spec":{"agentSandbox":{"enabled":true,"runtimeClass":"gvisor"}}}'Prerequisites: the agent‑sandbox CRDs installed,
and a gvisor (or kata) RuntimeClass on the nodes — on GKE enable
GKE Sandbox; on Proxmox
install gVisor (runsc) on each node. Without a matching RuntimeClass the sandboxed pods stay Pending,
so the tutorial leaves the sandbox off by default and bmad-guardrails keeps sandboxPolicy.required: false.
Observability of the sandbox — and a real gap. Sandboxed runs still emit the full agent telemetry (
sympozium.agent.*,sympozium.tool.invocations,gen_ai.usage.*) and asympozium.pod.createevent, and the pod'sruntimeClassName=gvisoris visible in the Kubernetes API. What Sympozium does not emit is per‑sandbox resource usage (the gVisor sandbox's own CPU/memory/syscall counts) — for that you fall back tokube-state-metrics+ cAdvisor (or the runtime's own metrics). That's exactly the kind of "missing observability area" this episode is meant to surface.
Memory is what turns the crew's stateless model calls into a team that gets better over time. Each
agent has persistent memory enabled and seeded — no external database; it lives in etcd as a
<agent>-memory ConfigMap holding a MEMORY.md file that is mounted read‑only into the pod,
prepended to the agent's context, and patched after each run.
The memory.seeds in the Ensemble give each agent starting context from run #1 (e.g. the Crew Manager
knows its routing map; Amelia knows the three review layers; the O11y Engineer knows "change management
first"). Over time the agents accumulate learned memory on top of the seeds.
Two layers of memory. This episode uses per‑persona memory — each agent's own
MEMORY.md. An Ensemble can also enable optional pack‑level shared memory: a store the whole team reads and writes through theworkflow_memory_search/workflow_memory_store/workflow_memory_listtools, with per‑persona read/write access control, plus an advanced Synthetic Membrane layer (selective permeability, provenance tracking, token budgets, circuit breakers, time decay). Here the shared layer is intentionally GitHub + Slack, so every cross‑agent fact stays visible to humans too — but see the Ensembles doc if you want the agents to share a private memory pool instead.
# What Amelia (code-reviewer / developer) remembers
kubectl -n default get configmap bmad-ensemble-code-reviewer-memory \
-o jsonpath='{.data.MEMORY\.md}'
# …or the Challenger's accumulated defect patterns
kubectl -n default get configmap bmad-ensemble-challenger-memory \
-o jsonpath='{.data.MEMORY\.md}'Talking points for the episode: continuity across runs with no external DB; seeds vs. learned memory; and how Amelia and the Challenger accumulate recurring‑defect patterns across PRs to speed up future reviews.
Channels let you task the team and watch it work from Slack. Sympozium's Slack channel uses Socket Mode, so you don't need a public webhook URL. (Adapted from the Sympozium Channels docs.)
- Create a Slack app → enable Socket Mode.
- App Home → Messages Tab: enable it and allow users to message the app.
- Event Subscriptions (bot events):
message.im,message.channels,app_mention. - Generate tokens: a bot token
xoxb-…(OAuth & Permissions) and an app tokenxapp-…(Basic Information → App‑Level Tokens, scopeconnections:write). - Reinstall the app after changing scopes/events.
Export both before running deployment.sh — it provisions the bmad-slack-tokens secret and binds the
slack channel to every agent:
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_APP_TOKEN=xapp-...
./deployment.sh --skip-platformFallback: if you omit
SLACK_APP_TOKEN, Sympozium falls back to the Slack Events API, which needs a publicly reachable webhook URL. Use Socket Mode for the tutorial.
kubectl -n default get agent | grep bmad-ensemble
for a in cto brainstormer product-manager architect story-writer \
code-reviewer testing-architect challenger devops-engineer o11y-engineer; do
kubectl -n default get agent "bmad-ensemble-$a" \
-o custom-columns=NAME:.metadata.name,CHANNELS:.status.channels --no-headers
done
kubectl -n default logs -l sympozium.ai/channel=slack -fThen DM the bot (or @mention it in a channel) — it spawns an AgentRun and replies in‑thread.
- Open or
@mentiona feature request in Slack (or file an issue in the target repo). - The Crew Manager (
cto) routes it to the right phase lead — research to Mary, requirements to John, architecture to Winston, story decomposition to the Story Writer. - Amelia (
code-reviewer,qwen3.5:122b) implements the next ready story, writes tests, and opens a single PR with a HANDOFF block; the Testing Architect adds coverage. - Amelia (review mode) and the Challenger run the adversarial layers and post APPROVE or REQUEST_CHANGES; the O11y Engineer checks the observability story.
- On APPROVE the Crew Manager merges; on REQUEST_CHANGES it loops back to implementation.
- Watch the whole crew in Slack, and the spans in your observability backend.
kubectl -n default get agentruns -wBecause it's an IsItObservable episode, we don't use Sympozium's built‑in collector. deployment.sh
deploys the OpenTelemetry Operator (cert‑manager is its prerequisite — already installed in the
platform step) and two collectors in the default namespace (infrastructure/observability/):
| Collector | Mode | Receives | Forwards |
|---|---|---|---|
oteld |
daemonset | node logs (filelog from /var/log/pods) + its own Prometheus metrics, k8s‑enriched |
Dynatrace (otlphttp) |
otel |
statefulset (gateway) | OTLP traces/metrics/logs from the app + cluster‑wide Prometheus scrape (kubernetes-pods) |
Dynatrace (otlphttp) |
Both run the contrib image otel/opentelemetry-collector-contrib:0.154.0 under the otelcontribcol
ServiceAccount (rbac.yaml). Their pipelines all follow the rule memory_limiter first, batch
last, with k8sattributes + resource + transform (Dynatrace dt.kubernetes.* enrichment) and
cumulativetodelta on metrics. They read the Dynatrace endpoint + token from a dynatrace Secret
(dynatrace_oltp_url / dt_api_token) that deployment.sh creates from DT_TENANT_URL / DT_API_TOKEN.
The episode values.yaml points the whole control plane and every agent instance at the gateway
(observability.endpoint and defaultInstance.otlpEndpoint → otel-collector.default.svc.cluster.local:4317),
tagged deployment.environment=isitobservable-episode. So agent runs, the controller, apiserver and
mcp‑bridge flow OTLP → otel gateway → Dynatrace; you'll see gen_ai.usage.* token metrics and
end‑to‑end agent traces across the whole BMAD loop.
On top of the OTLP path, deployment.sh installs the Dynatrace Operator (Helm,
oci://public.ecr.aws/dynatrace/dynatrace-operator, csidriver.enabled=false) and a DynaKube
(infrastructure/observability/dynatrace/dynakube.yaml) for infrastructure + Kubernetes API monitoring
and metadata enrichment (apiUrl: <DT_TENANT_URL>/api, ActiveGate with kubernetes-monitoring,
routing, dynatrace-api, metrics-ingest; Istio support on). It needs a dynakube Secret in the
dynatrace namespace (apiToken, optional dataIngestToken).
It also deploys the Dynatrace MCP as a dynatrace-mcp MCPServer CR
(sympozium/mcp/dynatrace-mcp.yaml, image mcp/dynatrace-mcp-server) wired
into the O11y Engineer agent (spec.agentConfigs[].mcpServers). That gives the agent a read‑only
slice of Dynatrace — list_problems, execute_dql, generate_dql_from_natural_language,
find_entity_by_name, chat_with_davis_copilot … — so it can validate live telemetry and surface
problems during its observability sweeps. Credentials come from a dynatrace-mcp-secret
(DT_ENVIRONMENT = the .apps. platform URL, DT_PLATFORM_TOKEN).
agents/control plane ──OTLP──▶ otel (gateway, default ns) ──▶ Dynatrace
nodes (logs+metrics) ─filelog▶ oteld (daemonset, default ns) ─▶ Dynatrace
infra/K8s API ────────── DynaKube / ActiveGate ────────▶ Dynatrace
O11y Engineer agent ◀──query── Dynatrace MCP (dt.* tools) ◀──── Dynatrace
Sympozium emits these OTel signals — enough to build a dashboard per agent and watch the BMAD loop:
| Signal | What it tells you |
|---|---|
sympozium.agent.runs, sympozium.agent.run.completed |
run throughput per agent / per persona |
sympozium.agent.duration_ms |
how long each agent run takes (per persona — e.g. architect vs review vs o11y) |
sympozium.tool.invocations |
which tools each agent uses, how often |
sympozium.access.denied |
policy in action — tool calls blocked by bmad-guardrails |
sympozium.pod.create, sympozium.errors |
scheduling activity and failures |
gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.chat |
local‑model token cost per run |
Attributes like sympozium.instance.name, sympozium.agentrun.id and deployment.environment let you
slice all of the above by agent and by episode.
This episode is meant to exercise most of Sympozium's surface — Ensemble, Agents, SkillPacks, Schedules,
Channels, memory, SympoziumPolicy, and the Agent Sandbox — and call out where observability runs
thin. The biggest gap today: when the Agent Sandbox is on, Sympozium does not emit per‑sandbox
resource metrics (the gVisor sandbox's own CPU/memory/syscalls). You can see that a run was sandboxed
(runtimeClassName=gvisor, sympozium.pod.create) and its agent‑level telemetry, but for sandbox
resource cost you currently fall back to kube-state-metrics + cAdvisor. Channel sidecars also emit
limited OTLP today. These are tracked as follow‑ups for the episode.
Because telemetry routes through the two default‑namespace collectors (not Sympozium's built‑in one),
the collectors' own health is observable too: the otel gateway exposes enableMetrics: true and both
collectors self‑scrape their :8888 internal metrics — watch otelcol_exporter_send_failed_* and
otelcol_processor_dropped_* to confirm data is actually reaching Dynatrace.
You don't have to build the views by hand. infrastructure/observability/dynatrace/
ships three dtctl‑ready artifacts, every tile grounded in the signals above (all DQL validated live
on the episode tenant, coalesce(new, old) semconv so they survive the gen_ai.system → gen_ai.provider.name
rename):
| Artifact | What it shows |
|---|---|
Notebook — notebooks/notebook-sympozium-observability.yaml |
Metrics → logs → traces walkthrough (the agentrun → agent.run → gen_ai.chat / execute_tool / skill.exec tree). |
Dashboard — Control Plane Health — dashboards/dashboard-control-plane-health.yaml |
Component CPU/memory, restarts/OOM, run throughput, errors by reason, allowed‑vs‑denied, handoff latency, delegation, collector pipeline. |
Dashboard — Agent / Ensemble Run — dashboards/dashboard-agent-ensemble-run.yaml |
Token consumption (total/input/output), top tools · models · skills, slowest tools & skills, config‑tuning KPIs, and a Memory section (reads vs writes, retrieve vs store latency). |
Deploy them once your dtctl context points at your tenant (.apps. platform host):
cd infrastructure/observability/dynatrace
for f in dashboards/*.yaml notebooks/*.yaml; do dtctl apply -f "$f"; doneEach file pins a stable id, so re‑applying updates the document instead of creating a duplicate.
Full prerequisites, token scopes and a round‑trip edit workflow are in
infrastructure/observability/dynatrace/README.md.
kubectl -n default delete ensemble bmad-ensemble # removes the agents + their instances
kubectl -n default delete sympoziumpolicy bmad-guardrails
helm -n sympozium-system uninstall sympozium sympozium-crds
# Observability:
kubectl -n default delete opentelemetrycollector otel oteld
kubectl delete -f infrastructure/observability/opentelemetry-collector/rbac.yaml
kubectl -n sympozium-system delete mcpserver dynatrace-mcp
kubectl delete -f infrastructure/observability/dynatrace/dynakube.yaml 2>/dev/null || true
helm -n dynatrace uninstall dynatrace-operator
kubectl delete -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
# Proxmox: tear down the workload cluster from the management cluster:
kubectl delete cluster "$CLUSTER_NAME" -n default
# GKE: gcloud container clusters delete sympozium --region=europe-west1- Sympozium docs — https://deploy.sympozium.ai/docs/
- OpenTelemetry Operator — https://github.com/open-telemetry/opentelemetry-operator
- Ship OpenTelemetry to Dynatrace (OTLP) — https://docs.dynatrace.com/docs/ingest-from/opentelemetry
- Ensembles — https://deploy.sympozium.ai/docs/concepts/ensembles/
- Channels (Slack) — https://deploy.sympozium.ai/docs/concepts/channels/
- Using Ollama — https://deploy.sympozium.ai/docs/guides/ollama/
- Helm Chart reference — https://deploy.sympozium.ai/docs/reference/helm/
- Sympozium — https://github.com/AlexsJones/sympozium
- Sympozium API types (CRDs: Ensemble, Agent, SympoziumPolicy) — https://github.com/sympozium-ai/sympozium/tree/main/api/v1alpha1
- Kubernetes Agent Sandbox — https://github.com/kubernetes-sigs/agent-sandbox
- GKE Sandbox (gVisor) — https://cloud.google.com/kubernetes-engine/docs/concepts/sandbox-pods
- Paperclip‑Bmad‑Crew (source of the crew personas + flow) — https://github.com/henrikrexed/Paperclip-Bmad-Crew
- BMAD‑METHOD (upstream skills) — https://github.com/bmad-code-org/BMAD-METHOD
- Cluster API Provider for Proxmox (CAPMOX) — https://github.com/ionos-cloud/cluster-api-provider-proxmox
- BMAD Method — https://github.com/bmad-code-org/BMAD-METHOD
- csi-driver-nfs — https://github.com/kubernetes-csi/csi-driver-nfs
- Dynatrace MCP Server — https://github.com/dynatrace-oss/dynatrace-mcp
