<?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: Siddham Jain</title>
    <description>The latest articles on DEV Community by Siddham Jain (@siddhamjain).</description>
    <link>https://dev.to/siddhamjain</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%2F4047889%2Fae40a91b-7528-44e0-a056-c7a74ddf63c6.jpg</url>
      <title>DEV Community: Siddham Jain</title>
      <link>https://dev.to/siddhamjain</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siddhamjain"/>
    <language>en</language>
    <item>
      <title>How I connected Apollo's sales data to cognee so an AI agent can answer questions about it</title>
      <dc:creator>Siddham Jain</dc:creator>
      <pubDate>Fri, 30 Oct 2026 18:15:07 +0000</pubDate>
      <link>https://dev.to/siddhamjain/how-i-connected-apollos-sales-data-to-cognee-so-an-ai-agent-can-answer-questions-about-it-40fo</link>
      <guid>https://dev.to/siddhamjain/how-i-connected-apollos-sales-data-to-cognee-so-an-ai-agent-can-answer-questions-about-it-40fo</guid>
      <description>&lt;p&gt;Sales teams keep track of the people and companies they sell to in a CRM, which is a database built for customer records. Apollo.io is one of these tools. It also runs sequences, which are scheduled series of outreach messages sent to a contact.&lt;/p&gt;

&lt;p&gt;An AI model can't answer questions about any of that on its own, because every conversation with it starts from zero. cognee is an open source project that gives AI agents a memory. You feed it your data, it builds a knowledge graph (a map of the things in your data and how they connect), and an agent can search that map later.&lt;/p&gt;

&lt;p&gt;A connector is the piece of code that moves data from a tool like Apollo into cognee. I built the Apollo connector for Mergetober, a month of open source work on cognee integrations, and it was merged on October 19.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the connector does
&lt;/h2&gt;

&lt;p&gt;Once your Apollo data is in cognee, you can ask something like "Which contacts at Acme are in an active sequence?" and get an answer based on your own records.&lt;/p&gt;

&lt;p&gt;Copying the data once would be simple. The harder part is that a CRM changes every day, so the connector has to handle two things on every later run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Only process records that changed. Without this, each run would send your whole CRM through the AI processing step again.&lt;/li&gt;
&lt;li&gt;Remove records you deleted in Apollo. If you delete a contact, your agent shouldn't keep answering questions about them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Apollo makes both of these harder than you'd expect, and most of this post is about why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;You'll need an Apollo API key, which means a paid plan or a trial, because free plans don't have API access to contact and account search. You'll also need an API key for an AI model provider, since cognee uses one to build the graph.&lt;/p&gt;

&lt;p&gt;The connector is in the cognee-community repo, and you run it from there with &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&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/topoteretes/cognee-community.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cognee-community/packages/connector/apollo
uv &lt;span class="nb"&gt;sync
export &lt;/span&gt;&lt;span class="nv"&gt;APOLLO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-apollo-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put your model key in a &lt;code&gt;.env&lt;/code&gt; file as &lt;code&gt;LLM_API_KEY&lt;/code&gt;, then run the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run python examples/example.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part of the example that does the work looks like this:&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apollo_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apollo_crm_main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cognee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apollo_crm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;write_disposition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_rows_per_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&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;&lt;code&gt;dataset_name&lt;/code&gt; is the place in cognee where this data lives. &lt;code&gt;resource_name&lt;/code&gt; is a name you pick for this Apollo account, and it has to stay the same between runs. &lt;code&gt;write_disposition="merge"&lt;/code&gt; tells cognee to update existing records. Leave it out and the second sync wipes the first one.&lt;/p&gt;

&lt;p&gt;The connector reads your contacts, your company accounts and your sequences. It leaves emails, phone numbers and social profiles out on purpose, and it doesn't use any of the Apollo endpoints that cost credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Apollo doesn't tell you what changed
&lt;/h2&gt;

&lt;p&gt;Many services stamp each record with the time it was last updated, so a connector can ask for everything newer than its last run. Apollo has an &lt;code&gt;updated_at&lt;/code&gt; field, but it doesn't work for this. I enrolled a contact in a sequence on a live account and the contact's timestamp stayed the same, even though that's the kind of change you'd want your agent to know about. Company records don't come back with the timestamp at all.&lt;/p&gt;

