Saltar al contenido principal

El cuerpo de la tarea

El cuerpo del SKILL.md son las instrucciones que sigue el agente. En una ejecución, corre dentro del wrapper /kadu:agent-run-task, que ya preparó todo: helpers de instrumentación, fecha de referencia, registro de las llamadas HTTP y la finalización. No rehagas nada de eso.

Los helpers​

Los helpers están en /tmp/agent_state.sh. Cada bloque de shell del cuerpo empieza con source /tmp/agent_state.sh.

HelperAlimentaPara qué
emit_event '<json>'pestaña Historialun evento por hito de la ejecución, no en un bucle cerrado
append_log '<texto>'pestaña Registroslíneas de log
log_http_call '<texto>'pestaña Registrostoda llamada a la API de un sistema externo, con los secrets enmascarados
create_todo '<json>'pestaña Pendientespendiente que sobrevive a la ejecución
mark_body_started—centinela obligatoria (abajo)

:::danger No redefinas los helpers Redefinir cualquiera de ellos en el cuerpo de la skill saca el envío del camino que enmascara los secrets. Usa los que grabó el wrapper. :::

La centinela​

Al final de la ejecución, el wrapper comprueba si el cuerpo marcó que empezó. Sin la marca, registra execution.body_skipped y reprueba la ejecución (exit 2). Por eso el cuerpo empieza así:

source /tmp/agent_state.sh
emit_event "$(jq -nc '{type:"step.started",level:"info",step:"1",message:"Buscando os pagamentos de ontem"}')"
mark_body_started

Eventos: type y message son obligatorios​

Todo evento necesita type y message; level, step y payload son opcionales. Un evento sin message se rechaza (400) y el envío ignora el error: el evento desaparece del historial, sin nada en el log. Si un paso desapareció del historial, sospecha de esto primero.

ClaveConvención
typestep.started y step.completed por paso
stepnúmero del paso como string ("1", "2")
levelthinking, info, success, warning o error
messagelo que está pasando, en una frase, en el idioma de la ejecución
payloaddatos estructurados del paso (conteos, ids)

Logs y llamadas HTTP​

log_http_call es para llamadas a APIs de integración (Fintera, Pagar.me, Linear, Slack), una entrada por llamada:

source /tmp/agent_state.sh
RESP=$(curl -sS -w '\n%{http_code}' -u "$FINTERA_TOKEN:X" \
"https://financeiro.fintera.com.br/entities/$FINTERA_ENTITY_ID/receivable_accounts?per_page=50")
log_http_call "$(printf 'GET /receivable_accounts?per_page=50\n→ %s' "$(tail -n1 <<<"$RESP")")"

Una herramienta de alto volumen (scanner, crawler, carga por lotes) hace cientos o miles de solicitudes: registrar cada una ahoga la pestaña Registros y esconde las llamadas que importan. Resume en una línea de append_log (nuclei: 1.240 requisições → 3 achados) y reserva log_http_call para las APIs.

No hay usuario en la ejecución​

La ejecución es automática. Toda pregunta es un bloqueo silencioso: la tarea queda detenida hasta agotar el límite de 60 minutos. Donde una skill interactiva preguntaría, la tarea decide y registra:

SituaciónQué hacer
Dato ambiguoelige la lectura más probable, di cuál elegiste y crea un pendiente con la duda
Un elemento fallócrea un pendiente con el error real y sigue con los siguientes
Volumen por encima de lo esperadoprocesa hasta el tope y crea un pendiente listando el excedente
Falta un prerrequisito absolutoguarda el resultado diciendo dónde se detuvo y sal con código distinto de cero
source /tmp/agent_state.sh
create_todo "$(jq -nc --arg doc "$CNPJ" \
'{title:"Pagamento sem conta a receber correspondente",description:"Nenhuma conta em aberto para o CNPJ no valor pago.",payload:{cnpj:$doc,valor:1520.00}}')"

Escribe la skill para los dos modos cuando también sea útil en Claude Code, con una persona al otro lado:

if [ -n "${GITHUB_ACTIONS:-}" ]; then MODE=task; else MODE=local; fi

Entradas​

VariableQué trae
$PARAMETERSlos parámetros del lanzamiento, en JSON, ya validados contra el parameters_schema
$REFERENCE_DATEla fecha de referencia (AAAA-MM-DD): el "hoy" de la ejecución
$LOCALEel idioma de quien lanzó; escribe eventos y resultado en él
$INPUT_FILESuna ruta por línea, solo con accepts_input_files: true. Los archivos ya están en disco.
$OUTPUT_FILES_DIRsolo con generates_output_files: true: todo lo que se guarde aquí se publica
secretscada clave de required_secrets llega como variable de entorno ($FINTERA_TOKEN)
source /tmp/agent_state.sh
PARAMS="${PARAMETERS:-}"; [ -z "$PARAMS" ] && PARAMS='{}'
REPO_URL=$(jq -r '.repo_url // empty' <<<"$PARAMS")

No inventes un valor ausente. Un parámetro esencial vacío es una falla honesta, y no un motivo para adivinar.

La finalización: tú grabas el archivo, el wrapper lo envía​

Al final, graba ./.execution-result.json con result y summary:

