Files
fit-boilerplate-wox/docs/wxo-best-practices.md
Felipe Arentsen b089c2ef18
Some checks failed
CI — Lint + Evals / lint (push) Has been cancelled
CI — Lint + Evals / smoke (push) Has been cancelled
feat: v1 — boilerplate WxO + web
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>
2026-05-16 14:59:44 +00:00

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.md para 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

  1. Nativeorchestrate agents test <agent> --input-file scenario.json (fixture-based)
  2. Agent behavior — verifica delegation/escalation correcta sobre dataset
  3. Runbook compliance — verifica que el ticket final tenga campos esperados
  4. 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.