&lt;p&gt;So the connector reads every record on each run and turns it into text. It then computes a fingerprint of that text, a short code that changes whenever the text changes. It saves the fingerprints, and on the next run any record with the same fingerprint as before is skipped.&lt;/p&gt;

&lt;p&gt;The downside is that reading everything uses up requests, and Apollo limits how many you can make per minute, per hour and per day. The connector stops before it hits a limit, saves its place, and picks up from there on the next run. A large account on a small plan will take several runs to get through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Apollo doesn't tell you what was deleted
&lt;/h2&gt;

&lt;p&gt;Apollo has no list of deleted records. All the connector can see is that a record it knew about isn't in the list anymore, and that doesn't prove it was deleted. The run might have stopped early, or the list might have been cut off, because Apollo search stops at 50,000 records.&lt;/p&gt;

&lt;p&gt;So the connector only acts on a list it read all the way to the end. For each record missing from that list, it asks Apollo about that one record directly. It forgets the record only if Apollo says it's gone, or if the record no longer matches the filters you chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code review found
&lt;/h2&gt;

&lt;p&gt;Before the connector was merged, goran-radonic, a cognee maintainer, reviewed it over two rounds and left 31 inline comments. Two of the problems they found show how easily the delete logic could go wrong.&lt;/p&gt;

&lt;p&gt;The first was about two Apollo accounts sharing one dataset. Both used the same default name, so in the reviewer's test, syncing the second account removed the first account's contacts, and the next sync of the first account removed the second's. The name is now required, and a run refuses to continue from saved progress that belongs to a different Apollo team.&lt;/p&gt;

&lt;p&gt;The second was in how I recognized a deleted record. Apollo reports one with an error message, and my check was too loose: a different error, such as "This contact does not exist in the stage you filtered", would also have counted as deleted. The check now accepts only the exact messages Apollo uses for deleted records, plus a plain 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it work?
&lt;/h2&gt;

&lt;p&gt;I tested it on a real Apollo trial account with records I made for the purpose: two companies, four contacts and one sequence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I did&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First sync&lt;/td&gt;
&lt;td&gt;7 documents stored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synced again with no changes&lt;/td&gt;
&lt;td&gt;all 7 skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edited one contact's job title&lt;/td&gt;
&lt;td&gt;6 skipped, 1 updated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deleted a contact and a company in Apollo&lt;/td&gt;
&lt;td&gt;both removed, nothing else touched&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That test covered getting data in and taking it out, and it ran on the version I first submitted. After the review changes I checked the fixes with the automated tests, which run against a fake version of Apollo, and with a few specific calls to the real API. The merged version has 83 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Accounts over 50,000 records need stage or list filters, otherwise deleted records aren't detected.&lt;/li&gt;
&lt;li&gt;Each contact keeps its latest 50 activity events.&lt;/li&gt;
&lt;li&gt;Apollo plans to remove the error fields the delete check reads on 16 February 2027, and it hasn't documented a replacement yet, so that check will need updating before then.&lt;/li&gt;
&lt;li&gt;The dataset contains names, job titles and companies, so share it only with people who are allowed to see your CRM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code and setup details are in the &lt;a href="https://github.com/topoteretes/cognee-community/tree/main/packages/connector/apollo" rel="noopener noreferrer"&gt;Apollo connector folder&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Imagine your ai agent fixing its own fu*ckups, here's how the rewind actually works</title>
      <dc:creator>Siddham Jain</dc:creator>
      <pubDate>Sun, 26 Jul 2026 14:14:54 +0000</pubDate>
      <link>https://dev.to/siddhamjain/imagine-your-ai-agent-fixing-its-own-fuckups-heres-how-the-rewind-actually-works-41b6</link>
      <guid>https://dev.to/siddhamjain/imagine-your-ai-agent-fixing-its-own-fuckups-heres-how-the-rewind-actually-works-41b6</guid>
      <description>&lt;p&gt;When an AI agent fails in production, you get told &lt;em&gt;that&lt;/em&gt; it failed. You almost never get to&lt;br&gt;
