<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tisha Chawla</title>
    <description>The latest articles on DEV Community by Tisha Chawla (@tisha).</description>
    <link>https://dev.to/tisha</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png</url>
      <title>DEV Community: Tisha Chawla</title>
      <link>https://dev.to/tisha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tisha"/>
    <language>en</language>
    <item>
      <title>Your Agent Has a 1987 Database Problem</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Mon, 21 Sep 2026 09:53:51 +0000</pubDate>
      <link>https://dev.to/tisha/your-agent-has-a-1987-problem-14g4</link>
      <guid>https://dev.to/tisha/your-agent-has-a-1987-problem-14g4</guid>
      <description>&lt;p&gt;Here is the shape of a bug I have watched people chase for days.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;14:22:03.204  tool    POST /v1/charges              201 Created
14:22:03.209  db      COMMIT orders/8812 status=paid
14:22:03.211  x       process killed (out of memory)
14:22:11.470  worker  resume th_44c1 from ckpt_19
14:22:11.882  graph   node=tools    call_7f3a has no result
14:22:11.955  tool    POST /v1/charges              201 Created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customer was charged twice, and nothing in that trace failed. No exception, no retry counter, no red span in the viewer. Both charges returned clean 201s, and the agent did exactly what it was built to do.&lt;/p&gt;

&lt;p&gt;So why did it charge twice? The answer sits one layer down, in how an agent loop records what it has already done.&lt;/p&gt;

&lt;h2&gt;
  
  
  How an agent loop actually works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A model has no memory.&lt;/strong&gt; Calling one is a pure function call. It does not remember the previous call, holds no connection to your database, and has no idea whether it charged someone thirty seconds ago. Everything it knows arrives as input, and the moment the call returns it knows nothing again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So the agent's memory is a list of messages your runtime owns.&lt;/strong&gt; The system prompt, the user's message, each assistant reply, each tool requested, each result handed back. That list lives in Postgres and gets passed to the model on every turn. When people say an agent knows something, this list is what they mean, because there is nowhere else for knowledge to live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One turn contains two separate writes.&lt;/strong&gt; Your code executes the tool, which moves money at a payment provider and writes a row in your own database. Then it appends the result to the list, and the runtime saves the list. Executing and saving happen at different moments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the gap between them, the agent's memory disagrees with the world.&lt;/strong&gt; The charge exists permanently at the provider. The list does not mention it.&lt;/p&gt;

&lt;p&gt;The rest of this article is about that gap.&lt;/p&gt;

&lt;p&gt;Whether it matters to you depends on what your tools actually do. An agent that searches, summarizes or drafts can crash and replay all day without harm, because running a read a second time costs latency and nothing else. The gap only bites when a tool changes something outside your process that you cannot take back, meaning money moving, an email going out, a ticket filed, a row written into somebody else's system. If your agent only reads, none of what follows is urgent. If it acts, all of it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened in the trace
&lt;/h2&gt;

&lt;p&gt;Two questions to answer: where was the gap, and what was in the checkpoint when the process died?&lt;/p&gt;

&lt;p&gt;LangGraph runs your agent as a graph. One node calls the model, a second node executes whatever tools the model asked for, and after each of them a component called the &lt;strong&gt;checkpointer&lt;/strong&gt; saves the message list.&lt;/p&gt;

&lt;p&gt;The checkpointer is the save function, and nothing more exotic than that. It writes the agent's current state to storage, keyed by conversation, so a different process can pick the run up later. In development it is usually an in-memory stub; in production it is Postgres. Each save it makes is a checkpoint, and &lt;code&gt;ckpt_19&lt;/code&gt; in the trace is one of them.&lt;/p&gt;

