<?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: DryDock</title>
    <description>The latest articles on DEV Community by DryDock (@drydock).</description>
    <link>https://dev.to/drydock</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%2F4038184%2Fb10cb190-2798-471c-b09b-ea9a12673411.png</url>
      <title>DEV Community: DryDock</title>
      <link>https://dev.to/drydock</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/drydock"/>
    <language>en</language>
    <item>
      <title>Seven Real Failures of an LLM Agent Operating a CAD Kernel (and How the Architecture Contained Them)</title>
      <dc:creator>DryDock</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:04:43 +0000</pubDate>
      <link>https://dev.to/drydock/seven-real-failures-of-an-llm-agent-operating-a-cad-kernel-and-how-the-architecture-contained-them-fg5</link>
      <guid>https://dev.to/drydock/seven-real-failures-of-an-llm-agent-operating-a-cad-kernel-and-how-the-architecture-contained-them-fg5</guid>
      <description>&lt;p&gt;I built a system where an LLM talks to a customer about a silicone casting mold, and a&lt;br&gt;
deterministic geometry kernel — OpenCASCADE, three decades of production C++ — does the&lt;br&gt;
actual mass-solving, boolean surgery, and part-splitting. The LLM never touches the kernel&lt;br&gt;
directly. It fills out a strictly validated contract; the kernel either produces a mold or&lt;br&gt;
explains, with numbers, why it can't.&lt;/p&gt;

&lt;p&gt;That split is the whole idea, and it's not a new one — it's a pragmatic instance of what the&lt;br&gt;
neuro-symbolic AI literature calls Type 6: a neural system that produces a structured&lt;br&gt;
artifact, and a symbolic engine that executes or verifies it. What I want to write about here&lt;br&gt;
isn't the idea. It's what happened when I actually built it: concrete failures, in a finished&lt;br&gt;
system with a green test suite, that the architecture caught — and, elsewhere in the build,&lt;br&gt;
at least one it didn't, which turned out to be the most instructive of all. This essay walks&lt;br&gt;
through five of the failures. The rest, including the one the architecture didn't catch, are&lt;br&gt;
in the longer playbook linked at the end.&lt;/p&gt;

&lt;p&gt;If you're building an agent over any domain where "close enough" isn't good enough — CAD,&lt;br&gt;
finance, compliance, infra — these are the shapes your failures will take too, even though&lt;br&gt;
the domain will look nothing like mine.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. The kernel that said "done" and returned nothing
&lt;/h2&gt;

&lt;p&gt;The operation that turns a solid block into a mold is a boolean subtraction: &lt;code&gt;block − cavity&lt;/code&gt;.&lt;br&gt;
I called OpenCASCADE, checked &lt;code&gt;IsDone()&lt;/code&gt;, and moved on — which is what the API's shape&lt;br&gt;
invites you to do, and what most wrappers do.&lt;/p&gt;

&lt;p&gt;Two things I found the hard way: in the binding I'm using, the cut operation exposes &lt;em&gt;only&lt;/em&gt;&lt;br&gt;
&lt;code&gt;IsDone()&lt;/code&gt; — no error report at all — and it can return &lt;code&gt;IsDone() == True&lt;/code&gt; while handing you&lt;br&gt;
an empty shape or a zero-volume solid. Separately, an &lt;em&gt;offset&lt;/em&gt; operation (used to build a thin&lt;br&gt;
outer shell) returned &lt;code&gt;IsDone() == True&lt;/code&gt; while producing a shell of volume zero — no exception,&lt;br&gt;
no negative flag, just success and emptiness.&lt;/p&gt;

&lt;p&gt;A three-decade-old kernel, and it lies about success in exactly the way you'd assume it&lt;br&gt;
couldn't.&lt;/p&gt;

&lt;p&gt;The fix is a stance, not a patch: &lt;strong&gt;success is never declared by the kernel — it's declared by&lt;br&gt;
postconditions the wrapper computes itself.&lt;/strong&gt; Because the pipeline constructs the block around&lt;br&gt;
the cavity, I know the exact expected volume by conservation (&lt;code&gt;v_block − v_cavity&lt;/code&gt;), so I can&lt;br&gt;
check it arithmetically, not heuristically:&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_block&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;v_cavity&lt;/span&gt;     &lt;span class="c1"&gt;# exact, by construction
&lt;/span&gt;&lt;span class="n"&gt;rel&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;v&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;5e-3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&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;volume &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; mm3 deviates &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; from expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; mm3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A kernel "success" that fails this check is recorded as a failed attempt and the escalation&lt;br&gt;
ladder (below) advances. The LLM never sees the lie — it only sees a job that hasn't succeeded&lt;br&gt;
yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The hollow solid that was never one shell
&lt;/h2&gt;