answer the only question that matters: &lt;strong&gt;what if the third agent had made a different call?&lt;/strong&gt;&lt;br&gt;
You can't replay the run. You can't isolate the step that broke it. You can't test a fix without&lt;br&gt;
re-running the whole thing and hoping.&lt;/p&gt;

&lt;p&gt;I spent a hackathon building &lt;strong&gt;Rewind&lt;/strong&gt; — a "time machine" for multi-agent runs — to see if&lt;br&gt;
SigNoz's traces plus LangGraph's checkpointer could actually answer that question. They can. This&lt;br&gt;
post is the honest version: how the replay works, and the three things that nearly broke it.&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%2Fg8rnnsh4bmkesfpwsq59.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%2Fg8rnnsh4bmkesfpwsq59.png" alt="Rewind forks a failed run and proves the fix — failed vs fixed, side by side" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup: a crew that ships a subtly wrong fix
&lt;/h2&gt;

&lt;p&gt;The observed workload is a four-agent coding crew (Planner → Coder → Tester → Reviewer) that&lt;br&gt;
fixes a GitHub-style issue. The issue is deliberately ambiguous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"An order line for &lt;strong&gt;more than 10 units&lt;/strong&gt; should receive a 10% discount."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tests, however, require the discount at &lt;em&gt;exactly&lt;/em&gt; 10 units. A weak model reads the spec&lt;br&gt;
literally and writes:&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;if&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# what the weak model wrote
&lt;/span&gt;    &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the boundary test fails with a very real assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def test_bulk_discount_boundary():
    assert cart_total([(10.0, 10)]) == 90.0
E   assert 100.0 == 90.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the failure I want to &lt;em&gt;reproduce and fix from a trace&lt;/em&gt;, not by re-reading logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making every agent step a span
&lt;/h2&gt;

&lt;p&gt;I instrument the crew two ways. &lt;code&gt;openinference-instrumentation-langchain&lt;/code&gt; auto-captures the&lt;br&gt;
LangChain/LangGraph internals, and a tiny custom context manager wraps each node so the span&lt;br&gt;
carries exactly the attributes I'll want to query later:&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;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;node_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crew.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind.node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind.thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;thread_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind.input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;short&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage_metadata&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind.cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used OpenTelemetry's &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" rel="noopener noreferrer"&gt;GenAI semantic conventions&lt;/a&gt;&lt;br&gt;
(&lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.usage.*_tokens&lt;/code&gt;) for the model/token fields so they're not&lt;br&gt;
bespoke, and a &lt;code&gt;rewind.&lt;/code&gt; namespace for the app-specific bits. Spans export over OTLP/HTTP to&lt;br&gt;
SigNoz's ingester on &lt;code&gt;:4318&lt;/code&gt;. The &lt;code&gt;rewind.thread_id&lt;/code&gt; is the thread that stitches an entire run&lt;br&gt;
together — remember it, it's the whole trick.&lt;/p&gt;

&lt;p&gt;Here's what lands in SigNoz — every planner/coder/tester/reviewer step, queryable:&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%2Ffemr53wp09htdjizyhoj.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%2Ffemr53wp09htdjizyhoj.png" alt="The rewind-crew spans in SigNoz's trace explorer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Gotcha #1: getting SigNoz up when Docker Desktop is gone
&lt;/h2&gt;

