<?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: horio</title>
    <description>The latest articles on DEV Community by horio (@forifor).</description>
    <link>https://dev.to/forifor</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%2F4122314%2Ffdbc052c-9c38-4e2f-be6a-fd085a34a889.png</url>
      <title>DEV Community: horio</title>
      <link>https://dev.to/forifor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/forifor"/>
    <language>en</language>
    <item>
      <title>I built an AI team that ships real work — and shows you the conversation</title>
      <dc:creator>horio</dc:creator>
      <pubDate>Sat, 12 Sep 2026 15:44:18 +0000</pubDate>
      <link>https://dev.to/forifor/i-built-an-ai-team-that-ships-real-work-and-shows-you-the-conversation-251a</link>
      <guid>https://dev.to/forifor/i-built-an-ai-team-that-ships-real-work-and-shows-you-the-conversation-251a</guid>
      <description>&lt;p&gt;Most "multi-agent" demos are bots narrating to each other. The log is the product; the work is secondary. I wanted the opposite: one request in, real files out, and a trail I could audit afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Team&lt;/strong&gt; is an open-source (MIT), local-first runtime where a Master plans, a Researcher / Builder / Reviewer actually do the work, and you get the artifacts &lt;strong&gt;plus&lt;/strong&gt; the real bot-to-bot messages, a timeline, and verification bound to each artifact revision.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/FORIFOR/Multibot" rel="noopener noreferrer"&gt;https://github.com/FORIFOR/Multibot&lt;/a&gt; · Site + 59s intro: &lt;a href="https://forifor.github.io/Multibot/" rel="noopener noreferrer"&gt;https://forifor.github.io/Multibot/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No scripted chat.&lt;/strong&gt; The chat panel is a projection of &lt;code&gt;message.sent&lt;/code&gt; events — messages that were really delivered to another bot's mailbox. A question from the Builder wakes the Researcher, which answers with &lt;code&gt;reply_to&lt;/code&gt;. Acknowledgements never wake a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence, not vibes.&lt;/strong&gt; Checks and review verdicts are events bound to an artifact revision hash. The final report is compiled from the event log; a model summary cannot upgrade "started" to "done".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime-enforced limits.&lt;/strong&gt; Tool scope, write scope, budget reservation (spent + reserved for in-flight calls), approvals with hash + nonce, cancel / resume / fork — enforced in code, not prompt wording. Unknown model prices refuse to start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-bot configuration.&lt;/strong&gt; Each bot inherits a default connection and model and can override endpoint, model, effort and system prompt (lockable). Configured vs. provider-reported model are both shown. No silent fallbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay never calls a model.&lt;/strong&gt; Fork from a checkpoint with a different model for one bot and compare. Export the whole run as JSONL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  No API key needed (Claude Code)
&lt;/h2&gt;

&lt;p&gt;The default connection runs every agent session through the local &lt;code&gt;claude -p&lt;/code&gt;. Claude Code owns the loop for one session; the team's tools (&lt;code&gt;send_message&lt;/code&gt;, &lt;code&gt;publish_artifact&lt;/code&gt;, &lt;code&gt;run_check&lt;/code&gt;, …) are exposed to it as MCP tools through a small stdio proxy that forwards each call to the runtime's ToolGateway. Policy, budget and the event log are identical to the API path; Claude Code's own built-in tools are disabled for these sessions. Cost and usage are read from the CLI's JSON result, and the model it actually used comes from &lt;code&gt;modelUsage&lt;/code&gt;, never from the model's own claims.&lt;/p&gt;

&lt;p&gt;Claude API (official SDK), any OpenAI-compatible chat endpoint and local Ollama work the same way, per bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real run, unedited
&lt;/h2&gt;