&lt;p&gt;At &lt;code&gt;ckpt_19&lt;/code&gt;, that saved list ended like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;assistant&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"I'll charge the customer."&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="err"&gt;tool_calls:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;charge_customer(cus_&lt;/span&gt;&lt;span class="mi"&gt;8812&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4900&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id=call_&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a question with no answer: a tool was requested, nothing has come back, and the graph's next move is to run the tools node.&lt;/p&gt;

&lt;p&gt;The tools node then did four things in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Called the payment provider.&lt;/li&gt;
&lt;li&gt;Took the 201 back.&lt;/li&gt;
&lt;li&gt;Committed the order row.&lt;/li&gt;
&lt;li&gt;Built the reply message that would close out &lt;code&gt;call_7f3a&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It was killed before it handed that reply back. Steps one and three are permanent and outside your control now, because the money moved and the order row committed. Steps two and four existed only in the memory of a process that no longer exists.&lt;/p&gt;

&lt;p&gt;So the checkpointer was never given anything to save, and &lt;code&gt;ckpt_19&lt;/code&gt; still ends exactly as it did above, with a tool call and no result.&lt;/p&gt;

&lt;p&gt;When the worker resumed eight seconds later it loaded that list and did the only thing available to it. An unanswered tool call means run the tool, so it ran the tool.&lt;/p&gt;

&lt;p&gt;Nothing malfunctioned. The graph followed its own rules against a record that was missing an entry, and in LangGraph this is documented behavior rather than a bug, because saving the list is a step that comes after the tool has already done its work.&lt;/p&gt;

&lt;p&gt;So the question is not why the agent repeated itself, since given that list it had no other option. The question is why the charge and the record of the charge were allowed to come apart at all. Two things needed to happen together and did not, which is a problem databases have been solving since 1978.&lt;/p&gt;

&lt;h2&gt;
  
  
  1978: all or nothing
&lt;/h2&gt;

&lt;p&gt;Move fifty dollars between two accounts and you have two writes: subtract from one, add to the other. If the machine dies in between, the money is not queued somewhere, it is gone, and nothing in the system knows it existed. The transaction had to be invented to stop this. Until it existed, an application that needed two writes to hold together had to track that itself and hope the machine stayed up, which is why banks reconciled their books overnight and fixed the discrepancies in the morning.&lt;/p&gt;

&lt;p&gt;The fix is not the one people assume. You cannot make two writes happen simultaneously, so databases stopped trying and did something else: they write down what they are about to do before doing any of it.&lt;/p&gt;

&lt;p&gt;That record goes into a log, a file the database only ever appends to. It is flushed to disk, meaning the operating system is forced to put the bytes on the disk itself rather than hold them in memory where a power cut would erase them. Only then does the database touch the actual rows.&lt;/p&gt;

&lt;p&gt;For the fifty dollar transfer, the log holds roughly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;txn 41  begin
txn 41  account A   100 -&amp;gt; 50
txn 41  account B    20 -&amp;gt; 70
txn 41  commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line decides everything. On restart the database reads the log. If the commit line is there, it applies every change above it. If the commit line is missing, it undoes them. No judgment is involved. One line either reached the disk or it did not.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;write-ahead logging&lt;/strong&gt;. Jim Gray described it in &lt;em&gt;Notes on Data Base Operating Systems&lt;/em&gt; in 1978, and in 1983 Härder and Reuter named the guarantees it provides: &lt;strong&gt;ACID&lt;/strong&gt;, whose A is &lt;strong&gt;atomicity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Atomicity is not what the word suggests. The two writes never happen at the same instant. They happen one after another, and the machine can die between them. What makes them atomic is that one commit line. It was written before either change was applied, and it decides afterwards whether both count or neither does.&lt;/p&gt;

&lt;p&gt;That only works because the database owns both accounts. They are rows in its own files, covered by its own log, so a single commit line can settle both at once.&lt;/p&gt;

&lt;p&gt;A charge at a payment provider is not a row in your database. Your log cannot describe it, your commit line cannot settle it, your rollback cannot undo it. Nothing marks that edge in your code either: no error, no type mismatch, the two calls look the same.&lt;/p&gt;

&lt;p&gt;So you need the 1978 trick without the database doing it for you. Somewhere you control, write down "I am about to charge this card" before you charge it, so a crash leaves a note behind and the next process can work out what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  1978 to 1991: trying to leave the box
&lt;/h2&gt;

&lt;p&gt;One database's log can promise nothing about what happened in another, and the obvious move is a bigger transaction spanning both.&lt;/p&gt;

&lt;p&gt;That runs into an obstacle nobody has removed since. Akkoyunlu, Ekanadham and Huber published the impossibility proof in 1975, and Gray named it the &lt;strong&gt;Two Generals paradox&lt;/strong&gt; in that same 1978 paper: you send a request, it times out, and you cannot distinguish "it never arrived" from "it arrived, ran, and the acknowledgment was lost coming back." Those two worlds look identical from where you stand, and they still look identical after you add retries, better timeouts or a service mesh. The ambiguity is not a gap in your tooling.&lt;/p&gt;

&lt;p&gt;The field built &lt;strong&gt;two-phase commit&lt;/strong&gt; anyway. A coordinator asks every participant to promise it can commit, waits for all of them to agree, then tells everyone to proceed. Standardized as X/Open XA in 1991, it delivers genuine atomicity across systems.&lt;/p&gt;

&lt;p&gt;The cost is availability. To keep its promise, each participant has to &lt;strong&gt;lock&lt;/strong&gt; the rows involved, meaning hold them so that nobody else can read or change them until the outcome is settled. If the coordinator dies between the promise and the instruction to commit, every participant sits holding those locks and waiting for orders that are not coming. It also requires every participant to implement the protocol, which looks harmless in 1991 and proves fatal twenty years later.&lt;/p&gt;

&lt;p&gt;Two-phase commit also assumes the work is short enough to hold locks through. What if it isn't?&lt;/p&gt;

&lt;h2&gt;
  
  
  1987: work that takes too long
&lt;/h2&gt;

&lt;p&gt;Book a trip, meaning a flight, then a hotel, then a car. Wrap all three in one transaction and you hold locks for the entire booking while a human decides whether they want the aisle seat, and everything touching those rows queues behind you. Garcia-Molina and Salem wrote this up at SIGMOD in 1987: &lt;strong&gt;long-lived transactions&lt;/strong&gt; hold resources for long periods and badly delay the shorter, more common transactions around them.&lt;/p&gt;

&lt;p&gt;Their answer was the &lt;strong&gt;saga&lt;/strong&gt;, which replaces one long transaction with a sequence of short ones that each commit immediately, pairing every step with a &lt;strong&gt;compensating step&lt;/strong&gt; that undoes it. If step four fails, you walk backwards through the compensators for three, two and one.&lt;/p&gt;

&lt;p&gt;Compensation is not rollback. You cannot recall a confirmation email or erase a charge the way a database undoes a write, so you cancel the booking, issue the refund, send the correction. The undo happens in meaning rather than in bytes, which makes it visible to everyone and sometimes involves apologizing to a real person.&lt;/p&gt;

&lt;p&gt;That changes how you order steps. Reversible work goes before the step past which you cannot turn back, which goes before work that can only move forward. Get it wrong and you will need to compensate something that has no compensator.&lt;/p&gt;

&lt;p&gt;The cost is isolation, which a saga gives up by construction. &lt;strong&gt;Isolation&lt;/strong&gt; is the promise that nobody sees your work until it is finished, and a saga cannot make that promise because its steps commit one at a time. Other processes will observe your partially completed work, and you decide in advance whether that is acceptable for each step.&lt;/p&gt;

&lt;p&gt;Remember the phrase long-lived transaction. A model call taking thirty seconds is one, and a human approval taking three days is an extreme version.&lt;/p&gt;

&lt;p&gt;Sagas still assume the other side cooperates. What happens when it won't?&lt;/p&gt;

&lt;h2&gt;
  
  
  2007 to 2019: giving up gracefully
&lt;/h2&gt;

&lt;p&gt;Two-phase commit needed every participant to speak the protocol, and then the internet arrived and almost nothing did. Neither Stripe nor S3 nor whatever external API you depend on implements XA, and none of them ever will, so for most real operations the bigger transaction is not slow or awkward but unavailable.&lt;/p&gt;

&lt;p&gt;Pat Helland had spent much of his career advocating for exactly these guarantees before publishing &lt;em&gt;Life Beyond Distributed Transactions: an Apostate's Opinion&lt;/em&gt; in 2007. The title is not ironic. People building large systems had already stopped assuming distributed transactions regardless of the textbooks, and his argument was that the field should work out what to do instead.&lt;/p&gt;

&lt;p&gt;What they worked out is a chain of three answers, each one forced by the answer before it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; Since you can never know whether your call happened, stop trying to know and make it safe to run twice. That property has a name, &lt;strong&gt;idempotence&lt;/strong&gt;: running something twice leaves the world in the same state as running it once, which is not the same as getting the same answer back twice.&lt;/p&gt;

&lt;p&gt;Recognizing a repeat requires a stable identity on every action. That identity is an &lt;strong&gt;idempotency key&lt;/strong&gt;, and Helland wrote the definitive treatment in 2012 as &lt;em&gt;Idempotence Is Not a Medical Condition&lt;/em&gt;. With it you stop chasing &lt;strong&gt;exactly-once delivery&lt;/strong&gt;, which is not achievable, and settle for &lt;strong&gt;at-least-once delivery&lt;/strong&gt; plus idempotent processing, which behaves like once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; That handles the external call but not the agreement between it and your own database. Since the two cannot commit atomically, fall back on 1978 and write the intent down first: in one transaction in your own database, the business row and a row saying "I intend to charge this card, under this key." Either both land or neither does, because both sit inside the same guarantee. A separate worker then reads intent rows and performs the calls, retrying until confirmed.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;transactional outbox&lt;/strong&gt;, and the failure it fixes finally gets its name, the &lt;strong&gt;dual write problem&lt;/strong&gt;: two writes to two systems, no coordination, and a window in the middle where a crash leaves you inconsistent. That window is the one in your agent loop. The outbox itself is not new, it is write-ahead logging moved one layer out of the database and into your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three.&lt;/strong&gt; The last piece arrived when people tired of writing saga logs and outbox relays by hand, badly. &lt;strong&gt;Durable execution&lt;/strong&gt; engines record every completed step to a log that is only ever appended to, then replay it after a crash to rebuild where the process had reached. The lineage runs from Amazon SWF through Cadence, built by Maxim Fateev and team at Uber in 2015 and open sourced in 2017, to Temporal in 2019. Replay only reconstructs the past if running your code again makes the same decisions, which is why determinism stops being a matter of taste in these systems and becomes structural.&lt;/p&gt;

&lt;p&gt;That is the inheritance: identity, intent written first, semantic undo, recorded steps. So how much of it did agent frameworks actually pick up?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where agents sit
&lt;/h2&gt;

&lt;p&gt;Two things write to your database on every turn: the framework's checkpointer, saving loop state, and your own tool code, writing the orders and the charges. In many production systems they point at the same Postgres instance and are never in the same transaction, which makes them the dual write problem arriving as a framework default rather than as anyone's decision.&lt;/p&gt;

&lt;p&gt;The consequences are sitting in public issue trackers, three of them describing the same seam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LangGraph &lt;a href="https://github.com/langchain-ai/langgraph/issues/9006" rel="noopener noreferrer"&gt;#9006&lt;/a&gt;&lt;/strong&gt; asks what the intended contract even is when a worker dies in the middle of a tool call. The reporter's framing is the sharpest summary I have read: many traces filed as "the agent failed" are really this seam, where the model never chose badly and the harness duplicated the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strands &lt;a href="https://github.com/strands-agents/harness-sdk/issues/4338" rel="noopener noreferrer"&gt;#4338&lt;/a&gt;&lt;/strong&gt; reaches it from a different angle. A hook raises after the tool completes, the result message never gets appended, and the conversation is left ending in an unresolved tool call that the agent replays. The issue names payments and order creation explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Agent Framework &lt;a href="https://github.com/microsoft/agent-framework/issues/7458" rel="noopener noreferrer"&gt;#7458&lt;/a&gt;&lt;/strong&gt; executes an approved tool before the run can fail, leaving the retry indistinguishable from a request that was never pending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far this is rediscovery, and the microservices answer applies unchanged. Agents add two wrinkles that are in none of the older papers.&lt;/p&gt;

&lt;p&gt;The first is that the idempotency key cannot come from the model. Deduplication only works if a repeat is recognizable, and a model that plans again will express the same intention with different wording, different argument order, sometimes a different tool. The second attempt is semantically identical and syntactically new, so a check built on the request body sails past it and the card is charged again. The key has to come from somewhere stable and outside the model's control, which in practice means position in the plan, something like &lt;code&gt;run_id:step_index&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The protocol has not caught up either. MCP, the Model Context Protocol that most agent tooling now speaks, has no idempotency key field, and JSON-RPC request IDs are regenerated on every retry, so a retried tool call is indistinguishable from a new one at the wire level. &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3182" rel="noopener noreferrer"&gt;SEP-3182&lt;/a&gt; proposed fixing that with an &lt;code&gt;idempotencyKey&lt;/code&gt; alongside &lt;code&gt;arguments&lt;/code&gt; on &lt;code&gt;tools/call&lt;/code&gt;, rejecting any key reused with different arguments. It has been closed.&lt;/p&gt;

&lt;p&gt;The second is that the determinism requirement is violated by design. Durable execution rebuilds state by replaying your code, assuming it makes the same choices twice, and you have installed the least deterministic component ever shipped at the center of the loop. The way out is to treat the model call as a result to look up rather than logic to execute again, recording the completion when it first happens and reading it back on replay.&lt;/p&gt;

&lt;p&gt;Four separate papers in 2026 independently rebuilt transactional machinery for agents, which is decent evidence the gap is real. It is also worth noticing that Cordon's effect outbox is the transactional outbox and SagaLLM's compensation is the 1987 saga, so the field is working this out from scratch rather than reading it.&lt;/p&gt;

&lt;p&gt;None of which you need in full to stop the double charge. What is the smallest version?&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest thing that fixes it
&lt;/h2&gt;

&lt;p&gt;One table and one rule. The rule is from 1978: write down what you are about to do, before you do it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;agent_actions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;key&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tool&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt;       &lt;span class="n"&gt;jsonb&lt;/span&gt;       &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;result&lt;/span&gt;     &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool call now begins by looking up its key, and there are only three things it can find.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you find&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No row&lt;/td&gt;
&lt;td&gt;First attempt&lt;/td&gt;
&lt;td&gt;Write the row, then call the tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row marked &lt;code&gt;done&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;It already happened&lt;/td&gt;
&lt;td&gt;Return the stored result, do not call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row marked &lt;code&gt;pending&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An earlier attempt died mid-call, and you cannot know whether it landed&lt;/td&gt;
&lt;td&gt;Call again, with the same key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the entire design. The code is that table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# identity from position, never from the model
&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insert into agent_actions (key, tool, args) values (%s, %s, %s) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on conflict (key) do nothing returning key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                  &lt;span class="c1"&gt;# a row already exists
&lt;/span&gt;        &lt;span class="n"&gt;prior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;select tool, args, status, result from agent_actions where key = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PlanDivergence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# see below
&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;                          &lt;span class="c1"&gt;# row two: already happened
&lt;/span&gt;
        &lt;span class="c1"&gt;# row three: still pending, so fall through and call again, keyed
&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update agent_actions set status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, result = %s where key = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this works: the row commits before the tool is called. A crash after that point always leaves a note behind, so the resumed run finds the row instead of a blank slate and never gets to decide freshly about something already done.&lt;/p&gt;

&lt;p&gt;Row three is the honest one. It does not pretend to know whether the charge landed, it hands the same key to the payment provider and lets their deduplication settle it. That is the at-least-once plus idempotency bargain from the last section, in one branch.&lt;/p&gt;

&lt;p&gt;There is a fourth thing you can find, and it is the one that catches people out. The row exists, but for a different action, because a key built from position assumes step three means the same thing on every attempt and a replanning model can put something else in that slot. Without the check you would hand an old result to a new intention. Refuse instead. MCP's own idempotency proposal reached the same conclusion when it specified that a key reused with different arguments is rejected outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each additional piece buys
&lt;/h2&gt;

&lt;p&gt;So what does that leave unsolved? Three things, and you will hit them in this order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; A row can sit in &lt;code&gt;pending&lt;/code&gt; forever if the process that claimed it never returns, so you add a worker that scans for stale rows and finishes them. That worker is the &lt;strong&gt;outbox relay&lt;/strong&gt;, and it also lets your agent step commit the intent and return immediately rather than blocking on a slow external call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; A plan with several steps can still end up half applied, because step three succeeding does not help when step four fails permanently. So each reversible step gets a compensator, the irreversible ones move to the end, and you walk backwards on failure. That is the &lt;strong&gt;1987 saga&lt;/strong&gt;, and you need it only once an action spans more than one external system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three.&lt;/strong&gt; A long run can lose expensive model calls on every crash, and asking the model again means the replayed agent may take a different path than the one you have already partly executed. So you record model completions alongside tool results. That is &lt;strong&gt;durable execution&lt;/strong&gt;, and by this point adopting Temporal or DBOS is usually cheaper than continuing to build it.&lt;/p&gt;

&lt;p&gt;Then break it deliberately, because almost nobody does and it is the only way to know any of this works.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kill the process after the charge but before the status update. The next attempt should find a &lt;code&gt;pending&lt;/code&gt; row and lean on the key rather than charging blindly.&lt;/li&gt;
&lt;li&gt;Kill it between the business commit and the checkpoint write, which is the bug from the top of this article. The resumed run should find the action row and skip the call.&lt;/li&gt;
&lt;li&gt;Kill it halfway through a plan with several steps. The compensators should run in reverse order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is about five lines of test, and each corresponds to an outage you would otherwise have later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memories, guesses and apologies
&lt;/h2&gt;

&lt;p&gt;All of that is machinery. It is worth asking what the machinery is for, and the best answer I know predates every framework in your stack.&lt;/p&gt;

&lt;p&gt;In 2009, Helland and David Campbell wrote &lt;em&gt;Building on Quicksand&lt;/em&gt;, arguing that once you accept that no component has complete knowledge, the only honest model left is memories, guesses and apologies. You remember what you have seen, you act on incomplete information and are sometimes wrong, and you build a mechanism for making it right afterwards.&lt;/p&gt;

&lt;p&gt;That maps onto an agent almost too neatly: the model is the guess machine, the message list is the memory, and the compensating transaction is the apology, which is the only one of the three most agent stacks have not built. We assembled the guess machine, bolted the memory onto the side, and the apology is still missing from most production systems.&lt;/p&gt;

&lt;p&gt;The work of figuring out what that apology should look like was done decades ago by people already burned by the same class of failure, and the papers are all still sitting there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Jim Gray, &lt;em&gt;Notes on Data Base Operating Systems&lt;/em&gt; (1978)&lt;/li&gt;
&lt;li&gt;Härder and Reuter, &lt;em&gt;Principles of Transaction-Oriented Database Recovery&lt;/em&gt; (1983), where ACID gets its name&lt;/li&gt;
&lt;li&gt;Garcia-Molina and Salem, &lt;a href="https://dl.acm.org/doi/10.1145/38714.38742" rel="noopener noreferrer"&gt;&lt;em&gt;Sagas&lt;/em&gt;&lt;/a&gt; (SIGMOD 1987)&lt;/li&gt;
&lt;li&gt;Pat Helland, &lt;a href="https://ics.uci.edu/~cs223/papers/cidr07p15.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;Life beyond Distributed Transactions: an Apostate's Opinion&lt;/em&gt;&lt;/a&gt; (CIDR 2007)&lt;/li&gt;
&lt;li&gt;Helland and Campbell, &lt;a href="https://dsf.berkeley.edu/cs286/papers/quicksand-cidr2009.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;Building on Quicksand&lt;/em&gt;&lt;/a&gt; (CIDR 2009)&lt;/li&gt;
&lt;li&gt;Pat Helland, &lt;a href="https://queue.acm.org/detail.cfm?id=2187821" rel="noopener noreferrer"&gt;&lt;em&gt;Idempotence Is Not a Medical Condition&lt;/em&gt;&lt;/a&gt; (ACM Queue 2012)&lt;/li&gt;
&lt;li&gt;Martin Kleppmann, &lt;a href="https://www.youtube.com/watch?v=5ZjhNTM8XU8" rel="noopener noreferrer"&gt;&lt;em&gt;Transactions: myths, surprises and opportunities&lt;/em&gt;&lt;/a&gt; (Strange Loop 2015)&lt;/li&gt;
&lt;li&gt;Chris Richardson, &lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;&lt;em&gt;Pattern: Transactional outbox&lt;/em&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chang and Geng, &lt;a href="https://arxiv.org/abs/2503.11951" rel="noopener noreferrer"&gt;&lt;em&gt;SagaLLM&lt;/em&gt;&lt;/a&gt; (2025), plus &lt;a href="https://arxiv.org/abs/2606.17573" rel="noopener noreferrer"&gt;Cordon&lt;/a&gt; and &lt;a href="https://arxiv.org/abs/2602.14849" rel="noopener noreferrer"&gt;Atomix&lt;/a&gt; (2026)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>architecture</category>
      <category>distributed</category>
    </item>
    <item>
      <title>Did your AI agent quietly blow all your money?</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Sat, 05 Sep 2026 08:05:32 +0000</pubDate>
      <link>https://dev.to/tisha/did-your-ai-agent-quietly-blow-all-your-money-5e9c</link>
      <guid>https://dev.to/tisha/did-your-ai-agent-quietly-blow-all-your-money-5e9c</guid>
      <description>&lt;h6&gt;
  
  
  Featured at Microsoft Developer - Open to Contributions
&lt;/h6&gt;

&lt;p&gt;You set a per-request limit. Every call passes it, no exceptions.&lt;/p&gt;

&lt;p&gt;Then your agent gets stuck in a loop: re-searching, re-checking, retrying the same step over and over. Every single one of those calls is still technically within limits. Nothing was watching the workflow as a whole, only each call, one at a time, blind to a loop right in front of it.&lt;/p&gt;

&lt;p&gt;A workflow that should cost $2 comes in at $20 instead. Run that workflow 10,000 times a day and the gap is not $18, it's $180,000.&lt;/p&gt;

&lt;p&gt;That blind spot is real, and it's measurable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;TokenOps&lt;/a&gt;&lt;/strong&gt; closes it: one budget for the whole run, checked before every call instead of reported after the fact.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/theagentplane" rel="noopener noreferrer"&gt;
        theagentplane
      &lt;/a&gt; / &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;
        tokenops
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Run-aware token governance for multi-agent systems.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TokenOps&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Cuts wasted agent spend by up to &lt;code&gt;65%&lt;/code&gt;, governing what the run has already spent before every call.&lt;/strong&gt;&lt;br&gt;
Toward token governance as a first-class discipline, not an afterthought.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/agent-tokenops/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2ba08bcf17b23f36af307e7fbcc65e7e0e3380812a5fd6bc0d932fd7f0298438/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f6167656e742d746f6b656e6f70732e737667" alt="PyPI"&gt;&lt;/a&gt;
&lt;a href="https://pepy.tech/project/agent-tokenops" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f801b6cea2a7eb160982cc60050353e11de2383bd1957dd0eed64389ae5aa2b9/68747470733a2f2f696d672e736869656c64732e696f2f706570792f64742f6167656e742d746f6b656e6f7073" alt="Downloads"&gt;&lt;/a&gt;
&lt;a href="https://github.com/theagentplane/tokenops/LICENSE.txt" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://github.com/theagentplane/tokenops/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ab402b788724989b6a85d5da07912431bf3ec3529b6a9d259e768e2a22bf5189/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f7468656167656e74706c616e652f746f6b656e6f70733f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://www.linkedin.com/company/the-agent-plane/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f35a923ea1436123c5afc39778dc592f4cc46d1029384fdf3f39f039a15c801e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c696e6b6564496e2d3041363643323f6c6f676f3d6c696e6b6564696e266c6f676f436f6c6f723d7768697465" alt="LinkedIn"&gt;&lt;/a&gt;
&lt;a href="https://github.com/theagentplane/tokenops/discussions" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/dce1418120314e52bb359c2875f28053b3f3f25bf7667cdaf68ac8086a9d3236/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f44697363757373696f6e732d3234323932663f6c6f676f3d676974687562266c6f676f436f6c6f723d7768697465" alt="GitHub Discussions"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/72317aa1a56ab4fda23e84bf8955df3c024d3f3b4480857c85aea62ee921a344/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f46656174757265642d4d6963726f736f6674253230446576656c6f7065722d354535453545" alt="Featured by Microsoft Developer"&gt;&lt;/a&gt;
&lt;a href="https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b28e8b59ce2771be47e8f463409addabe6168927f471be988f9891e177f10169/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f46656174757265642d436f6d6d616e642532304c696e652d354535453545" alt="Featured by Command Line"&gt;&lt;/a&gt;
&lt;a href="https://www.youtube.com/watch?v=GJX19pNhmSw" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ff1ed2aa3a26e2c81a6759e8322df6bf065e2a9dc6fd08f3dce3f7867d89f73d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f46656174757265642d4149253230456e67696e656572253230576f726c642773253230466169722d4646303030303f6c6f676f3d796f7574756265266c6f676f436f6c6f723d7768697465" alt="Featured by AI Engineer World's Fair"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Built by &lt;b&gt;&lt;a href="https://www.linkedin.com/in/susheemkoul/" rel="nofollow noopener noreferrer"&gt;Susheem Koul&lt;/a&gt;&lt;/b&gt; and &lt;b&gt;&lt;a href="https://www.linkedin.com/in/tisha-chawla/" rel="nofollow noopener noreferrer"&gt;Tisha Chawla&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/theagentplane/tokenops/main/docs/assets/devto-cover.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ftheagentplane%2Ftokenops%2Fmain%2Fdocs%2Fassets%2Fdevto-cover.png" alt="TokenOps: one budget for one whole agent run, enforced before every model call" width="720"&gt;&lt;/a&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;i&gt;See it stop a run mid-budget in the &lt;a href="https://github.com/theagentplane/tokenops#-quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt; below.&lt;/i&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;a href="https://github.com/theagentplane/tokenops#-core-features" rel="noopener noreferrer"&gt;Core features&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops#-quickstart" rel="noopener noreferrer"&gt;Quickstart&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops#-quickdeploy" rel="noopener noreferrer"&gt;Quickdeploy&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops#-how-tokenops-compares" rel="noopener noreferrer"&gt;How it compares&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops/docs/policies/" rel="noopener noreferrer"&gt;Policies&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops#-support" rel="noopener noreferrer"&gt;Support&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops#-open-to-contribution" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;🙌 Open to contribution&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;Token spend deserves the same first-class attention as compute or latency, and
we are growing the community working on that. Policies, actuators, and the
shared ledger are all open to extension. See
&lt;strong&gt;&lt;a href="https://github.com/theagentplane/tokenops/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;&lt;/strong&gt; to get started.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Core features&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;An AI agent's workflow can run up cost fast: dozens of small, individually
cheap steps that quietly add up to a surprisingly large bill. TokenOps sets
a single budget for the whole workflow and enforces it before every step
so spending never gets away…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  🎬 See it happen in ten seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;agent-tokenops
python &lt;span class="nt"&gt;-m&lt;/span&gt; tokenops.demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An agent makes 40 model calls. Budget for the whole run: $2.00.

  without TokenOps   40 calls run, spend $5.80
  with TokenOps      halted at call 12, spend $2.03

  $3.77 not spent. The run stopped itself.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No install beyond the package, no API keys, no server. No single call in that run was expensive. It was the 40 of them together that crossed the cap, exactly what a per-request limit can't see.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 Wire it into your own agent without reading the docs
&lt;/h2&gt;

&lt;p&gt;TokenOps ships an integration skill, a written procedure your coding assistant reads and follows so you do not have to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Claude Code&lt;/strong&gt;, from a clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/integrate-tokenops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In Cursor, Copilot, or anywhere else&lt;/strong&gt;, paste this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Integrate TokenOps into this agent, following &lt;a href="https://github.com/theagentplane/tokenops/blob/main/.claude/skills/integrate-tokenops/SKILL.md" rel="noopener noreferrer"&gt;https://github.com/theagentplane/tokenops/blob/main/.claude/skills/integrate-tokenops/SKILL.md&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It reads your agent's code, picks the right setup, wires the one enforcement point, and tells you what to check. That's the whole integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ What it does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg7xesogxg0qwi21ijyz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg7xesogxg0qwi21ijyz.png" alt="TokenOps core features: enforced pre-call, run-scoped budget, shared across processes, steers not just stops, tool calls count too, ten policies included" width="799" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Run-scoped budget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One cap for the whole workflow. Your finance team does not care that each call was "technically within limits."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enforced pre-call, not "oops, sorry" after&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Checks the total before the call goes out and refuses the one that would break budget, instead of quietly logging it for you to discover in an invoice.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-process&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Research, summarize, and review can be three separate services sharing one ledger, instead of each one secretly assuming it gets the whole budget to itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Steers, not just stops&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shrinks the next prompt, swaps to a cheaper model, or tells your agent it's going in circles, before it pulls the plug entirely.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool calls count too&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;That search result your agent stuffed into the next prompt is spend whether an LLM produced it or not. TokenOps counts it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ten policies included&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Documented, swappable. Bring your own if ours do not fit.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not a gateway, not a tracing dashboard. It governs the run, alongside whatever routing and observability you already have.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤝 Come build it with us
&lt;/h2&gt;

&lt;p&gt;TokenOps is early, 0.x, still finding its shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two areas take contributions without touching the governance core at all:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A new policy&lt;/strong&gt; (detector + decision) under &lt;code&gt;src/tokenops/control/policies/&lt;/code&gt;. Ten existing ones as reference, one doc each in &lt;a href="https://github.com/theagentplane/tokenops/tree/main/docs/policies" rel="noopener noreferrer"&gt;&lt;code&gt;docs/policies/&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new adapter&lt;/strong&gt; for another agent SDK, under &lt;code&gt;src/tokenops/adapters/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open issues tagged &lt;a href="https://github.com/theagentplane/tokenops/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22" rel="noopener noreferrer"&gt;&lt;code&gt;good first issue&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/theagentplane/tokenops/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22" rel="noopener noreferrer"&gt;&lt;code&gt;help wanted&lt;/code&gt;&lt;/a&gt; right now, including a dependency pin and a UI responsiveness fix. Small, self-contained, good first PR material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get set up:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/theagentplane/tokenops.git
&lt;span class="nb"&gt;cd &lt;/span&gt;tokenops
make &lt;span class="nb"&gt;install
&lt;/span&gt;make lint
make &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open an issue before a bigger PR so we can agree on approach first; typos and small docs fixes can go straight to a PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions or integration help:&lt;/strong&gt; &lt;a href="https://join.slack.com/t/theagentplane/shared_invite/zt-47lqx2xtc-0idr1cuLNJ_JDTgqxDiUsg" rel="noopener noreferrer"&gt;Slack&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/tokenops/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; · &lt;a href="https://calendly.com/theagentplane/theagentplane" rel="noopener noreferrer"&gt;office hours&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If this saved you from a bill you didn't see coming, or just made you check your own agent's budget, ⭐ &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;star the repo&lt;/a&gt;. It genuinely helps more people find it, and it costs you nothing but a click.&lt;/p&gt;

&lt;p&gt;Built by &lt;a class="mentioned-user" href="https://dev.to/tisha"&gt;@tisha&lt;/a&gt; and &lt;a class="mentioned-user" href="https://dev.to/susheem-k"&gt;@susheem-k&lt;/a&gt; &lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:10:32 +0000</pubDate>
      <link>https://dev.to/tisha/-1m23</link>
      <guid>https://dev.to/tisha/-1m23</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" class="crayons-story__hidden-navigation-link"&gt;Your AI agent can quietly blow 65% past budget&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/tisha" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png" alt="tisha profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/tisha" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Tisha
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Tisha
                
                
              
              &lt;div id="story-author-preview-content-4568895" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/tisha" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Tisha&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" id="article-link-4568895"&gt;
          Your AI agent can quietly blow 65% past budget
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Token spend deserves the same first-class attention as compute or latency, and we're growing the community working on that.</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Thu, 03 Sep 2026 23:45:44 +0000</pubDate>
      <link>https://dev.to/tisha/token-spend-deserves-the-same-first-class-attention-as-compute-or-latency-and-were-growing-the-3jj</link>
      <guid>https://dev.to/tisha/token-spend-deserves-the-same-first-class-attention-as-compute-or-latency-and-were-growing-the-3jj</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" class="crayons-story__hidden-navigation-link"&gt;Your AI agent can quietly blow 65% past budget&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/tisha" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png" alt="tisha profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/tisha" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Tisha
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Tisha
                
                
              
              &lt;div id="story-author-preview-content-4568895" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/tisha" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Tisha&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" id="article-link-4568895"&gt;
          Your AI agent can quietly blow 65% past budget
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/tisha/your-ai-agent-can-quietly-blow-65-past-budget-484j#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Thu, 03 Sep 2026 21:25:41 +0000</pubDate>
      <link>https://dev.to/tisha/-58e8</link>
      <guid>https://dev.to/tisha/-58e8</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni" class="crayons-story__hidden-navigation-link"&gt;You Recorded the Incident. Now Prove Your Fix Actually Works.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/tisha" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png" alt="tisha profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/tisha" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Tisha Chawla
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Tisha Chawla
                
                
              
              &lt;div id="story-author-preview-content-4207785" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/tisha" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960087%2F461d1521-802a-4dcb-b11e-7f2a7d88b7e6.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Tisha Chawla&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni" id="article-link-4207785"&gt;
          You Recorded the Incident. Now Prove Your Fix Actually Works.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/testing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;testing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;22&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              18&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>TokenOps video is out now from AI Engineer WF - SF</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:14:28 +0000</pubDate>
      <link>https://dev.to/tisha/tokenops-video-is-out-now-from-ai-engineer-wf-sf-gf5</link>
      <guid>https://dev.to/tisha/tokenops-video-is-out-now-from-ai-engineer-wf-sf-gf5</guid>
      <description>&lt;p&gt;&lt;a href="https://youtu.be/GJX19pNhmSw" rel="noopener noreferrer"&gt;https://youtu.be/GJX19pNhmSw&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>management</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Just published TokenOps in Microsoft Command Line! ⭐</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Fri, 14 Aug 2026 11:42:52 +0000</pubDate>
      <link>https://dev.to/tisha/just-published-4g9p</link>
      <guid>https://dev.to/tisha/just-published-4g9p</guid>
      <description>&lt;p&gt;If you find it useful, star the repo ⭐: &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;https://github.com/theagentplane/tokenops&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat tokens like infrastructure, not a surprise bill&lt;/li&gt;
&lt;li&gt;Track cost per agent run in real time&lt;/li&gt;
&lt;li&gt;Set clear budgets and limits before agents go off track&lt;/li&gt;
&lt;li&gt;Useful for anyone building production AI agents
Read it here: &lt;a href="https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/" rel="noopener noreferrer"&gt;https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pip install agent-tokenops&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Just published https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/
TokenOps helps devs control AI agent token costs in real time, per run. 
Star ⭐: https://github.com/theagentplane/tokenops</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:14:32 +0000</pubDate>
      <link>https://dev.to/tisha/just-published-106j</link>
      <guid>https://dev.to/tisha/just-published-106j</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcommandline.microsoft.com%2Fwp-content%2Fuploads%2F2026%2F08%2FScreenshot-2026-08-13-at-9.27.28-AM.png" height="469" class="m-0" width="799"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/" rel="noopener noreferrer" class="c-link"&gt;
            TokenOps: Real-time, run-scoped cost control for AI agents
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Budget caps and gateways can’t stop a runaway AI agent mid-run. TokenOps enforces one shared, run-scoped budget in real time, before the bill arrives.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcommandline.microsoft.com%2Fwp-content%2Fuploads%2F2026%2F05%2Ffavicon-150x150.png" width="150" height="150"&gt;
          commandline.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/theagentplane/tokenops" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fopengraph.githubassets.com%2F0c64d701dc39215af27ab249aee86b4145838ed7f223c16376db14171ef3c5bc%2Ftheagentplane%2Ftokenops" height="600" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer" class="c-link"&gt;
            GitHub - theagentplane/tokenops: Run-aware token governance for multi-agent systems. · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Run-aware token governance for multi-agent systems. - theagentplane/tokenops
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Debugging Multi-Agent Systems: Your Trace Tree Is Lying</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Wed, 12 Aug 2026 10:28:32 +0000</pubDate>
      <link>https://dev.to/tisha/debugging-multi-agent-systems-your-trace-tree-is-lying-1oi9</link>
      <guid>https://dev.to/tisha/debugging-multi-agent-systems-your-trace-tree-is-lying-1oi9</guid>
      <description>&lt;p&gt;&lt;em&gt;Co-written with &lt;a href="https://dev.to/susheem-k"&gt;@susheem-k&lt;/a&gt; / &lt;a href="https://dev.to/tisha"&gt;@tisha&lt;/a&gt;. We build &lt;a href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;Chronicle&lt;/a&gt; in the open at &lt;a href="https://theagentplane.github.io" rel="noopener noreferrer"&gt;AgentPlane&lt;/a&gt;. Originally published at &lt;a href="https://theagentplane.github.io/blog/bug-report-to-exact-trace.html" rel="noopener noreferrer"&gt;theagentplane.github.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's Tuesday. Someone on support forwards you a message: &lt;em&gt;"&lt;a href="mailto:priya@acmecorp.com"&gt;priya@acmecorp.com&lt;/a&gt; says the assistant told her something false, sometime yesterday afternoon."&lt;/em&gt; No conversation id. No message id. Just a name and a rough window of time. Maybe it's quieter than that: a thumbs-down on your own feedback button, or an alert your monitoring auto-files. Either way, what lands on your desk is never "here is the broken function." At best, it's a person and roughly when.&lt;/p&gt;

&lt;p&gt;Your agent, meanwhile, is not one function call. A single user message might fan out into an orchestrator calling a researcher, which calls a model, which calls a search tool, twice, with a retry in the middle. That one bad reply the user saw was produced by one specific call, three levels deep, somewhere inside a tree of a dozen calls made sometime in a two-hour window. You have a name and a time range. You need a call stack.&lt;/p&gt;

&lt;p&gt;This post closes that gap, from first principles, with the actual mechanism, not a hand-wave. By the end you'll know how to go from "a name and roughly when" down to a session, a message, and the exact call that misfired; the specific attribution bug that quietly breaks naive tracing in any system with parallel or repeated sub-agents (you have this bug right now if you haven't specifically fixed it); and the one-line change that fixes it, drawn from &lt;a href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;Chronicle&lt;/a&gt; (&lt;code&gt;chronicle#41&lt;/code&gt;, open for review as we write this).&lt;/p&gt;

&lt;h2&gt;
  
  
  First principles: the four IDs
&lt;/h2&gt;

&lt;p&gt;Before you can go from "a name and roughly when" to "here's the exact call that misfired," you need to know what identifies what, and where you actually start. There are four levels, and conflating any two of them is where most homegrown tracing setups go wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qrp7rxcy58m1vxlxdzh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qrp7rxcy58m1vxlxdzh.png" alt="Session contains messages, one message maps to one trace, one trace contains many envelopes" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session&lt;/strong&gt; (&lt;code&gt;session_id&lt;/code&gt;). One conversation. A user might send you ten messages over an hour; they all share one session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message&lt;/strong&gt;. One user turn inside that session. This is what a precise bug report gives you, when you're lucky enough to get one: "in this message, the agent said something wrong."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace&lt;/strong&gt; (&lt;code&gt;trace_id&lt;/code&gt;). One agent &lt;em&gt;run&lt;/em&gt;. In practice, one message turn produces one trace: the user sends a message, the agent does whatever it does (one call or twenty), and produces a reply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Envelope&lt;/strong&gt; (&lt;code&gt;envelope_id&lt;/code&gt;, OTel calls it a &lt;em&gt;span&lt;/em&gt;). One LLM call, one tool call, or one routing decision inside that run. A trace is an ordered tree of these.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resolving "yesterday afternoon" into a &lt;code&gt;session_id&lt;/code&gt; is ordinary application code, not Chronicle's job: look up the account, find the sessions active in that window. Chronicle's job starts once you have that &lt;code&gt;session_id&lt;/code&gt;. Single-shot service-to-service calls collapse the next two (no multi-turn session, just a &lt;code&gt;session_id&lt;/code&gt; and maybe a &lt;code&gt;caller_id&lt;/code&gt;). Multi-turn chat keeps &lt;code&gt;session_id&lt;/code&gt; constant across the conversation and mints a new &lt;code&gt;message_id&lt;/code&gt; (and a new &lt;code&gt;trace_id&lt;/code&gt;) on every turn. Either way, the shape is the same: &lt;strong&gt;you start with almost nothing, resolve down to a session and a message, and Chronicle gets you the rest of the way to the exact call.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is exactly the mapping problem we'd sketched out on a whiteboard before writing a line of code: name and time → session → message → trace, with the open question of how you ever get from "roughly yesterday afternoon" to "the actual execution graph." The rest of this post is the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just add tracing" doesn't get you there
&lt;/h2&gt;

&lt;p&gt;Say you've done the obvious thing: wrapped your LLM and tool calls so each one records an envelope with a &lt;code&gt;parent_envelope_id&lt;/code&gt;, and you build a tree out of that after the fact. This works in every demo. It works in your first three fixtures. Then it breaks silently, on the one trace you actually needed, because you have &lt;strong&gt;two branches of the same shape running in the same trace&lt;/strong&gt;, which in a multi-agent system is the common case, not the edge case.&lt;/p&gt;

&lt;p&gt;And if you already run OpenTelemetry against this agent, you are not exempt. The failure mode here isn't "we have no tracing." It's "our tracing is confidently wrong," which is worse, because a tree that renders cleanly is a tree you trust. You stop looking anywhere else. You spend the debugging session inside the wrong subtree, close the ticket with a fix that doesn't touch the actual bug, and it comes back a week later with a slightly different repro.&lt;/p&gt;

&lt;p&gt;Here's the concrete scenario: an orchestrator calls the same sub-agent twice, once per source it needs to research. Each call does an LLM planning step and a tool call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt;

&lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;web_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# plain code, not a boundary
&lt;/span&gt;    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;web_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# plain code, not a boundary
&lt;/span&gt;    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (source A)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (source B)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Notice &lt;code&gt;researcher&lt;/code&gt; and &lt;code&gt;orchestrator&lt;/code&gt; aren't boundaries themselves. That's deliberate: you mark &lt;strong&gt;decision nodes&lt;/strong&gt; (an LLM call, a tool call, a routing choice), not the whole call stack. This keeps recording overhead near zero and keeps your orchestration as plain code Chronicle never has to know about. The question is how those decision nodes learn who their parent is if nobody wrapped &lt;code&gt;researcher&lt;/code&gt; or &lt;code&gt;orchestrator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The naive answer is "attribute a new envelope to whichever envelope finished most recently." It's the simplest thing that could work, and it's wrong the moment two sibling subtrees are in flight or interleaved:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffxumqwfs2f5mkfq5j7uu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffxumqwfs2f5mkfq5j7uu.png" alt="Before: last-finished attribution misattributes researcher number 2's calls to researcher number 1. After: context-stack attribution nests them correctly." width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;researcher&lt;/code&gt;'s second call starts its &lt;code&gt;planner&lt;/code&gt; call, the "most recently finished" envelope is &lt;code&gt;web_search#1&lt;/code&gt; from the &lt;em&gt;first&lt;/em&gt; call, not anything belonging to the second &lt;code&gt;researcher&lt;/code&gt;. The tree you reconstruct silently welds the second research branch onto the first one. You go looking for why researcher #2 produced a bad answer, and every log line in front of you belongs to researcher #1. Nothing crashes. Nothing throws. The trace just quietly points you at the wrong code for an hour.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: open the span before the body runs
&lt;/h2&gt;

&lt;p&gt;The fix is one sentence: &lt;strong&gt;a boundary opens its span when it's entered, before the wrapped function runs, not after it returns.&lt;/strong&gt; That single change is the difference between "attribute by whatever happened to finish last" and "attribute by what's actually on the call stack right now," and that is precisely what OpenTelemetry's &lt;code&gt;Context&lt;/code&gt; does, and precisely what was missing.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified: what @boundary now does around your function
&lt;/span&gt;&lt;span class="n"&gt;span_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# push onto the active-span stack
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# anything this calls sees `span_id` as parent
&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                      &lt;span class="c1"&gt;# pop back to the caller's span
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Because &lt;code&gt;start_span()&lt;/code&gt; runs before &lt;code&gt;fn&lt;/code&gt;, any boundary invoked &lt;em&gt;while this one is still on the stack&lt;/em&gt; correctly parents to it, no matter what else happens to finish in between. Rerun the two-branch scenario above and the tree comes out right every time, regardless of timing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftheagentplane.github.io%2Fassets%2Fblog%2Fwaterfall.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Ftheagentplane.github.io%2Fassets%2Fblog%2Fwaterfall.svg" alt="Animated waterfall: orchestrator, two researcher branches each with a correctly nested llm and tool call" width="860" height="340"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trace: trace-9f2a1
  total: 164.5ms   resource/dims: message_id=msg_042 session_id=sess_abc

orchestrator#1   ████████████████████████████████████████████  164.5ms  custom
  researcher#1   ██████████████████████                          74.8ms  custom
    llm#1         ██████████████                                  42.3ms  llm
    web_search#1                ██████████                        31.7ms  tool
  researcher#2                          ████████████████████      72.3ms  custom
    llm#2                                ████████████              40.5ms  llm
    web_search#2                                       ██████      30.9ms  tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is what &lt;code&gt;graph.to_otel_waterfall()&lt;/code&gt; prints (there's also &lt;code&gt;graph.to_otel_tree()&lt;/code&gt;, which prints the same tree as indented lines instead of a timeline). Both are debug helpers you run locally or pipe into a log line, not a hosted product; if you want a shared dashboard your whole team browses, that's a separate concern from what a recording library should own.&lt;/p&gt;
&lt;h2&gt;
  
  
  The other half: getting &lt;code&gt;session_id&lt;/code&gt; and &lt;code&gt;message_id&lt;/code&gt; onto the trace
&lt;/h2&gt;

&lt;p&gt;Correct nesting gets you a trustworthy tree once you're looking at the right trace. It doesn't yet tell you &lt;em&gt;which&lt;/em&gt; trace, out of everything your agent ran today. For that, Chronicle added &lt;code&gt;dims&lt;/code&gt;: a flat &lt;code&gt;dict[str, str]&lt;/code&gt; you pass once, that gets copied onto every envelope in the run.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace-9f2a1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.chronicle/runs/prod.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess_abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# constant for the whole conversation
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# new value every turn
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. Chronicle doesn't store your chat history and doesn't own a "look up trace by message ID" index; that's a product concern for whatever's on the other end (your logging pipeline, your control plane, a support tool). We're building a complementing dashboard for exactly this lookup next (more on that in a future post). Until then, what Chronicle guarantees is that once you have a &lt;code&gt;session_id&lt;/code&gt; and a &lt;code&gt;message_id&lt;/code&gt;, every envelope in the matching trace carries them, so any store you point it at can build that index trivially: &lt;code&gt;grep&lt;/code&gt;, a SQL &lt;code&gt;WHERE&lt;/code&gt;, or a dashboard query, your choice.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;agent-chronicle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mark your decision nodes.&lt;/strong&gt; Wrap the LLM calls, tool calls, and routing choices you'd actually want to assert on in a test, not the whole orchestrator function.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt;

   &lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

   &lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;web_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;web_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Record a run with the ids you'll get back from a ticket.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt;

   &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace-9f2a1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.chronicle/runs/prod.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess_abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="nf"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;When a ticket comes in, pull the trace and print the tree.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecutionGraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_envelopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;envelopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_otel_waterfall&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn the bad run into a regression test.&lt;/strong&gt; Add &lt;code&gt;export="fixtures/traces/incident-001/"&lt;/code&gt; to the &lt;code&gt;record(...)&lt;/code&gt; call and the exact trace gets committed to git as a fixture. Replay it (&lt;code&gt;chronicle.replay_trace&lt;/code&gt;) with your fix applied and no live model calls, so "did this actually fix it" becomes a test you run in CI, not a hope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your agent is instrumented with OpenTelemetry already, &lt;code&gt;chronicle.instrument_otel()&lt;/code&gt; emits the same nested spans to your existing collector (&lt;code&gt;pip install agent-chronicle[otel]&lt;/code&gt;), so this isn't an either/or with the observability stack you already run.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this doesn't solve, on purpose
&lt;/h2&gt;

&lt;p&gt;We'd rather tell you the edges than let you find them the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chronicle doesn't store your session or message history.&lt;/strong&gt; It stamps the ids you give it onto envelopes. Owning "what messages exist in this session" is your app's job, not a recording library's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no hosted lookup UI yet.&lt;/strong&gt; &lt;code&gt;to_otel_tree()&lt;/code&gt; / &lt;code&gt;to_otel_waterfall()&lt;/code&gt; are local debug output. A dashboard where a support ticket resolves straight to a trace view is a control-plane concern we're building next, not something bolted into this library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay proves control flow, not answer quality.&lt;/strong&gt; Replaying a trace with your fix tells you the agent takes the right &lt;em&gt;path&lt;/em&gt; and calls the right tools with the right arguments, deterministically, with no live model calls. It won't tell you a subjectively "better" answer is actually better; that's a model-quality eval, a different tool for a different question.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  If this is a problem you have
&lt;/h2&gt;

&lt;p&gt;Star &lt;a href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;theagentplane/chronicle&lt;/a&gt; 🌟 if this is useful, it's the fastest way to tell us to keep going, and it's genuinely how a two-person OSS project gets found by the next person with this exact problem. Try it on one flaky agent, tell us where the API gets in your way, or open an issue with the trace that broke you. We're building this in public specifically so the next fix is shaped by a real incident, not a guess.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;We're &lt;a href="https://dev.to/susheem-k"&gt;@susheem-k&lt;/a&gt; and &lt;a href="https://dev.to/tisha"&gt;@tisha&lt;/a&gt;, building agent infrastructure in the open at &lt;a href="https://theagentplane.github.io" rel="noopener noreferrer"&gt;AgentPlane&lt;/a&gt;. Chronicle is one piece; &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;TokenOps&lt;/a&gt; (run-aware token governance) is the other. Follow along or &lt;a href="https://github.com/theagentplane" rel="noopener noreferrer"&gt;browse the org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/theagentplane" rel="noopener noreferrer"&gt;
        theagentplane
      &lt;/a&gt; / &lt;a href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;
        chronicle
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Record-and-replay for agent decision graphs: reproduce a prod agent failure as a committed regression test, and re-run your fix without live LLM calls.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Chronicle&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Record-and-replay for agent decision graphs.&lt;/strong&gt;&lt;br&gt;
Turn a production agent failure into a committed regression test, and re-run your fix without live LLM calls.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/theagentplane/chronicle/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/theagentplane/chronicle/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://pypi.org/project/agent-chronicle/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/dedbe488ca4512c162214cf9889e03b528680d69a020e60c2d5f92cda3714893/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f6167656e742d6368726f6e69636c652e737667" alt="PyPI"&gt;&lt;/a&gt;
&lt;a href="https://pypi.org/project/agent-chronicle/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/13466c63b4f964f1bc47e46eabb6ea9ebff2cb7777cba82af790a55719bbbfda/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31302532422d626c75652e737667" alt="Python"&gt;&lt;/a&gt;
&lt;a href="https://github.com/theagentplane/chronicle/blob/main/LICENSE.txt" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/07a7d0169027aac6d7a0bfa8964dfef5fbc40d5a2075cabb3d8bc67e17be3451/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://github.com/astral-sh/ruff" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d6c7524504b7d886a9d34c11f44b9d31b2de1a579325b42e932744c4575a063b/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f61737472616c2d73682f727566662f6d61696e2f6173736574732f62616467652f76322e6a736f6e" alt="Ruff"&gt;&lt;/a&gt;
&lt;a href="https://github.com/theagentplane/chronicle/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0c641468bf5baa7c42a30535c7e47a4eb3711e45c50c27e6a93bdb4840a3d8f5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f7468656167656e74706c616e652f6368726f6e69636c653f7374796c653d666c617426636f6c6f723d79656c6c6f77" alt="Stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/theagentplane/chronicle/main/docs/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ftheagentplane%2Fchronicle%2Fmain%2Fdocs%2Fdemo.gif" alt="Chronicle: record an incident, then verify the fix with a cut-point test" width="760"&gt;&lt;/a&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Lc8zRh9muoY" rel="nofollow noopener noreferrer"&gt;Watch the full walkthrough&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Chronicle records what your agent did at each decision point (its LLM calls, tool
calls, and routing choices) so you can reproduce a production failure as a committed
regression test and re-run your fix without live LLM calls. It targets one specific
real problem: control-flow and tool-safety regressions in multi-agent systems, caught
deterministically from recorded incidents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New here?&lt;/strong&gt; The &lt;a href="https://github.com/theagentplane/chronicle/blob/main/docs/onboarding.md" rel="noopener noreferrer"&gt;step-by-step onboarding guide&lt;/a&gt;
walks you from install to a committed regression test.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/theagentplane/chronicle#why-chronicle" rel="noopener noreferrer"&gt;Why&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#cut-point-replay" rel="noopener noreferrer"&gt;Cut-point replay&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#recording-entry-points" rel="noopener noreferrer"&gt;Recording&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#verification-layers" rel="noopener noreferrer"&gt;Verification&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#how-chronicle-compares" rel="noopener noreferrer"&gt;Compare&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#demos" rel="noopener noreferrer"&gt;Demos&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; · &lt;a href="https://github.com/theagentplane/chronicle#roadmap" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;b&gt;Key terms&lt;/b&gt; (boundary, Envelope, trace, dims, fixture, stub, live, cut-point)
&lt;br&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boundary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A decision &lt;strong&gt;node&lt;/strong&gt; you mark: an LLM call, a tool call, or a routing choice — not&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>40% of AI Agent Projects Will Be Canceled by 2027. Token Governance Is How You Avoid It.</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:55:47 +0000</pubDate>
      <link>https://dev.to/tisha/40-of-ai-agent-projects-will-be-canceled-by-2027-token-governance-is-how-you-avoid-it-38id</link>
      <guid>https://dev.to/tisha/40-of-ai-agent-projects-will-be-canceled-by-2027-token-governance-is-how-you-avoid-it-38id</guid>
      <description>&lt;p&gt;&lt;em&gt;By Tisha Chawla and Susheem Koul&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; AI agents spend tokens to think, and how many they spend is nondeterministic. The same task can cost wildly different amounts across runs, and a stuck recursive loop can spend without limit while still looking successful. You cannot budget a nondeterministic resource up front. You have to &lt;strong&gt;observe&lt;/strong&gt; it, &lt;strong&gt;attribute&lt;/strong&gt; it, and &lt;strong&gt;enforce&lt;/strong&gt; limits at runtime, from a control loop kept separate from the agent.&lt;/p&gt;

