Saltar al contenido principal

10 · Agentes y AI Engineering

Tool calling y harness

El modelo pide, tu programa ejecuta. Esquemas de herramientas, validación de argumentos y el runtime que controla esa frontera.

13 minCaso de estudio: docket (se abre en una ventana nueva)

Un modelo solo produce texto. Para que un sistema haga algo (leer un archivo, consultar una base de datos, abrir un pull request) hay que conectar ese texto con código ejecutable. El mecanismo estándar es el , y su regla fundamental cabe en una frase: el modelo pide, tu programa ejecuta.

En esa frontera se decide casi todo lo que importa en un . Veamos qué es una herramienta, qué hacer con un pedido antes de ejecutarlo y qué es el , el programa que controla esa frontera.

Qué es una herramienta

Una herramienta tiene cuatro partes, y solo tres llegan al modelo:

Parte Qué es Quién la usa
Nombre identificador único, p. ej. read_file el modelo, para pedirla
Descripción cuándo y para qué usarla el modelo, para decidir
Esquema de parámetros JSON Schema de los argumentos el modelo (genera) y el sistema (valida)
Handler el código que se ejecuta solo el sistema

Las tres primeras son la publicidad de la herramienta. El handler nunca sale del proceso. Esa separación es la base de todo el control posterior.

Del pedido a la ejecución

El ciclo de vida de un tool call tiene nueve pasos. Las demos suelen saltarse del 4 al 6; los sistemas serios concentran ahí su lógica.

1 register    build a registry {name -> Tool}
2 advertise   send specs to the model
3 select      model returns tool_calls [{id, name, arguments}]
4 parse       decode `arguments` (a JSON string)
5 validate    does the tool exist? required args present?
6 authorize   may THIS agent run THIS call with THESE args?
7 execute     run the handler, with a timeout
8 package     wrap the output as a `tool` message, same id
9 feed back   the result goes to the model next iteration

Validar argumentos

En el formato compatible con OpenAI, arguments es un string que contiene JSON, no un objeto. El modelo puede emitir JSON inválido, un array o un string vacío. La regla general es fail closed: si no puedes leer los argumentos, no puedes evaluar el pedido, y un pedido no evaluado no se ejecuta.

Los errores también son resultados

Hay que distinguir dos eventos que un sistema ingenuo mezcla: la herramienta corrió y falló (información útil para que el modelo corrija) y la herramienta no corrió porque no estaba permitida (un guardarraíl funcionando). En ambos casos hay que responder con palabras; un silencio lleva al modelo a reintentar a ciegas. Y el id del resultado debe coincidir con el del pedido: muchos endpoints rechazan la conversación entera si no.

# From src/docket/core/tools.py (ToolResult.as_tool_output)
if self.decision == "deny":
    kind = self.denial_kind or "invalid_call"
    return f"REFUSED [{kind}]: {self.reason}"
if self.decision == "ask":
    return f"AWAITING APPROVAL: {self.reason}"
if not self.ok:
    return f"ERROR: {self.error}\n{self.content}".strip()
return self.content

ToolResult separa executed de ok, y tool_result(call, content) en llm.py recibe el ToolCall completo para que un resultado nunca se atribuya a otra llamada.

Llamadas paralelas

Muchos proveedores permiten que una sola respuesta pida varias herramientas. Que se pidan juntas no obliga a ejecutarlas en paralelo: lecturas independientes admiten concurrencia; escrituras sobre el mismo recurso, no. docket las despacha en secuencia, en el orden pedido, y comprueba el cupo antes del lote completo (ver De la aplicación con LLM al agente). El mensaje del asistente y todos sus resultados se persisten juntos, como una unidad.

Qué es un harness

Resolver lo anterior exige mucho código que no tiene que ver con la tarea: límites, persistencia, permisos, aprobaciones, registro, contexto, manejo del proveedor. Ese código es el harness.

LLM       predicts text; executes nothing
Agent     LLM + goal + decision loop
Harness   code that runs that loop and decides what is allowed
Tools/Env the real world: files, processes, APIs, money

Muchos productos (Claude Code, Codex CLI, OpenHands) son a la vez agente y harness. Separarlos conceptualmente permite hacer la pregunta clave: ¿quién tiene la última palabra cuando el modelo pide ejecutar algo?

Poseer el bucle o envolverlo

Un framework de agentes suele ejecutar el bucle por ti. Entonces tus controles solo pueden vivir en los puntos de extensión que ofrezca (, callbacks, middleware). Si no hay un punto de intercepción antes de cada ejecución, no puedes decidir ahí.

Políticas testeadas y documentadas que no protegían nada: ese es el riesgo de envolver un bucle ajeno sin verificar dónde puedes intervenir. Usar un framework no está mal por sí mismo.

Para CTOs

La decisión es construir o adoptar el harness. Construirlo da control total de cada punto de intercepción, a cambio de mantener código no diferencial y perder integraciones. Adoptarlo es más rápido, pero tu gobernanza queda limitada a los hooks del proveedor. Si el requisito principal es controlar cada acción, exige una prueba: una política de denegación que efectivamente bloquee una llamada en tu entorno.

Un único chokepoint

Si existen dos caminos para ejecutar una herramienta y solo uno tiene controles, el sistema no tiene controles.

Un comentario que diga "no crees otro camino" es una regla que alguien puede ignorar. docket la convierte en un test: test_only_the_chokepoint_imports_the_handler_module, en tests/unit/core/test_tools.py, parsea con ast cada archivo de src/docket y falla si un módulo fuera de una lista permitida de cinco importa el módulo de handlers. Tests vecinos comprueban que dispatch_tool llama a evaluate_tool_call y que toolbox.py no contiene vocabulario de políticas. Un guard así cubre exactamente lo que codifica: vigila imports de toolbox, no cualquier atajo imaginable. Profundizamos en Testing y evals para agentes; las decisiones allow/ask/deny están en Gobernanza y human-in-the-loop.

Para llevar

  • Una herramienta es publicidad (nombre, descripción, esquema) más un handler que nunca llega al modelo.
  • Entre el pedido y la ejecución van parseo, validación y autorización; ante lo ilegible, fail closed.
  • Denegaciones y errores se devuelven al modelo en palabras, con el mismo id del pedido.
  • Quien posee el bucle posee los puntos de intercepción; envolver un bucle ajeno puede dejar tus políticas sin efecto.
  • Un único solo es real si algo lo verifica, por ejemplo un test que lee el código.

Comprueba lo aprendido

¿Por qué docket guarda los argumentos de un tool call como string crudo?

Para que la auditoría muestre exactamente lo que el modelo pidió, incluso JSON malformado, y para que el error de parseo lo decida el gate en lugar de convertirse silenciosamente en {}.

¿Qué evidencia llevó a docket a tomar el control del bucle (D-19)?

Cuatro plantillas de política pre_tool_call nunca se habían evaluado, porque el daemon externo poseía el turno y docket no tenía dónde engancharlas.

¿Qué garantiza el test AST de docket y qué no?

Garantiza que solo cinco módulos permitidos importan el módulo de handlers toolbox. No detecta caminos que no pasen por ese import: el handler de fetch vive en src/docket/edges/adapters/fetch.py, y un import directo de ese módulo desde otro lugar no lo haría fallar.