<?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: Witek ten Hove</title>
    <description>The latest articles on DEV Community by Witek ten Hove (@witekth).</description>
    <link>https://dev.to/witekth</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%2F4095838%2F9874aa9a-ec0a-4591-a986-5ed12e6495b8.png</url>
      <title>DEV Community: Witek ten Hove</title>
      <link>https://dev.to/witekth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/witekth"/>
    <language>en</language>
    <item>
      <title>My agents had their own branches. They did not have their own time.</title>
      <dc:creator>Witek ten Hove</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:14:17 +0000</pubDate>
      <link>https://dev.to/witekth/my-agents-had-their-own-branches-they-did-not-have-their-own-time-5bnj</link>
      <guid>https://dev.to/witekth/my-agents-had-their-own-branches-they-did-not-have-their-own-time-5bnj</guid>
      <description>&lt;p&gt;I spent a day building a code factory: a manager agent that reads a project's own build plan, works out what can be built in parallel, hands each subtask to an engineer in an isolated workspace, and merges the result behind a test gate.&lt;/p&gt;

&lt;p&gt;It works. Five subtasks built, tested, merged and logged in under a minute, on a backlog that was written weeks before the factory existed.&lt;/p&gt;

&lt;p&gt;Three things about it were wrong on the way there. None of them showed up in the design, in review, or in the tests. All three showed up in the log, and two of them only because I went looking for something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the thing is
&lt;/h2&gt;

&lt;p&gt;A manager agent on Google ADK, running &lt;code&gt;gemini-3.7-flash&lt;/code&gt;. Six tools: read the graph, read what the human pointed at, delegate, poll, merge, finish. The engineers work in their own &lt;code&gt;git worktree&lt;/code&gt; on their own branch, run the tests for the area they touched, and commit. The manager merges to one trial branch, one at a time.&lt;/p&gt;

&lt;p&gt;The design comes from a paper by Geng and Neubig on asynchronous software engineering agents, which measures something worth repeating: two runs on the same repository, the same model, differing only in which files got assigned, scored 8.7% and 34.3%. The expensive decision is not the code. It is what goes out next.&lt;/p&gt;

&lt;p&gt;So that is the decision I gave to the model, and only that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  One: the plan carries the tasks, not the order
&lt;/h2&gt;

&lt;p&gt;The manager was supposed to read the dependency graph out of the build plan. That is a 1,300-line document with 115 open subtasks, written over three weeks by someone who knew exactly what depended on what.&lt;/p&gt;

&lt;p&gt;I searched it for every phrasing I could think of. &lt;strong&gt;One subtask states its dependency in a form a machine can read.&lt;/strong&gt; One, in the whole document.&lt;/p&gt;

&lt;p&gt;The order lives in the head of whoever wrote the plan, and a manager cannot get in there. An agent asked to "derive the graph from the plan" will find that single edge, invent the rest, and say nothing about which is which.&lt;/p&gt;

&lt;p&gt;So the edges are now written by hand, per run, in a file the human can see and correct. The builder refuses a run where a subtask has no entry: an empty list means "checked, independent", and a missing entry means "nobody looked". Those are different things, and only one of them is safe.&lt;/p&gt;

&lt;p&gt;The one edge the plan does state is the interesting one. Subtask 7.3 requires four subtasks from another phase that are not part of this run. Every run I have done leaves 7.3 alone and says why on the dashboard. A manager that had guessed the graph would have built it, against a service that does not exist yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two: a guardrail in the prompt is not a guardrail
&lt;/h2&gt;

&lt;p&gt;Some files must not be touched by an engineer. The build plan itself is the sharpest case: it holds every checkbox, every log line, and a running total that has to be added up by hand. Two writers on that file is exactly how it went wrong twice in this repository already, and neither time did git say anything, because the two writers touched different lines.&lt;/p&gt;

&lt;p&gt;The first version of the manager's instruction said: do not touch these paths. That reads fine and is worth nothing. A model that can be asked not to do something can be argued into doing it, and the log will show a confident sentence either way.&lt;/p&gt;

&lt;p&gt;The check now runs on the diff, in code, immediately before the merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; base...branch | config.py &lt;span class="nt"&gt;--stand&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One match and the run ends. Not a warning, not a retry, not a fix. The reason is not tidiness: once a forbidden file has been touched, I can no longer tell whether the bookkeeping is right, and a factory whose bookkeeping might be wrong is worse than no factory.&lt;/p&gt;

