<?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: Zaam Chan</title>
    <description>The latest articles on DEV Community by Zaam Chan (@zaamchan).</description>
    <link>https://dev.to/zaamchan</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%2F4220858%2Fdf2fb72e-74fa-4ed6-9c2f-5cdd53c2cf20.png</url>
      <title>DEV Community: Zaam Chan</title>
      <link>https://dev.to/zaamchan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zaamchan"/>
    <language>en</language>
    <item>
      <title>Multi-agent demos you can actually reproduce</title>
      <dc:creator>Zaam Chan</dc:creator>
      <pubDate>Sun, 01 Nov 2026 16:24:44 +0000</pubDate>
      <link>https://dev.to/zaamchan/multi-agent-demos-you-can-actually-reproduce-1b6</link>
      <guid>https://dev.to/zaamchan/multi-agent-demos-you-can-actually-reproduce-1b6</guid>
      <description>&lt;p&gt;This post was written as part of the Kiroween 2026 hackathon.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/RomainCHEN/seance" rel="noopener noreferrer"&gt;Séance&lt;/a&gt;, a skeleton template for multi-agent "rooms" —&lt;br&gt;
and two apps from it. This is the part that would have been boring to write about: the personas and&lt;br&gt;
the UI. So instead I want to write about the bug that made the whole design worth it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem with multi-agent demos
&lt;/h2&gt;

&lt;p&gt;Every multi-agent product I have seen rebuilds the same three things, tangled together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who the agents are&lt;/strong&gt; — a prompt string inside a component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How a run unfolds&lt;/strong&gt; — inside an effect, next to a network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How a run is presented&lt;/strong&gt; — bespoke, per product.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tangling them costs more than tidiness. You cannot test a conversation, you cannot replay a demo,&lt;br&gt;
and changing a persona means editing TypeScript. And any demo that calls a model is at the mercy of&lt;br&gt;
the model: the recording and the live site drift apart within a week.&lt;/p&gt;

&lt;p&gt;Séance splits them: personas are YAML, the run is a pure function, the UI takes a transcript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;runRoom&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;personas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Transcript&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;Math.random&lt;/code&gt;, no &lt;code&gt;Date.now&lt;/code&gt;, no I/O. The seed chooses the &lt;em&gt;wording&lt;/em&gt;; the findings come from the&lt;br&gt;
artifact and the lenses. Same input, same transcript, forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Here is the first scoring formula I wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rawPoints&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looked reasonable. A room with more findings should have a higher risk, and dividing by the count&lt;br&gt;
normalises for room size. The sample artifact produced 84/100, which felt right, and every unit test&lt;br&gt;
passed.&lt;/p&gt;

&lt;p&gt;Then I wrote the property test that the spec had asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// adding a risk finding never lowers the score&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scoreFindings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;personaCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scoreFindings&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;riskFinding&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;personaCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It failed on the second generated input. Of course it did: adding a finding raises the numerator and&lt;br&gt;
&lt;em&gt;also&lt;/em&gt; raises the denominator, so a high but not top finding can be averaged down. The score could&lt;br&gt;
go &lt;strong&gt;down&lt;/strong&gt; when you pointed at a new risk.&lt;/p&gt;

&lt;p&gt;The fix is one line of algebra — divide by a capacity that does not depend on the findings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;personaCount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;capacityPerPersona&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// e.g. 3 × 30&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now monotonicity is structural rather than incidental.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why property-based testing earned its keep
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fast-check&lt;/code&gt; generated personas, artifacts and finding lists, and checked seven invariants. Six of&lt;br&gt;
them are boring until they fail:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Invariant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1&lt;/td&gt;
&lt;td&gt;determinism — equal inputs give deeply equal transcripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2&lt;/td&gt;
&lt;td&gt;the score is an integer in &lt;code&gt;[0, 100]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P3&lt;/td&gt;
&lt;td&gt;adding a &lt;code&gt;risk&lt;/code&gt; finding never lowers the score ← found the bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4&lt;/td&gt;
&lt;td&gt;every score maps to exactly one decision at 29/30 and 69/70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5&lt;/td&gt;
&lt;td&gt;one turn per persona, for any set of 1..8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6&lt;/td&gt;
&lt;td&gt;the engine never mutates its input array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P7&lt;/td&gt;
&lt;td&gt;ordering is independent of input order&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PBT is usually sold on parsers and arithmetic. It fits agent code better than it looks, because a&lt;br&gt;
pure agent engine &lt;em&gt;is&lt;/em&gt; arithmetic over generated data — there is no clock and no network, so a&lt;br&gt;
generated input is as legitimate as a hand-written one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the steering file did
&lt;/h2&gt;

&lt;p&gt;The single most valuable line in the repo is in &lt;code&gt;.kiro/steering/tech.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No &lt;code&gt;Math.random()&lt;/code&gt;, no &lt;code&gt;Date.now()&lt;/code&gt;, no &lt;code&gt;crypto.randomUUID()&lt;/code&gt; inside &lt;code&gt;seance-core&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Always-on steering means that rule applied to every file the agent touched, not just the one where&lt;br&gt;
determinism happened to come up in conversation. The predictable thing about an agent is that it&lt;br&gt;
will solve the problem in front of it; steering is how you make "in front of it" bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  The template is the product
&lt;/h2&gt;

&lt;p&gt;Two apps ship with the repo, and neither touches the engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Séance&lt;/strong&gt; — Security Specter, Perf Poltergeist, Style Wraith read a diff and return a merge
verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortem Séance&lt;/strong&gt; — Timeline Detective, Blast Analyst, Process Ghost read an incident timeline
and return a root-cause verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different domains, same three layers. If a third use case needs an engine change, that is a template&lt;br&gt;
bug, not a feature request.&lt;/p&gt;

&lt;p&gt;Try it: &lt;strong&gt;&lt;a href="https://romain.is-a.dev/seance/" rel="noopener noreferrer"&gt;https://romain.is-a.dev/seance/&lt;/a&gt;&lt;/strong&gt;. No key, no login, no network calls — press &lt;em&gt;Re-run with a new seed&lt;/em&gt; and&lt;br&gt;
watch the room change its voice without changing its evidence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source, tests and specs: &lt;a href="https://github.com/RomainCHEN/seance" rel="noopener noreferrer"&gt;github.com/RomainCHEN/seance&lt;/a&gt;. MIT.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
