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.
| Helper | Alimenta | Para qué |
|---|---|---|
emit_event '<json>' | pestaña Historial | un evento por hito de la ejecución, no en un bucle cerrado |
append_log '<texto>' | pestaña Registros | líneas de log |
log_http_call '<texto>' | pestaña Registros | toda llamada a la API de un sistema externo, con los secrets enmascarados |
create_todo '<json>' | pestaña Pendientes | pendiente 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.
| Clave | Convención |
|---|---|
type | step.started y step.completed por paso |
step | número del paso como string ("1", "2") |
level | thinking, info, success, warning o error |
message | lo que está pasando, en una frase, en el idioma de la ejecución |
payload | datos 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ón | Qué hacer |
|---|---|
| Dato ambiguo | elige 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 esperado | procesa hasta el tope y crea un pendiente listando el excedente |
| Falta un prerrequisito absoluto | guarda 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
| Variable | Qué trae |
|---|---|
$PARAMETERS | los parámetros del lanzamiento, en JSON, ya validados contra el parameters_schema |
$REFERENCE_DATE | la fecha de referencia (AAAA-MM-DD): el "hoy" de la ejecución |
$LOCALE | el idioma de quien lanzó; escribe eventos y resultado en él |
$INPUT_FILES | una ruta por línea, solo con accepts_input_files: true. Los archivos ya están en disco. |
$OUTPUT_FILES_DIR | solo con generates_output_files: true: todo lo que se guarde aquí se publica |
| secrets | cada 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
resultno 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".summaryes 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:
| Regla | Por 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 cambia | Una 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 externo | El 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 completo | La 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 grandes | Sesió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ña | Quién la alimenta |
|---|---|
| Historial | la 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) |
| Registros | la auxiliar registra una línea por herramienta o lote |
| Pendientes | la auxiliar crea los suyos |
| Resultado | solo 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"}')"
```