&lt;p&gt;The path matcher has its own test table with 18 cases, and the ones that matter most are the &lt;strong&gt;non&lt;/strong&gt;-matches. &lt;code&gt;services/watchdog/BUILDPLAN.md&lt;/code&gt; is allowed, because only the one at the root is the status document. A pattern that is too wide stops a run over work that was always fine, and that costs exactly as much as a rule that does not fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three: isolation is not concurrency
&lt;/h2&gt;

&lt;p&gt;This is the one I would have shipped.&lt;/p&gt;

&lt;p&gt;Someone asked me a simple question: do the engineers work in their own branches? They do. Own branch, own worktree, visible in &lt;code&gt;git worktree list&lt;/code&gt;. I pulled up the log to show it, and the log answered a question I had not asked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15:22:02  eng-1  klaar      7.1 done
15:22:03  eng-2  opgewarmd  worktree on proef/fase7-caid-eng-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first engineer finished before the second one started. The workspaces were isolated, the branches were real, the merges were test-gated, and the whole thing ran one after the other. The engineer loop was synchronous, and nothing in the design, the dashboard or the tests had any opinion about that.&lt;/p&gt;

&lt;p&gt;The paper this is built on is about &lt;em&gt;asynchronous&lt;/em&gt; collaboration. Isolation without concurrency is branch-and-merge without the reason for it. And in a demo it would have been a claim I could not support: two lanes side by side on the screen, filling in sequence.&lt;/p&gt;

&lt;p&gt;Engineers now start in their own thread and the manager does not wait. Merging stays sequential, which is not an omission. Integration is one at a time and test-gated precisely so that building can be parallel.&lt;/p&gt;

&lt;p&gt;Afterwards the log interleaves, which is the only way I know to check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15:25:48  eng-2  begonnen
15:25:48  eng-1  begonnen
15:25:48  eng-1  toets      groen
15:25:48  eng-2  toets      ...
15:25:48  eng-1  commit     d53e7ba
15:25:48  eng-2  commit     69e2bf1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the log could answer at all
&lt;/h2&gt;

&lt;p&gt;All three findings came out of one design decision I did get right, and it was the first one I made.&lt;/p&gt;

&lt;p&gt;Every line in the log carries its source. &lt;strong&gt;Reported&lt;/strong&gt; is what the agent says about itself: rich, early, and not to be trusted, because an agent that has lost the thread reports progress that is not there. &lt;strong&gt;Measured&lt;/strong&gt; is what git says: thin, late, and impossible to fake. A commit exists or it does not.&lt;/p&gt;

&lt;p&gt;The dashboard marks the two differently, and two of its eight warnings fire exactly where they disagree: an engineer that reports three times with no measured change, and silence on both sides past a threshold.&lt;/p&gt;

&lt;p&gt;Finding three was a timestamp comparison between two measured lines. If the log had held only what the agents said about themselves, the answer would have been "yes, both engineers worked on it", and that would have been true and useless.&lt;/p&gt;

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

&lt;p&gt;One run, on one repository, on a backlog I chose. The paper behind it measures a spread from +30.7 to −10.5 percentage points &lt;em&gt;between repositories&lt;/em&gt;, so a single result sits inside its own noise.&lt;/p&gt;

&lt;p&gt;The headline numbers are worth reading carefully as well. The abstract reports +25.6 percentage points, and that is the weakest of the three models tested. For the strongest model the same table shows +6.1 and +6.0, at roughly two to four times the cost, and the wall-clock goes &lt;strong&gt;up&lt;/strong&gt; in every row. Coordination buys accuracy, not speed. Anyone selling you the opposite has not read the runtime column.&lt;/p&gt;

&lt;p&gt;What I can say is narrower and I think more useful: the mechanism runs, the guardrail holds when tested against it, and the two failures I found were both invisible until something ran and wrote down what it actually did.&lt;/p&gt;




&lt;p&gt;Built at &lt;a href="https://dev.to/"&gt;Business Data Solutions&lt;/a&gt;, a Dutch consulting and training firm that helps organisations put their data to work. The factory runs against Plinkie, a price-comparison system we operate on Google Cloud, so the backlog it builds is a real one.&lt;/p&gt;

