feat: v1 — boilerplate WxO + web
Some checks failed
CI — Lint + Evals / lint (push) Has been cancelled
CI — Lint + Evals / smoke (push) Has been cancelled

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>
This commit is contained in:
Felipe Arentsen
2026-05-16 14:59:44 +00:00
commit b089c2ef18
70 changed files with 6739 additions and 0 deletions

407
docs/known-issues.md Normal file
View File

@@ -0,0 +1,407 @@
# Known Issues — Errores reales con su fix
Cada item acá fue pagado en sangre por Cotemar o Dun. Cuando te toque
debuggear, leé primero, googleá después.
---
## I-001 — `recursion_limit reached` (langgraph)
**Síntoma:** después de 2-3 tool calls el run muere con
`GraphRecursionError`.
**Causa:** WxO usa langgraph internamente con `recursion_limit=30`
hardcoded (no configurable vía YAML ni API). Cada turno ReAct quema
~10 frames.
**Fix:** Si tu flujo es lineal y conocido, **patrón meta-tool**: colapsá
N tools en una sola `run_full_case(id)` que el backend orquesta
internamente. El agente solo invoca esa tool.
```python
# Antes: agent llama 5 tools en orden
@tool def get_company(id): ...
@tool def search_credits(id): ...
@tool def build_profile(id): ...
@tool def package_zip(id): ...
@tool def upload(id): ...
# Después: agent llama 1 meta-tool
@tool
def run_full_case(case_id):
# Backend orquesta los 5 pasos internamente
# cada uno emite write_audit(...)
return {"package_url": "...", "audit_url": "..."}
```
**Origen:** Dun commit `895f5ee feat(orchestrate): consolidar 11 tools → run_full_case`.
---
## I-002 — Llama 3.3 70B printea tool calls como texto
**Síntoma:** después del turno ~3, el agente "responde" con texto que
literalmente dice:
```
Tool call: reset_password(username="juan.perez@cotemar.com")
```
en lugar de invocar la tool.
**Causa:** Drift del modelo Llama 3.3 70B con tool calling. Documentado
por IBM, mitigación recomendada: cambiar de modelo.
**Fix:**
1. **Cambiar `llm:` a `groq/openai/gpt-oss-120b`** en el YAML del agente.
2. Agregar **REGLA #0** explícita en el prompt:
> "NUNCA escribas la tool call como texto. Usá el protocolo de tool_calls."
**Origen:** Dun commit `84be316`.
---
## I-003 — Pydantic 422 "value is not a valid integer"
**Síntoma:** Una tool falla con `ValidationError: value is not a valid
integer` aunque vos jurás que mandás un int.
**Causa:** El LLM stringifica todo. Manda `"95727067"` cuando el schema
es `int`. Pydantic v2 strict mode rechaza.
**Fix:** Coerción explícita en el schema:
```python
from pydantic import BaseModel, field_validator
def _coerce_int(v):
if isinstance(v, str) and v.strip().isdigit():
return int(v)
return v
def _coerce_list(v):
if isinstance(v, str):
import json
try: return json.loads(v)
except: return [v]
return v
class ResetPasswordInput(BaseModel):
user_id: int
groups: list[str]
_coerce_user_id = field_validator("user_id", mode="before")(_coerce_int)
_coerce_groups = field_validator("groups", mode="before")(_coerce_list)
```
El template trae estos helpers en
[`wxo/tools/python/_coercion_helpers.py`](../wxo/tools/python/_coercion_helpers.py).
**Origen:** Dun commit `3cdf3bf`.
---
## I-004 — `kid not found` al `agents deploy --env live`
**Síntoma:** import del agente sale OK al draft, pero `deploy --env live`
falla con `kid not found`.
**Causa:** Las conexiones que el agente usa solo existen en el env `draft`.
La promoción a `live` requiere que las connections existan también en `live`.
**Fix:** Configurar connection en **ambos** envs:
```bash
for ENV in draft live; do
orchestrate connections configure -a mi_app --env $ENV --type team --kind key_value
orchestrate connections set-credentials -a mi_app --env $ENV -e "BASE_URL=..."
done
```
El `deploy-wxo.sh` del template ya lo hace.
**Origen:** Cotemar (debug session 2026-05-13).
---
## I-005 — OpenAPI import: "No description provided"
**Síntoma:** `orchestrate tools import -k openapi` falla con `No
description provided for operation X`.
**Causa:** ADK exige `description` por operación. FastAPI genera solo
`summary` cuando la función no tiene docstring.
**Fix:** Al exponer el spec, forzar el fallback:
```python
# En tu endpoint /orchestrate-tools-spec.json
for path_item in spec["paths"].values():
for op in path_item.values():
if isinstance(op, dict) and "responses" in op:
op["description"] = op.get("description") or op.get("summary") or "..."
```
**Origen:** Dun `backend/app/api/v1/spec.py:60-66`.
---
## I-006 — OpenAPI security no se hereda
**Síntoma:** Spec tiene `security:` global, pero ADK importa los tools
sin auth.
**Causa:** ADK no respeta `security` global. Requiere per-operation.
**Fix:** Re-declarar en cada op:
```python
for path_item in spec["paths"].values():
for op in path_item.values():
if isinstance(op, dict) and "responses" in op:
op["security"] = spec.get("security", [])
```
**Origen:** Dun commit `1bc04cf`.
---
## I-007 — Embed widget queda en "Cargando agente..."
**Síntoma:** Tu landing page con `<script src=".../webchat.js">` carga,
pero el widget muestra para siempre el spinner.
**Causa:** Embed Security ON en la configuración del tenant. Sin
credenciales anónimas el widget no puede iniciar la sesión.
**Fix manual (UI):**
> WxO Console → Settings → **Embed Security** → **Off**
Documentado en IBM:
<https://developer.watson-orchestrate.ibm.com/channels/establishing_channels>
**Origen:** Cotemar (2 horas de debug).
---
## I-008 — Coolify/Traefik 503 después de deploy
**Síntoma:** El container está corriendo (`docker ps` muestra healthy en
status... eventualmente unhealthy), pero el dominio público devuelve 503.
**Causa:** Healthcheck con `wget --spider` es buggy en busybox alpine —
a veces marca unhealthy aunque el endpoint responda. Traefik excluye el
backend.
**Fix:** Cambiar a `wget -qO-` o `curl -sf`:
```yaml
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/health"]
# o
test: ["CMD", "curl", "-sf", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
```
**Origen:** Cotemar (debug Coolify).
---
## I-009 — Coolify cert "TRAEFIK DEFAULT CERT"
**Síntoma:** El dominio sale con cert de Traefik default en vez de Let's
Encrypt.
**Causa:** Declaraste labels Traefik manuales en `docker-compose.yml`.
Coolify v4 los autogenera del panel FQDN.
**Fix:** Borrar todos los `traefik.http.routers.*` del compose. Dejar
solo:
```yaml
labels:
- "traefik.enable=true"
- "traefik.docker.network=coolify"
```
Y configurar el FQDN desde el panel de Coolify.
**Origen:** Dun commit `373a336`.
---
## I-010 — Tool error `ModuleNotFoundError: wxo.tools._compat`
**Síntoma:** El tool falla en runtime con import error referenciando
`_compat`.
**Causa:** ADK importa cada `tools.py` standalone, sin el package padre.
Los imports relativos del estilo `from .._compat import X` fallan.
**Fix:** Inlinear el shim al inicio del archivo:
```python
# tools/python/mi_tool.py
# === inline compat shim (NO eliminar — TRM lo necesita) ===
try:
from ibm_watsonx_orchestrate.agent_builder.tools import tool
except ImportError:
def tool(*args, **kwargs):
def deco(f): return f
return deco if not args else deco(args[0])
# ============================================================
@tool(name="mi_tool", ...)
def mi_tool(...):
...
```
El template ya viene así. **No reorganizar los imports.**
**Origen:** Cotemar.
---
## I-011 — `.env` no se recarga con `restart`
**Síntoma:** Cambiás `.env` y hacés `docker compose restart`, pero el
container sigue con los valores viejos.
**Causa:** `restart` no relee env vars. Solo `up` lo hace.
**Fix:**
```bash
docker compose up -d --force-recreate
```
**Origen:** Dun `INSTRUCCIONES.md:159-164`.
---
## I-012 — Caso atascado en `executing`
**Síntoma:** Un caso queda para siempre en estado `executing`. Polls
desde la UI no avanzan.
**Causa:** La conexión Orchestrate → backend se cayó silenciosamente
durante el run. El backend nunca recibió el callback final.
**Fix:** Watchdog en el GET del caso:
```python
@router.get("/cases/{case_id}")
def get_case(case_id):
case = db.get(case_id)
if case.status == "executing":
age = datetime.utcnow() - case.last_update
if age > timedelta(seconds=90):
case.status = "paused"
db.commit()
return case
```
**Origen:** Dun commit `23b815e`.
---
## I-013 — `asyncio.create_task` deja casos colgados
**Síntoma:** A veces el `start_run` falla silenciosamente y el caso
queda en estado inicial sin error.
**Causa:** `asyncio.create_task(orchestrate_start(...))` no espera ni
captura excepciones.
**Fix:** Usar `await` + wrapper que captura:
```python
async def _start_run_safe(case_id, ...):
try:
await orchestrate.start_run(...)
except Exception as e:
logger.exception("start_run failed")
await db.update_case(case_id, status="failed", error=str(e))
await _start_run_safe(case_id, ...)
```
**Origen:** Dun commit `fa47f14` + `abb5b73`.
---
## I-014 — Subcomando ADK cambió de nombre
**Síntoma:** Tu script de deploy falla porque `connections set-identifier`
no existe (o `connections configure --url` no existe).
**Causa:** Entre versiones del ADK los subcomandos se renombran.
**Fix:** Fallback en el script:
```bash
orchestrate connections set-identifier --url "$URL" -a mi_app 2>/dev/null \
|| orchestrate connections configure --url "$URL" -a mi_app 2>/dev/null \
|| echo "warning: no compatible subcommand found"
```
**Origen:** Dun `deploy.sh:65-74`.
---
## I-015 — Frontend descarga el index.html en vez del archivo real
**Síntoma:** Click en un botón de descarga, abre el archivo y resulta
ser el HTML de la SPA.
**Causa:** La función `downloadAuthenticated` confunde `fetchUrl` con
`blobUrl` (misma variable). El navegador redirige a la SPA.
**Fix:**
```ts
async function downloadAuthenticated(fetchUrl: string, filename: string) {
const res = await fetch(fetchUrl, { headers: authHeaders });
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob); // OJO: variable distinta
const a = document.createElement("a");
a.href = blobUrl;
a.download = filename;
a.click();
URL.revokeObjectURL(blobUrl);
}
```
**Origen:** Dun commit `4e7e3fd`.
---
## I-016 — Polling para de actualizar después de approve
**Síntoma:** UI hace polling cada 1.5s. Click en "approve" y el polling
muere.
**Causa:** El `useEffect` con `setInterval` no re-agenda el timer
después del fetch.
**Fix:** Re-agendar dentro del callback del fetch:
```ts
useEffect(() => {
let timer: number;
const poll = async () => {
const data = await fetchCase(id);
setData(data);
if (!["done", "failed", "paused"].includes(data.status)) {
timer = window.setTimeout(poll, 1500);
}
};
poll();
return () => clearTimeout(timer);
}, [id]);
```
**Origen:** Dun commit `83fb85c`.
---
Si pasaste varias horas buscando un bug y al final encontraste el fix,
agregalo acá. Este archivo es el repositorio de "lecciones que no quiero
volver a pagar".