&lt;p&gt;🛠️ &lt;strong&gt;We built this into an open-source tool.&lt;/strong&gt; &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;&lt;strong&gt;TokenOps&lt;/strong&gt;&lt;/a&gt; is run-aware token governance for multi-agent systems: a control plane + SDK that caps spend and steers behavior across a whole agent workflow, not per request.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;agent-tokenops
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;If this field guide is useful, &lt;strong&gt;&lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;⭐ star the repo&lt;/a&gt;&lt;/strong&gt; so more people building agents can find it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;First principles&lt;/li&gt;
&lt;li&gt;The problem&lt;/li&gt;
&lt;li&gt;Primitives for token governance&lt;/li&gt;
&lt;li&gt;The current space&lt;/li&gt;
&lt;li&gt;The discipline&lt;/li&gt;
&lt;li&gt;Where TokenOps fits&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  0. First principles
&lt;/h2&gt;

&lt;p&gt;Start from what is provably true, then build up.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A token is a metered unit of a paid resource.&lt;/strong&gt; Every model call consumes a countable number of tokens at a published price.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumption is nondeterministic.&lt;/strong&gt; Token generation is stochastic: at each step the model samples the next token from a probability distribution, so the same prompt can return a different completion, and a different number of output tokens, on every run. OpenAI states its APIs are nondeterministic by default, and that determinism is not guaranteed even with a fixed seed.&lt;sup id="fnref3"&gt;3&lt;/sup&gt; Agents amplify this. An agent picks its next action from that nondeterministic output, so the number of reasoning steps and tool calls, and therefore the total token spend, is a random variable, not a fixed cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can only govern what you can measure, attribute, and bound.&lt;/strong&gt; Measure it (how many), attribute it (whose), bound it (a ceiling that actually holds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot bound it by prediction.&lt;/strong&gt; A static estimate set in advance is wrong on the tail. The only bound that holds is enforced at runtime: watch spend as it accrues, and stop the run when it crosses a limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bound must sit outside the agent.&lt;/strong&gt; An agent chooses its own next call, so nothing in its own logic is guaranteed to stop it. Governance is an external control loop around the agent, not a feature inside it.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The three questions that decide whether you govern token spend:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How many tokens did this run spend? &lt;em&gt;(measure)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Who spent them: which user, which agent, which run? &lt;em&gt;(attribute)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;What stops a run before the invoice does? &lt;em&gt;(bound)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot answer all three, you are observing spend after the fact, not governing it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The problem
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One example, used throughout.&lt;/strong&gt; Agent A is a research agent: it calls a &lt;code&gt;search&lt;/code&gt; tool, reads the results, and hands findings to Agent B, a summarizer. Agent A runs on OpenAI, Agent B on Anthropic. We follow this single task through every failure mode below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flk931hx1u49v04rfkqrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flk931hx1u49v04rfkqrm.png" alt="Research Agent" width="799" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a token is.&lt;/strong&gt; A token is a chunk of text mapped to an integer ID from the model's fixed vocabulary. A subword tokenizer (for example, byte-pair encoding) splits text into these pieces, so one token can be a whole word, part of a word, or a single character.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;sup id="fnref4"&gt;4&lt;/sup&gt; Different models use different tokenizers, so the same sentence is a different token count on each. Rule of thumb in English: roughly 4 characters, or 0.75 words, per token.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; Output tokens cost several times more than input tokens.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why tokens became an ops problem.&lt;/strong&gt; Agents do not make one call, they loop. Anthropic measured agents using about &lt;strong&gt;4x&lt;/strong&gt; the tokens of a chat, and multi-agent systems about &lt;strong&gt;15x&lt;/strong&gt;, with token usage alone explaining roughly &lt;strong&gt;80%&lt;/strong&gt; of the variance in cost.&lt;sup id="fnref6"&gt;6&lt;/sup&gt; In our example, Agent A might call &lt;code&gt;search&lt;/code&gt; once, or twenty times. You do not know which until it runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 The token leak
&lt;/h3&gt;

&lt;p&gt;Agent A calls &lt;code&gt;search("pricing")&lt;/code&gt;. The result is weak, so it feeds its own output back in and calls &lt;code&gt;search("pricing")&lt;/code&gt; again, and again. This is a &lt;strong&gt;recursive loop&lt;/strong&gt;: the agent keeps invoking the same step because nothing tells it to stop. Fifty calls later it still returns a clean summary, so every functional dashboard shows success. The spend shows up in exactly one place: &lt;strong&gt;the bill&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is a &lt;strong&gt;token leak&lt;/strong&gt;: spend that grows with no bound and no attribution, whether from a silent retry loop, a context window that keeps growing, or an unflagged model upgrade. Provider guardrails do not catch it: OpenAI project budgets are alerts, not hard caps.&lt;sup id="fnref7"&gt;7&lt;/sup&gt; Unmanaged, this is now a top failure mode. Gartner predicts over &lt;strong&gt;40% of agentic AI projects will be canceled by end of 2027&lt;/strong&gt;, citing escalating cost.&lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The quieter leak: cache busting.&lt;/strong&gt; Repeated input can be cached at a steep discount: OpenAI documents 50% off cached input,&lt;sup id="fnref5"&gt;5&lt;/sup&gt; and Anthropic charges roughly 90% below base input for cache reads.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; The catch: caching matches the prompt &lt;strong&gt;from the top down&lt;/strong&gt;, only up to the first point where the text differs. If Agent A pastes a &lt;code&gt;current_timestamp&lt;/code&gt; or &lt;code&gt;session_id&lt;/code&gt; near the top of its system prompt, it throws away the cache for everything below it, and a large context that should cost a tenth of full price pays full price every turn. Same text, same step count, &lt;strong&gt;10x the bill&lt;/strong&gt;. The fix is structural: put static content (system prompt, tool definitions) first and volatile values last, and treat a drop in cache hit rate as a spend alarm on its own.&lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A third leak: reasoning bloat.&lt;/strong&gt; Reasoning models (OpenAI's o-series and GPT-5 reasoning, Anthropic extended thinking) spend hidden reasoning tokens server-side before emitting a single visible token, and those tokens are billed.&lt;sup id="fnref9"&gt;9&lt;/sup&gt; Two consequences. First, &lt;code&gt;max_output_tokens&lt;/code&gt; caps the &lt;em&gt;sum&lt;/em&gt; of reasoning and visible output,&lt;sup id="fnref9"&gt;9&lt;/sup&gt; so a runaway reasoning loop can burn the entire budget inside one call without returning a character. Second, if your breaker trips by parsing the output stream, it is blind during the thinking phase: the spend has already happened before the first chunk arrives. Read the usage totals the model reports, not just the visible stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Distributed token lineage
&lt;/h3&gt;

&lt;p&gt;The monthly bill arrives: one total from OpenAI, one from Anthropic. Neither number tells you that Agent A's recursive loop caused most of the cost, or which user started the run. &lt;strong&gt;Lineage&lt;/strong&gt; is the missing label on every call: which user, which agent (A or B), and which run produced it. OpenTelemetry's GenAI conventions define exactly these attributes: &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;, &lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;, plus agent and session identifiers.&lt;sup id="fnref10"&gt;10&lt;/sup&gt; Recent versions also add attributes for the costly hidden categories, cached and reasoning tokens, mirroring the provider payloads: OpenAI's &lt;code&gt;prompt_tokens_details.cached_tokens&lt;/code&gt; and &lt;code&gt;completion_tokens_details.reasoning_tokens&lt;/code&gt;, and Anthropic's &lt;code&gt;cache_read_input_tokens&lt;/code&gt;.&lt;sup id="fnref10"&gt;10&lt;/sup&gt; Track those, or your lineage misses exactly the tokens most likely to surprise you.&lt;/p&gt;

&lt;p&gt;One more catch: Agent A's OpenAI tokens and Agent B's Anthropic tokens are &lt;strong&gt;not the same unit&lt;/strong&gt;, because the tokenizers and prices differ. You cannot add raw tokens across providers. Convert each call to &lt;strong&gt;cost&lt;/strong&gt; first (one denominator, such as micro-dollars), then sum and enforce. Enforce on cost, not tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Isolate the guard
&lt;/h3&gt;

&lt;p&gt;Put the budget check inside Agent A's loop and two things break: the same bug that makes it loop can skip the check, and Agent B needs its own copy. So keep two planes apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data plane&lt;/strong&gt;: the agent doing the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control plane&lt;/strong&gt;: the code that meters, attributes, and halts spend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The meter and the limit live in the control plane, an out-of-band loop both agents call but neither can edit or bypass.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Primitives for token governance
&lt;/h2&gt;

&lt;p&gt;Six building blocks. Each answers one question. Together they are the vocabulary for everything below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Counts tokens and cost on every call&lt;/td&gt;
&lt;td&gt;How much did this consume?&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attribution context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tags every call with user, agent, run&lt;/td&gt;
&lt;td&gt;Whose spend is this?&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Budget / quota&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A hard ceiling on tokens or cost over a window&lt;/td&gt;
&lt;td&gt;What is the cap, and does it hold?&lt;/td&gt;
&lt;td&gt;
&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;sup id="fnref13"&gt;13&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A cap on tokens or requests per unit time&lt;/td&gt;
&lt;td&gt;How fast is too fast?&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref14"&gt;14&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Circuit breaker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trips and fails fast when a condition crosses a threshold&lt;/td&gt;
&lt;td&gt;What stops a bad run immediately?&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref15"&gt;15&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Control plane&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Where the rules live, out of band from the agent&lt;/td&gt;
&lt;td&gt;Who owns the decision?&lt;/td&gt;
&lt;td&gt;§1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The circuit breaker, precisely.&lt;/strong&gt; You wrap a protected call in an object that monitors it. When a chosen condition crosses a threshold, the breaker trips to an &lt;em&gt;open&lt;/em&gt; state and fails fast, returning an error without making the call. Classic breakers add a &lt;em&gt;half-open&lt;/em&gt; state to probe recovery.&lt;sup id="fnref15"&gt;15&lt;/sup&gt; A failure can be any condition you define, such as a timeout or an HTTP 429. For token governance, the condition is behavioral: a repeated action, a spend velocity, or a collapse in cache hit rate. The breaker trips to halt the run before the spend lands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How a behavioral check actually works.&lt;/strong&gt; You do not need brittle, domain-specific rules. Three system-level signals over an agent's step history cover most runaway modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic loop.&lt;/strong&gt; Hash each tool call's name and arguments, and trip when the same signature repeats inside a sliding window. A vector-similarity check on consecutive prompts catches near-duplicates, such as an agent feeding the same error back to a tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend velocity.&lt;/strong&gt; Track cost per step (the derivative of cost over steps). A sharp spike means the context is compounding, for example appending full raw logs on every failed turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency decay.&lt;/strong&gt; Watch the ratio of progress made to tokens burned. This one is the most heuristic and hardest to define cleanly, so treat it as a warning, not a hard trip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two are cheap and robust. Start there.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Beyond halt: two richer responses
&lt;/h3&gt;

&lt;p&gt;Halting is the simplest response to a trip, not the only one. Two production patterns are worth knowing, each with a caveat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model cascade (graceful degradation).&lt;/strong&gt; At a warning threshold (say 75% of a session budget), the control plane swaps the next call to a cheaper model and trims non-essential tools, trying a low-cost completion before any hard halt. This ships today: Cloudflare can switch to a cheaper model once a budget is exhausted,&lt;sup id="fnref13"&gt;13&lt;/sup&gt; and gateways like LiteLLM support model fallbacks.&lt;sup id="fnref12"&gt;12&lt;/sup&gt; &lt;strong&gt;Caveat:&lt;/strong&gt; a weaker model can reason worse and loop more, trading a hard failure for a quality risk and sometimes more spend. It is a tradeoff, not a free win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escrow and human-in-the-loop.&lt;/strong&gt; Instead of killing the run, the control plane suspends it, snapshots its state, and pings a human to inspect and resume. Real, but not free: pause-and-resume requires a durable, checkpointable runtime. LangGraph, for example, requires a checkpointer to persist state across an interrupt.&lt;sup id="fnref16"&gt;16&lt;/sup&gt; A lightweight in-process wrapper cannot serialize and resume an arbitrary call stack on its own.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The current space
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Five dimensions
&lt;/h3&gt;

&lt;p&gt;Score every tool on five questions. The first three are table stakes. The last two are where the gap is.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Meter&lt;/td&gt;
&lt;td&gt;Can you see tokens per call?&lt;/td&gt;
&lt;td&gt;
&lt;sup id="fnref10"&gt;10&lt;/sup&gt;&lt;sup id="fnref11"&gt;11&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attribute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Attribution context&lt;/td&gt;
&lt;td&gt;Whose spend: user, agent, run?&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref17"&gt;17&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enforce&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Budget, rate limit&lt;/td&gt;
&lt;td&gt;Can you stop spend in real time?&lt;/td&gt;
&lt;td&gt;
&lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;sup id="fnref13"&gt;13&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behavioral&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Circuit breaker&lt;/td&gt;
&lt;td&gt;Can you halt on a runaway pattern, not just a dollar line?&lt;/td&gt;
&lt;td&gt;
&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;sup id="fnref18"&gt;18&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ownership&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;td&gt;Which layer holds the control point?&lt;/td&gt;
&lt;td&gt;§1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.2 Five categories
&lt;/h3&gt;

&lt;p&gt;Every tool sits at one of five layers in the stack.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Stops spend?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor-native limits&lt;/td&gt;
&lt;td&gt;OpenAI, Anthropic, Azure OpenAI&lt;/td&gt;
&lt;td&gt;Coarse, often alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Out-of-process proxy you route through&lt;/td&gt;
&lt;td&gt;LiteLLM, Portkey, Cloudflare, Kong&lt;/td&gt;
&lt;td&gt;Yes, threshold-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Component&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-process library inside your app&lt;/td&gt;
&lt;td&gt;OpenLLMetry, LangChain callbacks&lt;/td&gt;
&lt;td&gt;Rare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Collector or dashboard&lt;/td&gt;
&lt;td&gt;Langfuse, Datadog, Arize, LangSmith&lt;/td&gt;
&lt;td&gt;No, after the fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The wire format everyone emits&lt;/td&gt;
&lt;td&gt;OpenTelemetry GenAI conventions&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.3 The gap
&lt;/h3&gt;

&lt;p&gt;Across these tools, most &lt;strong&gt;observe&lt;/strong&gt;, several &lt;strong&gt;enforce&lt;/strong&gt; a dollar or rate threshold (the gateway layer is moving fast: Cloudflare shipped per-user spend limits in June 2026&lt;sup id="fnref13"&gt;13&lt;/sup&gt;), but almost none &lt;strong&gt;halt a run on pathological behavior&lt;/strong&gt; (a runaway loop) before the threshold is hit.&lt;/p&gt;

&lt;p&gt;Enforcement also tends to live at the &lt;strong&gt;gateway&lt;/strong&gt;, which sees a stream of independent requests, not the &lt;strong&gt;in-process&lt;/strong&gt; context (the agent's step sequence and loop structure) needed to recognize a loop early. Behavioral signals like a collapsing cache hit rate or a semantic loop are easiest to read right next to the prompt structure and steps that caused them, which a gateway does not retain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-process, behavioral enforcement is the least served square in the landscape today.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The discipline
&lt;/h2&gt;

&lt;p&gt;You already do this for CPU and memory. You measure it, label it, watch it, and cap it. Tokens are simply the newest resource to earn the same four steps. The order matters, because each step depends on the one before it: you cannot attribute what you never measured, and you cannot enforce what you cannot see.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;In plain terms&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Instrument&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Meter it&lt;/td&gt;
&lt;td&gt;Emit one record per model and tool call, carrying token counts&lt;/td&gt;
&lt;td&gt;OpenTelemetry GenAI spans &lt;sup id="fnref10"&gt;10&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Attribute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tag it&lt;/td&gt;
&lt;td&gt;Stamp every record with user, agent, and run&lt;/td&gt;
&lt;td&gt;OTel attributes and vendor tags &lt;sup id="fnref17"&gt;17&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Watch it&lt;/td&gt;
&lt;td&gt;Stream the records so a climbing cost or a falling cache hit rate is visible live, not at month end&lt;/td&gt;
&lt;td&gt;Observability backends &lt;sup id="fnref17"&gt;17&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Enforce&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cap it&lt;/td&gt;
&lt;td&gt;Apply a budget, a rate limit, or a behavioral circuit breaker that trips on a recursive loop&lt;/td&gt;
&lt;td&gt;Budgets and breakers &lt;sup id="fnref12"&gt;12&lt;/sup&gt;&lt;sup id="fnref15"&gt;15&lt;/sup&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Walk Agent A and Agent B through it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instrument.&lt;/strong&gt; Every &lt;code&gt;search&lt;/code&gt; call and every model call, on both OpenAI and Anthropic, emits a record with its token usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribute.&lt;/strong&gt; Each record carries &lt;code&gt;agent=A&lt;/code&gt; or &lt;code&gt;agent=B&lt;/code&gt; and the run id, so the bill splits cleanly instead of arriving as one number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface.&lt;/strong&gt; Agent A's recursive search loop appears as a steeply climbing cost line the moment it starts, not four weeks later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce.&lt;/strong&gt; When Agent A repeats the same &lt;code&gt;search&lt;/code&gt; call past a set threshold, the breaker trips and halts the run before the spend lands.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All four steps live in the control plane (see 1.3), outside the agents, so neither Agent A nor Agent B can skip them.&lt;/p&gt;

&lt;p&gt;The four verbs are a way to organize the work, not an official standard. Every mechanism they point to (spans, attributes, budgets, breakers) is real and documented in the footnotes.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Where TokenOps fits
&lt;/h2&gt;

&lt;p&gt;The least-served square in section 3.3, &lt;strong&gt;in-process, behavioral enforcement that governs the whole run&lt;/strong&gt;, is exactly the square we built &lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;&lt;strong&gt;TokenOps&lt;/strong&gt;&lt;/a&gt; to fill.&lt;/p&gt;

&lt;p&gt;TokenOps is a &lt;strong&gt;control plane + SDK&lt;/strong&gt; for agent stacks. Entry agents register a run; every LLM and tool crossing shares one &lt;code&gt;run_id&lt;/code&gt; and one ledger. Mapping it back to the primitives from section 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Govern the run, not the request.&lt;/strong&gt; One &lt;code&gt;run_id&lt;/code&gt; spans every model, tool, and agent-to-agent hop, so the Agent A → Agent B workflow stays inside a &lt;em&gt;single&lt;/em&gt; budget, the multi-agent case a gateway can't see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared ledger across processes.&lt;/strong&gt; Spend, inflight, and halt state live in SQLite, so Agent A and Agent B can't each burn the full cap locally. This is the cross-provider, enforce-on-cost point from section 1.2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-path enforcement.&lt;/strong&gt; &lt;code&gt;wrap_complete&lt;/code&gt; runs detect → decide → apply &lt;em&gt;before&lt;/em&gt; the next LLM call, the control loop from section 1.3, out of band from the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steer or stop.&lt;/strong&gt; Actuators go beyond halt: &lt;code&gt;HALT&lt;/code&gt; · &lt;code&gt;MUTATE&lt;/code&gt; · &lt;code&gt;INJECT&lt;/code&gt; · reject/queue, the richer responses from section 2.1.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;agent-tokenops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its companion, &lt;a href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;&lt;strong&gt;Chronicle&lt;/strong&gt;&lt;/a&gt;, covers the record-and-replay half: it captures decision boundaries so a production incident becomes a deterministic regression test.&lt;/p&gt;

&lt;p&gt;TokenOps is early (0.x) and MIT-licensed. If this field guide gave you the vocabulary you were missing, the repo is where it becomes runnable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;github.com/theagentplane/tokenops&lt;/a&gt;&lt;/strong&gt;. If it's useful, &lt;strong&gt;&lt;a href="https://github.com/theagentplane/tokenops" rel="noopener noreferrer"&gt;⭐ star it&lt;/a&gt;&lt;/strong&gt; so more people building agents can find it. Issues and PRs welcome.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Built by Tisha Chawla and Susheem Koul.&lt;/em&gt;&lt;/p&gt;







&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;OpenAI, "What are tokens and how to count them." &lt;a href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them" rel="noopener noreferrer"&gt;https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Anthropic, "Pricing" (base input vs output; cache-read pricing). &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/about-claude/pricing&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;OpenAI, "Reproducible outputs with the seed parameter" (APIs are nondeterministic by default; determinism not guaranteed). &lt;a href="https://developers.openai.com/cookbook/examples/reproducible_outputs_with_the_seed_parameter" rel="noopener noreferrer"&gt;https://developers.openai.com/cookbook/examples/reproducible_outputs_with_the_seed_parameter&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;OpenAI, "tiktoken" (open-source tokenizer). &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;https://github.com/openai/tiktoken&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;OpenAI, "Prompt Caching in the API" (input vs output pricing; cached-input discount). &lt;a href="https://openai.com/index/api-prompt-caching/" rel="noopener noreferrer"&gt;https://openai.com/index/api-prompt-caching/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Anthropic, "How we built our multi-agent research system" (4x and 15x token multipliers; ~80% of cost variance). &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;https://www.anthropic.com/engineering/multi-agent-research-system&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;OpenAI, "Managing projects in the API platform" (project budgets are alerts, not hard caps). &lt;a href="https://help.openai.com/en/articles/9186755-managing-projects-in-the-api-platform" rel="noopener noreferrer"&gt;https://help.openai.com/en/articles/9186755-managing-projects-in-the-api-platform&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;Gartner, "Over 40% of Agentic AI Projects Will Be Canceled by End of 2027" (June 25, 2025). &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;OpenAI, "Reasoning models" (internal reasoning tokens are billed; &lt;code&gt;max_output_tokens&lt;/code&gt; caps reasoning plus visible output). &lt;a href="https://developers.openai.com/api/docs/guides/reasoning" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/guides/reasoning&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;OpenTelemetry, "Semantic conventions for generative AI" (status: Development). &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs/specs/semconv/gen-ai/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;OpenTelemetry, "Semantic conventions for generative AI metrics" (&lt;code&gt;gen_ai.client.token.usage&lt;/code&gt;). &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn12"&gt;
&lt;p&gt;LiteLLM, "Budgets and Rate Limits." &lt;a href="https://docs.litellm.ai/docs/proxy/users" rel="noopener noreferrer"&gt;https://docs.litellm.ai/docs/proxy/users&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn13"&gt;
&lt;p&gt;Cloudflare, "AI Gateway spend limits." &lt;a href="https://blog.cloudflare.com/ai-gateway-spend-limits/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/ai-gateway-spend-limits/&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn14"&gt;
&lt;p&gt;Kong, "Token Rate-Limiting and Tiered Access for AI Usage." &lt;a href="https://konghq.com/blog/engineering/token-rate-limiting-and-tiered-access-for-ai-usage" rel="noopener noreferrer"&gt;https://konghq.com/blog/engineering/token-rate-limiting-and-tiered-access-for-ai-usage&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn15"&gt;
&lt;p&gt;Martin Fowler, "CircuitBreaker" (popularizing Michael Nygard, &lt;em&gt;Release It!&lt;/em&gt;). &lt;a href="https://martinfowler.com/bliki/CircuitBreaker.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/CircuitBreaker.html&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn16"&gt;
&lt;p&gt;LangChain, "Persistence" (LangGraph checkpointers; pause and resume require a checkpointer). &lt;a href="https://docs.langchain.com/oss/python/langgraph/persistence" rel="noopener noreferrer"&gt;https://docs.langchain.com/oss/python/langgraph/persistence&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn17"&gt;
&lt;p&gt;Helicone, "Cost Tracking." &lt;a href="https://docs.helicone.ai/guides/cookbooks/cost-tracking" rel="noopener noreferrer"&gt;https://docs.helicone.ai/guides/cookbooks/cost-tracking&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn18"&gt;
&lt;p&gt;Portkey, "Budget Limits." &lt;a href="https://docs.portkey.ai/docs/product/ai-gateway/virtual-keys/budget-limits" rel="noopener noreferrer"&gt;https://docs.portkey.ai/docs/product/ai-gateway/virtual-keys/budget-limits&lt;/a&gt;&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>llmops</category>
    </item>
    <item>
      <title>You Recorded the Incident. Now Prove Your Fix Actually Works.</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:50:02 +0000</pubDate>
      <link>https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni</link>
      <guid>https://dev.to/tisha/you-recorded-the-incident-now-prove-your-fix-actually-works-2cni</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of &lt;a href="https://dev.to/tisha/your-agent-failed-in-prod-good-luck-reproducing-it-56ci"&gt;Your Agent Failed in Prod. Good Luck Reproducing It&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This work was presented at the AI Engineer World's Fair 2026 by Susheem Koul and Tisha Chawla.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In Part 1 the agent deleted the wrong record at 9:04, and you could not make it happen again. The fix was not to force the model to be deterministic. It was to &lt;strong&gt;record the run&lt;/strong&gt;: the exact prompt, the sampled completion, the tool calls, the retrieved chunks, the pinned model version. Freeze the run, not the model.&lt;/p&gt;

&lt;p&gt;So you did that. The incident is now a file on disk. You can open it and see, precisely, what the agent decided that night.&lt;/p&gt;

&lt;p&gt;Now comes the part nobody talks about. You still have to fix the bug. And you have to prove the fix works. This is where most teams quietly fall back into the swamp they just climbed out of.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap, one more time
&lt;/h2&gt;

&lt;p&gt;You change the code. Now you want to confirm it is fixed. So you run the agent again and watch.&lt;/p&gt;

&lt;p&gt;Stop. You just walked back into the trap.&lt;/p&gt;

&lt;p&gt;The moment you re-run to check your fix, you are regenerating. The model samples a fresh path. The retrieval returns slightly different chunks. The batch shape on the endpoint is different from the batch shape at 9:04. Your "it works now" is one draw from a distribution, and so was the failure. You have proven nothing, and you know it, because the failure never reproduced on demand in the first place.&lt;/p&gt;

&lt;p&gt;To verify a fix you need the exact opposite of a re-run. You need everything about that incident to stay frozen, except the one thing you changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scalpel, not a re-run
&lt;/h2&gt;

&lt;p&gt;That is the whole idea, and it has a name: &lt;strong&gt;cut-point replay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Take the recorded incident. Replay it. But mark one boundary as the cut-point. Everything upstream of it is served from the recording, byte for byte the same inputs the agent saw that night. The boundary you changed runs your new code, live. Everything downstream runs live too, so you can watch what your fix does to the rest of the run.&lt;/p&gt;

&lt;p&gt;You are not re-running the agent. You are dropping your new code into the middle of a frozen incident and asking one question: given exactly what happened up to this point, does my change do the right thing now?&lt;/p&gt;

&lt;p&gt;No model call. No API cost. No flakiness. The same incident, every time, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking through it
&lt;/h2&gt;

&lt;p&gt;Here is the deletion agent from Part 1. Two boundaries: the model decides, the tool acts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chronicle&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boundary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reset_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ReplayPlan&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chronicle.envelope.store&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EnvelopeStore&lt;/span&gt;

&lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# calls the model, returns a decision and tool calls
&lt;/span&gt;
&lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# the ungated tool that wiped prod at 9:04
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You already recorded the incident and froze it as a fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reset_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EnvelopeStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.chronicle/runs/incident.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deletion-incident-001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;                                    &lt;span class="c1"&gt;# the bad run, captured
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixtures/traces/deletion-incident-001/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recorded graph is exactly what you would expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent@1  -&amp;gt;  delete_file@1 (deleted prod)  -&amp;gt;  agent@2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the fix. One guard, in the tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guard: destructive op refused in prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the test that proves it, against the real incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reset_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixtures/traces/deletion-incident-001/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;ReplayPlan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# upstream: the exact decision the model made at 9:04
&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# cut-point: your new gated code runs for real
&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# downstream: watch what the agent does after the block
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captured_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the plan again, because it is the entire point. &lt;code&gt;agent@1&lt;/code&gt; is stubbed: the model does not run, you replay the decision it actually made that night. &lt;code&gt;delete_file@1&lt;/code&gt; is live: your new guard executes against those exact arguments. &lt;code&gt;agent@2&lt;/code&gt; is live: you get to see how the agent reacts to a refused deletion instead of a successful one.&lt;/p&gt;

&lt;p&gt;You changed one boundary and held the rest of history still. If the guard blocks the delete, the fix works. Not "worked once." Works, deterministically, on the recorded incident, in CI, with no API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two layers, put to work
&lt;/h2&gt;

&lt;p&gt;Part 1 argued for testing in two layers. Cut-point replay is Layer 1 in action: structural, deterministic, about control flow and tool safety. Did the right tool get called? Were the arguments shaped correctly? Was the destructive action refused? None of that needs the model, so none of it flakes.&lt;/p&gt;

&lt;p&gt;Layer 2 is for the questions structure cannot answer. If your fix was a prompt rewrite or a model bump, "is the output still correct" is a judgment, not an equality check. That is where an LLM-as-judge scores the new completion against the recorded gold one for meaning, not for bytes. Use Layer 1 to prove the machinery is right. Use Layer 2 to prove the words are still good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident becomes a regression test
&lt;/h2&gt;

&lt;p&gt;Here is the quiet payoff. That fixture under &lt;code&gt;fixtures/traces/&lt;/code&gt; is committed to git. It is now a permanent test. Six months from now, when someone refactors the tool router and the guard silently stops firing, this test goes red on their pull request, not on a customer's production database.&lt;/p&gt;

&lt;p&gt;The failure that was unreproducible at 9:04 becomes a green check that runs on every commit. That is the difference between an incident and a regression test: one is a story you tell, the other is a thing your CI enforces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you commit anything: redact
&lt;/h2&gt;

&lt;p&gt;A recorded run is a faithful copy of production. It contains the assembled prompt, the retrieved chunks, the tool arguments. That means it can contain customer names, emails, API keys, internal URLs, whatever your agent touched.&lt;/p&gt;

&lt;p&gt;You cannot commit that to git raw. Security and legal are right to block it, and a leaked secret in a fixture is a real incident of its own.&lt;/p&gt;

&lt;p&gt;So redaction is not a nice-to-have on the recording path, it is a gate. Scrub secrets and PII out of the envelope before it is written, keep the shape and the structure that your tests assert on, and drop the sensitive values. A recording you cannot safely commit is a recording you will not use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not fix
&lt;/h2&gt;

&lt;p&gt;Be honest with yourself about the boundary of the technique.&lt;/p&gt;

&lt;p&gt;Cut-point replay fixes bugs in your code: routing, guards, argument assembly, tool safety, orchestration. It reproduces those perfectly and lets you verify a fix cheaply.&lt;/p&gt;

&lt;p&gt;It does not fix a bad generation. If the model hallucinated a refund amount, replay will faithfully serve that hallucination back to you. Fixing that lives in Layer 2 and in prompt and model work, not in deterministic replay.&lt;/p&gt;

&lt;p&gt;Fixtures also drift. Treat them like snapshot tests: when the prompt or the schema changes on purpose, the fixture has to be re-recorded on purpose. And hosted model drift is still outside your control, which is exactly why you pin the version in the envelope so you at least know when it moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Record at the boundary, the full run, not just the prompt. (Part 1.)&lt;/li&gt;
&lt;li&gt;Reproduce the incident by replaying the recording, no model call.&lt;/li&gt;
&lt;li&gt;Pick the boundary you changed. That is your cut-point.&lt;/li&gt;
&lt;li&gt;Stub everything upstream from the recording. Run the cut-point live. Watch downstream.&lt;/li&gt;
&lt;li&gt;Assert on the cut-point result, the blocked flag, the tool call, the argument, not on the final prose.&lt;/li&gt;
&lt;li&gt;Commit the trace as a regression test so the incident can never quietly return.&lt;/li&gt;
&lt;li&gt;Redact before you commit. Always.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want the tooling instead of building it yourself, our team has put the record, cut-point replay, and two-layer verification into an open-source library called &lt;strong&gt;Chronicle&lt;/strong&gt;: &lt;a href="https://github.com/theagentplane/chronicle" rel="noopener noreferrer"&gt;github.com/theagentplane/chronicle&lt;/a&gt;. It is early and honest about its limits. Issues and war stories welcome.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/agent-chronicle/" rel="noopener noreferrer"&gt;Official PyPi Release Here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Install to record and replay your agentic workflow&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;agent-chronicle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>AIE WF Submission</title>
      <dc:creator>Tisha Chawla</dc:creator>
      <pubDate>Thu, 09 Jul 2026 07:18:14 +0000</pubDate>
      <link>https://dev.to/tisha/aie-wf-submission-4bjh</link>
      <guid>https://dev.to/tisha/aie-wf-submission-4bjh</guid>
      <description></description>
    </item>
  </channel>
</rss>