&lt;p&gt;If you are working out where an agent belongs in your own delivery process, or where it does not, we are happy to think along. You can reach us through the &lt;a href="https://dev.to/#contact"&gt;contact form&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/businessdatasolutions/plinkie" rel="noopener noreferrer"&gt;The repository, with the architecture diagram and setup instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web-owsxcee6ka-ez.a.run.app" rel="noopener noreferrer"&gt;Plinkie, the system the factory builds, running on Google Cloud&lt;/a&gt; (the interface is Dutch, because the market is Dutch)&lt;/li&gt;
&lt;li&gt;Geng and Neubig, &lt;em&gt;Effective Strategies for Asynchronous Software Engineering Agents&lt;/em&gt;, &lt;a href="https://arxiv.org/abs/2603.21489" rel="noopener noreferrer"&gt;arXiv:2603.21489&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wrote this piece for the purposes of entering the All Things Agentic Hackathon run by Google Cloud and Devpost.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Three model calls, and none of them reads a price</title>
      <dc:creator>Witek ten Hove</dc:creator>
      <pubDate>Fri, 28 Aug 2026 08:40:22 +0000</pubDate>
      <link>https://dev.to/witekth/three-model-calls-and-none-of-them-reads-a-price-25nn</link>
      <guid>https://dev.to/witekth/three-model-calls-and-none-of-them-reads-a-price-25nn</guid>
      <description>&lt;p&gt;We built an agent swarm that reprices a market every night on Google Cloud. The part worth writing about is not what the model does. It is where the model is not allowed to go.&lt;/p&gt;

&lt;p&gt;This project started with an advertisement. A good-looking monthly price for a car I might actually want, and a catch I only found by clicking through to the provider. The most common catch was the mileage: the advertised price assumed 5,000 kilometres a year and the longest available term. Drive a normal 10,000 and the figure moves. Sometimes the advertised price was not even current any more, and the provider's own page quoted considerably more.&lt;/p&gt;

&lt;p&gt;Every one of those numbers is defensible on its own. Together they make an offer look like a price when it is really a price under conditions, and the conditions are the part that gets left out.&lt;/p&gt;

&lt;p&gt;Plinkie is a comparison site built around that observation. Two design decisions follow from it directly. Every offer is stored per mileage tier rather than as a single headline figure, so a car appears at the mileage you actually drive instead of at the one that photographs best. And every price carries the moment we last saw it confirmed at the source: after 72 hours without confirmation, the site labels it as unconfirmed rather than showing it as fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs at night
&lt;/h2&gt;

&lt;p&gt;At 02:00 CET, Cloud Scheduler starts a Cloud Run Job. The job reads a register of sources from Firestore, applies a refresh policy per source, and fans the work out over Pub/Sub, where a failed task lands in a dead-letter topic after three attempts. Scrapers fetch each offer page and store it in Cloud Storage with its SHA-256, so we can always show which bytes a claim came from. The run finishes with a single transactional swap: until the whole set is ready, yesterday's data stays live.&lt;/p&gt;

&lt;p&gt;A recent run took 24 minutes across four sources. Nobody was watching it, which is the entire point of building it this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model actually sits
&lt;/h2&gt;

&lt;p&gt;The nightly run makes three model calls. Not one of them reads a price off a page.&lt;/p&gt;

&lt;p&gt;Extraction is ordinary code: one adapter module per source, tested against fragments cut from the real page in the real element order. Gemini enters afterwards, to check that code rather than to replace it. &lt;strong&gt;Blind verification&lt;/strong&gt; sends the stored HTML snapshot to Gemini 3.7 Flash in a batch job, without showing it what the parser concluded, and only a genuine contradiction reaches a human review queue. &lt;strong&gt;Deduplication&lt;/strong&gt; uses &lt;code&gt;gemini-embedding-001&lt;/code&gt; to find candidate pairs across providers, after which deterministic rules decide them. &lt;strong&gt;The terms-and-conditions harvest&lt;/strong&gt; turns provider PDFs into checkable clauses.&lt;/p&gt;

&lt;p&gt;The pricing engine itself carries no model at all, and it never will. We publish the formula on the site, versioned, with a changelog, so a visitor can redo the sum by hand. A formula that a model might compute differently next Tuesday is not a formula.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each Google service actually solved
&lt;/h2&gt;

&lt;p&gt;The stack was not chosen for a diagram. Each service closed a specific problem, and here is what each one did for us.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What it solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Run Jobs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A swarm that runs once a night and scales to zero for the other 23 hours. We pay for 24 minutes, not for a day.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch on the Agent Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Half the token price for work where nobody is waiting. The whole night, models included, stays under $2.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud DLP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Personal data removed from an uploaded contract before any model sees the file. The ordering is the promise, and DLP is what lets us make it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google ADK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LongRunningFunctionTool&lt;/code&gt; gave us an agent that pauses on a human judgement and resumes where it stopped, without us writing a state machine for it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firestore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A home for the two collections that change outside the run window, which keeps the nightly job the only writer of the source of truth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pub/Sub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A dead-letter topic, so one broken source degrades the run instead of ending it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The agent that cannot tell us a number
&lt;/h2&gt;

