Guía10 min

Truncar tool results en agentes: recortar el payload, no el contrato

Resumen

El tool_result cuenta en la ventana. Claude lo borra con clear_tool_uses_20250919 (umbral 100k tokens, keep 3). OpenAI exige un mensaje tool por id. Gemini Enterprise recorta historial a 32.000 caracteres. Esta guía fija qué recortar, qué jamás y el placeholder.

OpenAIAnthropicGemini
Flujo de un tool_result recortado antes de volver al modelo, con el id de la llamada intacto

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

El error no es “la tool devolvió mucho”. Es meter el HTML, el PDF o el dump SQL entero en el siguiente turno. Claude lo dice sin rodeos: el tool_result cuenta en la ventana igual que el system, las tools y las imágenes. OpenAI pide un mensaje role: tool por cada tool_call.id. Gemini, en Vertex, exige devolver todas las function_response. Recortar el payload está bien. Borrar el resultado o devolver uno de menos, no.

Esta guía no rediseña contratos —eso está en function calling confiable— ni cachea el prefijo —eso está en prompt caching. Aquí el foco es el cuerpo del resultado: qué cabe, qué se recorta y qué se deja fuera para RAG.

Fuentes verificadas el 3 de septiembre de 2026.

Qué cuenta (y por qué duele)

Claude: la ventana es todo lo que el modelo puede referenciar al generar, incluida su propia respuesta. En un request entran system, messages (con tool results, imágenes y documentos) y las definiciones de tools. El output también cuenta. Un prefijo cacheado sigue ocupando ventana: el caché cambia lo que pagas, no lo que cabe.

Si el input ya rebasa la ventana, Claude responde 400 invalid_request_error (“prompt is too long”). No hay “el modelo se las arregla”.

OpenAI documenta tool_calls como array. Cada item trae id. El follow-up es un mensaje tool con ese tool_call_id y el content del resultado. Asume varias llamadas. No hay, en esa página, un recorte automático del content.

Gemini (Vertex / Gemini Enterprise Agent Platform): si el modelo propone llamadas en paralelo, hay que devolver todas las function_response. Esa misma doc dice que la plataforma trunca el historial de conversación a 32.000 caracteres. No es un permiso para omitir una respuesta; es un techo de historial.

El runtime recorta el cuerpo del resultado y conserva el id de la llamada

Recorte en tu runtime (antes del modelo)

Hazlo , en el handler, no “espera a que el proveedor lo limpie”.

Regla: cada llamada sigue teniendo un resultado. El resultado puede ser corto.

const MAX = 4_000; // presupuesto tuyo, no constante del vendor

function clipToolResult(raw: string, tool: string) {
  if (raw.length <= MAX) return raw;
  return JSON.stringify({
    truncated: true,
    tool,
    chars: raw.length,
    head: raw.slice(0, MAX),
    hint: "Pide de nuevo con offset/id; no inventes la cola.",
  });
}

4_000 caracteres no está en ninguna doc. Es un techo operable para logs, HTML y búsquedas. Mídelo con el token counter del proveedor antes de mandar el turno.

Qué recortas:

  • Cuerpo de un archivo ya leído (read_file de 80 KB).
  • HTML / JSON crudo de un GET.
  • Hits de búsqueda (quédate con título + url + 2 líneas).

Qué no recortas:

  • id de la tool call (tool_use_id / tool_call_id).
  • Recibos de escritura: refund_id, message_id, ticket_id.
  • El error que el modelo necesita para reintentar. Claude pide is_error: true y el mensaje en content. Si lo recortas a “Error”, el modelo no sabe qué faltaba.

Claude: el tool_result puede ir vacío. Úsalo cuando la tool no tiene nada que devolver, no como truco para ahorrar tokens si sí hubo payload útil.

Claude también: el contenido no confiable (web, email, PDF de un usuario) vive dentro de tool_result, no en el system ni en un bloque user suelto. Recortar no te exime de esa frontera —sigue en prompt injection.

Claude: borrar resultados viejos en el servidor

Cuando el hilo es largo, recortar el último resultado no basta: los anteriores siguen en messages. Claude ofrece context editing (beta context-management-2025-06-27).

Estrategia clear_tool_uses_20250919:

ParámetroDefault documentadoQué hace
trigger100.000 input tokensA partir de ahí, limpia. También acepta umbral en tool_uses.
keep3 tool usesConserva los pares más recientes. Borra los más viejos primero.
exclude_toolsningunoNombres que nunca se limpian.
clear_tool_inputsfalsePor defecto solo borra el resultado. El tool_use (nombre + args) sigue visible.

El API sustituye el resultado viejo por un placeholder. Tu cliente sigue guardando el historial completo; el recorte ocurre antes de que el prompt llegue a Claude.

Implicación de caché: limpiar resultados invalida el prefijo cacheado en el punto del corte. Si priorizas hit rate, sube keep. Si priorizas ventana, bájalo.

exclude_tools para lo que no puedes reconstruir: charge, refund, send_message. Un read_file viejo sí se puede volver a pedir.

Compaction server-side (resumir el hilo) es otra palanca, para cuando el problema es la conversación entera, no el dump de una tool. No la uses para esconder un resultado que el modelo todavía necesita.

Los resultados viejos se sustituyen; los recibos de escritura se excluyen

OpenAI y Gemini: no hay “clear_tool_uses” gemelo

OpenAI: no documenta en function calling un borrado server-side de tool results. El recorte es tuyo. Sigue devolviendo un tool message por id. Si omites uno, el contrato del turno se rompe.

Gemini (Vertex): lo mismo en el lote paralelo —todas las function_response. El techo de 32.000 caracteres de historial en Gemini Enterprise Agent Platform es un recorte de plataforma, no un permiso para devolver menos funciones. Si tu agente vive ahí, no asumas que el turno 40 todavía tiene el read_file del turno 2: guarda el dato fuera (archivo, DB, RAG) y vuelve a buscarlo.

Fuera de esa plataforma, no inventes el mismo techo para la Gemini API pública.

Dónde vive el original

Si recortas, el original no se tira. Va a memoria / RAG o a un store tuyo (artifact_id → S3/R2/disco). El modelo recibe:

{"truncated":true,"artifact":"art_19","chars":81200,"head":"{ ...primeros 4k... }"}

Y una tool fetch_artifact({ id }) para el resto. Eso es más barato que reenviar 81k en cada turno, y sobrevive a clear_tool_uses y al techo de 32k de historial.

Checklist

  1. Cada tool_use / tool_call tiene resultado. Cero omisiones.
  2. El handler recorta dumps (archivos, HTML, búsquedas). No recorta ids ni recibos.
  3. Errores van con is_error: true (Claude) y texto útil.
  4. Claude largo: clear_tool_uses_20250919, keep: 3, exclude_tools para escrituras.
  5. El original vive fuera de la ventana. El modelo pide de nuevo por id.
  6. Mide tokens después del recorte, no antes.

Hub: construir agentes.