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
iddel 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.