&lt;p&gt;One question defeats a pipeline: where is the answer? Every provider puts "what does an extra kilometre cost" somewhere else on their own site, and moves it when they redesign. We tried a fixed crawl and it broke on contact with reality.&lt;/p&gt;

&lt;p&gt;So we built a scout with Google ADK. It browses the provider's own site, decides whether to go a level deeper, and offers the sentence it believes carries the answer. It runs on Gemini 3.7 Flash in realtime rather than in batch, and that exception is reasoned: a search decides its next request from the previous answer, which a batch job cannot do.&lt;/p&gt;

&lt;p&gt;Here is the part we would like you to look at. The tool the scout calls has this shape:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;leg_vondst_vast(url: str, citaat: str)&lt;/code&gt; A URL and a quote. There is no value parameter. The scout is structurally incapable of telling us that an extra kilometre costs 6 cents. It can only point at a page and at a sentence on it.&lt;/p&gt;

&lt;p&gt;Deterministic code does the rest. It reads the number out of the quote, checks the unit against the one the question demands, checks the range, refuses a quote containing two candidate numbers, and refuses any quote it cannot find word for word on the page we fetched ourselves. Eight locks in total. Each one exists because a dry run walked around the previous seven, and each one is now a regression test.&lt;/p&gt;

&lt;p&gt;The result is that a model picking the wrong sentence produces a rejection rather than a claim. 3,692 offers on the site now carry a warning about excess, early termination or extra kilometres, and every one of them shows the provider's URL, the literal sentence, and the date we read it. A reader can check any of it at the source in one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  One choice that went against the grain
&lt;/h2&gt;

&lt;p&gt;We started on Cloud SQL and took it out again. Four outages in a row turned out to share one cause: a cold start of 307 to 412 seconds, against a timeout we had set eight seconds too short. The start time is not tunable.&lt;/p&gt;

&lt;p&gt;The honest reading is that we had picked the wrong shape, not a bad product. A managed instance is built to stay warm for traffic that arrives all day. Our writer wakes up once a night, works for 24 minutes, and wants to disappear again. We replaced it with a DuckDB file in Cloud Storage, opened in-process by the job that owns it, and object generation enforces that there is exactly one writer. That removed the VPC peering, the auth-proxy sidecar, the start and stop logic, and the last password in the project.&lt;/p&gt;

&lt;p&gt;If you are building something that genuinely scales to zero, check whether your database does too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost to learn
&lt;/h2&gt;

&lt;p&gt;One failure is worth repeating, because it was quiet. The terms harvest reused a result reader from a neighbouring service, and the two marked their batch lines differently. Every key parsed as an empty string, every line failed to find its document, and the run reported a clean harvest of zero rows after a batch we had already paid for.&lt;/p&gt;

&lt;p&gt;The fix took ten minutes. The lasting fix was a counter in the run report that says how many lines found no document, with the cause printed next to it as soon as the number leaves zero. A system that can fail silently will eventually do it while you are asleep, and 02:00 is exactly when nobody is reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who built this
&lt;/h2&gt;

&lt;p&gt;Plinkie is built by &lt;a href="https://dev.to/"&gt;Business Data Solutions&lt;/a&gt;, a Dutch consulting and training firm that helps organisations put their data to work. This system was built by one person in ten days, runs in two fully separate Google Cloud projects, and costs under $2 per night to operate. The infrastructure is OpenTofu, the services are Python 3.13, and the site is Next.js.&lt;/p&gt;

&lt;p&gt;If you are weighing up where an agent belongs in your own systems, or where it does not, we are happy to think along. You can reach us through the &lt;a href="https://dev.to/#contact"&gt;contact form&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://web-owsxcee6ka-ez.a.run.app" rel="noopener noreferrer"&gt;Plinkie, running on Google Cloud&lt;/a&gt; (the interface is Dutch, because the market is Dutch)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web-owsxcee6ka-ez.a.run.app/formule" rel="noopener noreferrer"&gt;The published pricing formula&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/businessdatasolutions/plinkie" rel="noopener noreferrer"&gt;The repository, with the architecture diagram and setup instructions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wrote this piece for the purposes of entering the All Things Agentic Hackathon run by Google Cloud and Devpost.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