source /tmp/agent_state.sh
RESULT=$(cat <<'MD'
## Resumo

12 pagamentos baixados, 2 pulados (ver pendências).
MD
)
SUMMARY='{"processados":12,"pulados":2}'
jq -n --arg result "$RESULT" --argjson summary "$SUMMARY" \
'{result:$result, summary:$summary}' > ./.execution-result.json
  • result no es un estado. Es el Markdown final que muestra la pestaña Resultado, en el idioma de $LOCALE. {"result":"success"} hace que la pestaña muestre la palabra "success".
  • summary es un objeto de contadores, y no una frase.
  • Quien envía el resultado a la consola es el wrapper, a partir de ese archivo. No llames a la finalización en el cuerpo y nunca reenvíes los logs al final: la pestaña Registros se alimenta poco a poco, y un reenvío la sobrescribe.

Error fatal a mitad de camino: graba igualmente el .execution-result.json diciendo dónde se detuvo, y sal con código distinto de cero. El estado de falla viene del código de salida; no intentes marcar la falla por tu cuenta.

Tareas conversacionales​

Con conversational: true, la persona puede pedir ajustes después de que termina la ejecución (tarjeta Conversación en la consola, o POST /v1/executions/{id}/continue). Cada pedido crea una ejecución hija, y la ronda hija recibe:

  • los mismos parámetros, fecha de referencia y entorno de la anterior. El pedido llega en $ADDITIONAL_INSTRUCTIONS;
  • la sesión del agente de la anterior: el modelo ve lo que leyó, ejecutó y produjo;
  • los archivos de la anterior, en las mismas rutas: .execution-files/input/, .execution-files/output/ ($OUTPUT_FILES_DIR) y .execution-files/state/. El resto del directorio de trabajo se descarta entre rondas.

El wrapper y el cuerpo corren de nuevo, desde el principio: el modelo decide, por el pedido y por el historial, qué rehacer. Esto cambia cómo se escribe el cuerpo:

ReglaPor qué
Guarda en .execution-files/state/ lo que valga la pena reaprovechar (guion, assets, proyecto de edición)Es el único directorio de trabajo que se preserva, además de las entradas y salidas.
Guarda las salidas en $OUTPUT_FILES_DIR como siempre, sobrescribiendo el archivo que cambiaUna salida restaurada que vuelve sin cambios no se publica de nuevo; la modificada se publica como salida de la nueva ronda.
Haz idempotente todo efecto en un sistema externoEl cuerpo corre de nuevo en cada ronda. Sin verificar antes de crear, una ronda que solo ajusta un texto duplica el cobro, la cuenta o el correo de la primera.
Escribe el result de cada ronda como el estado actual completoLa pestaña Resultado de cada ejecución muestra solo su propio result.
No dependas de nada fuera de .execution-files/Los paquetes instalados, /tmp y el resto del directorio de trabajo no sobreviven.
Evita intermediarios grandesSesión, entradas, salidas y estado suman como máximo 1 GiB. Por encima de eso la ejecución termina normalmente, pero no se puede continuar.

Los dos ejecutores continúan conversaciones: claude_code retoma su transcript, goose su sesión (goose run --resume). La sesión es del ejecutor que la guardó: si el nivel o el override de ejecutor de la tarea cambia entre rondas, la siguiente se rechaza (409 EXECUTOR_CHANGED) y hay que lanzar la tarea de nuevo. En goose, los modelos más baratos de los niveles 1 y 2 reciben un historial que crece en cada ronda: comprueba que el resultado se sostiene después de algunas rondas.

Auxiliar que corre dentro de una tarea​

Una auxiliar pequeña (enviar un mensaje en Slack) no necesita instrumentación propia: la tarea que la llama registra el paso. Pero, cuando la tarea orquesta auxiliares que hacen la mayor parte del trabajo, las auxiliares mudas dejan el historial y los logs vacíos durante casi toda la ejecución.

La auxiliar llamada dentro de una ejecución corre en el mismo proceso y ve los helpers. Entonces instrumenta sus propios pasos:

PestañaQuién la alimenta
Historialla tarea abre y cierra cada fase; la auxiliar emite step.started/step.completed de sus pasos, con conteos (el elemento individual va a un archivo, no a un evento)
Registrosla auxiliar registra una línea por herramienta o lote
Pendientesla auxiliar crea los suyos
Resultadosolo la tarea graba el .execution-result.json; la auxiliar devuelve archivo y resumen

Documenta esto en una sección "Instrumentación de Kadu" de la auxiliar, para que quien la lea sepa en qué pestaña buscar.

Modelo de cuerpo​

# Olá, Kadu

Tarefa de exemplo: não acessa nenhum sistema externo e não faz perguntas.

## Passo 1 — início

```bash
source /tmp/agent_state.sh
emit_event "$(jq -nc '{type:"step.started",level:"info",step:"1",message:"Preparando o cumprimento"}')"
mark_body_started
PARAMS="${PARAMETERS:-}"; [ -z "$PARAMS" ] && PARAMS='{}'
NOME=$(jq -r '.nome // "time"' <<<"$PARAMS")
append_log "Parâmetros recebidos: $PARAMS"
```

## Passo 2 — resultado

Escreva um cumprimento curto para `$NOME`, no idioma de `$LOCALE`, citando a
data de referência (`$REFERENCE_DATE`). Grave o resultado:

```bash
source /tmp/agent_state.sh
RESULT="## Olá, $NOME!"
jq -n --arg result "$RESULT" '{result:$result, summary:{cumprimentos:1}}' > ./.execution-result.json
emit_event "$(jq -nc '{type:"step.completed",level:"info",step:"2",message:"Cumprimento gravado"}')"
```