Orquestar Message Batches API para cargas offline con custom_id unico, fragmentacion selectiva de fallos parciales y reintento con cap.
Construye orquestadores de la Message Batches API de Anthropic para procesar cargas masivas offline. [DOC] El patrón asigna un custom_id único por request para correlacionar resultados y aísla los fallos: cuando una fracción del lote falla, solo se reintentan esos custom_id, nunca el batch completo. [DOC] Ciclo de vida: create → poll processing_status → results() → fragmentar → retry selectivo. [CÓDIGO]
Entradas: lista de items con un ID de negocio estable, model, max_tokens, contenido del prompt, y un max_retries. [INFERENCIA]
Salidas: mapa custom_id → message para éxitos persistidos + lista de custom_id no resueltos tras agotar el cap, más (si se exige evidencia) un reporte JSON que pasa scripts/check.sh. [CONFIG]
false_positive_realtime en evals. [CONFIG]custom_id duplicados y el usuario pide saltar la validación de unicidad, o pide explícitamente sin custom_id / sin aislar fallos → rechaza, no degrades el patrón. [CONFIG]custom_id único y estable, derivado del ID de negocio (no de un índice de loop), para correlacionar y deduplicar de forma idempotente entre reintentos. [CÓDIGO]requests, valida unicidad de custom_id antes de enviar, y respeta los límites de tamaño/conteo del endpoint. [CÓDIGO]client.messages.batches.create(requests=...) y persiste el batch.id (checkpoint: sobrevive a un crash del orquestador). [CÓDIGO]processing_status con backoff hasta ended; nunca asumas finalización inmediata. [CÓDIGO]results(), indexando por custom_id. [CÓDIGO]result.type: succeeded → persiste; errored / expired / canceled → agrupa en sub-lote de reintento por custom_id. [CÓDIGO]custom_id afectados, aplicando el cap de reintentos; al agotarlo, devuelve los irresolubles para inspección, no en silencio. [CÓDIGO]custom_id = ID de negocio, no índice de loop. El índice rompe la correlación si el orden de items cambia entre reintentos; el ID de negocio es idempotente. [INFERENCIA]batch.id antes de hacer polling. Permite reanudar tras un crash sin recrear el batch (evita doble cobro). [INFERENCIA]expired/errored recurrentes producen un bucle infinito de creación de batches. [INFERENCIA]retrieve en batches que tardan minutos/horas. [INFERENCIA]Usa los assets de assets/ para certificar planes de batch: [CONFIG]
assets/message-batch-orchestration-contract.json: campos JSON obligatorios del reporte.assets/workload-policy.json: criterios offline, latency-tolerant, no streaming.assets/custom-id-policy.json: unicidad y estabilidad de custom_id.assets/lifecycle-policy.json: lifecycle create → poll processing_status → results.assets/retry-fragmentation-policy.json: fragmentación y retry selectivo con cap.assets/evidence-policy.json: evidencia mínima aceptada.Cuando el entregable sea JSON, valida offline con scripts/validate_message_batch_orchestration.py. [CÓDIGO] Para la smoke determinística completa ejecuta scripts/check.sh, que acepta fixtures válidos y rechaza mutaciones inválidas. [CÓDIGO]
# GOOD: batch offline con custom_id único + fragmentación selectiva de fallos
import time
from anthropic import Anthropic
client = Anthropic()
def build_requests(items):
seen = set()
requests = []
for it in items:
cid = it["id"] # ID de negocio estable, no índice de loop
if cid in seen:
raise ValueError(f"duplicate custom_id: {cid}") # gate de unicidad
seen.add(cid)
requests.append({
"custom_id": cid,
"params": {
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": it["prompt"]}],
},
})
return requests
def run_batch(items, max_retries=2):
pending = items
succeeded = {}
for attempt in range(max_retries + 1):
batch = client.messages.batches.create(requests=build_requests(pending))
# persiste batch.id aquí para poder reanudar tras un crash
while True:
status = client.messages.batches.retrieve(batch.id).processing_status
if status == "ended":
break
time.sleep(min(30, 2 ** attempt)) # backoff en el polling
failed = []
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
succeeded[result.custom_id] = result.result.message
else: # errored | expired | canceled -> aísla solo el fallo
failed.append(result.custom_id)
if not failed:
break
pending = [it for it in items if it["id"] in set(failed)] # retry selectivo
return succeeded, [it["id"] for it in pending if it["id"] not in succeeded]Modelo
claude-sonnet-4-5fijado por el contrato de evals de esta skill (large_dataset_checkpointing); cámbialo solo si el usuario nombra otro. [CONFIG]
# ANTI: loop síncrono real-time, sin custom_id, sin fail-isolation
for item in items: # uno por uno: caro y lento
resp = client.messages.create( # rompe rate limits a volumen
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": item["prompt"]}],
)
results.append(resp) # un fallo aborta todo el lote;
# sin custom_id no hay retry selectivoSi te sorprendes haciendo cualquiera de esto, vuelve a "Cómo construir": [INFERENCIA]
client.messages.create() en un loop sobre items offline → debías batchear.enumerate()/índice como custom_id → usa el ID de negocio.failed.processing_status != "ended" como terminal, o asumiendo finalización inmediata → sigue en polling con backoff.expired/errored recurrentes.Marca TODAS antes de dar por hecho el entregable: [DOC]
custom_id único y estable derivado del ID de negocio.custom_id se valida antes de enviar (gate que lanza en duplicado).processing_status usa backoff y espera el estado ended.result.type (succeeded aparte de errored/expired/canceled).scripts/check.sh.expired/canceled. Trátalos como fallidos reintegrables igual que errored; entran al sub-lote de retry bajo el mismo cap. [CÓDIGO]build_requests (no envíes); deduplica aguas arriba o pide regla de desempate. No silencies el duplicado. [CÓDIGO]batch.id persistido, reanuda con retrieve/results sin recrear el batch. [INFERENCIA]custom_id irresolubles para inspección; nunca retornes éxito vacío como si fuera completo. [INFERENCIA]katas-message-batch-processing.JaviMontano/jm-adk-beta
fdad39c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.