<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Interrupt on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/interrupt/</link><description>Recent content in Interrupt on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 17 Sep 2026 02:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/interrupt/index.xml" rel="self" type="application/rss+xml"/><item><title>Agente de atención al cliente con LangGraph: pedidos, devoluciones y sustituciones con el límite de aprobación humana en el código</title><link>https://blog.lo0.es/posts/agente-cliente-devoluciones-langgraph-interrupt-hitl/</link><pubDate>Thu, 17 Sep 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/agente-cliente-devoluciones-langgraph-interrupt-hitl/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El límite de la aprobación humana no está en el prompt ni en la interfaz. Está en un middleware que intercepta la llamada a la herramienta con efecto, evalúa una condición sobre los argumentos, el estado y el contexto, y si se cumple para el grafo antes de que la herramienta corra. Verificado: la interrupción salta en un nodo posterior al modelo y anterior a &lt;code>tools&lt;/code>, y la herramienta no se ejecuta hasta que un humano decide.&lt;/p>
&lt;p>El humano puede aprobar, editar los argumentos, rechazar con motivo o responder en lugar de la herramienta. Son las cuatro decisiones que admite &lt;code>HumanInTheLoopMiddleware&lt;/code> en langchain 1.4.1, y las cuatro tienen un contrato de datos fijo que la aplicación de aprobaciones tiene que hablar. Editar el importe de un reembolso de 120 a 90 hace que la herramienta corra con 90; se ha comprobado con la herramienta instrumentada.&lt;/p>
&lt;p>&lt;code>interrupt()&lt;/code> sin checkpointer no falla al interrumpir. Devuelve el valor en &lt;code>__interrupt__&lt;/code> como si todo fuera bien, y falla al reanudar con &lt;code>Cannot use Command(resume=...) without checkpointer&lt;/code>. Un despliegue sin persistencia parece funcionar hasta que el primer encargado pulsa aprobar.&lt;/p>
&lt;p>Al reanudar, el nodo que interrumpió se ejecuta entero otra vez. Con un nodo propio, lo que haya antes de la interrupción corre dos veces. Con el middleware, la interrupción vive en su propio nodo y la herramienta corre después, una vez. Lo que sigue pudiendo correr dos veces es la herramienta cuando el pod muere con ella en vuelo, y por eso el reembolso lleva el identificador de la llamada como clave de idempotencia.&lt;/p>
&lt;p>El checkpointer no sabe listar hilos interrumpidos. La cola de aprobaciones es una tabla propia que la aplicación escribe cuando &lt;code>invoke&lt;/code> devuelve &lt;code>__interrupt__&lt;/code> y borra cuando se reanuda. Sin esa tabla, la única forma de saber qué está esperando es recordar los identificadores de hilo.&lt;/p>
&lt;p>El rechazo llega al modelo como un mensaje en inglés que empieza por &lt;code>User rejected the tool call&lt;/code>. El prompt le dice al modelo qué hacer con él. Sin esa línea, el modelo puede disculparse en inglés con un cliente de Talavera.&lt;/p>
&lt;h2 id="estás-aquí-el-segundo-caso-donde-el-ciclo-se-rompe">Estás aquí: el segundo caso, donde el ciclo se rompe&lt;/h2>
&lt;p>Este es el caso 2 de la &lt;a href="https://blog.lo0.es/posts/agentes-langgraph-retail-marco-serie/">serie de agentes con LangGraph para retail&lt;/a>. El &lt;a href="https://blog.lo0.es/posts/agente-empleado-tienda-langgraph-rag-tools/">caso 1&lt;/a> construyó un asistente de sala que cabe en un ciclo modelo-herramientas. Este caso usa el mismo &lt;code>create_agent&lt;/code>, el mismo contexto y el mismo checkpointer, y añade lo que el ciclo no tiene: parar, esperar a una persona que puede tardar un día, y seguir desde el mismo sitio.&lt;/p>
&lt;p>El agente atiende a un cliente por el canal web sobre un pedido concreto. Puede informar del estado, iniciar una devolución con reembolso, o proponer una sustitución por otra referencia con stock. Las dos últimas tienen efecto en el dinero o en el inventario. La política de la tienda dice cuáles se hacen solas y cuáles firma un encargado.&lt;/p>
&lt;h2 id="la-analogía-el-vale-de-devolución-con-casilla-de-firma">La analogía: el vale de devolución con casilla de firma&lt;/h2>
&lt;p>En caja, una devolución de 20 euros con ticket la hace el dependiente. Una de 200, o sin ticket, o de un cliente que ya devolvió tres cosas este mes, lleva un vale con una casilla que firma el encargado. El dependiente rellena el vale entero, lo deja en la bandeja y atiende al siguiente. El encargado pasa cuando puede, lee el vale, y firma, tacha el importe y firma otro, o lo devuelve con una nota.&lt;/p>
&lt;p>El agente es el dependiente que rellena el vale. El middleware es la casilla de firma. El checkpointer es la bandeja donde el vale espera. Y el contrato de la interrupción es el formato del vale: si el encargado no entiende lo que hay escrito, no firma.&lt;/p>
&lt;h2 id="la-política-en-una-función">La política, en una función&lt;/h2>
&lt;p>La política de aprobación es lo primero que se escribe, y se escribe como una función que devuelve verdadero cuando hace falta un humano. Recibe la llamada a herramienta que el modelo ha propuesto, el estado del hilo y el contexto de ejecución.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain.agents.middleware&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ToolCallRequest&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">necesita_encargado&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">req&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ToolCallRequest&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">bool&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">req&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tool_call&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cliente&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">req&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">req&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tool_call&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;reembolsar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;importe&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">devoluciones_ultimos_30_dias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cliente&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">pedido_entregado_hace_menos_de&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;pedido&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">dias&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">30&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">req&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tool_call&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;sustituir&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">diferencia_pvp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku_original&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku_nuevo&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tres cosas de esta función. La primera: consulta la base de negocio, no el estado del agente. El historial de devoluciones del cliente está en &lt;code>devoluciones&lt;/code>, y es la fuente que un encargado aceptaría. La segunda: recibe &lt;code>req.state&lt;/code> y &lt;code>req.runtime&lt;/code>; verificado que &lt;code>state&lt;/code> es el estado completo del agente, con &lt;code>messages&lt;/code>, y que &lt;code>runtime.context&lt;/code> es el objeto de contexto que se pasó en &lt;code>invoke&lt;/code>. La tercera: se ejecuta después de que el modelo haya decidido llamar a la herramienta y antes de que la herramienta corra. Si devuelve falso, la herramienta corre sin parar. Si devuelve verdadero, el grafo para.&lt;/p>
&lt;h2 id="el-middleware-y-el-contrato-de-la-interrupción">El middleware y el contrato de la interrupción&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain.agents&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">create_agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain.agents.middleware&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">HumanInTheLoopMiddleware&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">aprobacion&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">HumanInTheLoopMiddleware&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">interrupt_on&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;reembolsar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;allowed_decisions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;approve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;edit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;reject&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;when&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">necesita_encargado&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sustituir&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;allowed_decisions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;approve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;reject&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;when&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">necesita_encargado&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;consultar_pedido&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;stock_alternativas&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description_prefix&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Operación fuera de política. Requiere aprobación de un encargado.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">agente&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_agent&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modelo&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tools&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">consultar_pedido&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">stock_alternativas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">reembolsar&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sustituir&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">middleware&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">prompt_cliente&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">errores&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">limites&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">aprobacion&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_schema&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ContextoCliente&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checkpointer&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">checkpointer&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Compilado, el grafo tiene un nodo más que el del caso 1: &lt;code>HumanInTheLoopMiddleware.after_model&lt;/code>. Ahí es donde se evalúa &lt;code>when&lt;/code> y se llama a &lt;code>interrupt&lt;/code>. Es un nodo posterior al modelo y anterior a &lt;code>tools&lt;/code>; verificado con una herramienta instrumentada que la herramienta no corre antes de la decisión.&lt;/p>
&lt;p>Cuando la política dice que sí, &lt;code>invoke&lt;/code> devuelve el estado con una clave &lt;code>__interrupt__&lt;/code>. Es una lista de objetos &lt;code>Interrupt&lt;/code> con &lt;code>value&lt;/code> e &lt;code>id&lt;/code>. El &lt;code>value&lt;/code> tiene este formato exacto, obtenido de una ejecución con un reembolso de 120 euros:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;action_requests&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;reembolsar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;pedido&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;P-20260917-0041&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;importe&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">120.0&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Operación fuera de política. Requiere aprobación de un encargado.\n\nTool: reembolsar\nArgs: {&amp;#39;pedido&amp;#39;: &amp;#39;P-20260917-0041&amp;#39;, &amp;#39;importe&amp;#39;: 120.0}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;review_configs&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;action_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;reembolsar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;allowed_decisions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;approve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;edit&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;reject&amp;#34;&lt;/span>&lt;span class="p">]}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>action_requests&lt;/code> lleva una entrada por herramienta interceptada en la misma vuelta del modelo; si el modelo pide un reembolso y una sustitución a la vez y las dos pasan la política, hay dos entradas y una sola interrupción. &lt;code>review_configs&lt;/code> le dice a la interfaz qué botones pintar. El campo &lt;code>description&lt;/code> es el prefijo más un volcado de la herramienta y los argumentos; para el encargado es mejor construir la descripción con una función, que &lt;code>InterruptOnConfig&lt;/code> admite en &lt;code>description&lt;/code>, y darle el nombre del cliente, la fecha del pedido y el motivo que el cliente dio.&lt;/p>
&lt;p>Con &lt;code>interrupt_on={&amp;quot;reembolsar&amp;quot;: True}&lt;/code>, sin diccionario, se permiten las cuatro decisiones y no hay condición: todo reembolso para. Es el valor para empezar y para las pruebas, no para la tienda.&lt;/p>
&lt;h2 id="las-cuatro-decisiones">Las cuatro decisiones&lt;/h2>
&lt;p>La reanudación entra con &lt;code>Command(resume=...)&lt;/code> y un diccionario con una lista &lt;code>decisions&lt;/code>, en el mismo orden que &lt;code>action_requests&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langgraph.types&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Command&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">agente&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">resume&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;decisions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;approve&amp;#34;&lt;/span>&lt;span class="p">}]}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;configurable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;thread_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hilo&lt;/span>&lt;span class="p">}},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Las cuatro, verificadas con la herramienta instrumentada:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Decisión&lt;/th>
&lt;th>Formato&lt;/th>
&lt;th>Qué pasa&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>approve&lt;/code>&lt;/td>
&lt;td>&lt;code>{&amp;quot;type&amp;quot;: &amp;quot;approve&amp;quot;}&lt;/code>&lt;/td>
&lt;td>La herramienta corre con los argumentos que propuso el modelo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>edit&lt;/code>&lt;/td>
&lt;td>&lt;code>{&amp;quot;type&amp;quot;: &amp;quot;edit&amp;quot;, &amp;quot;edited_action&amp;quot;: {&amp;quot;name&amp;quot;: &amp;quot;reembolsar&amp;quot;, &amp;quot;args&amp;quot;: {...}}}&lt;/code>&lt;/td>
&lt;td>La herramienta corre con los argumentos editados. Con el importe cambiado de 120 a 90, la herramienta recibió 90&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>reject&lt;/code>&lt;/td>
&lt;td>&lt;code>{&amp;quot;type&amp;quot;: &amp;quot;reject&amp;quot;, &amp;quot;message&amp;quot;: &amp;quot;Fuera de plazo, el pedido tiene 45 días.&amp;quot;}&lt;/code>&lt;/td>
&lt;td>La herramienta no corre. El modelo recibe un &lt;code>ToolMessage&lt;/code> con &lt;code>status=&amp;quot;error&amp;quot;&lt;/code> y el texto &lt;code>User rejected the tool call for reembolsar with reason: Fuera de plazo, el pedido tiene 45 días.&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>respond&lt;/code>&lt;/td>
&lt;td>&lt;code>{&amp;quot;type&amp;quot;: &amp;quot;respond&amp;quot;, &amp;quot;message&amp;quot;: &amp;quot;Ofrece un vale en vez de reembolso.&amp;quot;}&lt;/code>&lt;/td>
&lt;td>La herramienta no corre. El modelo recibe un &lt;code>ToolMessage&lt;/code> con &lt;code>status=&amp;quot;success&amp;quot;&lt;/code> y ese texto como si fuera el resultado&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Dos consecuencias para el diseño.&lt;/p>
&lt;p>&lt;strong>El rechazo llega al modelo en inglés.&lt;/strong> El marco &lt;code>User rejected the tool call ... with reason:&lt;/code> lo pone el middleware y no se configura. El motivo del encargado va en castellano detrás. El prompt del agente lleva esta línea: &amp;ldquo;Si una operación es rechazada por un encargado, comunica al cliente el motivo tal cual, en castellano, y no propongas repetirla&amp;rdquo;. Sin ella, se han visto respuestas que mezclan idiomas o que reintentan el reembolso con otro importe.&lt;/p>
&lt;p>&lt;strong>&lt;code>respond&lt;/code> es la puerta para la resolución manual.&lt;/strong> El encargado no aprueba ni rechaza; le dice al agente qué hacer. Como la herramienta no corre, el vale o el reembolso parcial que el encargado proponga lo tiene que emitir el propio encargado en el TPV. Para este agente se deja fuera de &lt;code>allowed_decisions&lt;/code> en las dos herramientas: el encargado edita o rechaza, y lo que quiera hacer distinto lo hace fuera. Es una decisión de tienda, no de código, y el sitio donde se toma es esa lista.&lt;/p>
&lt;h2 id="lo-que-se-persiste-y-lo-que-se-repite">Lo que se persiste y lo que se repite&lt;/h2>
&lt;p>Una interrupción es un checkpoint más. Con el middleware de aprobación y sin los límites del caso 1, la ejecución que para en un reembolso deja 6 checkpoints; la reanudación con &lt;code>edit&lt;/code> añade 4 y acaba en 10. El estado del hilo entre los dos &lt;code>invoke&lt;/code> está entero en Postgres: mensajes, la llamada propuesta, y la interrupción pendiente. &lt;code>agente.get_state(config)&lt;/code> lo devuelve con &lt;code>next=('tools',)&lt;/code> y una tarea con la interrupción en &lt;code>tasks[0].interrupts&lt;/code>.&lt;/p>
&lt;p>Sobre qué se repite hay dos casos distintos que no hay que mezclar.&lt;/p>
&lt;p>&lt;strong>La re-ejecución del nodo al reanudar.&lt;/strong> Es diseño de LangGraph: el nodo que llamó a &lt;code>interrupt&lt;/code> se ejecuta desde el principio cuando llega &lt;code>Command(resume=...)&lt;/code>, y la llamada a &lt;code>interrupt&lt;/code> devuelve entonces el valor reanudado. Verificado con un nodo propio que registra efectos antes y después de la interrupción: la secuencia es &lt;code>antes, antes, después&lt;/code>. Con el middleware esto no afecta a la herramienta, porque la interrupción vive en &lt;code>after_model&lt;/code> y la herramienta corre en &lt;code>tools&lt;/code>, un nodo después. Afecta a cualquier nodo propio que se escriba con &lt;code>interrupt&lt;/code> dentro, y la regla es que la interrupción sea lo primero del nodo.&lt;/p>
&lt;p>&lt;strong>La re-ejecución de una herramienta en vuelo.&lt;/strong> Si el pod muere con &lt;code>reembolsar&lt;/code> a medias, al reanudar el hilo la tarea del nodo &lt;code>tools&lt;/code> se ejecuta otra vez. Las tareas que terminaron antes del fallo no: sus escrituras quedan guardadas como pendientes y se reaplican. Verificado con un abanico de 20 tareas en el que una falla: el estado tras el fallo tiene 19 resultados, y la reanudación con &lt;code>invoke(None, config)&lt;/code> ejecuta solo la que faltaba. Pero la que estaba en vuelo cuando murió el proceso no deja escritura, y corre de nuevo.&lt;/p>
&lt;p>Por eso &lt;code>reembolsar&lt;/code> es idempotente por identificador de llamada:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain.tools&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">tool&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ToolRuntime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@tool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">reembolsar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pedido&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">importe&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">float&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ToolRuntime&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">ContextoCliente&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Emite un reembolso al medio de pago original del pedido.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">pool&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">connection&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transaction&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">previo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;SELECT id, estado FROM reembolsos WHERE clave_idempotencia = &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tool_call_id&lt;/span>&lt;span class="p">,),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fetchone&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">previo&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;reembolso&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">previo&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;repetido&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rid&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO reembolsos (pedido_id, importe, clave_idempotencia, aprobado_por, trace_id) &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;VALUES (&lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">) RETURNING id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">pedido&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">importe&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tool_call_id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;aprobado_por&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;langfuse_trace_id&amp;#34;&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">fetchone&lt;/span>&lt;span class="p">()[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pasarela&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">reembolsar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pedido&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">importe&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">referencia&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">rid&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;ok&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;reembolso&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">rid&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>runtime.tool_call_id&lt;/code> es el identificador que el modelo puso en la llamada, y se conserva en el checkpoint. Una re-ejecución trae el mismo identificador, y la columna &lt;code>clave_idempotencia&lt;/code> con restricción única lo para. La llamada a la pasarela va fuera de la transacción y con la referencia del reembolso, para que la pasarela deduplique por su lado si el proceso muere entre el &lt;code>INSERT&lt;/code> y la llamada.&lt;/p>
&lt;p>La durabilidad se fija en &lt;code>invoke&lt;/code>. Con &lt;code>durability=&amp;quot;sync&amp;quot;&lt;/code>, cada checkpoint se escribe antes de seguir al siguiente superstep. Es el valor para los hilos con dinero. &lt;code>&amp;quot;async&amp;quot;&lt;/code> escribe en paralelo con la ejecución y puede perder el último paso si el pod muere. &lt;code>&amp;quot;exit&amp;quot;&lt;/code> escribe al terminar y no vale para nada que pueda interrumpirse.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">agente&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">resume&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;decisions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">decisiones&lt;/span>&lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">durability&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sync&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="la-cola-de-aprobaciones">La cola de aprobaciones&lt;/h2>
&lt;p>El checkpointer guarda cada hilo, pero no tiene ninguna consulta que devuelva los hilos que están esperando a un humano. &lt;code>PostgresSaver.list&lt;/code> lista checkpoints de un hilo dado; no lista hilos. Y no hay columna de estado en &lt;code>checkpoints&lt;/code> que diga &amp;ldquo;interrumpido&amp;rdquo;.&lt;/p>
&lt;p>La cola es una tabla propia:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">aprobaciones_pendientes&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">thread_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">tienda&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">interrupt_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">peticion&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonb&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- el value de la interrupción
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">creada&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">timestamptz&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">asignada_a&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">trace_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">text&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La aplicación la escribe cuando &lt;code>invoke&lt;/code> devuelve &lt;code>__interrupt__&lt;/code> y la borra en la reanudación, en la misma transacción en la que registra quién decidió qué. Es también la tabla desde la que se mide el tiempo entre interrupción y decisión.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">resultado&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">agente&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">entrada&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">durability&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sync&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">interrupciones&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="n">resultado&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;__interrupt__&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">intr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">interrupciones&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">pool&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">connection&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO aprobaciones_pendientes (thread_id, tienda, cliente_id, interrupt_id, peticion, trace_id) &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;VALUES (&lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">) ON CONFLICT (thread_id) DO UPDATE SET peticion = EXCLUDED.peticion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;configurable&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;thread_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tienda&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">intr&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">intr&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;langfuse_trace_id&amp;#34;&lt;/span>&lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;estado&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;esperando_aprobacion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;mensaje_cliente&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">mensaje_espera&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">intr&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">)}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y del lado del encargado, la reanudación con el registro de quién decidió:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">decidir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">thread_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">encargado&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">decisiones&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">dict&lt;/span>&lt;span class="p">]):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">config&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;configurable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;thread_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">thread_id&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;aprobado_por&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">encargado&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;callbacks&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">langfuse_handler&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resultado&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">agente&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">resume&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;decisions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">decisiones&lt;/span>&lt;span class="p">}),&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">durability&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sync&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">pool&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">connection&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transaction&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;DELETE FROM aprobaciones_pendientes WHERE thread_id = &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">thread_id&lt;/span>&lt;span class="p">,))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">conn&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO decisiones_encargado (thread_id, encargado, decisiones) VALUES (&lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">, &lt;/span>&lt;span class="si">%s&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">thread_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">encargado&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">decisiones&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">resultado&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;messages&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">content&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El contexto del cliente no viaja en esta llamada. &lt;code>Command(resume=...)&lt;/code> se invoca sin &lt;code>context&lt;/code>, y el prompt dinámico y las herramientas lo leen del &lt;code>runtime&lt;/code>: en la reanudación, &lt;code>runtime.context&lt;/code> es &lt;code>None&lt;/code> si no se pasa. Verificado con un prompt dinámico que registra el contexto en cada llamada al modelo: &lt;code>C1&lt;/code> en la primera, &lt;code>None&lt;/code> tras &lt;code>Command(resume=...)&lt;/code> sin &lt;code>context&lt;/code>. Hay dos opciones: guardar el contexto del cliente en el estado del agente con un &lt;code>state_schema&lt;/code> propio, o pasarlo otra vez en la reanudación leyéndolo de la cola. Aquí se hace lo segundo, porque el contexto tiene el identificador del cliente y no se quiere en el checkpoint más de lo necesario. La función real de &lt;code>decidir&lt;/code> carga &lt;code>ContextoCliente&lt;/code> de &lt;code>aprobaciones_pendientes&lt;/code> y lo pasa en &lt;code>context=&lt;/code>.&lt;/p>
&lt;p>Quién puede reanudar es autorización de la capa HTTP: &lt;code>decidir&lt;/code> solo se llama desde un endpoint que exige rol de encargado y comprueba que la tienda del encargado es la de la fila. El checkpointer acepta cualquier &lt;code>Command(resume=...)&lt;/code> para cualquier &lt;code>thread_id&lt;/code>, y eso se documenta en el marco de la serie.&lt;/p>
&lt;h2 id="el-prompt">El prompt&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@dynamic_prompt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">prompt_cliente&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">ModelRequest&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">runtime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Atiendes al cliente &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">cliente_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> de la tienda &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">tienda&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> sobre su pedido. &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Consulta el pedido antes de proponer nada. &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Para devolver, usa reembolsar con el importe de las líneas devueltas; para cambiar por otra referencia, &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;comprueba stock_alternativas y usa sustituir. &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Algunas operaciones esperan la aprobación de un encargado; si es así, dile al cliente que la operación &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;queda pendiente y que recibirá confirmación, sin prometer un plazo. &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Si un encargado rechaza una operación, comunica el motivo tal cual, en castellano, y no la repitas &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;con otros valores. Nunca inventes un número de reembolso: solo el que devuelva la herramienta.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La frase sobre la aprobación pendiente importa porque el modelo no ve la interrupción. Desde su punto de vista, pidió una herramienta y la conversación se detuvo. Cuando el hilo se reanuda con &lt;code>approve&lt;/code>, lo siguiente que ve es el resultado de la herramienta, como si no hubiera pasado tiempo. Es la aplicación la que le dice al cliente que espere, con &lt;code>mensaje_espera&lt;/code>, no el modelo. Y en la reanudación, el mensaje que el modelo genera tras el resultado es el que se le envía al cliente por el canal que corresponda, con el retraso que haya habido.&lt;/p>
&lt;h2 id="una-devolución-fuera-de-política-de-principio-a-fin">Una devolución fuera de política, de principio a fin&lt;/h2>
&lt;p>Con las herramientas reales sobre el catálogo de prueba y las respuestas del modelo guionizadas, como en el caso 1:&lt;/p>
&lt;pre tabindex="0">&lt;code>Cliente: Quiero devolver el pedido P-20260917-0041, la cafetera llegó con la jarra rota.
[tools] consultar_pedido(pedido=&amp;#34;P-20260917-0041&amp;#34;) -&amp;gt; {&amp;#34;pedido&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;importe&amp;#34;: 120.0, &amp;#34;entregado&amp;#34;: true, &amp;#34;dias_desde_entrega&amp;#34;: 4, &amp;#34;lineas&amp;#34;: [...]}
[after_model] reembolsar(pedido=&amp;#34;P-20260917-0041&amp;#34;, importe=120.0) -&amp;gt; when=True (importe &amp;gt; 60)
[__interrupt__] {&amp;#34;action_requests&amp;#34;: [{&amp;#34;name&amp;#34;: &amp;#34;reembolsar&amp;#34;, &amp;#34;args&amp;#34;: {&amp;#34;pedido&amp;#34;: &amp;#34;P-20260917-0041&amp;#34;, &amp;#34;importe&amp;#34;: 120.0}, ...}], ...}
Aplicación -&amp;gt; Cliente: Tu solicitud de devolución está registrada y pendiente de revisión. Te confirmaremos por este canal.
[encargado, 3 horas después] edit: importe 90.0
[tools] reembolsar(pedido=&amp;#34;P-20260917-0041&amp;#34;, importe=90.0) -&amp;gt; {&amp;#34;ok&amp;#34;: true, &amp;#34;reembolso&amp;#34;: 7712}
Agente -&amp;gt; Cliente: Hemos aprobado un reembolso de 90 euros por la cafetera, con referencia 7712. El importe llegará a tu medio de pago en 3 a 5 días.
&lt;/code>&lt;/pre>&lt;p>El modelo no sabe por qué el importe es 90 y no 120: la edición cambia los argumentos de la llamada, no le explica nada. Si la tienda quiere que el cliente reciba una explicación (jarra de repuesto sin coste, reembolso del resto), el encargado la escribe en la aplicación y la aplicación la añade al mensaje final. Meter esa explicación en el modelo exigiría una herramienta de notas con &lt;code>respond&lt;/code>, y en este agente &lt;code>respond&lt;/code> está fuera de &lt;code>allowed_decisions&lt;/code>.&lt;/p>
&lt;h2 id="lo-que-se-mide">Lo que se mide&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Dónde&lt;/th>
&lt;th>Qué dice&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Interrupciones por cada cien conversaciones&lt;/td>
&lt;td>&lt;code>aprobaciones_pendientes&lt;/code> (histórico)&lt;/td>
&lt;td>Si la política para demasiado o demasiado poco. Entre 10 y 25 es la zona de trabajo; por encima, el umbral de importe es bajo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tiempo entre interrupción y decisión, p50 y p95&lt;/td>
&lt;td>&lt;code>creada&lt;/code> frente a la fila de &lt;code>decisiones_encargado&lt;/code>&lt;/td>
&lt;td>Lo que el cliente espera. El p95 dice si hay un turno sin encargado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decisiones por tipo&lt;/td>
&lt;td>&lt;code>decisiones_encargado&lt;/code>&lt;/td>
&lt;td>Muchos &lt;code>edit&lt;/code> con el mismo patrón son una regla que falta en la política; muchos &lt;code>reject&lt;/code> con el mismo motivo, una condición que falta en &lt;code>when&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reembolsos con &lt;code>repetido = true&lt;/code>&lt;/td>
&lt;td>&lt;code>reembolsos&lt;/code>&lt;/td>
&lt;td>Re-ejecuciones reales. Debe ser cercano a cero y distinto de cero alguna vez; si nunca pasa, la clave de idempotencia no se está probando&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Operaciones con efecto sin fila en &lt;code>decisiones_encargado&lt;/code> y con &lt;code>when&lt;/code> verdadero&lt;/td>
&lt;td>Cruce de &lt;code>reembolsos&lt;/code> con &lt;code>decisiones_encargado&lt;/code>&lt;/td>
&lt;td>Debe ser cero. Es la auditoría de que el límite funciona&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokens y coste por conversación, con y sin interrupción&lt;/td>
&lt;td>Langfuse, metadato &lt;code>caso=cliente&lt;/code>&lt;/td>
&lt;td>Una conversación interrumpida tiene dos trazas o una con hueco, según se configure el callback; las dos formas valen si se agrupan por &lt;code>thread_id&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La última fila de la tabla es la que se enseña al responsable. No dice cuánto ahorra el agente; dice que ningún euro ha salido sin la firma que la política exige. Es la condición para que el resto de métricas importen.&lt;/p>
&lt;h2 id="checklist">Checklist&lt;/h2>
&lt;ul>
&lt;li>La política es una función &lt;code>when&lt;/code> que consulta la base de negocio, no el prompt.&lt;/li>
&lt;li>&lt;code>allowed_decisions&lt;/code> está decidido por herramienta y &lt;code>respond&lt;/code> está fuera salvo en herramientas de notas.&lt;/li>
&lt;li>&lt;code>description&lt;/code> es una función que da al encargado el contexto que necesita, no el volcado por defecto.&lt;/li>
&lt;li>La aplicación escribe &lt;code>aprobaciones_pendientes&lt;/code> al recibir &lt;code>__interrupt__&lt;/code> y la borra al reanudar, en transacción con &lt;code>decisiones_encargado&lt;/code>.&lt;/li>
&lt;li>Las herramientas con efecto usan &lt;code>runtime.tool_call_id&lt;/code> como clave de idempotencia con restricción única.&lt;/li>
&lt;li>&lt;code>invoke&lt;/code> lleva &lt;code>durability=&amp;quot;sync&amp;quot;&lt;/code> en los hilos con dinero.&lt;/li>
&lt;li>La reanudación pasa &lt;code>context=&lt;/code> cargado de la cola; no se confía en que el &lt;code>runtime&lt;/code> lo recuerde.&lt;/li>
&lt;li>El endpoint de reanudación exige rol de encargado y comprueba la tienda.&lt;/li>
&lt;li>El prompt dice qué hacer con un rechazo y no promete plazos.&lt;/li>
&lt;li>Hay una consulta de auditoría que cruza efectos con decisiones y da cero.&lt;/li>
&lt;/ul>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>&lt;code>interrupt&lt;/code> sin checkpointer no falla al interrumpir.&lt;/strong> Devuelve &lt;code>__interrupt__&lt;/code> con normalidad y el estado se pierde. El error, &lt;code>Cannot use Command(resume=...) without checkpointer&lt;/code>, sale en la reanudación. Un entorno de pruebas sin persistencia da falsos positivos hasta que alguien intenta aprobar.&lt;/p>
&lt;p>&lt;strong>La herramienta no corre antes de la decisión, pero el modelo ya la ha pedido.&lt;/strong> Lo que se aprueba es una llamada con argumentos concretos. Si el encargado tarda un día y el stock ha cambiado, &lt;code>sustituir&lt;/code> puede fallar al ejecutarse con los argumentos aprobados. La herramienta tiene que volver a comprobar, y el mensaje de error tiene que decir que la aprobación caducó.&lt;/p>
&lt;p>&lt;strong>El nodo que interrumpe se ejecuta dos veces.&lt;/strong> Con el middleware no importa; con un nodo propio, todo lo anterior a &lt;code>interrupt&lt;/code> se repite. La interrupción va primero.&lt;/p>
&lt;p>&lt;strong>La herramienta en vuelo se repite si muere el pod.&lt;/strong> Las que terminaron no. La clave de idempotencia es el identificador de la llamada, y hay que probar la re-ejecución a propósito, matando el pod con una herramienta lenta, para ver la fila con &lt;code>repetido = true&lt;/code>.&lt;/p>
&lt;p>&lt;strong>El rechazo llega en inglés.&lt;/strong> &lt;code>User rejected the tool call for ... with reason:&lt;/code> es texto fijo del middleware. Sin instrucción en el prompt, el modelo puede contestar en inglés o reintentar.&lt;/p>
&lt;p>&lt;strong>No hay lista de hilos interrumpidos.&lt;/strong> Ni en el checkpointer ni en el estado. La cola es una tabla propia, y si la aplicación se cae entre el &lt;code>invoke&lt;/code> y el &lt;code>INSERT&lt;/code>, hay un hilo esperando que nadie ve. La escritura de la cola va lo más cerca posible del &lt;code>invoke&lt;/code>, y una tarea periódica cruza &lt;code>aprobaciones_pendientes&lt;/code> con los hilos que tienen &lt;code>next=('tools',)&lt;/code> y una interrupción en &lt;code>get_state&lt;/code>, recorriendo los hilos del día desde &lt;code>consultas_asistente&lt;/code>.&lt;/p>
&lt;p>&lt;strong>El contexto no se recuerda en la reanudación.&lt;/strong> &lt;code>runtime.context&lt;/code> es lo que se pase en esa llamada. Sin &lt;code>context=&lt;/code>, el prompt dinámico ve &lt;code>None&lt;/code> y falla al formatear.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>El límite de aprobación humana es una función, un middleware y una tabla. La función decide cuándo se pregunta; el middleware para el grafo antes de la herramienta y fija el contrato de la pregunta y de la respuesta; la tabla es la cola que el checkpointer no tiene. Alrededor, tres decisiones que no son de LangGraph pero que el código obliga a tomar: qué decisiones se permiten por herramienta, cómo se hace idempotente lo que tiene efecto, y quién puede reanudar.&lt;/p>
&lt;p>El siguiente caso deja al cliente y mira al almacén. Un agente de compras tiene que revisar miles de referencias contra las incidencias de sus proveedores y proponer una reposición. Ahí el problema no es parar, es abrir en abanico sin tirar el servidor de inferencia, agregar sin condiciones de carrera y no pagar dos veces por la misma referencia.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agentes-langgraph-retail-marco-serie/">Agentes con LangGraph para retail: el marco de la serie&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/servir-agentes-langgraph-sin-langgraph-api/">Servir agentes de LangGraph sin langgraph-api&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-empleado-tienda-langgraph-rag-tools/">Agente de empleado de tienda con LangGraph: procedimientos, stock y roturas&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-cadena-suministro-langgraph-send-map-reduce/">Agente de cadena de suministro con LangGraph: incidencias y reposición con map-reduce&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/deepagents-sdk-libre-servidor-con-licencia/">deepagents en un cluster propio: el SDK es libre, el servidor no&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/temporal-durable-execution-coste-agentes/">Ejecución durable con Temporal y el coste de los agentes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/aislar-agentes-ia-cliente-cluster/">Aislar agentes de IA por cliente en el cluster&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>Human-in-the-loop en LangChain 1.x: &lt;a href="https://docs.langchain.com/oss/python/langchain/human-in-the-loop">https://docs.langchain.com/oss/python/langchain/human-in-the-loop&lt;/a>&lt;/li>
&lt;li>Interrupciones en LangGraph: &lt;a href="https://docs.langchain.com/oss/python/langgraph/interrupts">https://docs.langchain.com/oss/python/langgraph/interrupts&lt;/a>&lt;/li>
&lt;li>Persistencia y durabilidad en LangGraph: &lt;a href="https://docs.langchain.com/oss/python/langgraph/persistence">https://docs.langchain.com/oss/python/langgraph/persistence&lt;/a>&lt;/li>
&lt;li>Código de &lt;code>langchain.agents.middleware.human_in_the_loop&lt;/code> (langchain 1.4.1): &lt;code>InterruptOnConfig&lt;/code>, &lt;code>HITLRequest&lt;/code>, &lt;code>HITLResponse&lt;/code>, &lt;code>ApproveDecision&lt;/code>, &lt;code>EditDecision&lt;/code>, &lt;code>RejectDecision&lt;/code>, &lt;code>RespondDecision&lt;/code>&lt;/li>
&lt;li>Código de &lt;code>langgraph.types&lt;/code> (langgraph 1.2.11): &lt;code>interrupt&lt;/code>, &lt;code>Command&lt;/code>, &lt;code>Interrupt&lt;/code>&lt;/li>
&lt;li>Código de &lt;code>langgraph.pregel.main&lt;/code> (langgraph 1.2.11): firma de &lt;code>invoke&lt;/code>, parámetro &lt;code>durability&lt;/code>&lt;/li>
&lt;/ul></description></item></channel></rss>