Boilerplate completo para construir soluciones agénticas sobre IBM watsonx Orchestrate (ADK 2.x) con una capa web encima. Destilado de cotemar-poc-n1 y dun-casos-prueba. Incluye: - 11 docs (best practices, architecture patterns, ADK cheatsheet, observability, tool authoring, deployment, RUNBOOK, DEPLOY_TO_NEW_WOX, known-issues con 16 errores reales y fix, eval strategy, INDEX) - 13 templates WxO (orchestrator/specialist/single-meta agents, 3 connections, KB + runbook, observable_tool decorator, coercion helpers, Python tools, OpenAPI spec, backend filter endpoint, webhook validator HMAC, MCP connection) - 6 scripts (deploy idempotente con fallback ADK, undeploy, reset, check-adk-version, new-specialist scaffold, eval-agents runner) - 4 eval pieces (linter de best practices, runner, smoke-test, direct backend probe) + scenario templates - 8 subagentes Claude (.claude/agents/) — wxo-architect, wxo-agent-author, wxo-tool-author, runbook-author, mock-builder, backend-tool-builder, eval-author, web-layer-builder - Skill bundle fit-wxo-bootstrap/ (SKILL.md + 3 templates) listo para copiar a ~/.claude/skills/ - Web layer default FastAPI+HTMX con vista timeline observable + endpoint receptor de trazas - Docker compose Coolify-ready (healthcheck wget -qO-, sin labels Traefik, network split internal:true) - CI Gitea workflow con lint + smoke Best practices enforced por evals/lint_wxo_yaml.py: - A1: máx 10 tools por agente - A3: orchestrator sin tools de remediación - A6: agente sin propósito (react sin tools ni KB) - T1: @observable_tool obligatorio, no @tool directo - T3: _compat shim inline en tools.py - D1: docker-compose sin wget --spider - I-005: OpenAPI con description per-operation - I-009: sin labels Traefik manuales Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
WxO Best Practices
Destiladas de Cotemar (cotemar-poc-n1) y Dun (dun-casos-prueba). Cada regla
fue paga con dolor — el linter en evals/lint_wxo_yaml.py enforcea las
críticas, el resto es disciplina humana.
🏗 Arquitectura de agentes
A1 — Máximo 10 tools por agente
LLM se confunde con catálogos grandes. Más de 10 → degrada la precisión de selección de tool. Si necesitás más, partí en specialists. (enforced)
A2 — Especialistas por dominio, nada de "todistas"
Un agente que sabe AD + Ops + RRHH falla más que tres que saben cada uno lo suyo. Aunque sean específicos chicos, separá. (enforced — orchestrator no puede tener tools de remediación)
A3 — El orchestrator nunca remedia
Su único poder es clasificar, delegar y crear/leer tickets. Si necesitás
reset_password, restart_service, create_user — eso vive en un
specialist. (enforced)
A4 — Specialists se importan ANTES que el orchestrator
El orchestrator declara a los specialists como collaborators. Si no
existen aún en el tenant, el import del orchestrator falla. El
deploy-wxo.sh ya respeta el orden, no lo cambies.
A5 — Una KB por agente (least privilege) cuando hay KB
El especialista AD no debe poder leer el runbook de Ops. Si todos comparten una KB, el LLM cita el runbook equivocado y razona desde la fuente equivocada.
A6 — KB es opcional
Agentes API-driven o MCP-driven no necesitan KB. No declares KB vacía solo "por completitud" — eso confunde el linter. Si no la necesitás, omitila. (enforced — agente sin KB ni tools falla)
A7 — Patrón "meta-tool" para flujos lineales
Crítico. langgraph en WxO tiene recursion_limit=30 hardcoded. Cada
turno ReAct quema ~10 frames → más de 2 tool calls explota. Si tu flujo es
lineal y conocido (caso A → B → C → D), colapsá los 4 tools en uno
solo (run_full_case) que el backend orquesta internamente. El agente
solo invoca esa meta-tool. La observabilidad de los substeps se preserva
con write_audit (ver §O1).
🧠 Prompting
P1 — Hardcodear el modelo, documentar el fallback
llm: groq/openai/gpt-oss-120b es el preferred para tool-calling.
Fallback: meta-llama/llama-3-3-70b-instruct PERO con la advertencia de
que después del turno ~3 emite tool calls como texto plano (ver
known-issues.md).
P2 — REGLA #0 explícita en el prompt
"NUNCA escribas la tool call como texto. Usá el protocolo de tool_calls."
Esto es necesario porque el LLM (especialmente llama) tiene drift y empieza a printear tool calls. Codificar el anti-pattern en el prompt reduce la frecuencia.
P3 — Instrucciones con escenarios concretos
No "sos un agente útil que ayuda con tickets". Sí: ejemplos enumerados de
input → razonamiento → output esperado. Mínimo 3 ejemplos por agente. Ver
cotemar-poc-n1/wxo/agents/mesa_n1_cotemar.agent.yaml como referencia.
P4 — Tabla de escalamiento explícita en el prompt
Si el agente puede escalar, la decisión "qué razón → qué grupo → qué persona" tiene que estar tabulada en el prompt, no implícita. El LLM no inventa, sigue tablas.
P5 — style: react por defecto
Para agentes con tools. Si no hace tool calling, podés usar style: chat,
pero es raro. (enforced — si un agente declara tools pero no react, warning)
🛠 Tools
T1 — @observable_tool siempre, nunca @tool directo
El decorator captura inputs/outputs/latency/side_effects y los emite como traza estructurada. Sin esto perdés visibilidad y las evals no pueden verificar comportamiento intermedio. (enforced)
T2 — Coerción de tipos en todo schema Pydantic
LLMs estringifican ints y arrays (mandan "95727067" cuando el schema
pide int). Pydantic 422 → agent falla. Toda input model lleva:
from pydantic import BaseModel, field_validator
from wxo.tools.python._coercion_helpers import _coerce_int, _coerce_list
class MyInput(BaseModel):
company_id: int
groups: list[str]
_coerce_company_id = field_validator("company_id", mode="before")(_coerce_int)
_coerce_groups = field_validator("groups", mode="before")(_coerce_list)
(enforced)
T3 — _compat.py inline en cada archivo de tools
La ADK importa cada tools.py standalone. Si tu archivo hace
from .._compat import ..., el TRM falla con ModuleNotFoundError.
El shim de compat se inlinea al inicio de cada archivo. El template ya
lo trae. (enforced)
T4 — Toda tool lee BASE_URL del environment de su connection
Nunca hardcodear la URL del sistema externo. La connection inyecta
BASE_URL. Cambio de tenant = cambio de credencial, no de código.
T5 — Una connection = un dominio = un app_id
3 conexiones (unus, dynatrace, ad) en Cotemar = 3 dominios. Mezclar varios dominios en una connection rompe el aislamiento de credenciales.
T6 — Conexiones en BOTH draft Y live
Si solo configurás draft, el agents deploy --env live falla con
kid not found. El deploy-wxo.sh hace los dos pases siempre.
T7 — Tipos de tool: elegir el correcto
- OpenAPI (preferido si tenés backend): un solo spec, ADK importa todo, version-controlled
- Python
@tool(preferido para mocks): rápido para prototipar, fácil de testear - MCP (preferido si el sistema externo lo expone): trae types + permisos
- Ver
tool-authoring-guide.mdpara el árbol de decisión.
T8 — OpenAPI: description per-operation OBLIGATORIO
ADK falla con "No description provided" si solo hay summary. Si tu
backend usa FastAPI sin docstrings, exponer el spec así:
op["description"] = op.get("summary") or "Endpoint XYZ"
(enforced — el linter lee el spec y verifica)
T9 — OpenAPI: security per-operation, NO global
ADK no hereda el security global. Re-declaralo en cada operación.
T10 — Filtrar el spec OpenAPI que exponés a WxO
No mandes el openapi.json completo de tu backend. Filtralo a una
allowlist PUBLIC_TOOLS y resolvé $ref transitivamente para que solo
se expongan los schemas usados. Ver wxo/tools/openapi/_backend_filter_endpoint.py.
🔌 Conexiones
C1 — key_value para mocks sin auth, api_key para producción
Mocks demo: auth_type: key_value con un solo campo BASE_URL. Producción:
auth_type: api_key con header custom (no Authorization).
C2 — Header custom, no Bearer
Usar Authorization: Bearer … choca con el IAM token que Orchestrate ya
inyecta para llamar al tool. Usar X-Orchestrate-Token o similar.
C3 — set-credentials en cada deploy
Tu deploy-wxo.sh invoca connections set-credentials siempre, no solo
al crear. Rotación de secretos = re-deploy del script.
C4 — Fallback de subcomandos ADK
Diferentes versiones de la ADK usan set-identifier --url vs
configure --url. El script intenta los dos y warnea si ninguno existe.
📚 Knowledge Bases
KB1 — Runbooks como .txt con secciones fijas
- Trigger (cuándo aplica)
- Precondiciones (qué chequeo antes)
- Pasos (1, 2, 3 — verbo en imperativo)
- Éxito (qué confirma que salió bien)
- Fallo (qué hacer si falla cada paso)
- Escalamiento (a quién/cómo si excede autoridad)
KB2 — Embeddings: ibm/slate-125m-english-rtrvr
Default sano para español + inglés mezclados. Si tu corpus es solo español, evaluá modelos multilingual de IBM.
🚢 Deploy / Coolify
D1 — Healthcheck con wget -qO- o curl -sf
wget --spider es buggy en busybox → marca container unhealthy → Traefik
da 503. (enforced en lint del docker-compose)
D2 — NO declarar labels Traefik manuales en docker-compose
Coolify v4 los autogenera del panel FQDN. Si los declarás vos, sale "TRAEFIK DEFAULT CERT" en lugar de Let's Encrypt. Solo:
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
D3 — Network split: internal: true para servicios sin internet
Solo el web/landing va al network coolify. Backend, DB, workers → red
interna. Mejor aislamiento + cero exposición accidental.
D4 — --force-recreate después de cambiar .env
restart no relee variables de entorno. Si cambiás .env, hacé
docker compose up -d --force-recreate.
🔍 Observabilidad
O1 — write_audit("tool.<name>", payload) desde el backend
Cada handler escribe un audit log con el case_id o correlation_id. La
UI reconstruye el timeline desde acá, no desde trazas de Orchestrate
(que son volátiles y no queryable).
O2 — Outputs persistidos para granular retry
ExecutionStep.output_payload guarda content_b64, zip_b64,
package_url. Si el caso falla en el paso 4, reanudás desde 3 sin
regenerar 1-2.
O3 — Webhook receiver con HMAC-SHA256
Si recibís webhooks de WxO (Plan A o run lifecycle), validá firma:
X-Orchestrate-Timestamp + Nonce + Signature, skew tolerance 300s,
408 si stale, 401 si firma mala.
O4 — Watchdog para estados atascados
Si un caso queda en executing >90s, el siguiente GET lo auto-promueve
a paused para que el usuario reintente. Sin esto, conexiones caídas
dejan basura.
🧪 Evals
E1 — Cuatro capas de eval
- Native —
orchestrate agents test <agent> --input-file scenario.json(fixture-based) - Agent behavior — verifica delegation/escalation correcta sobre dataset
- Runbook compliance — verifica que el ticket final tenga campos esperados
- Web layer — Playwright o requests asserts sobre la UI
Ver eval-strategy.md para detalles.
E2 — Fixtures JSON versionados
Bajo evals/scenarios/*.json, ejecutables con orchestrate agents test.
Cada scenario cubre un caso (happy path, edge case, failure).
E3 — Smoke test en bash
evals/smoke-test.sh cubre login → create → execute → poll → assert.
Útil para CI y para reproducir bugs reportados.
E4 — Direct backend probe
evals/direct-backend-probe.sh golpea el backend directamente con curl +
el token para aislar "¿es problema del agente o de la auth?". 200 con
error de lógica → agent issue. 401 → connection/credentials.
🚦 Versionado del ADK
V1 — Pin a ibm-watsonx-orchestrate==2.1.0
Cambios entre minor versions han roto schemas. requirements.txt pinea
exact version.
V2 — check-adk-version.sh avisa de versiones nuevas
Compara el pin con el último en PyPI y warneaa. NO actualices sin probar todas las evals primero.
Para ver dónde se aplica cada regla, leé el código del linter:
../evals/lint_wxo_yaml.py.