&lt;p&gt;I deployed SigNoz with &lt;strong&gt;Foundry&lt;/strong&gt; (&lt;code&gt;casting.yaml&lt;/code&gt; → &lt;code&gt;casting.yaml.lock&lt;/code&gt; → &lt;code&gt;cast&lt;/code&gt;), which is a&lt;br&gt;
genuinely clean one-command install. But my machine had no Docker Desktop, and the leftovers&lt;br&gt;
fought me: &lt;code&gt;docker&lt;/code&gt; pointed at a dangling symlink, &lt;code&gt;docker compose&lt;/code&gt; was a broken cli-plugin, and&lt;br&gt;
Foundry's &lt;code&gt;cast&lt;/code&gt; died on &lt;code&gt;docker-credential-desktop executable not found&lt;/code&gt;. The fixes, in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;brew install colima docker&lt;/code&gt; and &lt;code&gt;colima start&lt;/code&gt; — a lightweight Docker engine, no Desktop.&lt;/li&gt;
&lt;li&gt;relink the compose plugin into &lt;code&gt;~/.docker/cli-plugins/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;delete the &lt;code&gt;credsStore: "desktop"&lt;/code&gt; line from &lt;code&gt;~/.docker/config.json&lt;/code&gt; so Foundry could
authenticate to the registry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is in a tutorial. If you're on a clean Mac without Docker Desktop, Colima + fixing&lt;br&gt;
&lt;code&gt;config.json&lt;/code&gt; is the path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the telemetry &lt;em&gt;back&lt;/em&gt; out of SigNoz
&lt;/h2&gt;

&lt;p&gt;Here's the part I care about most: Rewind doesn't keep its own trace store. To render a run's&lt;br&gt;
timeline, the backend &lt;strong&gt;queries SigNoz&lt;/strong&gt; for its own spans via the v5 query API:&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;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;query_range&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requestType&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;raw&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;compositeQuery&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;builder_query&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;spec&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signal&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;traces&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;filter&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expression&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;service.name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rewind-crew&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND rewind.thread_id = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run-64c55532&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND rewind.node EXISTS&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;selectFields&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;rewind.node&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;fieldContext&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;attribute&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;rewind.cost_usd&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;fieldContext&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;attribute&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;gen_ai.usage.input_tokens&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;fieldContext&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;attribute&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="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;The &lt;code&gt;filter.expression&lt;/code&gt; string syntax is the thing to get right in v5 — it's a mini query&lt;br&gt;
language (&lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;EXISTS&lt;/code&gt;), and &lt;code&gt;fieldContext: "attribute"&lt;/code&gt; vs &lt;code&gt;"resource"&lt;/code&gt; matters for&lt;br&gt;
whether SigNoz finds your field. Once that clicked, the same query grouped by &lt;code&gt;rewind.node&lt;/code&gt;&lt;br&gt;
became a &lt;strong&gt;reliability heatmap&lt;/strong&gt; across every run: pass rate, average cost per node, and where&lt;br&gt;
failures surface. Across 24 seeded runs mine sat at a 25% pass rate with the failures localizing&lt;br&gt;
to the Tester and Reviewer nodes — which is exactly right, because that's where a wrong &lt;code&gt;&amp;gt;&lt;/code&gt; vs&lt;br&gt;
&lt;code&gt;&amp;gt;=&lt;/code&gt; gets caught.&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%2Fqbpnhjuguulbfhpbyedo.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%2Fqbpnhjuguulbfhpbyedo.png" alt="Reliability heatmap, sourced from the SigNoz Query Builder — 25% pass rate, Tester/Reviewer are the hot cells" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Gotcha #2: the replay is real because LangGraph makes it real
&lt;/h2&gt;

&lt;p&gt;The reproduce-and-fork isn't a simulation, and that's the honest core of the project. The crew&lt;br&gt;
writes to a LangGraph &lt;code&gt;SqliteSaver&lt;/code&gt; checkpointer, and the backend shares that same file. So to&lt;br&gt;
fork a run I use LangGraph's native &lt;a href="https://langchain-ai.github.io/langgraph/" rel="noopener noreferrer"&gt;time-travel&lt;/a&gt;:&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;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&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;get_state_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# every checkpoint of the run
&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pick_snapshot_before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# rewind to just before the coder
&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;update_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# change model / guardrail
&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;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fork_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# re-run everything downstream, live
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nodes after the checkpoint actually re-execute with the new state, and the forked run emits&lt;br&gt;
its own spans — so it becomes a &lt;strong&gt;second trace in SigNoz&lt;/strong&gt;, not a diffed copy. In Rewind's UI you&lt;br&gt;
can see the reproduced timeline, the exact code the model wrote, and the real pytest output that&lt;br&gt;
failed:&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%2F6fmosbnl9a0ole4h5elz.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%2F6fmosbnl9a0ole4h5elz.png" alt="The reproduced timeline: the buggy  raw `&gt; 10` endraw , the failing pytest, and the fork panel" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I fork the failing run with a spec guardrail and a stronger model, the coder rewrites the&lt;br&gt;
line as &lt;code&gt;if quantity &amp;gt;= 10&lt;/code&gt;, and the same pytest that was red goes green. Failed and fixed, side&lt;br&gt;
by side, both backed by real traces — and the fix costs a few more tokens, which the cost deltas&lt;br&gt;
make honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: know when &lt;em&gt;not&lt;/em&gt; to depend on the live query
&lt;/h2&gt;