&lt;p&gt;Sometimes the exact boolean path is too fragile for a given geometry, so I fall back to a mesh&lt;br&gt;
boolean library and rebuild the result as a stitched solid: build a face per triangle, sew them,&lt;br&gt;
construct a solid from the sewn shells.&lt;/p&gt;

&lt;p&gt;My first version did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;solid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BRepBuilderAPI_MakeSolid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TopoDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Shell_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sewed&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nc"&gt;Solid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# wrong
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One closed surface, one solid — a mental model that's correct for the shapes you doodle while&lt;br&gt;
writing the function, and wrong for the shape this function exists to handle. The result of&lt;br&gt;
"block minus a fully contained cavity" is a &lt;strong&gt;hollow solid&lt;/strong&gt;, and a hollow solid's boundary is&lt;br&gt;
&lt;em&gt;at least two&lt;/em&gt; closed shells — the outer wall and the wall of the interior void. That's not an&lt;br&gt;
edge case; it's the normal case, because a mold is precisely a block with a cavity in it. The&lt;br&gt;
naive version silently drops the cavity wall from the solid's description, and the kernel&lt;br&gt;
doesn't warn you — it just hands back a solid that's topologically incomplete, and the&lt;br&gt;
brokenness surfaces three steps later as a volume that makes no sense.&lt;/p&gt;

&lt;p&gt;Representation crossings — translating one shape of data into another — are where systems&lt;br&gt;
quietly rot, because each side's documentation describes its own world, never the &lt;em&gt;shape of&lt;br&gt;
the traffic between them&lt;/em&gt;. The fix was to stop assuming cardinality and observe it: iterate&lt;br&gt;
over every shell the sewing step actually returns, not "the" shell.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The retry ladder that testifies
&lt;/h2&gt;

&lt;p&gt;Topological booleans fail — not as a bug, but intrinsically: coincident faces and tolerance&lt;br&gt;
mismatches put exact boolean operations on a knife's edge. The standard trick is "fuzzy"&lt;br&gt;
tolerance: let the kernel treat near-coincident geometry as coincident, and escalate the fuzz&lt;br&gt;
if the exact attempt fails.&lt;/p&gt;

&lt;p&gt;The naive version of this is a &lt;code&gt;while&lt;/code&gt; loop that jitters a parameter until something passes,&lt;br&gt;
with a log line somewhere. When a job ships, nobody can say what numerical regime produced it;&lt;br&gt;
when a job fails, the error says "boolean failed" and the forensics start from zero.&lt;/p&gt;

&lt;p&gt;So instead, the escalation ladder is data, not control flow. Every rung — tolerance tried,&lt;br&gt;
outcome, timing — is a typed record in the result, not a log line:&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;BooleanAttempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fuzzy_tolerance&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;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&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;kernel_error&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;postcondition_failed&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;mesh_fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;detail&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="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success with escalation, the job still emits a warning — needing extra fuzz is a geometry&lt;br&gt;
smell worth surfacing, not burying. On exhaustion, the &lt;em&gt;complete&lt;/em&gt; attempt list travels inside&lt;br&gt;
the error, so the failure report tells you which tolerances were tried, which failed in the&lt;br&gt;
kernel versus which failed a postcondition, and by how much — an engineer, or the orchestrating&lt;br&gt;
LLM, can tell "this geometry is fundamentally degenerate" from "we were 0.6% off at every rung"&lt;br&gt;
without re-running anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;EXPORT_FAILED&lt;/code&gt; or &lt;code&gt;OUTPUTS_UNSATISFIABLE&lt;/code&gt;? Why the answer is architecture
&lt;/h2&gt;

&lt;p&gt;A client can ask for specific physical parts by name. During export, I hit a question that&lt;br&gt;
sounds like naming trivia: if the spec asks for a rigid outer shell but the job doesn't have&lt;br&gt;
one enabled, what error is that?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPORT_FAILED&lt;/code&gt; already existed for "couldn't write an artifact" — and it's in the small set of&lt;br&gt;
codes that map to &lt;code&gt;status: "failed"&lt;/code&gt;, meaning &lt;em&gt;infrastructure trouble, retry with the same&lt;br&gt;
input&lt;/em&gt;. But the missing-part case isn't infrastructure trouble. Given the same spec, it's&lt;br&gt;
deterministic — no retry conjures a shell from a job that never enabled one.&lt;/p&gt;

