<?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: Bogdan</title>
    <description>The latest articles on DEV Community by Bogdan (@bzdvdn).</description>
    <link>https://dev.to/bzdvdn</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%2F4100592%2F716a9243-d306-4e8a-9ecf-9a2f549da77f.jpg</url>
      <title>DEV Community: Bogdan</title>
      <link>https://dev.to/bzdvdn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bzdvdn"/>
    <language>en</language>
    <item>
      <title>We stopped drawing graphs: an event-driven runtime for agents</title>
      <dc:creator>Bogdan</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:27:29 +0000</pubDate>
      <link>https://dev.to/bzdvdn/we-stopped-drawing-graphs-an-event-driven-runtime-for-agents-4919</link>
      <guid>https://dev.to/bzdvdn/we-stopped-drawing-graphs-an-event-driven-runtime-for-agents-4919</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Most agent frameworks ask you to draw a graph. A knowledge question&lt;br&gt;
doesn't have a fixed path, and you can't ship an answer you can't audit. We built&lt;br&gt;
&lt;strong&gt;reactifact&lt;/strong&gt;: think Celery, but tasks wake on typed artifacts, not messages.&lt;br&gt;
It's &lt;code&gt;pip install reactifact&lt;/code&gt;, 3 core deps, and a run you can reproduce.&lt;/p&gt;


&lt;h2&gt;
  
  
  We kept drawing graphs we couldn't finish
&lt;/h2&gt;

&lt;p&gt;We were building a devops knowledge assistant. Ask it a real question —&lt;br&gt;
&lt;em&gt;"why did our infra costs jump in Q2?"&lt;/em&gt; — and answering it needs a Confluence&lt;br&gt;
page, two GitLab MRs, a CSV of cloud costs, some arithmetic, a verification step,&lt;br&gt;
and maybe a follow-up question. The &lt;strong&gt;next&lt;/strong&gt; question needs a different subset of&lt;br&gt;
all that.&lt;/p&gt;

&lt;p&gt;Every framework we tried asked us to &lt;strong&gt;draw that path&lt;/strong&gt;: nodes, edges,&lt;br&gt;
conditional routing, a shared state dict. But there is no universal graph. The&lt;br&gt;
path depends on the question and on the data. Encoding every combination as edges&lt;br&gt;
turned into a wiring exercise — and the framework still couldn't tell us &lt;em&gt;why&lt;/em&gt; it&lt;br&gt;
produced the answer it produced.&lt;/p&gt;

&lt;p&gt;So we stopped describing execution and described &lt;strong&gt;state&lt;/strong&gt; instead.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea: state is primary, execution is derived
&lt;/h2&gt;

&lt;p&gt;Instead of "node A then node B", you declare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;what artifacts exist&lt;/strong&gt; — typed objects like &lt;code&gt;Question&lt;/code&gt;, &lt;code&gt;Evidence&lt;/code&gt;, &lt;code&gt;Claim&lt;/code&gt;,
&lt;code&gt;Calculation&lt;/code&gt;, &lt;code&gt;Answer&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;what each agent consumes and produces&lt;/strong&gt; — &lt;code&gt;Consume(Evidence)&lt;/code&gt;,
&lt;code&gt;produces=Answer&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runtime looks at what just changed and derives what runs next. Nothing calls&lt;br&gt;
anything. There is no graph to draw — the schedule is a function of state.&lt;/p&gt;

&lt;p&gt;If you know Celery, you already know the model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Celery&lt;/th&gt;
&lt;th&gt;reactifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a task&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@produce(Model)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;delay()&lt;/code&gt; / &lt;code&gt;apply_async()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;you don't call it — creating the input artifact &lt;em&gt;is&lt;/em&gt; the trigger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;routing key / queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Consume(Type)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chain / group / chord&lt;/td&gt;
&lt;td&gt;several &lt;code&gt;consumes&lt;/code&gt; / &lt;code&gt;produces&lt;/code&gt;; the runtime derives the order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retries, &lt;code&gt;acks_late&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;guards + &lt;code&gt;Budget&lt;/code&gt;, an honest &lt;code&gt;None&lt;/code&gt; instead of a wrong result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;result backend&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;Context&lt;/code&gt; — typed, versioned artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Runtime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Single process today (no broker, no worker pool) — it's a Celery-shaped &lt;em&gt;model&lt;/em&gt;,&lt;br&gt;
not its distributed runtime.&lt;/p&gt;
&lt;h2&gt;
  
  
  The second thing we couldn't get: an answer you can audit
&lt;/h2&gt;

&lt;p&gt;The other half of the problem is trust. An LLM writes confident prose and&lt;br&gt;
unreliable arithmetic, and most frameworks leave "who computed this number, from&lt;br&gt;
which source?" to whatever logs you remembered to add.&lt;/p&gt;

&lt;p&gt;In reactifact, the answer is an artifact like everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calculations are computed, not generated.&lt;/strong&gt; A &lt;code&gt;Produce&lt;/code&gt; doing arithmetic over
a CSV computes it in plain Python and hands the model the &lt;em&gt;result&lt;/em&gt; to explain —
never the raw numbers to guess from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every derived artifact links to its inputs&lt;/strong&gt; — &lt;code&gt;Answer —supported_by→
Variance —calculated_from→ Spend, Table&lt;/code&gt;. That's a queryable graph, not a log
line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A run is reproducible.&lt;/strong&gt; Artifacts are versioned, and a content hash
(&lt;code&gt;context_hash&lt;/code&gt;) is identical across deterministic runs, so you can re-run and
verify: &lt;code&gt;reactifact replay &amp;lt;store&amp;gt; --session &amp;lt;id&amp;gt; --verify &amp;lt;hash&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The audit is a report.&lt;/strong&gt; &lt;code&gt;reactifact.audit&lt;/code&gt; renders the answer with a sha256
per artifact, its producing author, and the provenance edges.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Proof, offline, in one command
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;examples/fintech_audit&lt;/code&gt; answers a finance question — &lt;em&gt;"what's the Q2 cloud spend&lt;br&gt;
variance, and does policy require approval?"&lt;/em&gt; — from a transactions CSV, a budget&lt;br&gt;
CSV, and a policy document. No API key, nothing leaves the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -m examples.fintech_audit.main   # from a repo checkout, no API key

variance vs budget: +12.5%   ($45,000 actual vs $40,000 budget, 10% policy threshold → over)
answer:  Q2 cloud spend was $45,000 against a $40,000 budget (+12.5%) —
         exceeds the 10% policy threshold. CFO approval is required.
audit:   Answer —supported_by→ {Variance, Spend, Table, Policy}
         answer sha256 5461290d…  ·  context sha256 24449f6f…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa99ldpqmbpr1dvflsyku.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa99ldpqmbpr1dvflsyku.gif" alt="fintech_audit demo: the variance, the answer, and the reproducible context hash — a second run prints the same hash." width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run it twice: the hash is the same. Change a fact, and only what actually&lt;br&gt;
consumed it recomputes — because &lt;code&gt;Artifact.version&lt;/code&gt; tracks real reads, not a&lt;br&gt;
graph edge someone drew by hand.&lt;/p&gt;

