Files
fit-boilerplate-wox/docs/known-issues.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

11 KiB

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.

# 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:

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.

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:

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:

# 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:

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 SecurityOff

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:

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:

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:

# 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:

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:

@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:

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:

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:

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:

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".