&lt;p&gt;Here's why the distinction is load-bearing rather than pedantic: the agent's protocol is to&lt;br&gt;
apologize and retry on &lt;code&gt;failed&lt;/code&gt;, and to renegotiate the spec on &lt;code&gt;rejected&lt;/code&gt;. Ship the missing-&lt;br&gt;
part case as &lt;code&gt;failed&lt;/code&gt;, and the agent retries forever against a wall, eventually surfacing "a&lt;br&gt;
technical problem on our side" for what's actually a one-field spec inconsistency it could have&lt;br&gt;
fixed in one turn. The fix was a new code, &lt;code&gt;OUTPUTS_UNSATISFIABLE&lt;/code&gt;, outside the failed set,&lt;br&gt;
carrying both sides of the story in its context — requested parts, produced parts, why —&lt;br&gt;
so the agent can propose the exact correction instead of retrying blind.&lt;/p&gt;

&lt;p&gt;The general test: &lt;em&gt;given identical input, does this failure reproduce?&lt;/em&gt; Deterministic means the&lt;br&gt;
input must change. Non-deterministic means the input is fine and the infrastructure isn't. Every&lt;br&gt;
exact domain has tempting misclassifications on this line — "insufficient funds" is not a&lt;br&gt;
payment-gateway outage — and getting it wrong either burns retries on the impossible or makes&lt;br&gt;
users redesign around a hiccup.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The fallback that changed nothing downstream
&lt;/h2&gt;

&lt;p&gt;When the fuzzy-tolerance ladder above is exhausted, the system falls back to a completely&lt;br&gt;
different engine: tessellate both solids, difference them as meshes, then reconstruct a&lt;br&gt;
stitched OCCT solid from the result triangles (case #2). Hundreds of lines of downstream&lt;br&gt;
pipeline — plane search, part splitting, registration keys, channel drilling — are 100%&lt;br&gt;
native to the exact-geometry representation.&lt;/p&gt;

&lt;p&gt;The tempting design is to let the alternate representation flow forward: teach every&lt;br&gt;
downstream stage to also handle meshes. That doubles the surface every future stage has to&lt;br&gt;
validate, and runs your worst geometry through your least-exercised code.&lt;/p&gt;

&lt;p&gt;Instead, the fallback converts back at the seam: the reconstructed solid is wrapped to expose&lt;br&gt;
the &lt;em&gt;exact same interface&lt;/em&gt; the primary path produces — same methods, same shape — so nothing&lt;br&gt;
downstream changes by a single line, and it's gated by the &lt;em&gt;same&lt;/em&gt; postconditions as the primary&lt;br&gt;
path (case #1). The one thing that isn't erased is the semantic difference: a tessellated&lt;br&gt;
result is faceted geometry, fine for a 3D-printable format, a lie if exported as exact CAD data&lt;br&gt;
— so the system statically refuses that combination in the contract, and dynamically reports&lt;br&gt;
"this needed the approximate path" when it happens. The interface is preserved for the code;&lt;br&gt;
the degradation is preserved as information for the human.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't cover
&lt;/h2&gt;

&lt;p&gt;Five failures, and the pattern that contained all of them, isn't the whole story. Two more&lt;br&gt;
case files from the same build didn't make it into this essay — including the single most&lt;br&gt;
instructive failure in the whole project: a one-line sign bug that survived every test in the&lt;br&gt;
suite because the default test direction happened to make the bug invisible, and that neither&lt;br&gt;
the contract nor the postconditions could have caught, for reasons that turn out to generalize&lt;br&gt;
past geometry entirely. The other missing case shows what happens when every individual field&lt;br&gt;
in a spec validates and the spec as a whole is still physically impossible.&lt;/p&gt;

&lt;p&gt;Both of those, plus the architecture that ties all seven together — contract&lt;br&gt;
boundary, error taxonomy, structured evidence trail — are in the longer&lt;br&gt;
playbook. The system itself, mold-compiler, is real and running.&lt;/p&gt;

&lt;p&gt;→ Read the full playbook: &lt;a href="https://drydockdev.gumroad.com/l/uzsubs" rel="noopener noreferrer"&gt;https://drydockdev.gumroad.com/l/uzsubs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>cad</category>
    </item>
  </channel>
</rss>