&lt;p&gt;The number is computed, and its provenance recorded, in plain Python (trimmed&lt;br&gt;
from &lt;a href="https://github.com/bzdvdn/reactifact/blob/master/examples/fintech_audit/produce.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/fintech_audit/produce.py&lt;/code&gt;&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="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Variance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reacts_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Spend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# wakes when a Spend artifact exists
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_variance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProduceCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;spend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trigger&lt;/span&gt;                         &lt;span class="c1"&gt;# the artifact that triggered this run
&lt;/span&gt;    &lt;span class="n"&gt;budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cloud_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# read from budget.csv
&lt;/span&gt;    &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;   &lt;span class="c1"&gt;# deterministic — never the model
&lt;/span&gt;    &lt;span class="n"&gt;variance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_once_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;spend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                   &lt;span class="c1"&gt;# stable id → idempotent re-runs
&lt;/span&gt;        &lt;span class="nc"&gt;Variance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;within_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculated_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# provenance edge, queryable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you get beyond the model
&lt;/h2&gt;

&lt;p&gt;A task queue gives you execution. Layering state, versions, and provenance on top&lt;br&gt;
of the type system gives you a few things for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Determinism where it matters&lt;/strong&gt; — compute in code, let the model reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching and merge&lt;/strong&gt; — fork an exploration, run different agents on each
fork, three-way &lt;code&gt;merge()&lt;/code&gt; with an explicit conflict instead of silent
last-write-wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop as state&lt;/strong&gt; — &lt;code&gt;effects.ask(...)&lt;/code&gt; is just another effect; a
human is another reaction, not a special case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt; — reproduce a past run from its recorded model calls and commit
chain, offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP both ways&lt;/strong&gt; — call any MCP server's tools, or expose your own &lt;code&gt;Context&lt;/code&gt;
as one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it's small: three core dependencies (&lt;code&gt;pydantic&lt;/code&gt;, &lt;code&gt;httpx&lt;/code&gt;, &lt;code&gt;python-dotenv&lt;/code&gt;),&lt;br&gt;
fully typed, no framework-of-the-week dependency tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;reactifact is &lt;strong&gt;pre-1.0&lt;/strong&gt; (&lt;code&gt;0.10.0&lt;/code&gt;), maintained by one person, and&lt;br&gt;
&lt;strong&gt;single-process&lt;/strong&gt; today — no broker, no worker pool, no managed platform. If you&lt;br&gt;
need a mature ecosystem and hosted execution right now, use LangGraph; it's a&lt;br&gt;
better fit. reactifact is for teams building open-ended, &lt;em&gt;auditable&lt;/em&gt; knowledge&lt;br&gt;
agents — internal assistants over docs, ops/incident tooling, finance/compliance&lt;br&gt;
workflows — where "why did it say that?" has to have an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&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;reactifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/bzdvdn/reactifact" rel="noopener noreferrer"&gt;https://github.com/bzdvdn/reactifact&lt;/a&gt;&lt;/strong&gt; · Docs and offline demos:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://bzdvdn.github.io/reactifact/" rel="noopener noreferrer"&gt;https://bzdvdn.github.io/reactifact/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you build agents over docs, CSVs, or repos, I want to hear what breaks for&lt;br&gt;
you. That's more useful to me than a star.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>speckeep: a spec-driven development workflow for coding agents</title>
      <dc:creator>Bogdan</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:07:03 +0000</pubDate>
      <link>https://dev.to/bzdvdn/speckeep-a-spec-driven-development-workflow-for-coding-agents-3om7</link>
      <guid>https://dev.to/bzdvdn/speckeep-a-spec-driven-development-workflow-for-coding-agents-3om7</guid>
      <description>&lt;h1&gt;
  
  
  speckeep: a spec-driven development workflow for coding agents
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;code&gt;speckeep&lt;/code&gt; is a small spec-driven workflow for coding agents: specs, plans, tasks and proof live in plain markdown, agents get them as &lt;code&gt;/spk-&amp;lt;phase&amp;gt;&lt;/code&gt; commands, and a CLI gate makes "done" checkable in CI. Single Go binary, MIT. Try it with &lt;code&gt;speckeep demo ./my-demo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every team using a coding agent hits the same three failures, usually in the same week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Amnesia.&lt;/strong&gt; You explain the same feature three sessions in a row, because context lives in a chat window that closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift.&lt;/strong&gt; The agent builds something reasonable — just not what you asked for. Nobody notices until review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phantom "done".&lt;/strong&gt; The agent says it's finished. No tests, no evidence, no trace from requirement to code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prompting harder doesn't fix this. The problem isn't the model — it's that there is no durable artifact between "what we want" and "what the agent did", and nothing that forces the agent to prove the second matches the first.&lt;/p&gt;

&lt;p&gt;That's the gap &lt;a href="https://github.com/bzdvdn/speckeep" rel="noopener noreferrer"&gt;&lt;code&gt;speckeep&lt;/code&gt;&lt;/a&gt; fills. The workflow is a strict chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;constitution -&amp;gt; spec -&amp;gt; [inspect] -&amp;gt; plan -&amp;gt; tasks -&amp;gt; implement -&amp;gt; archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;constitution&lt;/strong&gt; — the project's non-negotiables (stack, conventions, quality bar).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spec&lt;/strong&gt; — one feature, with stable IDs: requirements &lt;code&gt;RQ-*&lt;/code&gt; and acceptance criteria &lt;code&gt;AC-*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;inspect&lt;/strong&gt; &lt;em&gt;(optional)&lt;/em&gt; — a deep quality review before you commit to a plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;plan&lt;/strong&gt; — surfaces (files) to touch, decisions, risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tasks&lt;/strong&gt; — ordered, phased, each with a &lt;code&gt;Touches:&lt;/code&gt; surface map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;implement&lt;/strong&gt; — executes tasks, one phase at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;archive&lt;/strong&gt; — closes the feature once every task has proof.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optional &lt;code&gt;verify&lt;/code&gt; is an on-demand audit; &lt;code&gt;propose&lt;/code&gt; is a one-shot fast lane for small changes; &lt;code&gt;converge&lt;/code&gt; is the cheap closing loop.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice — init a workspace, read the health check, dry-run a refresh:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjgf38bi0ofngqzhueth.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjgf38bi0ofngqzhueth.gif" alt="speckeep: init a workspace (skills + AGENTS.md), inspect it, run doctor and a dry-run refresh" width="760" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: discipline per token
&lt;/h2&gt;

&lt;p&gt;Most process fails on agents for one reason — it asks for too much context at once. speckeep's design rule is the opposite: &lt;strong&gt;every phase loads only the minimum it needs, and every phase leaves behind one compact artifact.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each phase is a command you invoke in your agent. In Claude Code, Codex, Cursor, Copilot and others, that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/spk-constitution
/spk-spec Add CSV export to the reports table
/spk-plan csv-export
/spk-tasks csv-export
/spk-implement csv-export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part that makes it different: proof, not vibes
&lt;/h2&gt;

&lt;p&gt;Here's the mechanism that changed agent behaviour more than any prompt.&lt;/p&gt;

&lt;p&gt;Every task in &lt;code&gt;tasks.md&lt;/code&gt; must carry a &lt;code&gt;Proof:&lt;/code&gt; line before it can be checked off, and every task maps to the surfaces it touches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Surface Map&lt;/span&gt;

| Surface                               | Tasks |
| ------------------------------------- | ----- |
| apps/api/src/routes/reports.ts        | T2.2  |
| packages/reporting/src/csv.ts         | T2.1  |
| apps/web/src/reports/ExportButton.tsx | T2.3  |

&lt;span class="gu"&gt;## Phase 2: MVP Slice&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [x] T2.1 Implement CSV serialization — the exporter produces headers + rows. Touches: packages/reporting/src/csv.ts
      Proof: test packages/reporting/src/csv.test.ts TestSerializeReports
&lt;span class="p"&gt;-&lt;/span&gt; [x] T2.2 Wire the export endpoint — the route streams the CSV. Touches: apps/api/src/routes/reports.ts
      Proof: test apps/api/src/routes/reports.test.ts TestExportRoute
&lt;span class="p"&gt;-&lt;/span&gt; [ ] T2.3 Add the button + empty state — the UI triggers export and handles zero rows. Touches: apps/web/src/reports/ExportButton.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI reads exactly those lines. If a task is marked done without proof, &lt;code&gt;check&lt;/code&gt; says so by name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ speckeep check export-report .
verdict:  blocked
detail:   error [] closed task(s) without Proof in specs/active/export-report/tasks.md:
          T1.1, T1.2 — add a Proof: line to tasks.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;speckeep guard&lt;/code&gt; is the same idea for CI — it exits non-zero while any active feature isn't closeable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ speckeep guard .
╔═════════════════════════════════════════════════════╗
║ speckeep guard                                      ║
║ verdict: fail — 1 feature(s) are not ready to close ║
╚═════════════════════════════════════════════════════╝

- export-report            implement    implement    blocked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the agent's claims are checked by a machine, not by your patience — and because the gates are deterministic, the agent can run them itself and self-correct before it ever reports back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for the repo you actually have: a large monorepo
&lt;/h2&gt;

&lt;p&gt;This is where speckeep earns its keep for us. Our main repo is a monorepo with a lot of code across many packages. For an agent, that's a trap: read too little and it edits the wrong package; read too much and the context window is full of irrelevant files before the real work starts.&lt;/p&gt;

&lt;p&gt;Three pieces fix that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;REPOSITORY_MAP.md&lt;/code&gt; — a navigation index the agent can trust.&lt;/strong&gt;&lt;br&gt;
Run &lt;code&gt;/spk-repo-map&lt;/code&gt; (directly, or when a task changes structure) and speckeep generates/updates a compact map: entry points, top-level modules, key paths, and a "where to edit" table — all derived from the actual tree, kept under a line budget so it stays cheap to read. Instead of walking the repo, the agent opens the map, picks the right 2–3 surfaces, and starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;CONSTITUTION.md&lt;/code&gt; — one source of truth for conventions.&lt;/strong&gt;&lt;br&gt;
In a monorepo, conventions differ per package and nobody remembers all of them. The constitution captures them once (language rules, layering, test expectations, what not to do), so every phase and every agent reads the same rules instead of guessing — and specs get checked against them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;spec.md&lt;/code&gt; + &lt;code&gt;Touches:&lt;/code&gt; — an explicit, tiny surface area per task.&lt;/strong&gt;&lt;br&gt;
The spec pins the acceptance criteria; the task list pins the exact files each task may touch. The agent's working set is now "this feature, these surfaces" — not "this repo".&lt;/p&gt;

&lt;p&gt;Put together, that's how a complex, cross-package task gets closed without the agent reading the whole monorepo: &lt;strong&gt;the map narrows where to look, the constitution narrows how to build, the spec and &lt;code&gt;Touches:&lt;/code&gt; narrow what to change.&lt;/strong&gt; And because the same artifacts persist, the next session doesn't start from zero.&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%2Fnqo9xbe91iluneyk3kk6.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%2Fnqo9xbe91iluneyk3kk6.png" alt="speckeep dashboard: features, phase, ready-for and task progress" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  A real cycle, end to end
&lt;/h2&gt;

&lt;p&gt;Say you want CSV export on a reports page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Init and spec&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;speckeep init &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--shell&lt;/span&gt; sh &lt;span class="nt"&gt;--agents&lt;/span&gt; claude
&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;/spk-spec --name "CSV export for reports"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent writes &lt;code&gt;specs/active/csv-export-for-reports/spec.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Goal&lt;/span&gt;

Allow users to download the reports table as a CSV file.

&lt;span class="gu"&gt;## Acceptance Criteria&lt;/span&gt;

&lt;span class="gs"&gt;**AC-001**&lt;/span&gt; Export produces a file
Given the Reports page has at least one row
When the user clicks "Export CSV"
Then a .csv file downloads with column headers and all visible rows

&lt;span class="gs"&gt;**AC-002**&lt;/span&gt; Empty state is handled
Given the reports table is empty
When the user clicks "Export CSV"
Then a friendly message is shown and no file is downloaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Plan → tasks&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/spk-plan csv-export-for-reports
/spk-tasks csv-export-for-reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a small, readable package — and it lives in your repo, not in a chat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;specs/active/csv-export-for-reports/
  spec.md      # goal + AC-001..AC-00N
  plan.md      # surfaces, decisions, risks
  tasks.md     # phases, Touches:, Proof:
  verify.md    # optional audit report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Implement, then close&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/spk-implement csv-export-for-reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each task gets code &lt;strong&gt;and&lt;/strong&gt; a &lt;code&gt;Proof:&lt;/code&gt; line. When the last one is checked, the agent closes the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ speckeep converge csv-export-for-reports .
OK: feature is converged: tasks complete with valid Proof coverage — safe to archive
$ speckeep archive csv-export-for-reports . --compact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there's a gap, it appends follow-up tasks and loops — with a hard stop after two rounds instead of grinding forever. And if you want a formal audit before archiving, set &lt;code&gt;workflow.verify: required&lt;/code&gt; and the archive gate demands a &lt;code&gt;verify: pass&lt;/code&gt; report first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--compact&lt;/code&gt; keeps just &lt;code&gt;summary.md&lt;/code&gt; plus a git pointer instead of copying every artifact. The whole feature is a handful of markdown files you can read in two minutes. That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day one: adopt it in 5 minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; (details in Install below):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"https://raw.githubusercontent.com/bzdvdn/speckeep/main/scripts/install.sh"&lt;/span&gt; | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Init your real repo and confirm it's healthy:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   speckeep init &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--agents&lt;/span&gt; claude,opencode
   speckeep doctor &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write the rules down once&lt;/strong&gt; — run &lt;code&gt;/spk-constitution&lt;/code&gt; and point it at your stack, layering and test expectations. In a monorepo this is the highest-leverage ten minutes you'll spend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Map the codebase&lt;/strong&gt; — run &lt;code&gt;/spk-repo-map&lt;/code&gt; to generate &lt;code&gt;REPOSITORY_MAP.md&lt;/code&gt;, and re-run it whenever the structure changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Take one real (small) task through the chain:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   /spk-spec &amp;lt;task&amp;gt;  -&amp;gt;  /spk-plan  -&amp;gt;  /spk-tasks  -&amp;gt;  /spk-implement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the first &lt;code&gt;Proof:&lt;/code&gt; line appear — that's the moment it clicks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wire the CI gate&lt;/strong&gt; — copy &lt;a href="https://github.com/bzdvdn/speckeep/blob/master/contrib/ci/speckeep-guard.yml" rel="noopener noreferrer"&gt;&lt;code&gt;contrib/ci/speckeep-guard.yml&lt;/code&gt;&lt;/a&gt; into &lt;code&gt;.github/workflows/&lt;/code&gt;, or add the action directly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bzdvdn/speckeep/.github/actions/speckeep@main&lt;/span&gt;
     &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now "done" is enforced on every pull request.&lt;/p&gt;

&lt;p&gt;If step 5 feels heavy for a first try, use &lt;code&gt;/spk-propose&lt;/code&gt; instead — same proof requirement, one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for us
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Re-explaining the same feature every session&lt;/td&gt;
&lt;td&gt;The spec and tasks are already in the repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review = "did the agent do what we asked?"&lt;/td&gt;
&lt;td&gt;Review = "do we agree with the spec?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-package work stalled on "which package owns this?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;REPOSITORY_MAP.md&lt;/code&gt; answers it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"It's done" → "actually, no" loops&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Proof:&lt;/code&gt; catches it before a human does&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why not just use AGENTS.md, ready-made skills, or another spec tool?
&lt;/h2&gt;

&lt;p&gt;Fair question — and it's the one I'd ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. hand-rolled &lt;code&gt;AGENTS.md&lt;/code&gt; / your agent's built-in skills.&lt;/strong&gt; You absolutely can encode a process there, and there's zero setup. What you don't get is &lt;em&gt;enforcement&lt;/em&gt;: nothing turns "you should add tests" into a failing exit code, and nothing survives as a durable artifact when the session ends. speckeep &lt;em&gt;is&lt;/em&gt; skills — it ships as skill files and commands for 19 agents — but it also adds the two things skills alone can't: persistent artifacts and deterministic gates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. personal skill packs (e.g. &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;&lt;code&gt;mattpocock/skills&lt;/code&gt;&lt;/a&gt;).&lt;/strong&gt; Great resources, and genuinely complementary. Those skills make your agent sharper &lt;em&gt;inside a session&lt;/em&gt; — interviewing you about a plan, driving TDD, reviewing code. speckeep makes your definition of done &lt;em&gt;survive the session&lt;/em&gt; and gives CI a way to enforce it. You can run both: use interactive skills for the conversation, speckeep for the spec → tasks → proof spine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. other spec tools (OpenSpec, Spec Kit).&lt;/strong&gt; Same family, different tradeoff:&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;speckeep&lt;/th&gt;
&lt;th&gt;OpenSpec&lt;/th&gt;
&lt;th&gt;Spec Kit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workflow style&lt;/td&gt;
&lt;td&gt;Strict phase chain, narrow context&lt;/td&gt;
&lt;td&gt;Fluid, artifact-guided&lt;/td&gt;
&lt;td&gt;Thorough multi-step SDD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default context&lt;/td&gt;
&lt;td&gt;Smallest&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Largest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact overhead&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brownfield / monorepo&lt;/td&gt;
&lt;td&gt;High (repo-map + constitution)&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine-checkable "done"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;speckeep guard&lt;/code&gt; (exit code in CI)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;One Go binary&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Other skills make your agent smarter in a session. speckeep makes your &lt;em&gt;definition of done&lt;/em&gt; survive the session — and gives CI a way to enforce it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Works with the agent you already use
&lt;/h2&gt;

&lt;p&gt;speckeep ships adapters for 19 targets: &lt;strong&gt;Claude Code, Codex, Cursor, Copilot, OpenCode, Windsurf, Cline, Amazon Q, Gemini, aider, Devin, Goose, Jules, Refact, Roo Code, Kilo Code, Qwen Code, Trae, Codiumate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's &lt;em&gt;skills-first&lt;/em&gt; and it does the per-tool research for you, because the tools genuinely differ. In Claude Code, top-level skills are directly slash-invocable. In OpenCode, skills are model-invoked and &lt;strong&gt;not&lt;/strong&gt; in the &lt;code&gt;/&lt;/code&gt; palette — so &lt;code&gt;/spk-&amp;lt;phase&amp;gt;&lt;/code&gt; is reached through the &lt;code&gt;/skills&lt;/code&gt; picker. speckeep handles that so you don't find out the hard way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;speckeep init my-project &lt;span class="nt"&gt;--agents&lt;/span&gt; claude,opencode,cursor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also writes an &lt;code&gt;AGENTS.md&lt;/code&gt; block so any agent — even ones without a skills system — knows the workflow and the gates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The escape hatches (because process shouldn't be dogma)
&lt;/h2&gt;

&lt;p&gt;Real teams need to move fast sometimes. speckeep has explicit, bounded ways to skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/spk-propose&lt;/code&gt;&lt;/strong&gt; — one-shot: idea → &lt;code&gt;spec.md&lt;/code&gt; + &lt;code&gt;tasks.md&lt;/code&gt;, straight to implement. Falls back to &lt;code&gt;/spk-spec&lt;/code&gt; if the idea is ambiguous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Express lane&lt;/strong&gt; — small changes close from &lt;code&gt;spec.md&lt;/code&gt; + &lt;code&gt;tasks.md&lt;/code&gt;; &lt;code&gt;plan.md&lt;/code&gt; is optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/spk-hotfix&lt;/code&gt;&lt;/strong&gt; — emergency fix outside the chain (≤ 3 files, no re-planning).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/spk-challenge&lt;/code&gt;&lt;/strong&gt; — adversarial review of a spec/plan when you want a hostile second opinion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/spk-scope&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;/spk-glossary&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;/spk-handoff&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;/spk-recap&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;/spk-rollback&lt;/code&gt;&lt;/strong&gt; — small, single-purpose tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline is strict where it pays off and skippable where it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating in
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;speckeep import openspec &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# openspec/changes/&amp;lt;slug&amp;gt;/ -&amp;gt; speckeep layout&lt;/span&gt;
speckeep import speckit &lt;span class="nb"&gt;.&lt;/span&gt;    &lt;span class="c"&gt;# specs/&amp;lt;slug&amp;gt;/ -&amp;gt; speckeep layout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It rebuilds requirement/acceptance IDs where it can and never overwrites existing features. Coming from DraftSpec (this project's predecessor), &lt;code&gt;speckeep migrate&lt;/code&gt; handles the move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and try it in 30 seconds
&lt;/h2&gt;

&lt;p&gt;One Go binary, no runtime, MIT licensed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux / macOS&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"https://raw.githubusercontent.com/bzdvdn/speckeep/main/scripts/install.sh"&lt;/span&gt; | bash

&lt;span class="c"&gt;# or: brew install bzdvdn/speckeep/speckeep&lt;/span&gt;
&lt;span class="c"&gt;# or: npx speckeep ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try a fully populated example without touching your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;speckeep demo ./my-demo
speckeep dashboard ./my-demo
speckeep check export-report ./my-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docs (EN/RU): &lt;strong&gt;&lt;a href="https://bzdvdn.github.io/speckeep/" rel="noopener noreferrer"&gt;https://bzdvdn.github.io/speckeep/&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Repo: &lt;strong&gt;&lt;a href="https://github.com/bzdvdn/speckeep" rel="noopener noreferrer"&gt;https://github.com/bzdvdn/speckeep&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is &lt;em&gt;not&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I'd rather you adopt it knowing the tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not an orchestrator.&lt;/strong&gt; No daemon, no agents-on-agents, no vector DB. It's files plus a CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is strict by design.&lt;/strong&gt; If you hate checklists and IDs, this will feel like overhead — until the first time an agent's "done" turns out to be fiction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It optimises for brownfield monorepos.&lt;/strong&gt; The value is highest on real codebases with messy history and many packages, not greenfield demos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's young.&lt;/strong&gt; The workflow is stable; the ecosystem around it is still growing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why we built it
&lt;/h2&gt;

&lt;p&gt;We used agents daily in a large monorepo and kept paying the same tax: re-explaining context, re-reviewing drift, and re-verifying work that was reported as finished. The trio that finally moved the needle was &lt;strong&gt;repo-map + constitution + spec&lt;/strong&gt; — a map of the codebase, one set of rules, and a durable spec with proof.&lt;/p&gt;

&lt;p&gt;speckeep is the smallest structure we found that actually changes agent behaviour — because it's the one the agent can't argue with. Either the &lt;code&gt;Proof:&lt;/code&gt; line is there or the gate fails.&lt;/p&gt;

&lt;p&gt;If that resonates: star the repo, run &lt;code&gt;speckeep demo&lt;/code&gt;, and tell me two things — &lt;strong&gt;which agent you use&lt;/strong&gt;, and &lt;strong&gt;where the workflow breaks on your codebase&lt;/strong&gt;. That feedback is what shapes the next release.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/bzdvdn/speckeep" rel="noopener noreferrer"&gt;https://github.com/bzdvdn/speckeep&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://bzdvdn.github.io/speckeep/" rel="noopener noreferrer"&gt;https://bzdvdn.github.io/speckeep/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Changelog: &lt;a href="https://github.com/bzdvdn/speckeep/blob/master/CHANGELOG.md" rel="noopener noreferrer"&gt;https://github.com/bzdvdn/speckeep/blob/master/CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>go</category>
    </item>
    <item>
      <title>Edit One Input, Only Its Real Dependents Recompute — No Graph, No Command</title>
      <dc:creator>Bogdan</dc:creator>
      <pubDate>Mon, 07 Sep 2026 10:43:20 +0000</pubDate>
      <link>https://dev.to/bzdvdn/edit-one-input-only-its-real-dependents-recompute-no-graph-no-command-34jh</link>
      <guid>https://dev.to/bzdvdn/edit-one-input-only-its-real-dependents-recompute-no-graph-no-command-34jh</guid>
      <description>&lt;p&gt;&lt;em&gt;(not committed — local draft, review before pushing to dev.to)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most agent frameworks — LangGraph included — model a pipeline as nodes&lt;br&gt;
connected by edges, sharing one big state object (a &lt;code&gt;TypedDict&lt;/code&gt;). That's a&lt;br&gt;
fine mental model until you ask a very ordinary question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I edit one input, which parts of my pipeline actually need to&lt;br&gt;
recompute?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The honest answer in a shared-state graph is: &lt;strong&gt;the framework has no&lt;br&gt;
idea.&lt;/strong&gt; Nodes don't declare "I read field X" anywhere the framework can&lt;br&gt;
see — they just receive the whole state and return updates to it. So&lt;br&gt;
either you re-run everything downstream of wherever you re-enter the&lt;br&gt;
graph (coarse — you recompute things nothing about your edit ever&lt;br&gt;
touched), or you hand-roll the missing piece yourself: dirty flags, a&lt;br&gt;
manual dependency map, comparing old-vs-new values inside every node,&lt;br&gt;
&lt;code&gt;Command(goto=...)&lt;/code&gt; to jump back into specific nodes.&lt;/p&gt;

&lt;p&gt;That bookkeeping is exactly what you get for free when state lives in&lt;br&gt;
typed, versioned artifacts and a node's dependency &lt;em&gt;is&lt;/em&gt; what it declares&lt;br&gt;
it consumes — which is the core idea behind &lt;a href="https://github.com/bzdvdn/ctxloom" rel="noopener noreferrer"&gt;ctxloom&lt;/a&gt;, a small&lt;br&gt;
Python agent framework I've been building. This post walks through one&lt;br&gt;
concrete, checkable example of why that matters, with real numbers, not&lt;br&gt;
just an architecture diagram.&lt;/p&gt;
&lt;h2&gt;
  
  
  The example: a cost model
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hours × Rate → LaborCost → Tax, Discount → Total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every artifact in ctxloom carries a version number, bumped only when it's&lt;br&gt;
actually recomputed. Start the model, then edit &lt;strong&gt;only&lt;/strong&gt; &lt;code&gt;TaxRate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- initial calculation ---
  LaborCost    value=500.0      version=0
  Tax          value=40.0       version=0
  Discount     value=25.0       version=0
  Total        value=515.0      version=0

&amp;gt;&amp;gt;&amp;gt; editing ONLY TaxRate (0.08 -&amp;gt; 0.12)

--- after editing TaxRate ---
  LaborCost    value=500.0      version=0   ← untouched
  Tax          value=60.0       version=1   ← recomputed
  Discount     value=25.0       version=0   ← untouched
  Total        value=535.0      version=1   ← recomputed (consumes Tax)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LaborCost&lt;/code&gt; and &lt;code&gt;Discount&lt;/code&gt; don't consume &lt;code&gt;TaxRate&lt;/code&gt; — they are never&lt;br&gt;
&lt;em&gt;invoked&lt;/em&gt; for this edit, not invoked-and-decided-nothing-changed. Edit&lt;br&gt;
&lt;code&gt;Hours&lt;/code&gt; instead and it correctly cascades through everything, because&lt;br&gt;
everything really does depend on it transitively through &lt;code&gt;LaborCost&lt;/code&gt; —&lt;br&gt;
the model isn't "isolated by construction," it's precise about what&lt;br&gt;
actually depends on what.&lt;/p&gt;
&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;p&gt;Here's the same shape in LangGraph — state is one shared dict, and a node&lt;br&gt;
reads whatever fields it wants from it while the framework can't see&lt;br&gt;
which ones:&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;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;discount_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;labor_cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;labor_cost_node&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="n"&gt;State&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;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;labor_cost&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&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;tax_node&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="n"&gt;State&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;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;tax&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax_rate&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;discount_node&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="n"&gt;State&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;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;discount&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discount_rate&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;total_node&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="n"&gt;State&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;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;total&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&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="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labor_cost_node&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tax_node&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount_node&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_node&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&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;tax&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labor_cost&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;discount&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax&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;total&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discount&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;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;compiled&lt;/span&gt; &lt;span class="o"&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;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the user edits &lt;code&gt;tax_rate&lt;/code&gt; and you want to refresh the total. What do&lt;br&gt;
you invoke? &lt;code&gt;compiled.invoke(state)&lt;/code&gt; re-enters at &lt;code&gt;START&lt;/code&gt; and recomputes&lt;br&gt;
&lt;code&gt;labor_cost&lt;/code&gt; too, even though nothing about it changed — the graph has no&lt;br&gt;
concept of "this input changed, that one didn't." To actually skip&lt;br&gt;
&lt;code&gt;labor_cost&lt;/code&gt;, you're writing the missing piece by hand: pass an extra&lt;br&gt;
"what changed" flag through the state, add an &lt;code&gt;if&lt;/code&gt; at the top of each&lt;br&gt;
node comparing its own inputs to what they were last time, wire a&lt;br&gt;
&lt;code&gt;Command(goto=...)&lt;/code&gt; to jump in below &lt;code&gt;labor_cost&lt;/code&gt; — a dependency tracker&lt;br&gt;
you now own and maintain, node by node, forever.&lt;/p&gt;

&lt;p&gt;Here's the same model in ctxloom. No graph, no state dict, no dirty&lt;br&gt;
flags — each formula is a tiny class, and its &lt;code&gt;consumes&lt;/code&gt; list &lt;em&gt;is&lt;/em&gt; the&lt;br&gt;
dependency declaration:&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;class&lt;/span&gt; &lt;span class="nc"&gt;LaborCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;

&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_tax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;labor_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LaborCost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tax_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaxRate&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;labor_cost&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;tax_rate&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Tax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;labor_cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tax_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LaborCost&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaxRate&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;compute_tax&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;Editing a fact is a normal 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="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tax_rate_artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TaxRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.12&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;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arun&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime fires &lt;code&gt;ARTIFACT_UPDATED&lt;/code&gt; for &lt;code&gt;TaxRate&lt;/code&gt;; only agents whose&lt;br&gt;
&lt;code&gt;consumes&lt;/code&gt; names &lt;code&gt;TaxRate&lt;/code&gt; (or something downstream of it) ever run.&lt;br&gt;
&lt;code&gt;labor_cost_agent&lt;/code&gt; isn't in that set, so it's never invoked — not&lt;br&gt;
"invoked, compared, decided no-op." Add a fifth formula tomorrow that&lt;br&gt;
depends on &lt;code&gt;Tax&lt;/code&gt;, and nothing above changes; it just declares&lt;br&gt;
&lt;code&gt;Consume(Tax)&lt;/code&gt; and starts reacting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest caveat:&lt;/strong&gt; convergence isn't always a single step. Editing&lt;br&gt;
&lt;code&gt;Hours&lt;/code&gt; made &lt;code&gt;Total&lt;/code&gt; jump from version 0 straight to version 3, not to 1&lt;br&gt;
— because &lt;code&gt;Tax&lt;/code&gt; and &lt;code&gt;Discount&lt;/code&gt; land their updates in separate passes as&lt;br&gt;
the pipeline reaches a fixpoint, and &lt;code&gt;Total&lt;/code&gt; reacts to each one. Same&lt;br&gt;
phenomenon a spreadsheet's own multi-pass recalculation engine has. It&lt;br&gt;
doesn't undermine the core claim (&lt;code&gt;LaborCost&lt;/code&gt;/&lt;code&gt;Discount&lt;/code&gt; really do stay&lt;br&gt;
untouched when only &lt;code&gt;TaxRate&lt;/code&gt; changes), but a sharp reader will ask, so&lt;br&gt;
it's worth stating instead of glossing over.&lt;/p&gt;
&lt;h2&gt;
  
  
  The other half: branching
&lt;/h2&gt;

&lt;p&gt;The same "state lives in artifacts, not one shared blob" design pays off&lt;br&gt;
a second way that has nothing to do with recompute (see&lt;br&gt;
&lt;a href="https://github.com/bzdvdn/ctxloom/tree/master/examples/forklab" rel="noopener noreferrer"&gt;&lt;code&gt;examples/forklab&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;code&gt;context.branch()&lt;/code&gt;&lt;br&gt;
forks an isolated copy of the whole state, you run different&lt;br&gt;
agents/strategies on each fork independently, and &lt;code&gt;merge()&lt;/code&gt; reconciles&lt;br&gt;
them — a real three-way merge, raising an explicit conflict if both&lt;br&gt;
forks touched the same artifact differently, instead of silently picking&lt;br&gt;
one.&lt;/p&gt;

&lt;p&gt;LangGraph's checkpointer gives you linear time-travel through &lt;em&gt;one&lt;/em&gt;&lt;br&gt;
thread's history — genuinely useful, but not the same thing as two&lt;br&gt;
divergent branches getting reconciled back into one. There's no&lt;br&gt;
comparable primitive to point at there; it's not "awkward to do," it's&lt;br&gt;
just not something the framework does.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where this doesn't matter
&lt;/h2&gt;

&lt;p&gt;If your pipeline is genuinely fixed — step 1 always then step 2 always&lt;br&gt;
then step 3, no branching by data, no expectation that inputs get edited&lt;br&gt;
independently after the fact — none of this buys you anything, and a&lt;br&gt;
drawn graph you can see in one file is more readable. This is&lt;br&gt;
specifically about systems where "what needs to recompute" is a real&lt;br&gt;
question with a data-dependent answer, not a fixed sequence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The cost-model example above is a real, runnable file, no LLM required —&lt;br&gt;
it lives at&lt;br&gt;
&lt;a href="https://github.com/bzdvdn/ctxloom/tree/master/examples/ledger" rel="noopener noreferrer"&gt;&lt;code&gt;examples/ledger&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
in the repo:&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/bzdvdn/ctxloom
&lt;span class="nb"&gt;cd &lt;/span&gt;ctxloom
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; examples.ledger.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/bzdvdn/ctxloom" rel="noopener noreferrer"&gt;github.com/bzdvdn/ctxloom&lt;/a&gt;. I'd be&lt;br&gt;
curious whether people run into the "which nodes actually need to rerun"&lt;br&gt;
problem often enough for this to matter in their own agent pipelines, or&lt;br&gt;
whether it's a non-issue in practice because most pipelines stay small&lt;br&gt;
enough that a full re-run is cheap either way.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why "why did our infra costs jump in Q2?" doesn't fit a graph</title>
      <dc:creator>Bogdan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 21:44:29 +0000</pubDate>
      <link>https://dev.to/bzdvdn/why-why-did-our-infra-costs-jump-in-q2-doesnt-fit-a-graph-1nmk</link>
      <guid>https://dev.to/bzdvdn/why-why-did-our-infra-costs-jump-in-q2-doesnt-fit-a-graph-1nmk</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: some questions don't have a fixed path through your data (search docs, hit a table, compute, verify, answer — in whatever order/combination the question needs), and drawing a graph for that class of question means either enumerating every path up front or hiding an if/else forest inside one node. &lt;a href="https://github.com/bzdvdn/ctxloom" rel="noopener noreferrer"&gt;ctxloom&lt;/a&gt; replaces the graph with typed artifacts and agents that react to their appearance — below is the same use case built both ways, side by side.&lt;/p&gt;

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

&lt;p&gt;Picture a typical question from a finance lead in an internal chat assistant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why did our infra costs jump in Q2?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Answering this honestly requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finding relevant documents — the pricing guide, the discount policy (Confluence/docs).&lt;/li&gt;
&lt;li&gt;Pulling structured data — a CSV/table of monthly spend (GitLab/S3/DB).&lt;/li&gt;
&lt;li&gt;Computing an aggregate — not "roughly", an exact number from the table.&lt;/li&gt;
&lt;li&gt;Cross-checking textual claims against the numbers — not letting the model invent a cause the data doesn't support.&lt;/li&gt;
&lt;li&gt;Returning the answer together with proof: where each part came from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next question — "what if we hadn't moved to the Pro plan?" — needs a different path: a different source, a different calculation, a different verification chain. There is no universal graph for this &lt;em&gt;class&lt;/em&gt; of questions — you can draw a graph for one specific question, but not for the class.&lt;/p&gt;

&lt;p&gt;This is exactly what typical graph frameworks (LangGraph, CrewAI, etc.) make you pay for in complexity: either you draw a graph for every possible path up front, or you end up with a hidden branching if/else inside one node that nobody can later explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this looks in ctxloom
&lt;/h2&gt;

&lt;p&gt;ctxloom has no execution graph — it has &lt;strong&gt;artifacts&lt;/strong&gt; (typed, versioned objects) and &lt;strong&gt;agents that react to their appearance&lt;/strong&gt;. The breakdown above is just a chain of artifacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question
   │
   ▼
SourceRef (ranked references to sources)
   │
   ▼
TypedDoc / Spreadsheet (lazily resolved content)
   │
   ├──► Evidence (facts extracted from text)
   │        │
   │        ▼
   │      Claim (a statement + verification against Evidence)
   │
   └──► Calculation (an aggregate computed by code, not the model)
            │
            ▼
          Answer ──supported_by──► Claim, Calculation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each node in this chain is a separate &lt;code&gt;Produce&lt;/code&gt;: a small, testable unit of logic that only knows "what I need on input" and "what I create on output". Agents declare &lt;code&gt;consumes&lt;/code&gt;/&lt;code&gt;produces&lt;/code&gt;, and which agent runs next isn't decided by the programmer up front — the runtime figures it out from which artifact just appeared.&lt;/p&gt;

&lt;p&gt;The repo's &lt;code&gt;examples/knowledge&lt;/code&gt; demo has 9 agents and not a single explicit edge between them — here's the actual flow map (&lt;code&gt;python -m ctxloom graph examples.knowledge.agents&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;planner ──creates──► ResearchTurn ──consumed by──► search_scout, progress_evaluator
search_scout ──creates──► SourceRef ──consumed by──► resolver, table_resolver
resolver ──creates──► TypedDoc ──consumed by──► evidence_builder
evidence_builder ──creates──► Evidence ──consumed by──► verifier
table_resolver ──creates──► Spreadsheet ──consumed by──► calculator
calculator ──creates──► Calculation ──consumed by──► answer_builder
verifier ──creates──► Claim ──consumed by──► answer_builder
answer_builder ──consumes ResearchTurn──► creates Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question "how much does GPU cost in total?" goes through text search, table search, a deterministic calculation, and verification — &lt;strong&gt;in a single run&lt;/strong&gt; — because each agent reacts to what appeared, not to what someone expected to see at this step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provenance: not "a source mentioned in the prompt", a traceable chain
&lt;/h2&gt;

&lt;p&gt;The key difference from a typical RAG chatbot: the answer doesn't just &lt;em&gt;reference&lt;/em&gt; a source in text ("according to the CSV...") — it's &lt;strong&gt;linked&lt;/strong&gt; to it structurally:&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;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supported_by&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain &lt;code&gt;Answer → supported_by → Claim → derived_from → Evidence → extracted_from → TypedDoc → materialized_from → SourceRef&lt;/code&gt; isn't a log line — it's a graph of relations you can walk programmatically: show "where this answer came from" in a UI, run an audit of "which documents back the last 100 answers", or find every answer that depends on a document that later turned out to be stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculations are calculated, not guessed
&lt;/h2&gt;

&lt;p&gt;The model here isn't the source of truth for numbers. &lt;code&gt;calculator&lt;/code&gt; is plain code aggregating a CSV; the LLM is used only where reasoning is actually needed (phrasing the answer, extracting a fact from text), not where arithmetic is needed. If the aggregate disagreed with what the text evidence claims, &lt;code&gt;verifier&lt;/code&gt; catches it before the answer ever reaches the user — because checking a claim against numbers is its own deterministic &lt;code&gt;Produce&lt;/code&gt;, not part of a prompt that "usually works out".&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys you in practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility&lt;/strong&gt; — the same question against the same context version takes the same execution path; &lt;code&gt;Context&lt;/code&gt; is versioned like git, so you can &lt;code&gt;diff&lt;/code&gt;/&lt;code&gt;rollback&lt;/code&gt;/&lt;code&gt;replay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability&lt;/strong&gt; — every derived artifact knows what it was derived from; you don't have to reconstruct the reasoning chain from logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility without rewriting the graph&lt;/strong&gt; — a new data source is just another &lt;code&gt;Source&lt;/code&gt; that &lt;code&gt;fan_out_sources&lt;/code&gt; picks up automatically; there's no graph to edit because there's no graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest failure&lt;/strong&gt; — if a source is unavailable or a calculation doesn't add up, a &lt;code&gt;Produce&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; instead of the model "filling in" a plausible but wrong answer; the question escalates via HITL (&lt;code&gt;effects.ask&lt;/code&gt;) if a human is needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Head-to-head: the same use case, two implementations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  On ctxloom
&lt;/h3&gt;

&lt;p&gt;No router, no shared dict, no fan-out/fan-in node. &lt;code&gt;search_docs&lt;/code&gt; and &lt;code&gt;search_table&lt;/code&gt; both react to the same &lt;code&gt;Question&lt;/code&gt; independently — "GPU cost" needing both docs and the table just happens, it isn't a case anyone routes to.&lt;/p&gt;

&lt;p&gt;(Simplified for readability into 5 agents — the real &lt;code&gt;examples/knowledge&lt;/code&gt; demo is 9, because it also handles chat-turn routing, a lifecycle agent that decides when a turn has enough evidence to answer even if only one branch applies, and HITL escalation. Names here don't match the repo 1:1; see the link at the end for the literal code.)&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;ctxloom&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produce&lt;/span&gt;


&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SourceRef&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find&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="n"&gt;Question&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;question&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs_source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# your retrieval logic
&lt;/span&gt;        &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Spreadsheet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find&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="n"&gt;Question&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;question&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_costs_table&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                  &lt;span class="c1"&gt;# resolves the CSV
&lt;/span&gt;

&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_evidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find&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="n"&gt;TypedDoc&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;doc&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;extract_facts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;           &lt;span class="c1"&gt;# your extraction logic
&lt;/span&gt;        &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Calculation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find&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="n"&gt;Spreadsheet&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;sheet&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# real arithmetic, not the LLM
&lt;/span&gt;    &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Calculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;computed_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_all&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="n"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;find&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="n"&gt;Calculation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;calc&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                                      &lt;span class="c1"&gt;# the other half hasn't landed yet — fine, we'll be re-triggered
&lt;/span&gt;    &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;derived_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checked_against&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find&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="n"&gt;Claim&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;claim&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supported_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;search_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_table&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;evidence_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDoc&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;extract_evidence&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;calc_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Spreadsheet&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;verify_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Calculation&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;verify_claim&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;answer_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;build_answer&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;verify_claim&lt;/code&gt; still has a two-line readiness guard — that part isn't magic, someone has to know what "ready" means. What ctxloom removes is the &lt;em&gt;graph wiring&lt;/em&gt; around that guard: there's no join node, no conditional edge deciding who calls &lt;code&gt;verify_claim&lt;/code&gt;, no entry in a router that has to know both &lt;code&gt;search_docs&lt;/code&gt; and &lt;code&gt;search_table&lt;/code&gt; exist. &lt;code&gt;verify_agent&lt;/code&gt; just declares it consumes both artifact types; the runtime re-triggers it on either one showing up, and the guard clause is the only place "readiness" is decided — once, locally, next to the logic that uses it.&lt;/p&gt;

&lt;h3&gt;
  
  
  On LangGraph
&lt;/h3&gt;

&lt;p&gt;To be concrete about the cost, here's roughly what the same use case takes in LangGraph. You model state as one shared, untyped dict, and you draw every path explicitly:&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;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;doc_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
    &lt;span class="n"&gt;table_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
    &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
    &lt;span class="n"&gt;calculation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_question&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="n"&gt;State&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# you write this: does the question need the table, the docs, or both?
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;looks_numeric&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_docs&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="p"&gt;...&lt;/span&gt;       &lt;span class="c1"&gt;# -&amp;gt; doc_hits
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_table&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="p"&gt;...&lt;/span&gt;      &lt;span class="c1"&gt;# -&amp;gt; table_hits
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_evidence&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="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# -&amp;gt; evidence, reads doc_hits
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&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="p"&gt;...&lt;/span&gt;         &lt;span class="c1"&gt;# -&amp;gt; calculation, reads table_hits
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_claim&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="p"&gt;...&lt;/span&gt;      &lt;span class="c1"&gt;# -&amp;gt; claims, reads evidence + calculation
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_answer&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="p"&gt;...&lt;/span&gt;      &lt;span class="c1"&gt;# -&amp;gt; answer, reads claims
&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&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="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_docs&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_table&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extract_evidence&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calculate&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_claim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify_claim&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;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_answer&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;set_conditional_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route_question&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;search_docs&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;search_docs&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;search_table&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;search_table&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;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_docs&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;extract_evidence&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_table&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;calculate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# but "how much does GPU cost in total?" needs BOTH docs and table in one turn —
# so route_question's binary choice is already wrong, and you need a fan-out/
# fan-in node instead, plus a join condition that waits for both branches.
&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;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;needs_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_claim&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculate&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;verify_claim&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_claim&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;build_answer&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This already shows the pain, before adding a single new question type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;route_question&lt;/code&gt; grows combinatorially.&lt;/strong&gt; The real question — "how much does GPU cost in total?" — needs &lt;em&gt;both&lt;/em&gt; branches, so the binary router above is already wrong on the first real input. Fixing it means a fan-out/fan-in pair: a node that dispatches to both, and a join node whose only job is re-implementing the readiness check ctxloom gets from &lt;code&gt;consumes&lt;/code&gt; for free — except now it's graph topology, reviewed by reading edges, not by reading the function that uses the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State is an untyped shared dict, and that's a real bug source, not a style complaint.&lt;/strong&gt; Say someone renames &lt;code&gt;calculation&lt;/code&gt; to &lt;code&gt;cost_calculation&lt;/code&gt; while touching &lt;code&gt;calculate&lt;/code&gt;. &lt;code&gt;verify_claim&lt;/code&gt; still reads &lt;code&gt;state["calculation"]&lt;/code&gt; — no import error, no type error, no test failure until the field is &lt;code&gt;None&lt;/code&gt; at runtime and &lt;code&gt;verify_claim&lt;/code&gt; silently verifies against stale or missing data. In ctxloom the equivalent break is &lt;code&gt;find(inputs, Calculation)&lt;/code&gt; returning &lt;code&gt;None&lt;/code&gt; where a type actually changed — caught by the type checker, not by production behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance isn't a thing.&lt;/strong&gt; &lt;code&gt;state["evidence"]&lt;/code&gt; and &lt;code&gt;state["calculation"]&lt;/code&gt; are just values that get overwritten; if you want "what exactly did the answer depend on", you build that bookkeeping yourself, by hand, in every node, and keep it in sync with the graph by hand too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new source or a new claim type means editing the graph.&lt;/strong&gt; Adding a third search path (say, a ticketing system) means touching &lt;code&gt;route_question&lt;/code&gt;, adding a node, and adding edges — not just adding a node next to the existing ones, and not without re-reading the routing logic to make sure the new branch doesn't shadow an existing one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a knock on LangGraph for what it's built for — short, mostly-linear or a-few-known-branches flows, it's a fine fit. The mismatch is specifically the "open-ended question over heterogeneous sources, next question needs a different path" shape: a problem that doesn't have a fixed set of paths, forced into a data structure that requires one to be drawn in advance. That mismatch — and having no structural way to say "this answer is backed by exactly these facts" without hand-rolling it — is the actual reason we built ctxloom instead of building this on top of an existing graph framework: state changes already contain everything needed to decide "what runs next" and "what this depended on", so encoding that separately, by hand, as a graph plus a provenance dict, is redundant work that silently drifts from the real state the moment someone touches one side without the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/bzdvdn/ctxloom" rel="noopener noreferrer"&gt;github.com/bzdvdn/ctxloom&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;This use case, runnable, no API key required: &lt;a href="https://github.com/bzdvdn/ctxloom/tree/master/examples/knowledge" rel="noopener noreferrer"&gt;&lt;code&gt;examples/knowledge&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;.venv/bin/python examples/knowledge/chat.py&lt;/code&gt; (deterministic fallbacks when no LLM key is set)&lt;/li&gt;
&lt;li&gt;Feature-by-feature comparison, including where ctxloom is &lt;em&gt;not&lt;/em&gt; the right choice: &lt;a href="https://github.com/bzdvdn/ctxloom/blob/master/docs/en/comparison.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/en/comparison.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something with a similar "no fixed path" shape, I'd like to hear about it — and if you try ctxloom, a star on the repo or an issue with what didn't work is the most useful feedback I can get.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop drawing the graph: reactive agents over versioned artifacts</title>
      <dc:creator>Bogdan</dc:creator>
      <pubDate>Tue, 01 Sep 2026 21:53:31 +0000</pubDate>
      <link>https://dev.to/bzdvdn/stop-drawing-the-graph-reactive-agents-over-versioned-artifacts-49d4</link>
      <guid>https://dev.to/bzdvdn/stop-drawing-the-graph-reactive-agents-over-versioned-artifacts-49d4</guid>
      <description>&lt;h1&gt;
  
  
  Stop drawing the graph: reactive agents over versioned artifacts
&lt;/h1&gt;

&lt;p&gt;Most agent frameworks make you &lt;strong&gt;draw the graph&lt;/strong&gt;: connect nodes, wire memory, declare control flow. But a knowledge problem is not a workflow.&lt;/p&gt;

&lt;p&gt;Take a realistic question: &lt;em&gt;"Why did infrastructure costs increase in Q2?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer may need Confluence docs, GitLab merge requests, CSV spend data, a calculation, source verification — and a clarifying question. The &lt;em&gt;next&lt;/em&gt; question needs a &lt;strong&gt;different&lt;/strong&gt; path. There is no universal graph here, and asking a developer to draw one for every possible question is asking them to predict the future.&lt;/p&gt;

&lt;p&gt;So we built an agent runtime where &lt;strong&gt;you don't describe execution at all&lt;/strong&gt;. You describe what artifacts exist and what agents can do with them; the runtime derives what runs next from state changes. Agents react to events. There is no graph and no node pipeline.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;ctxloom&lt;/strong&gt; — a reactive, artifact-driven agent runtime, now open source.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;The whole loop is: &lt;em&gt;create an artifact → agents react → one atomic patch → context advances&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A knowledge question — say, &lt;em&gt;"how much does GPU inference cost?"&lt;/em&gt; — becomes a chain of typed artifacts: &lt;code&gt;UserQuery → TypedDoc → Evidence → Claim → Answer&lt;/code&gt;. Each is produced by an agent that reacts to the &lt;em&gt;previous&lt;/em&gt; artifact. No graph describes this chain; it falls out of what each agent consumes and produces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARTIFACT CREATED / UPDATED
       │
       ▼
     AGENTS REACT ──self.effects──► Effects ──compile──► Patch
       ▲                                                      │
       └──────────────────────────────────────────────────────┘
                                                        Context v+1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event that wakes an agent is &lt;em&gt;derived&lt;/em&gt; from that same change — the causal chain can never drift from the actual state.&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;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ctxloom&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RuntimeResources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;structured_llm&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FindingBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Conclusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;React to a Question; deterministic fallback when the LLM is absent (§67).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Question&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;question&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;structured_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FindingBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&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="nb"&gt;id&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="s"&gt;finding:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add a second agent that reacts to &lt;code&gt;Finding&lt;/code&gt; and links it back — provenance for free:&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;@produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Conclusion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;conclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;finding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Finding&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finding&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;conclusion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Conclusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarized.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;supported_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conclusion&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;conclusion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supported_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# provenance edge (§34)
&lt;/span&gt;

&lt;span class="n"&gt;agents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyzer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;analyze_question&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concluder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;conclude&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;RuntimeResources&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# no LLM wired: offline deterministic run
&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how much does GPU inference cost?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nc"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Conclusion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# find the terminal artifact
&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supported_by&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="c1"&gt;# provenance: what supports it
&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;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;# "GPUs accelerate inference."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two agents, no graph. The runtime matches &lt;code&gt;Question → analyze_question → Finding → conclude → Conclusion&lt;/code&gt; by reacting to state changes — and if the LLM is missing, &lt;code&gt;structured_llm&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; and the run degrades gracefully instead of hallucinating.&lt;/p&gt;

&lt;p&gt;You can see this fully worked out in the &lt;a href="https://github.com/bzdvdn/ctxloom/tree/master/examples/knowledge" rel="noopener noreferrer"&gt;knowledge demo&lt;/a&gt;: search fan-out, evidence extraction, deterministic claim verification, and a &lt;code&gt;supported_by&lt;/code&gt; answer with sources.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this isn't just another framework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Effects instead of "return a change"
&lt;/h3&gt;

&lt;p&gt;Most frameworks ask a unit of work to &lt;em&gt;return&lt;/em&gt; its result, and some orchestrator applies it. ctxloom inverts this: a producer &lt;strong&gt;states what should change&lt;/strong&gt; and returns &lt;code&gt;None&lt;/code&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&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="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence:q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer:q1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supported_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&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;answered&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="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One atomic commit.&lt;/strong&gt; Nothing applies until the produce returns — the whole step lands or none of it does. No half-applied states, no manual rollback ladders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles are objects, not ids.&lt;/strong&gt; &lt;code&gt;evidence&lt;/code&gt; is created above and linked below — the same object, no lookup by number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop is just another effect.&lt;/strong&gt; &lt;code&gt;self.effects.ask(...)&lt;/code&gt; poses a question; &lt;code&gt;effects.resume(...)&lt;/code&gt; resolves it. A human is not a special case bolted onto the side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Determinism by default
&lt;/h3&gt;

&lt;p&gt;The dominant pattern is "let the LLM do everything". ctxloom takes the opposite stance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calculations are actually calculated.&lt;/strong&gt; &lt;code&gt;CSVSource → Spreadsheet → Calculation&lt;/code&gt; over structured data is deterministic code — not a hallucinated number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eligibility is a decision of the state.&lt;/strong&gt; A produce's guard decides &lt;em&gt;whether&lt;/em&gt; it reacts; the LLM is called only on genuinely generative steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honesty beats pretending.&lt;/strong&gt; On failure a produce returns &lt;code&gt;None&lt;/code&gt; rather than a confident guess. The model is a reasoning component, never the source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb: &lt;em&gt;if it can be computed, compute it; the LLM only does what requires language and judgment.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Provenance and versioning for free
&lt;/h3&gt;

&lt;p&gt;Every run is a commit. You get &lt;code&gt;diff&lt;/code&gt;, &lt;code&gt;rollback&lt;/code&gt;, &lt;code&gt;branch()&lt;/code&gt; and &lt;code&gt;merge()&lt;/code&gt; for the state of a conversation, plus deterministic replay for auditing.&lt;/p&gt;

&lt;p&gt;And every derived artifact links back to what produced it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Answer → supported_by → Claim → derived_from → Evidence → extracted_from → Doc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"&lt;em&gt;Why did the agent say that?&lt;/em&gt;" is not a hand-wave — it's a walk across these links. That is accountability, and it's structural, not cosmetic.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;13 runnable examples&lt;/strong&gt;, all offline-capable (no API keys required): a multi-source knowledge chat, a web researcher, a hypothesis laboratory, an ops assistant, budget-aware replanning, branch-and-merge exploration, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A port matrix&lt;/strong&gt; mapping canonical agent patterns — ReAct, reflection, map-reduce, supervisor, summarize, time-travel, plan-and-execute — onto this idiom. You don't throw away what you already know; you see how it falls out of state instead of a diagram.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability built in.&lt;/strong&gt; Every run records agent spans, reads/writes, LLM calls and token usage — a local web dashboard, exportable to Langfuse or Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A thin web layer.&lt;/strong&gt; &lt;code&gt;ChatAssistant&lt;/code&gt; + &lt;code&gt;create_chat_router&lt;/code&gt; mount a session-persisted SSE chat on &lt;em&gt;your&lt;/em&gt; FastAPI app, with logged fallbacks instead of 500s.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When would you &lt;em&gt;not&lt;/em&gt; want this?
&lt;/h2&gt;

&lt;p&gt;Honesty matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed pipelines.&lt;/strong&gt; If your task really is a fixed sequence of five steps and nothing reacts, a plain function or a classic graph is the right tool. Pick what fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure playgrounds.&lt;/strong&gt; If you only want the model to "figure it out" and don't care about determinism, provenance or accountability, ctxloom adds ceremony you can skip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everything in between — agents that gather evidence, verify claims, calculate, respect budgets, ask humans, and can be audited — reactivity beats drawing a graph.&lt;/p&gt;




&lt;p&gt;If this resonates, try it — every example runs offline, no API keys needed:&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; &lt;span class="s2"&gt;"ctxloom[web]"&lt;/span&gt;
python ./examples/knowledge/web.py    &lt;span class="c"&gt;# multi-source chat with sourced answers&lt;/span&gt;
python ./examples/repair/web.py       &lt;span class="c"&gt;# room renovation: plan, estimate, CSV export&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 30-second takeaway: &lt;strong&gt;describe artifacts, not wiring.&lt;/strong&gt; The &lt;code&gt;knowledge&lt;/code&gt; demo&lt;br&gt;
turns a question into evidence → claims → a &lt;code&gt;supported_by&lt;/code&gt; answer in minutes,&lt;br&gt;
offline — and the same loop scales to research, ops, or anything where the&lt;br&gt;
answer has to be &lt;em&gt;accountable&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/bzdvdn/ctxloom" rel="noopener noreferrer"&gt;github.com/bzdvdn/ctxloom&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Design argument: &lt;a href="https://github.com/bzdvdn/ctxloom/blob/master/docs/en/why-ctxloom.md" rel="noopener noreferrer"&gt;why-ctxloom.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Port matrix: &lt;a href="https://github.com/bzdvdn/ctxloom/blob/master/docs/en/port-matrix.md" rel="noopener noreferrer"&gt;port-matrix.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>agents</category>
      <category>llm</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