&lt;p&gt;On 2026-09-13 I ran this request through the local Claude Code CLI: &lt;em&gt;"From this product description, build a Japanese launch page and three social-post drafts. Record assumptions for anything missing. Stop before publishing. Have the Reviewer verify."&lt;/em&gt;&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model (reported by the provider)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-opus-5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;Master chose 1 builder task + 1 reviewer task and skipped the researcher; 8 recorded assumptions (no prices, no invented numbers, placeholder URLs only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deliverables&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;index.html&lt;/code&gt; (single-file page), &lt;code&gt;posts.md&lt;/code&gt;, &lt;code&gt;HANDOFF.md&lt;/code&gt;, &lt;code&gt;final-report.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;10 programmatic checks → all pass; reviewer verdict 6/6, plus 4 optional findings sent back as a real message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage&lt;/td&gt;
&lt;td&gt;39 model turns · 35 tool calls · $1.66 list-price equivalent · 18 min 37 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;completed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The generated files, the final report and the 77-event JSONL log are committed under &lt;code&gt;docs/evidence/&lt;/code&gt; without edits. Four runs total: run 1 finished &lt;em&gt;partial&lt;/em&gt; and exposed a bug (a reviewer verifying two tasks had only its last verdict applied — fixed), runs 2–3 hit limits that were then tuned. One request is not a benchmark, and the prompts are original seeds.&lt;/p&gt;

&lt;p&gt;The demo video on the site drives the real UI and runtime with a &lt;strong&gt;scripted test provider&lt;/strong&gt; (labelled on screen) so it is deterministic and free to reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One request → a checked plan.&lt;/strong&gt; The Master turns the request into deliverables, assumptions and a task DAG (structured output). The runtime validates schema, cycles, owners, tools, write scopes and limits before anything runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent bots, real delivery.&lt;/strong&gt; Each bot has its own conversation state, mailbox, task scope, tools and workspace. A worker receives its task, input artifact refs and its own inbox — not the whole history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify, revise, report from evidence.&lt;/strong&gt; The Reviewer runs checks against a specific revision. Fail → the Builder revises → re-review, bounded. The final report is compiled from the event log.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stack: Python 3.12 / FastAPI / SQLite (WAL, append-only events, SSE), React + TypeScript. macOS seatbelt sandbox for builder commands (no network, writes only inside the task workspace); elsewhere a plain subprocess that says so in every result.&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;git clone https://github.com/FORIFOR/Multibot &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;Multibot
&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv venv .venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; .venv/bin/python &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.[dev]'&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ../backend
.venv/bin/agentteam probe    &lt;span class="c"&gt;# real capability check through `claude -p`&lt;/span&gt;
.venv/bin/agentteam serve    &lt;span class="c"&gt;# http://127.0.0.1:8787&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd love feedback on two things: whether the review → revise → re-review loop bound to revisions holds up on your tasks, and where "fewer messages, more evidence" (workers get their task, artifact refs and inbox — not the history) breaks down.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>agents</category>
    </item>
    <item>
      <title>I let an AI make phone calls, then took the word "booked" away from it</title>
      <dc:creator>horio</dc:creator>
      <pubDate>Sat, 12 Sep 2026 15:39:30 +0000</pubDate>
      <link>https://dev.to/forifor/i-let-an-ai-make-phone-calls-then-took-the-word-booked-away-from-it-5484</link>
      <guid>https://dev.to/forifor/i-let-an-ai-make-phone-calls-then-took-the-word-booked-away-from-it-5484</guid>
      <description>&lt;p&gt;Getting an AI to place a phone call takes an evening. The trouble starts after that.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It said "your table is booked" when the restaurant had said no such thing.&lt;/li&gt;
&lt;li&gt;It talked to a voicemail greeting for two minutes, politely asking for a reservation.&lt;/li&gt;
&lt;li&gt;It accepted a price above the budget because the conversation had a nice flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three happened on real calls to my own phone. So I built a runtime that &lt;strong&gt;takes the completion decision away from the model and gives it to code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/FORIFOR/oathra" rel="noopener noreferrer"&gt;https://github.com/FORIFOR/oathra&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx oathra demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two AIs start a phone call in your browser, no API key. One plays a restaurant, the other wants a table. 7 pm is full, the restaurant offers 7:30, the caller takes it, gives a name, and only when the restaurant says "you're all set" does the run become MISSION COMPLETE.&lt;/p&gt;

&lt;p&gt;The result is not a summary. Every field is anchored to something the &lt;em&gt;other party&lt;/em&gt; said.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19:30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"callee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7 is fully booked, but we do have 7:30."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"span"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7:30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"explicit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Completion is a formula, not an opinion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;complete = connected ∧ date.verified ∧ time.verified ∧ partySize.verified ∧ confirmed.verified ∧ constraints hold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the caller says "great, that's confirmed", it is recorded and ignored. Only the callee's "you're booked" fills &lt;code&gt;confirmed&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence rules
&lt;/h2&gt;

&lt;p&gt;This part is deterministic parsers and rules. No LLM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"7 is full, but 7:30 works" → no evidence for 7:00 (negated clause)&lt;/li&gt;
&lt;li&gt;"the 13th, sorry, the 14th" → the 14th survives, with a supersede trail&lt;/li&gt;
&lt;li&gt;"probably fine" → neither agreement nor confirmation&lt;/li&gt;
&lt;li&gt;price changes after "you're booked" → the confirmation goes stale and must be re-obtained&lt;/li&gt;
&lt;li&gt;"OK, I'll look elsewhere" → not an acceptance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a command that generates ten thousand hostile clerks to break this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oathra &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;--adversarial&lt;/span&gt; 10000
&lt;span class="c"&gt;# never-confirm / wrong-restate / negate-then-offer / silent-hangup / caller-echo-trap&lt;/span&gt;
&lt;span class="c"&gt;# False Completion: 0 / 10000 adversarial runs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fuzzer caught real bugs. The first one: "¥21,100" was split at the comma, became "¥100", and the agent happily accepted. More recently I let GPT-4o mini and Gemini &lt;em&gt;play the clerk&lt;/em&gt; (&lt;code&gt;oathra eval --callee openai&lt;/code&gt;), which found refusals that quoted a number being read as offers, and product names being parsed as serial numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real phone calls
&lt;/h2&gt;

&lt;p&gt;The same runtime drives real calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oathra setup phone      &lt;span class="c"&gt;# pick a voice engine and a carrier&lt;/span&gt;
oathra phone doctor     &lt;span class="c"&gt;# which layer is broken&lt;/span&gt;
oathra call &lt;span class="nt"&gt;--to&lt;/span&gt; +81…   &lt;span class="c"&gt;# dial&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Carriers (Twilio direct, or Plivo / your own SIP through a LiveKit gateway) and voice engines (GPT-Live, OpenAI Realtime, Deepgram + LLM + TTS) are chosen independently.&lt;/p&gt;

&lt;p&gt;Calls to my own phone:&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;Stack&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Deepgram + GPT-4o-mini + TTS&lt;/td&gt;
&lt;td&gt;11.8 s to first reply. It kept asking "can you hear me?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Same, streaming TTS&lt;/td&gt;
&lt;td&gt;2.2 s. Talked to voicemail for 2 minutes; noise interrupted it 13 times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;GPT-Live (full duplex)&lt;/td&gt;
&lt;td&gt;~0.4 s. A 6 min 25 s chat with no errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Call three was when it felt usable. GPT-Live is $0.05/min for the session; Twilio to a Japanese mobile is ¥28.78/min.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it honestly stands
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Twilio direct and GPT-Live are verified on real calls&lt;/li&gt;
&lt;li&gt;Plivo and custom SIP are implemented from provider docs; PSTN not yet verified&lt;/li&gt;
&lt;li&gt;MCP server is v0.2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every number above was measured by me and can be reproduced with the commands in the README. Scenarios are YAML and welcome as PRs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://forifor.github.io/oathra/en/" rel="noopener noreferrer"&gt;https://forifor.github.io/oathra/en/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>voice</category>
    </item>
  </channel>
</rss>