&lt;p&gt;SigNoz v0.134 moved its auth around (login is under &lt;code&gt;/api/v2/sessions&lt;/code&gt;, not where older guides&lt;br&gt;
say), and I burned time before making a call I'd defend: &lt;strong&gt;the checkpointer is the source of&lt;br&gt;
truth for the timeline; the SigNoz query is enrichment.&lt;/strong&gt; If the query path is configured, the&lt;br&gt;
timeline gets real tokens/cost from SigNoz and every card deep-links to the live trace; if it&lt;br&gt;
isn't, the app still reproduces and forks from the checkpointer. A demo that hard-depends on one&lt;br&gt;
network call to a freshly-booted observability stack is a demo that dies on stage. Degrade&lt;br&gt;
gracefully.&lt;/p&gt;

&lt;p&gt;The same telemetry drives a dashboard and a failure-rate alert, both built programmatically via&lt;br&gt;
the SigNoz API — the alert plots the failure count against a threshold, straight from the Query&lt;br&gt;
Builder:&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%2Ftt20fgjtnhmyusi3ene5.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%2Ftt20fgjtnhmyusi3ene5.png" alt="A traces-based failure-rate alert, plotted with the SigNoz Query Builder" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick your span attributes before you write a single query.&lt;/strong&gt; I renamed fields twice because
I hadn't decided that &lt;code&gt;rewind.thread_id&lt;/code&gt; was the join key. Design the schema first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fieldContext&lt;/code&gt; is not optional trivia.&lt;/strong&gt; In the v5 query API, marking a field &lt;code&gt;attribute&lt;/code&gt; vs
&lt;code&gt;resource&lt;/code&gt; is the difference between data and an empty result set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard widgets need a &lt;code&gt;layout&lt;/code&gt;.&lt;/strong&gt; I posted widgets with no &lt;code&gt;id&lt;/code&gt;/&lt;code&gt;layout&lt;/code&gt; and got a dashboard
that rendered the empty "Welcome" state — the panels were stored but never placed on the grid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the framework do the replay.&lt;/strong&gt; I almost hand-rolled a "re-run from step N" engine.
LangGraph's &lt;code&gt;get_state_history&lt;/code&gt; + &lt;code&gt;update_state&lt;/code&gt; + &lt;code&gt;invoke(None, cfg)&lt;/code&gt; already &lt;em&gt;is&lt;/em&gt; that engine,
and it's the reason the fork is trustworthy instead of a mock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two proofs beat one.&lt;/strong&gt; pytest is the hard gate (it can't be argued with); an LLM-as-judge
score rides along as a soft quality signal on the timeline. Different jobs, both useful.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Rewind reproduces a production agent failure, forks one decision, and proves the fix with a real&lt;br&gt;
passing test — and none of it required a custom trace store or a faked replay. SigNoz's spans&lt;br&gt;
already held the full I/O and cost; LangGraph's checkpointer already held the executable state.&lt;br&gt;
The work was joining them on one thread id. If you're building agents, that pairing —&lt;br&gt;
OpenTelemetry traces for &lt;em&gt;what happened&lt;/em&gt;, a checkpointer for &lt;em&gt;do it again differently&lt;/em&gt; — is worth&lt;br&gt;
stealing.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/siddham-jain/rewind" rel="noopener noreferrer"&gt;github.com/siddham-jain/rewind&lt;/a&gt;. Built for the&lt;br&gt;
Agents of SigNoz hackathon.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
