<?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: WonderLab</title>
    <description>The latest articles on DEV Community by WonderLab (@wonderlab).</description>
    <link>https://dev.to/wonderlab</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%2F3797373%2F25beba30-d8d4-4d2e-9ec6-170356089350.jpg</url>
      <title>DEV Community: WonderLab</title>
      <link>https://dev.to/wonderlab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wonderlab"/>
    <language>en</language>
    <item>
      <title>Open Source Project #168: LoopX — Long-Horizon Agent Control Plane Running on Top of Codex/Claude Code</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Tue, 01 Sep 2026 01:55:48 +0000</pubDate>
      <link>https://dev.to/wonderlab/open-source-project-168-loopx-long-horizon-agent-control-plane-running-on-top-of-codexclaude-4n8c</link>
      <guid>https://dev.to/wonderlab/open-source-project-168-loopx-long-horizon-agent-control-plane-running-on-top-of-codexclaude-4n8c</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Keep the loop moving. Keep the judgment human."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is &lt;strong&gt;article #168&lt;/strong&gt; in the "One Open Source Project a Day" series. Today's project is &lt;strong&gt;LoopX&lt;/strong&gt; — a long-horizon agent control plane, 5,288 Stars, Apache-2.0, authored by huangruiteng.&lt;/p&gt;

&lt;p&gt;LoopX addresses a problem that a single session can't solve: &lt;strong&gt;an agent can finish a task in one conversation, but what about work that spans days or weeks?&lt;/strong&gt; Objectives shift, evidence goes stale, decision gates appear, agents hand work off to other agents… chat memory and a timer are nowhere near enough to govern any of that.&lt;/p&gt;

&lt;p&gt;LoopX isn't an agent framework. It's a &lt;strong&gt;control plane&lt;/strong&gt; running on top of your existing agent harnesses (Codex App / Claude Code / OpenCode / etc.): it owns the persistent state, decides "what happens next, whether to continue, and what needs human judgment," while the agent harness executes one bounded turn at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You'll Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;LoopX's core concepts: the roles of Goal / Gate / Todo / Evidence / Quota&lt;/li&gt;
&lt;li&gt;The Five Questions framework for the control plane&lt;/li&gt;
&lt;li&gt;Integration patterns with nine agent harnesses&lt;/li&gt;
&lt;li&gt;Available capability paths (Issue Fix / Explore / Auto Research, etc.)&lt;/li&gt;
&lt;li&gt;Quick start: install, connect a project, run your first loop&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Experience with Claude Code, Codex, or similar AI coding agents&lt;/li&gt;
&lt;li&gt;Basic familiarity with long-horizon work management (goals, task decomposition, review workflows)&lt;/li&gt;
&lt;li&gt;Python fundamentals&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;LoopX's core mental model is an &lt;strong&gt;agent-native Kanban for long-running work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A regular Kanban manages human tasks. LoopX "cards" carry: identity (who owns this task), authority (who can claim it), evidence (what was accomplished), and continuation (context for the next step). "Moving a card" isn't clicking a button — it's a validated operator like &lt;code&gt;claim&lt;/code&gt;, &lt;code&gt;gate&lt;/code&gt;, &lt;code&gt;monitor&lt;/code&gt;, or &lt;code&gt;writeback&lt;/code&gt;. The board is a projection view; LoopX state is the source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Author / Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Author&lt;/strong&gt;: huangruiteng&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary language&lt;/strong&gt;: Python 3.11+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: Apache-2.0 (from v0.4.8; earlier releases under MIT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://huangruiteng.github.io/loopx/" rel="noopener noreferrer"&gt;huangruiteng.github.io/loopx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User manual&lt;/strong&gt;: &lt;a href="https://my.feishu.cn/wiki/CaL5wMk9ui17ngkWzeUcMlAYnZg" rel="noopener noreferrer"&gt;Feishu Wiki&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Stats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ GitHub Stars: &lt;strong&gt;5,288+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🍴 Forks: 476+&lt;/li&gt;
&lt;li&gt;📄 License: Apache-2.0&lt;/li&gt;
&lt;li&gt;📅 Created: 2026-05-31&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;p&gt;LoopX compresses control-plane responsibilities into five questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What LoopX keeps visible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What is the objective?&lt;/td&gt;
&lt;td&gt;The active goal, explicit scope, and current authority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What happens next?&lt;/td&gt;
&lt;td&gt;Ordered user and agent todos, ownership, claims, and leases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What needs human judgment?&lt;/td&gt;
&lt;td&gt;Concrete user gates — not a vague "waiting for owner"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What evidence changed?&lt;/td&gt;
&lt;td&gt;Compact run history, validation, blockers, accepted writeback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May the loop continue?&lt;/td&gt;
&lt;td&gt;Quota, capabilities, safe fallback paths, scheduler hints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;These five questions map to five product promises&lt;/strong&gt;: long-horizon state / semantic decisions / human-agent collaboration / recovery / governance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Control-Plane Stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;objective / issue / project
   │
   ▼
LoopX state: objective + gates + todos + scope + evidence + quota
   │
   ├─ human judgment needed? ── yes ──▶ ask a concrete question and wait
   │
   ├─ safe fallback available? ────────▶ run one bounded agent slice
   │
   ▼
Codex / Claude Code / Cursor / shell agent executes one turn
   │
   ▼
write evidence + handoff + next todo ──▶ quota decides the next tick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent harness executes the work. LoopX governs the state that lets work continue across runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four-Layer Responsibility Separation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kernel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Owns durable Goal, Todo, Gate, Evidence, Quota, Recovery, Scheduling state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Capability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines a stable, provider-neutral contract for one bounded, verifiable caller outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provider&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Calls external systems and returns bounded observations and readback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extension&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packages and operates an optional Provider with install/upgrade/rollback lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Execution path: &lt;code&gt;Agent → Capability → Provider&lt;/code&gt;. Control path returns: &lt;code&gt;Provider readback → Capability transition → Kernel&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requires Python 3.11+&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; loopx
loopx workflow-skills &lt;span class="nt"&gt;--install&lt;/span&gt;
loopx doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS/Linux: POSIX shell. Windows: PowerShell 7 (no compatibility layer needed).&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect a Project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-project
loopx connect
loopx status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a first-time setup, use the guided path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loopx start-goal &lt;span class="nt"&gt;--guided&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--goal-text&lt;/span&gt; &lt;span class="s2"&gt;"Your long-running objective"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Core Loop — Five Steps
&lt;/h3&gt;

&lt;p&gt;Any custom runner calls these five commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loopx quota should-run    &lt;span class="c"&gt;# should this registered agent act right now?&lt;/span&gt;
loopx todo claim          &lt;span class="c"&gt;# claim the current work slice&lt;/span&gt;
loopx todo update         &lt;span class="c"&gt;# record what changed&lt;/span&gt;
loopx refresh-state       &lt;span class="c"&gt;# prepare context for the next turn&lt;/span&gt;
loopx quota spend-slot    &lt;span class="c"&gt;# account for a completed, validated slice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Integration with Nine Agent Harnesses
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent Harness&lt;/th&gt;
&lt;th&gt;Recommended entry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex App&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ask the agent to run &lt;code&gt;loopx connect&lt;/code&gt;, then use &lt;code&gt;$loopx &amp;lt;task&amp;gt;&lt;/code&gt; or &lt;code&gt;/skills&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex App (SSH)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx agent-onboard --agent-type codex-app-ssh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connect inline, then &lt;code&gt;$loopx &amp;lt;task&amp;gt;&lt;/code&gt; or &lt;code&gt;/skills&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Install the opt-in adapter, then &lt;code&gt;/loopx &amp;lt;task&amp;gt;&lt;/code&gt; + &lt;code&gt;/loop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KunlunCode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loopx-kunluncode connect&lt;/code&gt; + native Goal Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenCode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Install the static command facade; opt into &lt;code&gt;--with-goal-bridge&lt;/code&gt; for recurring goals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loopx slash-commands --install --surface pi&lt;/code&gt;, then &lt;code&gt;/loopx &amp;lt;task&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ZCode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx slash-commands --install --surface zcode&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek Harness (dsh)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loopx[deepseek-harness]&lt;/code&gt; + &lt;code&gt;loopx turn run-once&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor / shell / custom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;loopx connect&lt;/code&gt; + call LoopX from your runner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All integrations share the same control-plane state. Switching harnesses doesn't lose Goal state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Capability Paths
&lt;/h2&gt;

&lt;p&gt;LoopX wraps generic control-plane primitives into capability paths with concrete owned outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need to...&lt;/th&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Turn a public issue into a reviewable, evidence-backed change&lt;/td&gt;
&lt;td&gt;Issue Fix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx issue-fix&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qualify the exact final diff before delivery&lt;/td&gt;
&lt;td&gt;Change Quality&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx capability show change-quality-qualification&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explore uncertain research without losing hypotheses and findings&lt;/td&gt;
&lt;td&gt;Explore&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx capability show explore&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebase decisions on current evidence and verified outcomes&lt;/td&gt;
&lt;td&gt;Decision Context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx capability show decision-context&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Produce scheduled or triggered reports with receipts&lt;/td&gt;
&lt;td&gt;Periodic Report&lt;/td&gt;
&lt;td&gt;&lt;code&gt;loopx capability show periodic-report&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;List all installed capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loopx capability list &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Auto Research: Parallel Multi-Agent Work
&lt;/h3&gt;

&lt;p&gt;LoopX ships a built-in KNN demo that shows proposer / executor / evaluator/promoter agents working in parallel:&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;# Browse available presets&lt;/span&gt;
loopx preset list
loopx preset show daily-triage

&lt;span class="c"&gt;# See the Auto Research demo path&lt;/span&gt;
&lt;span class="c"&gt;# demo/auto_research/README.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Evidence of Long-Running Work
&lt;/h2&gt;

&lt;p&gt;LoopX is explicit: these are not one-turn demos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenViking open-source contribution arc (200+ elapsed hours)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The author manages their own contributions to the OpenViking repository using LoopX, spanning 200+ hours of elapsed project time across many bounded turns. The Issue Fix capability maintains rolling repository context, revision-stamped fix knowledge, and reviewer-facing preferences across that window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto ML Experiment (200+ elapsed hours)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A redacted owner-run showcase: in a 200+ hour elapsed window, hypotheses, matched evidence, invalid lineages, running replicates, and promote/stop gates stay visible in one graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independent user cases from the community:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One user: 13h+ C++ precision improvement task, stayed aligned across multiple stages, triggered public research, improved final precision&lt;/li&gt;
&lt;li&gt;One user: 4-day unattended run, ongoing useful work, periodic report surface&lt;/li&gt;
&lt;li&gt;One user: 7 merged PRs, Engine refactor, reported 1B+ token scale&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Dashboard and Observability
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch local dashboard (PWA mode)&lt;/span&gt;
loopx dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or build the experimental Tauri desktop window:&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="nb"&gt;cd &lt;/span&gt;apps/desktop/loopx-control-plane
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Session dash for tracking multi-goal fleet progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loopx dash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Daily state inspection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loopx status
loopx &lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;--goal-id&lt;/span&gt; your-project-goal
loopx quota should-run &lt;span class="nt"&gt;--goal-id&lt;/span&gt; your-project-goal
loopx diagnose
loopx review-packet      &lt;span class="c"&gt;# compact owner-facing view: decisions, evidence, gates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/huangruiteng/loopx" rel="noopener noreferrer"&gt;huangruiteng/loopx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/loopx/" rel="noopener noreferrer"&gt;loopx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://huangruiteng.github.io/loopx/" rel="noopener noreferrer"&gt;huangruiteng.github.io/loopx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;User manual (Feishu)&lt;/strong&gt;: &lt;a href="https://my.feishu.cn/wiki/CaL5wMk9ui17ngkWzeUcMlAYnZg" rel="noopener noreferrer"&gt;CaL5wMk9ui17ngkWzeUcMlAYnZg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Developer Book&lt;/strong&gt;: &lt;a href="https://huangruiteng.github.io/loopx/docs/book/" rel="noopener noreferrer"&gt;Chinese&lt;/a&gt; · &lt;a href="https://huangruiteng.github.io/loopx/docs/book/en/" rel="noopener noreferrer"&gt;English&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt;: &lt;a href="https://discord.gg/XmGgQyCFZd" rel="noopener noreferrer"&gt;discord.gg/XmGgQyCFZd&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;LoopX represents a clear-eyed engineering judgment: &lt;strong&gt;the agent problem isn't just "can it do this in one turn" — it's "after multiple days, multiple runs, and multiple harnesses, can you still tell where the work is, what changed, and who does what next?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three things worth noting:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Control plane" vs. "agent framework" is a deliberate positioning.&lt;/strong&gt; LoopX doesn't execute work — it manages state. This is the same design thinking as Kubernetes being an "orchestration plane" rather than a "container runtime." It means you can swap agents (Codex today, Claude Code tomorrow) without losing Goal state or evidence history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Five Questions framework is an actionable design.&lt;/strong&gt; Many systems claim to "support long-horizon agents," but "long-horizon" stays vague. LoopX breaks it down into five concrete questions: objective / next step / human judgment gates / evidence changes / whether to continue. Each maps to a piece of code, a CLI command, and a verifiable contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't do is also a design choice.&lt;/strong&gt; LoopX explicitly states: it's not an autonomous production controller, it doesn't grant credentials, it doesn't approve destructive operations, it won't mark an unverified run as successful. At a time when agent capability boundaries are still unclear, this kind of explicit self-limitation is engineering honesty.&lt;/p&gt;

&lt;p&gt;If you have multi-day AI agent work — machine learning experiments, large code refactors, sustained open-source contributions — LoopX offers the most complete open-source control-plane solution currently available.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Explore &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills, each validated against real enterprise workflows. No hype, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;personal site&lt;/a&gt; for more insights and interesting products.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>claude</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Code Agent Anatomy (16): AgentTeams — Why Is One Agent Not Enough?</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Tue, 01 Sep 2026 01:54:28 +0000</pubDate>
      <link>https://dev.to/wonderlab/code-agent-anatomy-16-agentteams-why-is-one-agent-not-enough-108m</link>
      <guid>https://dev.to/wonderlab/code-agent-anatomy-16-agentteams-why-is-one-agent-not-enough-108m</guid>
      <description>&lt;h2&gt;
  
  
  Starting with a Real Dilemma
&lt;/h2&gt;

&lt;p&gt;Imagine you give a code agent this task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Help me refactor this 5000-line Python project entirely into async style, fill in the unit tests, then run a CI verification."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What happens when a single agent receives this?&lt;/p&gt;

&lt;p&gt;It will modify files one by one, then go write tests, then run CI after the tests are done. The whole process runs serially, with frequent file reads, file writes, and model calls along the way — just iterating over all the files might consume tens of thousands of tokens, filling the context window to the brim.&lt;/p&gt;

&lt;p&gt;What's more problematic: refactoring files and writing tests are two things that could actually happen simultaneously. But a single agent can't parallelize — it has only one ReAct loop and can only do one thing at a time.&lt;/p&gt;

&lt;p&gt;This is the problem AgentTeams is trying to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusions First
&lt;/h2&gt;

&lt;p&gt;AgentTeams is an &lt;strong&gt;experimental multi-agent collaboration system&lt;/strong&gt; in MyCodeAgent. Its core design philosophy is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;AgentTeams' Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Task too long, single agent context can't hold it&lt;/td&gt;
&lt;td&gt;Break the task across multiple agents, each agent only sees its own portion of context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need to process multiple subtasks in parallel&lt;/td&gt;
&lt;td&gt;Multiple agents run simultaneously, independent of each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results need to be aggregated&lt;/td&gt;
&lt;td&gt;Coordinator agent collects output from each sub-agent and merges into a final answer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It provides six tools: &lt;code&gt;TeamCreate&lt;/code&gt;, &lt;code&gt;SendMessage&lt;/code&gt;, &lt;code&gt;TeamStatus&lt;/code&gt;, &lt;code&gt;TeamDelete&lt;/code&gt;, &lt;code&gt;TeamFanout&lt;/code&gt;, &lt;code&gt;TeamCollect&lt;/code&gt;, and supports three execution modes: &lt;code&gt;in-process&lt;/code&gt; (in-process), &lt;code&gt;tmux&lt;/code&gt; (dedicated terminal), &lt;code&gt;auto&lt;/code&gt; (auto-select).&lt;/p&gt;

&lt;p&gt;But this system was ultimately &lt;strong&gt;removed from the stable release&lt;/strong&gt;. Understanding why it was designed and why it was removed is more valuable than simply learning "how to use it."&lt;/p&gt;




&lt;h2&gt;
  
  
  1. What Makes Multi-Agent Systems Really Hard
&lt;/h2&gt;

&lt;p&gt;Many people, hearing "multi-agent" for the first time, think it sounds simple: isn't it just running a few more model instances?&lt;/p&gt;

&lt;p&gt;In reality, once you have multiple agents, you immediately face a series of problems that simply don't exist in single-agent systems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: How do you provide context?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each agent has its own context window. The primary agent can't just copy the entire conversation history for each sub-agent — that would cause context explosion. So you need to decide: how much context does each sub-agent get? Which parts? In what format?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: How do you merge results?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sub-agent A modified &lt;code&gt;utils.py&lt;/code&gt;, sub-agent B also modified &lt;code&gt;utils.py&lt;/code&gt; — they're unaware of each other's existence. How do you merge after both are done? Who decides which version is more accurate?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3: How do you handle failures?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single agent failure means the current task fails — just retry. But if 10 sub-agents are running in parallel and 3 fail, what then? Re-run all of them? Only re-run the failed ones? Do the failed parts affect the results of other agents?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 4: Who coordinates?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a primary agent is responsible for coordinating other agents, the primary agent itself becomes a complex state machine — it needs to know which subtasks are complete, which are still running, which failed, and needs to merge all results at the right moment. This coordination logic is itself enormously complex.&lt;/p&gt;

&lt;p&gt;AgentTeams' design is an attempt to answer these four questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Basic Model of AgentTeams
&lt;/h2&gt;

&lt;p&gt;AgentTeams abstracts multi-agent collaboration into the concept of a &lt;strong&gt;Team&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;Primary agent (coordinator)
    │
    ├── TeamCreate: create a team
    │
    ├── TeamFanout: distribute tasks to multiple members
    │       │
    │       ├── Member agent 1 ← runs independently, has its own context
    │       ├── Member agent 2 ← runs independently, has its own context
    │       └── Member agent 3 ← runs independently, has its own context
    │
    ├── TeamCollect: wait and collect all members' results
    │
    └── TeamDelete: clean up the team after task completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each team has a &lt;strong&gt;coordinator&lt;/strong&gt; (usually the primary agent that created the team) and several &lt;strong&gt;members&lt;/strong&gt; (agents executing subtasks). Members are mutually independent and don't communicate directly — all messages are relayed through the coordinator.&lt;/p&gt;

&lt;p&gt;This design solves the "how to merge results" problem: the coordinator is the only role with a global view, and it decides how to merge.&lt;/p&gt;

&lt;p&gt;Member agent execution modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;in-process&lt;/code&gt;: runs in the same Python process, lightweight, but can't truly parallelize (limited by the GIL)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tmux&lt;/code&gt;: runs in a dedicated tmux pane, truly parallel, but with higher startup overhead&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auto&lt;/code&gt;: auto-select based on environment&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. A Concrete Use Case
&lt;/h2&gt;

&lt;p&gt;Using the earlier "refactor + test" example, the AgentTeams workflow looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary agent:
1. Call TeamCreate, create a team
2. Scan the project, divide 50 Python files into 5 groups of 10 each
3. Call TeamFanout, distribute 5 "refactor these 10 files" tasks to 5 member agents
4. Wait, while also starting to write the integration test framework code
5. Call TeamCollect, collect the refactoring results from all 5 members
6. Merge results, resolve conflicts
7. Run CI verification
8. Call TeamDelete, clean up the team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 member agents work simultaneously, each only handling 10 files, so the context window isn't filled by the entire project. The primary agent can do other work while waiting.&lt;/p&gt;

&lt;p&gt;This flow is logically sound. The problems arise at the implementation level — which is what article 19 will cover.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Difference Between Sub-Agents and Team Members
&lt;/h2&gt;

&lt;p&gt;Having read this far, you might ask: how is this different from the Task sub-agent discussed in article 08?&lt;/p&gt;

&lt;p&gt;This is an excellent question, and the answer is critical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task sub-agent (stable feature)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The primary agent delegates a &lt;strong&gt;specific task&lt;/strong&gt; to a sub-agent&lt;/li&gt;
&lt;li&gt;The sub-agent completes it and returns the &lt;strong&gt;result&lt;/strong&gt; to the primary agent&lt;/li&gt;
&lt;li&gt;The entire process is &lt;strong&gt;synchronous&lt;/strong&gt;: the primary agent waits while the sub-agent finishes, then continues&lt;/li&gt;
&lt;li&gt;Suited for "I'm not good at this, let an expert handle it" scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AgentTeams members (experimental feature)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The primary agent distributes &lt;strong&gt;multiple instances of the same type of task&lt;/strong&gt; to multiple members&lt;/li&gt;
&lt;li&gt;Members &lt;strong&gt;run in parallel&lt;/strong&gt;, the primary agent doesn't wait for them — it can continue doing other things&lt;/li&gt;
&lt;li&gt;TeamCollect eventually collects all results together&lt;/li&gt;
&lt;li&gt;Suited for "the same thing needs to be done many times, and can be done in parallel" scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simply put: Task is "you go do it, I'll wait"; AgentTeams is "you all go do it separately, I'll do something else first, tell me when you're done."&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Why This Direction Matters
&lt;/h2&gt;

&lt;p&gt;AgentTeams was ultimately removed, but the direction it represents — &lt;strong&gt;parallel multi-agent collaboration&lt;/strong&gt; — is a real and important need in the AI agent field.&lt;/p&gt;

&lt;p&gt;Currently almost all mature agent frameworks (LangGraph, AutoGen, CrewAI, etc.) are trying to solve similar problems, but with different approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some use &lt;strong&gt;graphs (DAGs)&lt;/strong&gt; to describe dependencies between agents&lt;/li&gt;
&lt;li&gt;Some use &lt;strong&gt;role-playing&lt;/strong&gt; to let different agents play different roles&lt;/li&gt;
&lt;li&gt;Some use &lt;strong&gt;shared blackboards&lt;/strong&gt; to let agents communicate through a central state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgentTeams chose the &lt;strong&gt;team + coordinator&lt;/strong&gt; model, which is the design most closely resembling human team collaboration.&lt;/p&gt;

&lt;p&gt;Having understood the design motivation, the next article looks at how messages are passed between agents — these are the blood vessels of the entire collaboration system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Highlights
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Context Isolation Is the First Principle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each member agent has its own context and won't be "distracted" by seeing too much irrelevant information. This solves the context pollution problem when a single agent handles long tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Coordinator Has the Only Global View&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Members don't communicate directly; all coordination goes through the primary agent. This simplifies state management — you don't need to handle the complex situation of agent A and agent B messaging each other and waiting on each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Three Execution Modes for Three Scenarios&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;in-process&lt;/code&gt; is lightweight and fast, suited for development and testing; &lt;code&gt;tmux&lt;/code&gt; is truly parallel, suited for production use; &lt;code&gt;auto&lt;/code&gt; lets the framework decide, lowering the barrier to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;AgentTeams' Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Why do we need multiple agents?&lt;/td&gt;
&lt;td&gt;Single agent context is limited and can't parallelize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is the core abstraction?&lt;/td&gt;
&lt;td&gt;Team = coordinator + multiple members&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do members run?&lt;/td&gt;
&lt;td&gt;Three modes: in-process / tmux / auto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difference from Task sub-agent?&lt;/td&gt;
&lt;td&gt;Task is synchronous delegation, AgentTeams is parallel distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final fate?&lt;/td&gt;
&lt;td&gt;Experimental, removed from stable release (reason in article 19)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  About the Source Code for This Series
&lt;/h2&gt;

&lt;p&gt;All analysis in this series is based on the open-source project &lt;a href="https://github.com/chendongqi/MyCodeAgent" rel="noopener noreferrer"&gt;MyCodeAgent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The AgentTeams implementation code has been removed from the stable release and is preserved in Git history (commit &lt;code&gt;f497b172&lt;/code&gt;). You can view design traces via: &lt;code&gt;docs/research-archive.md&lt;/code&gt;, &lt;code&gt;docs/plans/2026-07-12-lean-runtime/tasks/M2-03-remove-agent-teams.md&lt;/code&gt;.&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/chendongqi/MyCodeAgent
&lt;span class="nb"&gt;cd &lt;/span&gt;MyCodeAgent
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# Fill in your LLM API key&lt;/span&gt;
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Visit &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated AI Agent and skills marketplace where every piece of content is validated against real enterprise workflows. No hype, only things that actually work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For more practical insights and interesting products, visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>multiagent</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>Code Agent Anatomy (15): Harness Design Part 5 — Observability</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Mon, 31 Aug 2026 07:30:59 +0000</pubDate>
      <link>https://dev.to/wonderlab/code-agent-anatomy-15-harness-design-part-5-observability-1o43</link>
      <guid>https://dev.to/wonderlab/code-agent-anatomy-15-harness-design-part-5-observability-1o43</guid>
      <description>&lt;h2&gt;
  
  
  The Last Piece of the Puzzle
&lt;/h2&gt;

&lt;p&gt;The previous four articles dissected the agent's control flow, context engineering, tool pipeline, and fault recovery. All of these mechanisms share a common problem: &lt;strong&gt;when something goes wrong, how do you know what happened?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An agent running in the background — which tool the model chose, how many times history was compacted, which permission rule blocked a call — these all happen inside the code but are invisible by default. This article dissects MyCodeAgent's observability system, examining how it turns internal behavior into queryable records.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusions First
&lt;/h2&gt;

&lt;p&gt;The observability system consists of three layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Emission layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RuntimeRunner._emit()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unified event entry point — all observable facts in the loop originate here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CompositeRuntimeEventSink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delivers one event to multiple sinks simultaneously, mutually isolated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TraceRuntimeEventSink&lt;/code&gt; + &lt;code&gt;TranscriptRuntimeEventSink&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Write to JSONL diagnostic logs and append-only fact transcript respectively&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One event is emitted, two paths are taken simultaneously — diagnostics and recovery each get what they need.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Unified Emission Point: All Events Originate from One Place
&lt;/h2&gt;

&lt;p&gt;Every "thing worth recording" in the loop is emitted through the same method:&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="c1"&gt;# runtime/loop.py  RuntimeRunner
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_emit&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;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_emit_runtime_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;run_id&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="nf"&gt;_get_transcript_run_id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&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;&lt;code&gt;_emit&lt;/code&gt; accepts three elements: &lt;strong&gt;event type&lt;/strong&gt;, &lt;strong&gt;payload&lt;/strong&gt;, &lt;strong&gt;step number&lt;/strong&gt; — constructing a &lt;code&gt;RuntimeEvent&lt;/code&gt; object and handing it to the sink router.&lt;/p&gt;

&lt;p&gt;Call sites are scattered throughout the loop, but there's only one entry point. This means: adding a new sink (for example, sending events to Prometheus) only requires modifying the &lt;code&gt;CompositeRuntimeEventSink&lt;/code&gt; constructor — the loop itself doesn't need to change.&lt;/p&gt;

&lt;p&gt;Emission points cover all key moments in 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;message          → message written to history (user/assistant/tool roles)
state_transition → state transitions (USER_INPUT / TOOLS_EXECUTED / MODEL_RECOVERY_RETRY ...)
tool_lifecycle   → tool four phases (requested / started / completed / failed)
checkpoint       → context compaction checkpoint
terminal         → loop termination (completed / max_steps / token_budget ...)
prompt_assembly  → fingerprints of each prompt layer per step
tool_schema      → hash of tool schema (detects tool list changes)
model_output     → raw model response (including token usage)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Dual Sink: Diagnostics and Persistence Each Serve Their Own Purpose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# runtime/events.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_runtime_event_sink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace_logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CompositeRuntimeEventSink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CompositeRuntimeEventSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TraceRuntimeEventSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace_logger&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;TranscriptRuntimeEventSink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recorder&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;&lt;code&gt;CompositeRuntimeEventSink&lt;/code&gt; delivers the same event to two sinks:&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;def&lt;/span&gt; &lt;span class="nf"&gt;emit&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RuntimeEvent&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="ow"&gt;in&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;sinks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&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="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Sink failure only logs a warning, does not affect loop state
&lt;/span&gt;            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Runtime event sink failed for %s: %s&lt;/span&gt;&lt;span class="sh"&gt;"&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="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isolation is key: a Transcript sink write failure doesn't affect the Trace sink, nor does it affect the loop continuing to run. &lt;strong&gt;Failures in observability infrastructure must not stop the agent.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace Sink: Diagnostic Logs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;TraceRuntimeEventSink&lt;/code&gt; translates events into diagnostic format and forwards them to &lt;code&gt;TraceLogger&lt;/code&gt;, writing to &lt;code&gt;memory/traces/trace-{session}.jsonl&lt;/code&gt;. This is the log for &lt;strong&gt;development and debugging&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each line is a JSON object with timestamp, session_id, step, event name, and payload&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool_lifecycle&lt;/code&gt; events are additionally split into &lt;code&gt;tool_call&lt;/code&gt; (when called) and &lt;code&gt;tool_result&lt;/code&gt; (when completed), allowing HTML reports to display them separately&lt;/li&gt;
&lt;li&gt;After the file is written, an HTML report can be generated for direct human reading&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transcript Sink: Recoverable Facts
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;TranscriptRuntimeEventSink&lt;/code&gt; only handles five types of "recoverable fact" events (message/state_transition/tool_lifecycle/checkpoint/terminal), writing to &lt;code&gt;memory/transcripts/transcript-{session}.jsonl&lt;/code&gt;. This is the log for &lt;strong&gt;crash recovery&lt;/strong&gt; (see article 14) — it doesn't record debug details, only the minimal set of facts needed to reconstruct state.&lt;/p&gt;

&lt;p&gt;The same event is emitted, two paths each take what they need: Trace takes all details, Transcript takes only the facts needed for state recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. TraceLogger: JSONL Streaming Write
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# extensions/tracing/logger.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_event&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;event_obj&lt;/span&gt; &lt;span class="o"&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;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_utc_now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;session_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;step&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&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;_sanitizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# sanitize first
&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;_current_run_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# keep in memory for this run
&lt;/span&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_write_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# write to disk immediately
&lt;/span&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_update_stats&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;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# update statistics
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_write_line&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;event_obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&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="k"&gt;with&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;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                             &lt;span class="c1"&gt;# write lock to prevent concurrent corruption
&lt;/span&gt;        &lt;span class="k"&gt;if&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;_file_handle&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;_file_handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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;_file_handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;            &lt;span class="c1"&gt;# flush immediately on every write
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;log_event&lt;/code&gt; flushes immediately — same strategy as Transcript, aimed at shrinking the crash window: if the process crashes between two events, already-flushed events won't be lost.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_current_run_events&lt;/code&gt; keeps all events from the current run in memory, consumed when generating the HTML report (at &lt;code&gt;finalize()&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Prompt Fingerprint: Detecting Prompt Drift
&lt;/h2&gt;

&lt;p&gt;At the start of each step, &lt;code&gt;trace_model_request_state()&lt;/code&gt; computes and emits fingerprints for each layer of the prompt:&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="c1"&gt;# runtime/events.py  trace_model_request_state()
&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&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;constitution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constitution_fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_contracts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_contracts_fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project_rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_rules_fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runtime_signals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runtime_signals_fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_assembly&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="bp"&gt;...&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changed_layers&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;key&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&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="n"&gt;value&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;&lt;code&gt;changed_layers&lt;/code&gt; lists the &lt;strong&gt;layers that changed&lt;/strong&gt; in this step compared to the previous step. For example, if Skills were updated, the &lt;code&gt;tool_contracts&lt;/code&gt; layer fingerprint changes, and the trace will show &lt;code&gt;"changed_layers": ["tool_contracts"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This solves a debugging pain point: model behavior suddenly changes at a certain step, but it's unclear why. By comparing fingerprints, you can precisely identify which prompt layer drifted.&lt;/p&gt;

&lt;p&gt;The same treatment applies to tool schemas:&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;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_schema&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. TraceSanitizer: Sanitization Makes Logs Shareable
&lt;/h2&gt;

&lt;p&gt;Trace logs may contain sensitive data. &lt;code&gt;TraceSanitizer&lt;/code&gt; performs a scan before each event is written to disk:&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="c1"&gt;# extensions/tracing/sanitizer.py
&lt;/span&gt;&lt;span class="n"&gt;SENSITIVE_KEYS&lt;/span&gt; &lt;span class="o"&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;api_key&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;token&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;password&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;authorization&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;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;PATTERNS&lt;/span&gt; &lt;span class="o"&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;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-[a-zA-Z0-9]{20,}&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;sk-***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;# OpenAI key
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer\s+[a-zA-Z0-9._+/=-]{20,}&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;Bearer ***&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_sanitize_dict&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&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;SENSITIVE_KEYS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# key hit: replace value directly
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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="nf"&gt;_sanitize_string&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="c1"&gt;# sanitize usernames in paths
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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="nf"&gt;sanitize&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="c1"&gt;# recursively handle nested structures
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two types of coverage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key blacklist&lt;/strong&gt;: any key matching &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, etc. gets its value replaced with &lt;code&gt;***&lt;/code&gt; regardless of the value&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value regex&lt;/strong&gt;: patterns matching &lt;code&gt;sk-...&lt;/code&gt;, &lt;code&gt;Bearer ...&lt;/code&gt;, etc. cover cases where the key name is innocuous but the value is sensitive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usernames in paths (&lt;code&gt;/home/alice/&lt;/code&gt;, &lt;code&gt;/Users/alice/&lt;/code&gt;) are also replaced with &lt;code&gt;***&lt;/code&gt;, preventing developers from accidentally leaking their local username.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TRACE_SANITIZE=false&lt;/code&gt; disables this; it's enabled by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Highlights
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Event-Driven, Not Log Instrumentation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Observable points in the loop emit semantic events (&lt;code&gt;state_transition&lt;/code&gt;, &lt;code&gt;tool_lifecycle&lt;/code&gt;) rather than &lt;code&gt;print&lt;/code&gt; or &lt;code&gt;logger.info&lt;/code&gt;. Events have structured payloads that downstream can process with code; logs can only be read by human eyes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sink Failure Doesn't Affect the Loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CompositeRuntimeEventSink&lt;/code&gt; calls each sink inside try/except; failures only log a warning. Observability infrastructure is a "side channel" for the agent, not the main channel. Issues with the main channel are what matter — side channel failures shouldn't bring down the main channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Two JSONL Files, Two Perspectives&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Trace JSONL is the diagnostic perspective: what happened, at which step, when — aimed at developer debugging. The Transcript JSONL is the recovery perspective: which facts need to be retained to reconstruct state — aimed at crash recovery. The same event stream, two purposes, written to disk separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Fingerprint Rather Than Diff&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prompt change detection uses fingerprints (sha256) rather than storing full text for diffing: saves storage, comparison is O(1), and in the trace you just look at &lt;code&gt;changed_layers&lt;/code&gt; to know which layer changed — no need to open two files and compare manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design Choice&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Engineering Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Emission point&lt;/td&gt;
&lt;td&gt;Unified &lt;code&gt;_emit()&lt;/code&gt;, not scattered&lt;/td&gt;
&lt;td&gt;Adding a sink doesn't require modifying the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;CompositeRuntimeEventSink&lt;/td&gt;
&lt;td&gt;Multiple sinks mutually isolated; single failure doesn't affect others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnostic log&lt;/td&gt;
&lt;td&gt;TraceLogger JSONL + flush&lt;/td&gt;
&lt;td&gt;Streaming write, small crash window, can generate HTML report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery log&lt;/td&gt;
&lt;td&gt;TranscriptRuntimeEventSink&lt;/td&gt;
&lt;td&gt;Writes only minimal fact set, not mixed with debug details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift detection&lt;/td&gt;
&lt;td&gt;Prompt fingerprint&lt;/td&gt;
&lt;td&gt;O(1) comparison, precisely locates which prompt layer changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanitization&lt;/td&gt;
&lt;td&gt;TraceSanitizer key blacklist + value regex&lt;/td&gt;
&lt;td&gt;Logs are shareable without fear of leaking API keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;This completes all of Part 4 Harness Engineering. A recap of these five articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;11&lt;/strong&gt;: Control flow — single loop, immutable state machine, completion gate, exhaustive termination paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12&lt;/strong&gt;: Context engineering — History and ModelView separation, read-time projection, dual-trigger compaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13&lt;/strong&gt;: Tool pipeline — concurrent grouping, four-checkpoint execution, two-layer byte budget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14&lt;/strong&gt;: Fault recovery — classified retry, Transcript fact log, UncertainAction explicit modeling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15&lt;/strong&gt;: Observability — event-driven, dual-sink routing, fingerprint drift detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These five mechanisms together form the "skeleton" of an agent framework — determining how long it can run, how much it can self-heal after errors, how much it can recover after a crash, and how deep you can investigate when something goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Source Code for This Series
&lt;/h2&gt;

&lt;p&gt;All analysis in this series is based on the open-source project &lt;a href="https://github.com/chendongqi/MyCodeAgent" rel="noopener noreferrer"&gt;MyCodeAgent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source code has been annotated at key locations following the order in which topics are covered in this series — you can read the articles alongside the code, or clone the repo and run, modify, and extend it to build your own agent.&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/chendongqi/MyCodeAgent
&lt;span class="nb"&gt;cd &lt;/span&gt;MyCodeAgent
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# Fill in your LLM API key&lt;/span&gt;
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Visit &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated AI Agent and skills marketplace where every piece of content is validated against real enterprise workflows. No hype, only things that actually work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For more practical insights and interesting products, visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>harness</category>
      <category>tracing</category>
    </item>
    <item>
      <title>Open Source Project #167: Cumora — Team Chat Where AI Agents Are First-Class Teammates</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Mon, 31 Aug 2026 07:29:47 +0000</pubDate>
      <link>https://dev.to/wonderlab/open-source-project-167-cumora-team-chat-where-ai-agents-are-first-class-teammates-3jno</link>
      <guid>https://dev.to/wonderlab/open-source-project-167-cumora-team-chat-where-ai-agents-are-first-class-teammates-3jno</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Where agent teams gather."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is &lt;strong&gt;article #167&lt;/strong&gt; in the "One Open Source Project a Day" series. Today's project is &lt;strong&gt;Cumora&lt;/strong&gt; — a cross-platform collaboration tool where AI agents and humans share the same chat room, 3,248 Stars, MIT license, authored by yetone.&lt;/p&gt;

&lt;p&gt;Cumora addresses a real gap: most AI agent tools are either question-answer tools ("you ask, it answers") or fully autonomous agents running in isolation — but real team work requires humans and agents to share context, know what each other is doing, and coordinate. Cumora's answer: treat agents as genuine team members with the same standing as humans. Same chat history, same Kanban board, same calendar. Agents have their own memory, personas, real email addresses, can claim tasks proactively, and coordinate with other agents without stepping on each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You'll Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Two agent runtime modes: the difference between Cumora Cloud and BYOA (Bring Your Own Agent)&lt;/li&gt;
&lt;li&gt;Local engine options for BYOA: Claude Code / Codex / OpenCode and more&lt;/li&gt;
&lt;li&gt;Three-layer anti-collision coordination for multi-agent rooms&lt;/li&gt;
&lt;li&gt;Technical architecture: React + Express + Postgres + Redis + Kubernetes&lt;/li&gt;
&lt;li&gt;Local development setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Familiarity with LLMs and AI agents at a basic level&lt;/li&gt;
&lt;li&gt;Basic Node.js/TypeScript development experience&lt;/li&gt;
&lt;li&gt;Some background with Electron or cross-platform desktop apps is helpful&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;Cumora's core premise: agents are not tools, they're teammates. So it's not a "plug in an AI assistant" product — its agents share the same first-class status as humans: same group chats, same DMs, same Kanban cards, same calendar. Each agent has a name, avatar, memory, and will speak up unprompted, claim tasks, and even has a real personal email address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Author / Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Author&lt;/strong&gt;: yetone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://cumora.ai" rel="noopener noreferrer"&gt;cumora.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web App&lt;/strong&gt;: &lt;a href="https://app.cumora.ai" rel="noopener noreferrer"&gt;app.cumora.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS beta&lt;/strong&gt;: &lt;a href="https://testflight.apple.com/join/GtRKgPpS" rel="noopener noreferrer"&gt;TestFlight&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Stats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ GitHub Stars: &lt;strong&gt;3,248+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🍴 Forks: 397+&lt;/li&gt;
&lt;li&gt;📄 License: MIT&lt;/li&gt;
&lt;li&gt;📅 Created: 2026-08-17&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Two Agent Runtime Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cumora Cloud (Managed)
&lt;/h3&gt;

&lt;p&gt;Each agent runs in its own Kubernetes pod, with the brain being a multi-hop tool-calling loop on the OpenAI Responses API. Available tools include bash commands, file operations, browser, email, memory, and skills.&lt;/p&gt;

&lt;p&gt;Advantage: zero setup, always online. Drawback: uses Cumora's OpenAI quota; you can't bring your own Claude Code or local agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  BYOA — Bring Your Own Agent
&lt;/h3&gt;

&lt;p&gt;Run the agent's brain on your own machine (laptop or VPS):&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;# Install and run the agent daemon&lt;/span&gt;
npx cumora agent computer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported local engines for BYOA:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Agent Engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code (Anthropic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI Codex CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grok&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grok Build (xAI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cursor-agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cursor Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mario Zechner's Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key design&lt;/strong&gt;: the I/O surface (the &lt;code&gt;cumora&lt;/code&gt; CLI protocol) is fully decoupled from the brain. Commands like &lt;code&gt;cumora reply&lt;/code&gt;, &lt;code&gt;cumora dm&lt;/code&gt;, &lt;code&gt;cumora memory&lt;/code&gt;, &lt;code&gt;cumora workspace&lt;/code&gt;, &lt;code&gt;cumora card&lt;/code&gt; are thin shims that POST their argv to &lt;code&gt;/runtime/cli&lt;/code&gt;. The transport layer (Server-Sent Events + REST) is engine-agnostic. BYOA only swaps the brain and the host — it reuses everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: the server never holds the user's provider API keys. Your Claude Code / Codex credentials stay on your machine.&lt;/p&gt;

&lt;p&gt;A single daemon can host multiple independent agents, each with their own isolated home directory, memory, skills, and notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Computer: A Unified Mental Model
&lt;/h3&gt;

&lt;p&gt;Cumora introduces "Computer" as a first-class concept — whether cloud or local, an agent always runs on some Computer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Computers
──────────────────────────────
☁  Cumora Cloud      ● online
   engine: managed · 4 agents

💻 MacBook Pro        ● online
   Claude Code · 3 agents
   "Iris is thinking…"

🖥  prod-vps-01        ○ offline
   Codex · 2 agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating an agent means "pick which Computer it lives on." If a Computer goes offline, its agents show as &lt;em&gt;sleeping&lt;/em&gt; rather than broken. There's no special "BYOA agent" type — just agents on different Computers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Agent Coordination: Three Defense Layers Against Collisions
&lt;/h2&gt;

&lt;p&gt;This is the deepest engineering in Cumora. When multiple agents share a chat room, the problem is simple: they can all wake up simultaneously, read the same messages, and each decide to respond — so the same thing gets done twice.&lt;/p&gt;

&lt;p&gt;There are two failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Race collisions&lt;/strong&gt;: two agents simultaneously INSERT a message, both posting "3" in a counting game&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brain misjudgment&lt;/strong&gt;: the agent's view is correct (it sees the latest messages) but the model still makes the wrong decision&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These require different fixes: &lt;strong&gt;code mechanisms&lt;/strong&gt; for collisions, &lt;strong&gt;prompt engineering&lt;/strong&gt; for misjudgments. Never use a prompt to fix a race condition, and never add a code mechanism when the model is making a clear decision in front of correct state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defense Layer 1: Freshness Gate
&lt;/h3&gt;

&lt;p&gt;Before an agent can submit a reply, the server checks: "Is the last message you saw actually the latest?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent reads messages → decides to reply → submits reply
                                               ↓
                                     Server checks:
                              seen_cursor &amp;gt;= latest_msg_id?
                                    yes → allow
                                    no  → HOLD (push newer messages to agent to re-decide)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A HELD reply isn't discarded — the agent sees the newer messages and re-decides whether it still needs to respond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defense Layer 2: Atomic Task Claiming
&lt;/h3&gt;

&lt;p&gt;Kanban card claiming is an atomic operation. Two agents cannot "simultaneously claim" the same card — the server uses a database-level lock to ensure only one agent can successfully claim a task. Others receive "already claimed" and do not retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defense Layer 3: Small-Brain Triage Gate
&lt;/h3&gt;

&lt;p&gt;When an agent is woken up, a lightweight cheap model first decides: "Does this message actually need a response from me?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New message arrives
    ↓
Small brain (cheap model): is this directed at me?
    yes → wake the big brain (big model) for full processing
    no  → ignore, no big model tokens consumed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces unnecessary token usage and lowers the probability of multiple agents waking simultaneously and making conflicting decisions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CI includes a dedicated guard: &lt;code&gt;npm run guard:big-brain&lt;/code&gt; — verifies that only agent turns may call the big model, catching accidental usage.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Technical Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Electron / PWA / iOS / Android         ┌─────────────────┐
 ┌──────────────────┐   HTTP / WS       │   App workers   │──▶ OpenAI (Responses API)
 │    React UI      │ ◀───────────────▶ │  Express + ws   │──▶ Resend (email out)
 └──────────────────┘                   │    (any N)      │──▶ APNs / FCM (push)
                                        └───┬────────┬────┘
 Cloudflare Workers                         │        │ kubectl
 ┌─────────────────┐   webhooks / R2   ┌────▼───┐ ┌──▼──────────────┐
 │ email-gate      │ ────────────────▶ │Postgres│ │ Agent pods (K8s)│
 │ r2-gate (CDN)   │                   │ Redis  │ │ or BYOA daemons │
 └─────────────────┘                   └────────┘ └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React 18 + Vite + TypeScript + Tailwind&lt;/td&gt;
&lt;td&gt;Pure UI; desktop/mobile/web/admin share the same component tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Express + ws + Drizzle ORM&lt;/td&gt;
&lt;td&gt;Stateless Node service, horizontally scalable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres (source of truth) + Redis (pub/sub fan-out + presence)&lt;/td&gt;
&lt;td&gt;Multiple instances stay in sync via Redis bus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent runtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes pods (cloud) / BYOA daemon (local)&lt;/td&gt;
&lt;td&gt;Two paths, unified &lt;code&gt;cumora&lt;/code&gt; CLI protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;Inbound/outbound email gateway, signed CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Repository layout:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;React renderer (desktop/mobile/web/admin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API + WebSocket + agent runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;electron/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Desktop shell (auto-update)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ios/&lt;/code&gt;, &lt;code&gt;android/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Capacitor native shells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-cli/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;npm package &lt;code&gt;cumora&lt;/code&gt; — the BYOA daemon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent-fuse/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go FUSE driver mounting cloud agent workspaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;workers/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare Workers (email gate, CDN)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;benchmarks/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-agent coordination benchmarks (chain/counting/werewolf/kanban)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Local Development
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Prerequisites: local Postgres and Redis&lt;/span&gt;
createdb &lt;span class="nt"&gt;-h&lt;/span&gt; localhost cumora
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...

npm run setup          &lt;span class="c"&gt;# install dependencies&lt;/span&gt;
npm run dev:all        &lt;span class="c"&gt;# Vite renderer on :5180 + API server on :5181&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:5180" rel="noopener noreferrer"&gt;http://localhost:5180&lt;/a&gt; (PWA mode) or run &lt;code&gt;npm run electron:dev&lt;/code&gt; for the desktop window.&lt;/p&gt;

&lt;p&gt;The schema is created idempotently on boot. An empty database seeds a starter team (6 agents, 3 humans, 9 conversations) with &lt;strong&gt;zero messages&lt;/strong&gt; — everything that appears in chat is produced live.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt; is the only hard requirement; everything else has a sane local default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;DATABASE_URL  &lt;span class="c"&gt;# default: postgres://$USER@localhost:5432/cumora&lt;/span&gt;
REDIS_URL     &lt;span class="c"&gt;# default: redis://localhost:6379&lt;/span&gt;
PORT          &lt;span class="c"&gt;# default: 5181&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;                   &lt;span class="c"&gt;# unit tests (node:test) for server + workers&lt;/span&gt;
npm run &lt;span class="nb"&gt;test&lt;/span&gt;:integration   &lt;span class="c"&gt;# integration suite (needs local Postgres/Redis)&lt;/span&gt;
npm run typecheck &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run server:typecheck
npm run guard:big-brain    &lt;span class="c"&gt;# CI guard: only agent turns may use the big model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/yetone/cumora" rel="noopener noreferrer"&gt;yetone/cumora&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://cumora.ai" rel="noopener noreferrer"&gt;cumora.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Web App&lt;/strong&gt;: &lt;a href="https://app.cumora.ai" rel="noopener noreferrer"&gt;app.cumora.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;iOS beta&lt;/strong&gt;: &lt;a href="https://testflight.apple.com/join/GtRKgPpS" rel="noopener noreferrer"&gt;TestFlight&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📥 &lt;strong&gt;Desktop download&lt;/strong&gt;: &lt;a href="https://github.com/yetone/cumora-releases/releases/latest" rel="noopener noreferrer"&gt;cumora-releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;BYOA docs&lt;/strong&gt;: &lt;a href="https://github.com/yetone/cumora/blob/main/docs/BYOA.md" rel="noopener noreferrer"&gt;docs/BYOA.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Coordination design&lt;/strong&gt;: &lt;a href="https://github.com/yetone/cumora/blob/main/docs/COORDINATION.md" rel="noopener noreferrer"&gt;docs/COORDINATION.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Cumora represents a specific judgment: the next frontier for AI agents isn't stronger solo agents — it's genuine collaboration between agents and humans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three things worth noting:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"First-class citizen" is an engineering commitment, not just a product claim.&lt;/strong&gt; Agents and humans share the same data model (&lt;code&gt;kind='agent'&lt;/code&gt; vs &lt;code&gt;kind='user'&lt;/code&gt;, both in the &lt;code&gt;participants&lt;/code&gt; table), the same messaging system, and the same Kanban board. Whatever an agent can do, a human can too — and vice versa. This is not "AI as a plugin."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent coordination engineering is far harder than it looks.&lt;/strong&gt; COORDINATION.md documents real lessons learned the hard way: using prompts to fix race conditions (wrong), using code mechanisms to replace model judgment (wrong), silent model version upgrades silently breaking coordination behavior (a real production incident). This document is a rare and valuable record of multi-agent systems engineering experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The BYOA security model is worth studying.&lt;/strong&gt; The server never holds provider API keys. The I/O interface is decoupled from the brain, which means any agent engine that supports CLI interaction can in principle be plugged in. This is an extensible design rather than hardcoded integrations for a handful of specific agents.&lt;/p&gt;

&lt;p&gt;If you're building workflows that require human-agent collaboration, or studying the engineering behind multi-agent coordination, Cumora's codebase and documentation are both worth your time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Explore &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills, each validated against real enterprise workflows. No hype, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;personal site&lt;/a&gt; for more insights and interesting products.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>agents</category>
      <category>claude</category>
      <category>electron</category>
    </item>
    <item>
      <title>Code Agent Anatomy (14): Harness Design Part 4 — Fault Recovery</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:38:52 +0000</pubDate>
      <link>https://dev.to/wonderlab/code-agent-anatomy-14-harness-design-part-4-fault-recovery-5h7i</link>
      <guid>https://dev.to/wonderlab/code-agent-anatomy-14-harness-design-part-4-fault-recovery-5h7i</guid>
      <description>&lt;h2&gt;
  
  
  Errors Are the Norm, Not the Exception
&lt;/h2&gt;

&lt;p&gt;For a long-running agent, errors are inevitable: the model occasionally returns empty content, context window exceeded, network jitter, a process crashes mid-tool-execution...&lt;/p&gt;

&lt;p&gt;The question isn't "how to avoid errors" — it's &lt;strong&gt;what the agent should do after an error occurs&lt;/strong&gt;. This article dissects MyCodeAgent's fault recovery system: runtime tiered retries, the persistent fact log, and state recovery after a crash.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusions First
&lt;/h2&gt;

&lt;p&gt;The fault recovery system has two dimensions:&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;Mechanism&lt;/th&gt;
&lt;th&gt;Problem Solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime self-healing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Error classification + tiered retry&lt;/td&gt;
&lt;td&gt;Don't give up immediately on model errors; try automatic recovery first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Crash recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Transcript fact log + ResumeLoader&lt;/td&gt;
&lt;td&gt;Resume from the interruption point after a process crash, no need to start over&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two dimensions work together: runtime self-healing handles predictable errors, while Transcript ensures that even if self-healing fails and the process crashes, no completed work is lost.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Error Classification: Understand the Error Type First
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# runtime/model_errors.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelErrorKind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;EMPTY_RESPONSE&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty_response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# Model returned empty content — no text and no tool_call
&lt;/span&gt;    &lt;span class="n"&gt;PROMPT_TOO_LONG&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# Context limit exceeded, model refused to process
&lt;/span&gt;    &lt;span class="n"&gt;MAX_OUTPUT&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# Model output truncated (finish_reason="length")
&lt;/span&gt;    &lt;span class="n"&gt;API_ERROR&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;         &lt;span class="c1"&gt;# RuntimeError, usually an API layer issue
&lt;/span&gt;    &lt;span class="n"&gt;UNKNOWN_MODEL_ERROR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_model_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;classify_model_error()&lt;/code&gt; accepts an exception object or response metadata and outputs a classification result with a &lt;code&gt;recoverable&lt;/code&gt; flag. Classification is the prerequisite for retry decisions — different errors have completely different recovery strategies, and blanket retries only waste quota.&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;def&lt;/span&gt; &lt;span class="nf"&gt;_looks_like_prompt_too_long&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Uses keyword matching rather than specific exception types
&lt;/span&gt;    &lt;span class="c1"&gt;# Reason: different providers throw different exception classes,
&lt;/span&gt;    &lt;span class="c1"&gt;# but error messages usually contain these phrases
&lt;/span&gt;    &lt;span class="n"&gt;patterns&lt;/span&gt; &lt;span class="o"&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;prompt too long&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;context length&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;context window&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;too many tokens&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;maximum context length&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;request too large&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="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;String matching rather than exception types — this is an intentional design choice. Different LLM providers throw different exception classes, but the wording in error messages is highly consistent, making keyword matching more universal.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Runtime Self-Healing: Three Error Types, Three Recovery Paths
&lt;/h2&gt;

&lt;p&gt;Errors occur inside the inner &lt;code&gt;while True&lt;/code&gt; loop; successful recovery triggers &lt;code&gt;continue&lt;/code&gt; to retry the current step without consuming the outer step quota.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 1: PROMPT_TOO_LONG → Compact and Retry
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model call throws exception → classify → PROMPT_TOO_LONG
  → reactive_compact() (LLM summarizes old history, produces a checkpoint)
  → build_model_view() (read-time projection, gets the compacted message list)
  → inner continue, retry invoke_raw()
  → compaction fails → MODEL_RECOVERY_FAILED → terminate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry limit is 1 (hardcoded in &lt;code&gt;_get_model_recovery_limit&lt;/code&gt;). Compaction itself can also fail (LLM call timeout, insufficient turns) — if so, it goes directly to the termination path without looping forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 2: EMPTY_RESPONSE → Inject Hint and Retry
&lt;/h3&gt;

&lt;p&gt;The model returned empty content (&lt;code&gt;response_text&lt;/code&gt; is empty and no &lt;code&gt;tool_calls&lt;/code&gt;) — usually because the model "hesitated" — unsure whether to continue with a tool or give a final answer.&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;if&lt;/span&gt; &lt;span class="n"&gt;classification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ModelErrorKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMPTY_RESPONSE&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;retry_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;retry_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Last response had empty content and no tool_calls. Please reply with a final answer in content, or use a tool call.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Append the hint to the end of this round's messages — NOT written to history_manager
&lt;/span&gt;    &lt;span class="c1"&gt;# Only affects this retry's model view, does not pollute the permanent history
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_messages&lt;/span&gt; &lt;span class="o"&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# inner retry
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the hint message is &lt;strong&gt;not written to &lt;code&gt;history_manager&lt;/code&gt;&lt;/strong&gt; — it's only temporarily appended to the message list for this send. This keeps history clean; the model won't see this "debug hint" on the next step.&lt;/p&gt;

&lt;p&gt;The retry limit defaults to 1 (configurable via &lt;code&gt;empty_response_retry_limit&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 3: MAX_OUTPUT → No Recovery Currently
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;finish_reason="length"&lt;/code&gt; means the model output was forcibly truncated. In theory this could be recovered with a "continuation" strategy, but the current recovery limit is 0:&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;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ModelErrorKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAX_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_output_recovery_limit&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="ow"&gt;or&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;# Returns 0 → retry_count &amp;gt;= retry_limit immediately → terminate directly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This effectively means no retry — it goes to &lt;code&gt;MODEL_ERROR&lt;/code&gt; termination. This is an MVP placeholder: the structure is reserved, but the strategy hasn't been implemented yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Transcript: Append-Only Fact Log
&lt;/h2&gt;

&lt;p&gt;Runtime self-healing handles "predictable errors." But what if the process crashes outright? In that case, a persistent fact log is needed — one that can reconstruct "where things were before the crash."&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="c1"&gt;# runtime/transcript.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TranscriptStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One JSONL file per session, one event per line, append-only.&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;append_event&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TranscriptEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TranscriptEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&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;_lock&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="nf"&gt;_repair_trailing_record&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# First repair any potentially incomplete last line
&lt;/span&gt;            &lt;span class="k"&gt;with&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Flush immediately on every write to reduce data loss window
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five event types cover all key facts in the loop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event Type&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;th&gt;Recovery Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MESSAGE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message role + content + metadata&lt;/td&gt;
&lt;td&gt;Reconstruct HistoryManager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STATE_TRANSITION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transition reason + details&lt;/td&gt;
&lt;td&gt;Reconstruct last LoopState&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TOOL_LIFECYCLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Four-phase tool status&lt;/td&gt;
&lt;td&gt;Identify which tools didn't complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHECKPOINT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compaction summary + split point&lt;/td&gt;
&lt;td&gt;Reconstruct CompactStore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TERMINAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Termination reason&lt;/td&gt;
&lt;td&gt;Determine if completed normally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;_repair_trailing_record()&lt;/code&gt; handles a special case: the process crashes between &lt;code&gt;write&lt;/code&gt; and &lt;code&gt;\n&lt;/code&gt;, leaving a partial JSON on the last line. Before each write, it checks and truncates this line, ensuring every line in the file is valid JSON:&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;def&lt;/span&gt; &lt;span class="nf"&gt;_repair_trailing_record&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="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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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="c1"&gt;# Normal ending, no repair needed
&lt;/span&gt;    &lt;span class="c1"&gt;# No newline at end: try parsing the last line
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;# Parse succeeded: just missing the newline, add it
&lt;/span&gt;        &lt;span class="k"&gt;with&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Parse failed: this line is a partial write, truncate it
&lt;/span&gt;        &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. ResumeLoader: Rebuilding State from the Event Stream
&lt;/h2&gt;

&lt;p&gt;After a crash and restart, &lt;code&gt;ResumeLoader&lt;/code&gt; "replays" the event stream from the JSONL file to reconstruct &lt;code&gt;ResumeState&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="c1"&gt;# Iterate over all events, handling each by type
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&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;MESSAGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;history_messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;CHECKPOINT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;payload &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;CompactStore&lt;/span&gt; &lt;span class="n"&gt;later&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;TERMINAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;payload &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;determine&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="n"&gt;normally&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;TOOL_LIFECYCLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;tool_events&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;accumulates&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="n"&gt;states&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool state handling is the most complex part, because a tool call has four phases and a crash can occur between any of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requested → started → completed / failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ResumeLoader&lt;/code&gt; makes four judgments for each tool call's set of states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State Set&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Recovery Handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contains &lt;code&gt;completed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Successfully completed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;completed_tool_results&lt;/code&gt;, don't replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contains &lt;code&gt;failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Already failed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;failed_tool_results&lt;/code&gt;, don't replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only &lt;code&gt;requested&lt;/code&gt;, no &lt;code&gt;started&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requested but not yet started&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pending_tool_calls&lt;/code&gt;, can be re-triggered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has &lt;code&gt;started&lt;/code&gt;, but no &lt;code&gt;completed&lt;/code&gt;/&lt;code&gt;failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Started but result unknown&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uncertain_actions&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  5. UncertainAction: Explicit Modeling of Uncertainty
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;started&lt;/code&gt; but no result is the most tricky situation — the tool executed, but we don't know if it succeeded.&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;UNSAFE_UNCERTAIN_REPLAY_TOOLS&lt;/span&gt; &lt;span class="o"&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;Edit&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;Bash&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;Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;uncertain_actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;UncertainAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;replay_allowed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;UNSAFE_UNCERTAIN_REPLAY_TOOLS&lt;/span&gt;&lt;span class="p"&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;&lt;code&gt;replay_allowed&lt;/code&gt; is classified by idempotency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Read/Grep/Glob&lt;/code&gt;: idempotent, replay has no side effects, &lt;code&gt;replay_allowed=True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Edit/Bash/Task&lt;/code&gt;: have side effects, unknown if the previous run succeeded — blindly replaying might duplicate file modifications or command executions, &lt;code&gt;replay_allowed=False&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uncertainty is not silently handled — it's &lt;strong&gt;explicitly exposed to the user&lt;/strong&gt;: when recovering via CLI, the uncertain actions list is printed, letting the user decide whether to continue. This is "transparent fault recovery" — the framework doesn't pretend to know what happened; instead, it honestly reports.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Highlights
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Classification Before Retry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All recovery paths start with classification; different errors take different strategies. This avoids the most common fault-tolerance anti-pattern: blindly retrying all errors, wasting retry budget on both transient and unrecoverable errors alike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Inner while Loop Isolates Retries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Retries happen via &lt;code&gt;continue&lt;/code&gt; in the inner loop; the outer step counter &lt;code&gt;step&lt;/code&gt; stays unchanged. The &lt;code&gt;max_steps=50&lt;/code&gt; quota is entirely spent on productive ReAct iterations, not consumed by error recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Transcript Append-Only, Never Modified&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consistent with the &lt;code&gt;HistoryManager&lt;/code&gt; design principle: write only, never modify historical events. This guarantees the integrity of the event stream — the state reconstructed after a crash and restart is identical to the state before the crash, with no risk of "introducing new problems during repair."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Uncertainty Explicitly Modeled&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Uncertain actions are not an implementation detail — they're a design concept. It acknowledges that "there are situations the framework cannot automatically recover from." Rather than silently skipping or pretending to recover, it honestly informs the user and lets humans make the judgment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design Choice&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Engineering Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Error classification&lt;/td&gt;
&lt;td&gt;Keyword matching + recoverable flag&lt;/td&gt;
&lt;td&gt;Cross-provider compatible, classification before retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry isolation&lt;/td&gt;
&lt;td&gt;Inner while + continue&lt;/td&gt;
&lt;td&gt;Retries don't consume step quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;Append-only JSONL + flush&lt;/td&gt;
&lt;td&gt;Small crash window, file always reconstructable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last-line repair&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_repair_trailing_record&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Handles edge case of crash between write/flush&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncertainty&lt;/td&gt;
&lt;td&gt;UncertainAction + replay_allowed&lt;/td&gt;
&lt;td&gt;Side-effect tools not blindly replayed, transparently informs user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  About the Source Code for This Series
&lt;/h2&gt;

&lt;p&gt;All analysis in this series is based on the open-source project &lt;a href="https://github.com/chendongqi/MyCodeAgent" rel="noopener noreferrer"&gt;MyCodeAgent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source code has been annotated at key locations following the order in which topics are covered in this series — you can read the articles alongside the code, or clone the repo and run, modify, and extend it to build your own agent.&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/chendongqi/MyCodeAgent
&lt;span class="nb"&gt;cd &lt;/span&gt;MyCodeAgent
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# Fill in your LLM API key&lt;/span&gt;
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Visit &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated AI Agent and skills marketplace where every piece of content is validated against real enterprise workflows. No hype, only things that actually work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For more practical insights and interesting products, visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>harnessengineering</category>
      <category>llm</category>
    </item>
    <item>
      <title>One Open Source Project a Day (No. 166): Needle 2 — A 14 MB On-Device Tool-Calling Model</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Sun, 30 Aug 2026 00:35:02 +0000</pubDate>
      <link>https://dev.to/wonderlab/one-open-source-project-a-day-no-166-needle-2-a-14-mb-on-device-tool-calling-model-4col</link>
      <guid>https://dev.to/wonderlab/one-open-source-project-a-day-no-166-needle-2-a-14-mb-on-device-tool-calling-model-4col</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"The 2-bit model you deploy is the model that was trained."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the &lt;strong&gt;166th&lt;/strong&gt; article in the "One Open Source Project a Day" series. Today's project is &lt;strong&gt;Needle 2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On-device deployment of large language models runs into the same three-way constraint every time: parameter count, memory, and latency. To get AI running on a phone or embedded device, you either sacrifice accuracy, speed, or both.&lt;/p&gt;

&lt;p&gt;Cactus Compute found a counterintuitive way around this wall: &lt;strong&gt;don't build a general model — build only for tool calling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Needle 2 is a foundation model designed specifically for tool calling, structured extraction, and device control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;45M parameters&lt;/strong&gt;, CQ2-bit quantized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single 14 MB binary&lt;/strong&gt; — no separate model files to manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed ~28 MB session RAM&lt;/strong&gt; — memory does not grow with conversation length&lt;/li&gt;
&lt;li&gt;Matches FunctionGemma 270M (six times the size) on tool-calling benchmarks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;9.2k Stars, Apache 2.0, already deployed in production on the Pebble Index Ring and other wearables.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Will Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Needle 2's SAN (Simple Attention Network) architecture and its three core design decisions&lt;/li&gt;
&lt;li&gt;Why CQ2-bit quantization has an advantage over post-training quantization&lt;/li&gt;
&lt;li&gt;How confidence gating enables intelligent local/cloud routing&lt;/li&gt;
&lt;li&gt;How to wire up tools with the &lt;code&gt;@needle.tool&lt;/code&gt; decorator&lt;/li&gt;
&lt;li&gt;The full LoRA fine-tuning → &lt;code&gt;.cact&lt;/code&gt; export pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Familiarity with the concept of Function Calling / Tool Use&lt;/li&gt;
&lt;li&gt;Python basics (comfortable with decorators)&lt;/li&gt;
&lt;li&gt;A rough understanding of model quantization (know what INT4/INT8 means)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What It Is
&lt;/h3&gt;

&lt;p&gt;Needle 2 is not a scaled-down general-purpose chat model. It is a &lt;strong&gt;foundation model designed from scratch specifically for tool-calling tasks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The question it answers is: if a model only needs to do one thing well — "receive a natural-language query → decide which tool to call → generate correct parameters → return a structured result" — how small can it get?&lt;/p&gt;

&lt;p&gt;The answer is 14 MB, 28 MB at runtime.&lt;/p&gt;

&lt;p&gt;To put those numbers in context: the average mobile app install is larger than this. 28 MB of RAM is zero pressure on any Android phone in 2026. On a Raspberry Pi 5 it decodes at 500+ tok/s.&lt;/p&gt;

&lt;h3&gt;
  
  
  Author / Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Company&lt;/strong&gt;: Cactus Compute, Inc. (San Francisco)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core members&lt;/strong&gt;: Henry Ndubuaku, Karen Mosoyan, Jakub Mroz, and several co-founders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus&lt;/strong&gt;: On-device AI inference startup; the Needle series is its flagship product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact&lt;/strong&gt;: &lt;a href="mailto:founders@cactuscompute.com"&gt;founders@cactuscompute.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Stats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ GitHub Stars: &lt;strong&gt;9,200+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🍴 Forks: &lt;strong&gt;592&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;📄 License: Apache 2.0&lt;/li&gt;
&lt;li&gt;🧪 Paper: &lt;a href="https://arxiv.org/abs/2607.18363" rel="noopener noreferrer"&gt;arxiv.org/abs/2607.18363&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🤗 Weights: &lt;a href="https://huggingface.co/Cactus-Compute/needle2" rel="noopener noreferrer"&gt;HuggingFace Cactus-Compute/needle2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 Install: &lt;code&gt;pip install cactus-needle&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Problem It Solves
&lt;/h3&gt;

&lt;p&gt;Needle 2 provides three capabilities — all variations of the same fundamental transformation: natural language → structured output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Natural-language query
    ↓
Needle 2 inference engine (14 MB binary, 28 MB RAM)
    ↓
┌──────────────┬──────────────────┬─────────────────┐
│  Tool calls  │ Structured JSON  │  Device actions  │
└──────────────┴──────────────────┴─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every output includes a &lt;strong&gt;confidence score&lt;/strong&gt; that you can threshold to decide whether to handle the request locally or escalate to a cloud model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage Scenarios
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Offline voice assistant on wearables&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pebble Index Ring already uses Needle in production. Voice commands are parsed into API calls locally — no network required.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IoT and smart home control&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run local voice control on a Raspberry Pi or ESP32. "Turn off the bedroom lights" → &lt;code&gt;turn_off(room="bedroom")&lt;/code&gt; — zero cloud latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Document structured extraction&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract unstructured text (invoices, tables, reports) into strongly-typed JSON. Schema constraints guarantee the output format is correct.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;On-device mobile AI agent&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run a local agent on Android/iOS that handles simple tasks, and routes complex ones to the cloud based on confidence — saves API costs and reduces latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spatial computing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decodes at 400–1,500 tok/s on Meta Quest 3S or Apple Vision Pro — fast enough for real-time spatial computing scenarios.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick Start
&lt;/h3&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;cactus-needle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tool calling:&lt;/strong&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;

&lt;span class="nd"&gt;@needle.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get the current weather for a city.

    Args:
        city: The city name to query.
    &lt;/span&gt;&lt;span class="sh"&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;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sky&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;clear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@needle.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send a message to a contact.

    Args:
        to: Recipient name or number.
        body: Message content.
    &lt;/span&gt;&lt;span class="sh"&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;sent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Needle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the weather like in Lagos right now?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# [{"city": "Lagos", "temp_c": 27, "sky": "clear"}]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Structured extraction:&lt;/strong&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;

&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&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;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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_amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line_items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unit_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;}],&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;Invoice #1042, Total: $384.00. 3x Widget @ $128.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&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;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {"invoice_number": "1042", "total_amount": 384.0, "line_items": [...]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Confidence gating:&lt;/strong&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set an alarm for 8am tomorrow&lt;/span&gt;&lt;span class="sh"&gt;"&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&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.7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Not confident enough — escalate to cloud
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Handle locally
&lt;/span&gt;    &lt;span class="nf"&gt;execute_tool_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Core Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;@needle.tool&lt;/code&gt; decorator&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decorate any Python function; the docstring is automatically parsed into a tool schema&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;needle.Field&lt;/code&gt; constraints (range, regex, enum) are encoded into the JSON Schema and enforced at the grammar level during inference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Grammar-constrained decoding&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured output is not a soft request ("please output JSON"). The inference engine only permits grammatically legal characters at each token position&lt;/li&gt;
&lt;li&gt;Grammar constraints also allow skipping &lt;strong&gt;98% of the vocabulary projection&lt;/strong&gt; computation — a major source of speed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Confidence score&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every call returns &lt;code&gt;confidence&lt;/code&gt;: the minimum of a posterior calibration head score and the decoding probability&lt;/li&gt;
&lt;li&gt;"The failure mode is escalation, not wrong execution" — better to hand off to the cloud than to execute incorrectly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Tool retrieval&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When more than 5 tools are registered, embedding-based retrieval kicks in automatically; only the top 5 most relevant tools per turn enter the context&lt;/li&gt;
&lt;li&gt;"An unselected tool is unreachable, not merely unlikely" — eliminates hallucinated tool calls at the root&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Engram memory system&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;World knowledge is stored in a hashed n-gram table; retrieval costs zero arithmetic operations&lt;/li&gt;
&lt;li&gt;Knowledge and computation are fully decoupled — this is what makes the Hadamard MLP's parameter savings possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Benchmark Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;FunctionGemma 270M (f16)&lt;/th&gt;
&lt;th&gt;LFM2.5 230M (f16)&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Needle 2 (CQ2)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile Actions (961 rows)&lt;/td&gt;
&lt;td&gt;64.0%&lt;/td&gt;
&lt;td&gt;69.1%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;63.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DroidCall (200 rows)&lt;/td&gt;
&lt;td&gt;17.5%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seal-Tools in-domain&lt;/td&gt;
&lt;td&gt;16.3%&lt;/td&gt;
&lt;td&gt;26.9%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seal-Tools out-of-domain&lt;/td&gt;
&lt;td&gt;15.6%&lt;/td&gt;
&lt;td&gt;17.0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;270M (f16)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;230M (f16)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45M (CQ2)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MFLOPs / token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;540&lt;/td&gt;
&lt;td&gt;460&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Needle 2 matches or beats the strongest competitors on three out of four benchmarks at one-sixth the parameter count — the hallmark of a purpose-built model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SAN Architecture: Three Counterintuitive Design Decisions
&lt;/h3&gt;

&lt;p&gt;Needle 2 uses a Simple Attention Network (SAN) instead of a standard Transformer, built around three design choices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 1: Hadamard MLP instead of a standard FFN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The FFN layer in a standard Transformer accounts for roughly two-thirds of total parameters. SAN replaces dense projections with a &lt;strong&gt;fixed Walsh-Hadamard transform&lt;/strong&gt; — a linear transformation with no learnable parameters — consuming almost none of the parameter budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standard FFN (parameter-heavy):  x → W1 → ReLU → W2 → output
Hadamard MLP (almost parameter-free):  x → H (fixed transform) → output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a capability trade-off. The "knowledge storage" job is simply moved entirely to the Engram memory system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 2: Engram memory = hashed n-gram table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;World knowledge ("Paris is the capital of France," "syntax of list.append()") lives in a hashed n-gram lookup table. Retrieval at inference time costs zero multiplications and zero parameter consumption.&lt;/p&gt;

&lt;p&gt;This separation keeps the "computation part" of the model (Attention + Hadamard) extremely lean without reducing knowledge density.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 3: Multi-lane hyper-connections&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 27-layer × 512-wide network uses multi-lane residual connections for more flexible routing than standard residuals — effectively gaining the routing capacity of a wider model without increasing width.&lt;/p&gt;

&lt;h3&gt;
  
  
  CQ2-bit: Train for Quantization From Day One
&lt;/h3&gt;

&lt;p&gt;Most model quantization is a post-training operation: train at float16/bfloat16, then quantize to INT4 or INT8. This inevitably loses precision — more so at lower bit widths.&lt;/p&gt;

&lt;p&gt;Needle 2's &lt;strong&gt;Cactus Quants (CQ2-bit)&lt;/strong&gt; is optimized for 2-bit quantization from the start of training:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standard pipeline: FP16 training → post-training quantization → INT4 deploy (precision loss)
CQ2 pipeline: CQ2-aware training → CQ2 deploy (training target = deployment target)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official framing: &lt;strong&gt;"The 2-bit model you deploy is the model that was trained."&lt;/strong&gt; No train-to-deploy precision gap. Quantization is a design goal, not a compromise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Sources of Inference Speed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Source 1: Weights unpacked in vector registers, never decompressed to memory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional quantized models decompress weights from low precision to high precision before each computation step. CQ2-bit weights can be operated on directly within SIMD registers, eliminating the decompression step entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source 2: Grammar constraints skip 98% of vocabulary projection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Standard LLM generation evaluates softmax over the entire vocabulary (tens of thousands of tokens) at every step. Needle 2's outputs are structured JSON; at any given position, only a tiny number of characters are grammatically legal. The engine can skip the vast majority of candidates before softmax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source 3: Automatic CPU instruction-set detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single binary includes optimized paths for multiple instruction sets: SDOT (ARMv8.4), NEON (generic ARM), AVX2 (x86), RISC-V Vector Extension, WASM SIMD — the runtime picks the best path automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  LoRA Fine-Tuning: From Data to .cact File
&lt;/h3&gt;

&lt;p&gt;Fine-tuning Needle 2 is far simpler than fine-tuning a general-purpose LLM, because the task scope is narrow and well-defined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Prepare data&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;{"query": "Remind me about the meeting tomorrow at 3pm", "tools": [{"name": "set_reminder", ...}], "answers": [{"name": "set_reminder", "arguments": {"time": "tomorrow 3pm", "message": "meeting"}}]}
{"query": "What's the weather today?", "tools": [{"name": "set_reminder", ...}], "answers": []}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: include roughly 1/8 irrelevant examples (&lt;code&gt;"answers": []&lt;/code&gt;) to prevent the model from calling a tool on every single input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Data augmentation (optional)&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;needle generate-data &lt;span class="nt"&gt;--augment&lt;/span&gt; data.jsonl &lt;span class="nt"&gt;--num-samples&lt;/span&gt; 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: LoRA training&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;&lt;span class="c"&gt;# CPU training&lt;/span&gt;
needle finetune data.jsonl &lt;span class="nt"&gt;--epochs&lt;/span&gt; 10 &lt;span class="nt"&gt;--out&lt;/span&gt; adapter.pkl

&lt;span class="c"&gt;# GPU acceleration&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"cactus-needle[gpu]"&lt;/span&gt;
needle finetune data.jsonl &lt;span class="nt"&gt;--epochs&lt;/span&gt; 10 &lt;span class="nt"&gt;--out&lt;/span&gt; adapter.pkl

&lt;span class="c"&gt;# Apple Silicon&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"cactus-needle[metal]"&lt;/span&gt;
needle finetune data.jsonl &lt;span class="nt"&gt;--epochs&lt;/span&gt; 10 &lt;span class="nt"&gt;--out&lt;/span&gt; adapter.pkl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Export to .cact&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;&lt;span class="c"&gt;# The LoRA adapter is merged into weights at export time&lt;/span&gt;
needle build checkpoints/needle2.pkl &lt;span class="nt"&gt;--lora&lt;/span&gt; adapter.pkl &lt;span class="nt"&gt;--out&lt;/span&gt; tuned.cact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Load and use&lt;/strong&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="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;needle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Needle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;my_tools&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tuned.cact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-fine-tuning benchmarks show accuracy improvements of &lt;strong&gt;21–58 percentage points&lt;/strong&gt;, with fine-tuned Needle 2 surpassing DeepSeek V4 Flash on domain-specific tasks — the core advantage of a specialized model trained on a fixed tool set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Speed by Device
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Decode Speed&lt;/th&gt;
&lt;th&gt;RAM Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raspberry Pi 5&lt;/td&gt;
&lt;td&gt;500+ tok/s&lt;/td&gt;
&lt;td&gt;~28 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Vision Pro&lt;/td&gt;
&lt;td&gt;1,500 tok/s&lt;/td&gt;
&lt;td&gt;~28 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta Quest 3S&lt;/td&gt;
&lt;td&gt;400–800 tok/s&lt;/td&gt;
&lt;td&gt;~28 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Samsung A-series phone (&amp;lt;$200)&lt;/td&gt;
&lt;td&gt;300–700 tok/s&lt;/td&gt;
&lt;td&gt;~28 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESP32-S3 (microcontroller)&lt;/td&gt;
&lt;td&gt;Supported (needs external RAM)&lt;/td&gt;
&lt;td&gt;~28 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RAM footprint is &lt;strong&gt;fixed&lt;/strong&gt; — it does not grow regardless of conversation length. This is the direct result of the 256-token sliding-window KV cache and is a hard requirement for embedded deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Links &amp;amp; Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Official Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/cactus-compute/needle" rel="noopener noreferrer"&gt;https://github.com/cactus-compute/needle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://cactuscompute.com/needle" rel="noopener noreferrer"&gt;https://cactuscompute.com/needle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🤗 &lt;strong&gt;Weights&lt;/strong&gt;: &lt;a href="https://huggingface.co/Cactus-Compute/needle2" rel="noopener noreferrer"&gt;huggingface.co/Cactus-Compute/needle2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📄 &lt;strong&gt;Paper&lt;/strong&gt;: &lt;a href="https://arxiv.org/abs/2607.18363" rel="noopener noreferrer"&gt;arxiv.org/abs/2607.18363&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;pip install cactus-needle&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cactuscompute.com/blog/needle" rel="noopener noreferrer"&gt;Cactus Compute Blog&lt;/a&gt; — design rationale for Needle 1&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2607.18363" rel="noopener noreferrer"&gt;SAN Architecture Paper&lt;/a&gt; — full architectural details&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Specialized beats general (on fixed tasks)&lt;/strong&gt;: 45M parameters focused on tool calling matches or exceeds models six times larger on three benchmarks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CQ2-bit = train-to-deploy&lt;/strong&gt;: quantization is not a precision trade-off; it was the design target from day one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SAN triple combo&lt;/strong&gt;: Hadamard MLP saves parameters, Engram memory stores knowledge, multi-lane residuals expand routing capacity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence gating is calibrated, not guessed&lt;/strong&gt;: it is a reliable probability signal that can drive local/cloud routing decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed 28 MB RAM&lt;/strong&gt;: the 256-token sliding window guarantees memory does not grow with conversation length — a hard requirement for embedded deployment&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Who This Is For
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-device AI / edge computing engineers&lt;/strong&gt; who need reliable tool-calling capability on resource-constrained hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IoT and smart home developers&lt;/strong&gt; building offline voice command parsing with zero cloud dependency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app developers&lt;/strong&gt; who want to embed a local agent without relying on cloud APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI researchers&lt;/strong&gt; interested in SAN architecture, CQ2-bit quantization, and on-device inference optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One-Line Verdict
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Needle 2 proves one thing: when you narrow the problem scope enough, 14 MB can beat 270 MB. That is not a trick — it is an architectural philosophy.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Check out &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find more useful knowledge and interesting products on my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>llm</category>
      <category>edgecomputing</category>
      <category>functioncall</category>
    </item>
    <item>
      <title>Enterprise Knowledge Base (04): HyperGraphRAG Benchmark — Multi-hop Reasoning with Hypergraph Structure</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Sat, 29 Aug 2026 02:43:42 +0000</pubDate>
      <link>https://dev.to/wonderlab/enterprise-knowledge-base-04-hypergraphrag-benchmark-multi-hop-reasoning-with-hypergraph-54ao</link>
      <guid>https://dev.to/wonderlab/enterprise-knowledge-base-04-hypergraphrag-benchmark-multi-hop-reasoning-with-hypergraph-54ao</guid>
      <description>&lt;h2&gt;
  
  
  What This Article Is About
&lt;/h2&gt;

&lt;p&gt;This is the fourth benchmark in the series. The previous three articles tested four frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QAnything v2&lt;/strong&gt; (vector RAG, article 02)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LightRAG 1.5.6&lt;/strong&gt; (graph + vector hybrid, article 02)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphRAG 3.1.1&lt;/strong&gt; (LLM-driven knowledge graph, article 03)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HippoRAG 2.0&lt;/strong&gt; (hippocampus-inspired PPR graph diffusion, article 03)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one tests the last framework: &lt;strong&gt;HyperGraphRAG 1.0.6&lt;/strong&gt; (NeurIPS 2025).&lt;/p&gt;




&lt;h2&gt;
  
  
  What HyperGraphRAG Actually Does
&lt;/h2&gt;

&lt;p&gt;Traditional knowledge graph edges are binary: entity A → entity B. This means a relationship like "A, B, and C jointly participate in an event" must be decomposed into multiple binary edges — and the decomposition loses the joint semantic context.&lt;/p&gt;

&lt;p&gt;HyperGraphRAG uses &lt;strong&gt;hyperedges&lt;/strong&gt; to address this: a single hyperedge can connect any number of entities at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional graph:  A → B, B → C, A → C  (3 separate edges, context fragmented)
Hypergraph:         {A, B, C} connected by a single hyperedge (joint context preserved)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline from documents to hypergraph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Documents → LLM extracts entity sets (co-occurring entities per passage, not pairwise)
          → Build hypergraph (each entity set = one hyperedge)
          → BGE vectorizes entities
          → Query: vector recall for seed entities → hypergraph diffusion
                 → extract relevant passages → LLM answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The theoretical expectation: for "multi-entity joint participation" relationships, hypergraph extraction should surface more signal than binary graph extraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;HyperGraphRAG is not on PyPI. There is no &lt;code&gt;pip install hypergraphrag&lt;/code&gt;, and &lt;code&gt;pip install -e .&lt;/code&gt; fails because there is no &lt;code&gt;setup.py&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt;. The working approach: add the source directory to &lt;code&gt;PYTHONPATH&lt;/code&gt; and install the requirements from the README:&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="nt"&gt;-r&lt;/span&gt; requirements.txt   &lt;span class="c"&gt;# graspologic, nano-vectordb, etc.&lt;/span&gt;

&lt;span class="c"&gt;# Run with:&lt;/span&gt;
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/HyperGraphRAG python run_eval.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SiliconFlow API: Two Hidden Constraints
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Constraint 1: Content filter blocks environment variable patterns (HTTP 400, code 20015)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The test documents contain many &lt;code&gt;.env&lt;/code&gt; example snippets like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;EMBEDDING_MODEL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jina-embeddings-v4&lt;/span&gt;
&lt;span class="py"&gt;EMBEDDING_BINDING&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jina&lt;/span&gt;
&lt;span class="py"&gt;EMBEDDING_ASYMMETRIC&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ALLCAPS=value&lt;/code&gt; format triggers SiliconFlow's content safety filter. Fix: regex-strip any lines matching sensitive patterns before sending embedding requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint 2: bge-large-en-v1.5 has a ~512-token input limit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Testing revealed that inputs over roughly 1000 characters return 400. Dense markdown tables (every &lt;code&gt;|&lt;/code&gt; and &lt;code&gt;%&lt;/code&gt; consumes a token) can exceed 512 tokens at just 900 characters.&lt;/p&gt;

&lt;p&gt;Final setting: truncate each text to 700 characters (word-boundary truncation), giving a buffer for high-density content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Stability During Long Runs
&lt;/h3&gt;

&lt;p&gt;GLM-4-flash and SiliconFlow both produced intermittent &lt;code&gt;ConnectTimeout&lt;/code&gt; and HTTP 500 errors during the 7-hour indexing run. Two necessary changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set LLM client timeout to 180s, with 6 retry attempts (10/20/30/40/50/60s backoff)&lt;/li&gt;
&lt;li&gt;Switch from one-shot &lt;code&gt;rag.insert(all_docs)&lt;/code&gt; to &lt;strong&gt;per-document insert with a progress file&lt;/strong&gt;: each successfully indexed document is immediately checkpointed; a crash and restart continues from where it left off&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without both changes, finishing a 443-minute index build in a single run is unrealistic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Numbers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;HyperGraphRAG 1.0.6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Index build time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;443.2 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundary refusal rate&lt;/td&gt;
&lt;td&gt;26.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P90 latency&lt;/td&gt;
&lt;td&gt;49.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average latency&lt;/td&gt;
&lt;td&gt;29.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-hop match&lt;/td&gt;
&lt;td&gt;0.081&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop match&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.171&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundary match&lt;/td&gt;
&lt;td&gt;0.021&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Answer matching uses Jaccard keyword overlap, not LLM judge; LLM is GLM-4-flash throughout&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-hop: 0.171 — Tied with LightRAG, Not the Breakout
&lt;/h3&gt;

&lt;p&gt;The theoretical advantage of hyperedges didn't materialize here. HyperGraphRAG's 0.171 on multi-hop is within noise of LightRAG's 0.178.&lt;/p&gt;

&lt;p&gt;A few plausible explanations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The test documents are technical reference materials (configuration guides, API docs). Multi-entity joint relationships are rare in this genre — that's precisely where hyperedges help most, but the signal isn't there to exploit&lt;/li&gt;
&lt;li&gt;Hyperedge quality depends on LLM extraction quality; GLM-4-flash's performance limits what the graph can represent&lt;/li&gt;
&lt;li&gt;BGE truncation to 700 characters may have dropped some semantic signal from longer entity descriptions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Index Build Time: 443 Minutes
&lt;/h3&gt;

&lt;p&gt;GraphRAG took 31 minutes, HippoRAG took 36 minutes. HyperGraphRAG took 443 minutes — roughly 7.4 hours.&lt;/p&gt;

&lt;p&gt;The cause: HyperGraphRAG issues &lt;strong&gt;multiple LLM calls per chunk&lt;/strong&gt; to extract entity sets, and &lt;code&gt;llm_model_max_async=1&lt;/code&gt; (GLM rate limit). With 124 chunks, ~4 LLM calls each, plus the accumulated timeout retries, the total inflates significantly.&lt;/p&gt;

&lt;p&gt;In a production setup with a higher-concurrency LLM (GPT-4o), the indexing time would likely drop to 30–60 minutes. GLM's low concurrency is the amplifying factor here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Boundary Refusal: 26.3% — Better Than Expected for Graph RAG
&lt;/h3&gt;

&lt;p&gt;5 of 19 boundary questions (26.3%) were correctly declined. This is higher than GraphRAG's 15.8% and HippoRAG's 5.3%.&lt;/p&gt;

&lt;p&gt;Likely reason: HyperGraphRAG's hybrid mode (local + global search) may be more willing to return "not found in the knowledge base" when neither local graph traversal nor global context returns relevant hits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Five-Framework Comparison
&lt;/h2&gt;

&lt;p&gt;Combining all four articles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Index time&lt;/th&gt;
&lt;th&gt;Refusal rate&lt;/th&gt;
&lt;th&gt;P90 latency&lt;/th&gt;
&lt;th&gt;Single-hop&lt;/th&gt;
&lt;th&gt;Multi-hop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QAnything v2&lt;/td&gt;
&lt;td&gt;Vector RAG&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52.2 s&lt;/td&gt;
&lt;td&gt;0.111&lt;/td&gt;
&lt;td&gt;0.162&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LightRAG 1.5.6&lt;/td&gt;
&lt;td&gt;Graph+Vector&lt;/td&gt;
&lt;td&gt;~8 min&lt;/td&gt;
&lt;td&gt;10.5%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.4 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.082&lt;/td&gt;
&lt;td&gt;0.178&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphRAG 3.1.1&lt;/td&gt;
&lt;td&gt;Graph RAG&lt;/td&gt;
&lt;td&gt;31 min&lt;/td&gt;
&lt;td&gt;15.8%&lt;/td&gt;
&lt;td&gt;32.4 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.107&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.211&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HippoRAG 2.0&lt;/td&gt;
&lt;td&gt;Graph RAG&lt;/td&gt;
&lt;td&gt;36 min&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;84.0 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.122&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.163&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HyperGraphRAG 1.0.6&lt;/td&gt;
&lt;td&gt;Hypergraph RAG&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;443 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;26.3%&lt;/td&gt;
&lt;td&gt;49.3 s&lt;/td&gt;
&lt;td&gt;0.081&lt;/td&gt;
&lt;td&gt;0.171&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Patterns across the five frameworks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Index cost and multi-hop performance are decoupled&lt;/strong&gt;: GraphRAG achieved the highest multi-hop score (0.211) with a 31-minute build. HyperGraphRAG spent 443 minutes and got 0.171. More indexing time does not buy better retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LightRAG has the best cost-effectiveness&lt;/strong&gt;: 8-minute build, second-best multi-hop (0.178), lowest P90 latency (19.4s). Its only weakness is low refusal rate (10.5%) — the graph tends to produce an answer regardless of relevance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refusal capability is a design decision, not an emergent property&lt;/strong&gt;: QAnything's 26.3% refusal rate comes from explicit confidence-threshold filtering. Graph RAG frameworks don't ship with a refusal mechanism and show near-zero rates as a result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single-hop: graph structure adds nothing&lt;/strong&gt;: HippoRAG's 0.122 is the best among graph RAG approaches, but vector RAG and graph RAG both cluster in the 0.08–0.12 range. For direct factual retrieval, knowledge graph overhead doesn't help.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Which Framework Should You Choose?
&lt;/h2&gt;

&lt;p&gt;Based on the five-framework data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default to LightRAG&lt;/strong&gt; for most use cases: fast indexing, strong multi-hop, lowest latency. Add a confidence post-processing step to improve refusal behavior if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose GraphRAG&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-hop reasoning is your primary use case (policy cross-referencing, causal chain analysis)&lt;/li&gt;
&lt;li&gt;You have LLM budget for the indexing cost&lt;/li&gt;
&lt;li&gt;You're using a model with reliable structured output (GPT-4o, Claude) to enable community reports; the GLM structured output issues in this test disabled that feature entirely&lt;/li&gt;
&lt;li&gt;30-minute build times are acceptable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose QAnything&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need strong out-of-the-box refusal capability with minimal configuration&lt;/li&gt;
&lt;li&gt;You want a self-contained deployment without managing graph infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wait on HyperGraphRAG&lt;/strong&gt;: the hyperedge concept is theoretically compelling, but version 1.0.6 has real production friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No PyPI package; source-only installation&lt;/li&gt;
&lt;li&gt;7-hour build time in this configuration&lt;/li&gt;
&lt;li&gt;Multi-hop performance didn't surpass LightRAG in a technical document domain&lt;/li&gt;
&lt;li&gt;Check back when a production-grade release lands&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code Highlights
&lt;/h2&gt;

&lt;p&gt;Full code at &lt;code&gt;llm-in-action/kb-04-hypergraphrag-eval/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text truncation + sanitization&lt;/strong&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;_SENSITIVE_LINE_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[^\n]*(?:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;api[_\-]?key|secret|token|password|Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;|sk-[A-Za-z0-9]{10,}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;|[A-Z][A-Z0-9_]{3,}=[^\s\n]+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# ENV_VAR=value
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)[^\n]*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_MAX_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;  &lt;span class="c1"&gt;# bge-large-en-v1.5 via SiliconFlow ~512 tokens
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_truncate&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="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="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_SENSITIVE_LINE_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[REDACTED]&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&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="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;_MAX_CHARS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;truncated&lt;/span&gt; &lt;span class="o"&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;_MAX_CHARS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;last_space&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &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="n"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;last_space&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;last_space&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;truncated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Per-document insert with checkpoint resume&lt;/strong&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;def&lt;/span&gt; &lt;span class="nf"&gt;build_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Path&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;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_progress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# load completed list
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fpath&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc_files&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;fpath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;indexed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# skip already done
&lt;/span&gt;        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fpath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;              &lt;span class="c1"&gt;# insert one at a time
&lt;/span&gt;        &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;indexed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fpath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;save_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# checkpoint immediately
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Next: &lt;strong&gt;Series Wrap-Up&lt;/strong&gt; — benchmark methodology limitations, what this test set measures (and doesn't), and how to design a production-grade RAG evaluation from scratch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Check out &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills validated in real-world, enterprise-grade workflows. Not demos — things that actually work in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find more on my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hypergraphrag</category>
      <category>opensource</category>
      <category>rag</category>
      <category>knowledgebase</category>
    </item>
    <item>
      <title>One Open Source Project a Day (No. 165): ntfy — Push Notifications with a Single curl</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Sat, 29 Aug 2026 02:42:24 +0000</pubDate>
      <link>https://dev.to/wonderlab/one-open-source-project-a-day-no-165-ntfy-push-notifications-with-a-single-curl-3fi2</link>
      <guid>https://dev.to/wonderlab/one-open-source-project-a-day-no-165-ntfy-push-notifications-with-a-single-curl-3fi2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Push notifications without complexity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the &lt;strong&gt;165th&lt;/strong&gt; article in the "One Open Source Project a Day" series. Today's project is &lt;strong&gt;ntfy&lt;/strong&gt; (pronounced "notify").&lt;/p&gt;

&lt;p&gt;You have probably run into this situation before: a long-running script finishes and you want to know the result. A backup job completes successfully and you want a quick tap on your shoulder. A server's disk is nearly full and you need to know immediately.&lt;/p&gt;

&lt;p&gt;There are plenty of solutions — Email, Slack, a Telegram bot — but every single one requires creating an account, configuring an API key, and installing an SDK. By the time you are done, the notification plumbing has become more work than the original task.&lt;/p&gt;

&lt;p&gt;ntfy strips the whole thing down to its essence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Backup complete!"&lt;/span&gt; ntfy.sh/my-channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. One line, and the message lands on your phone. No account, no API key, no configuration whatsoever. 33.8k Stars, fully self-hostable, and a staple tool for ops engineers, developers, and automation enthusiasts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Will Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;How ntfy's publish-subscribe model works&lt;/li&gt;
&lt;li&gt;Advanced message features: priority, tags, action buttons, delayed delivery, and attachments&lt;/li&gt;
&lt;li&gt;How to run a private ntfy server with Docker&lt;/li&gt;
&lt;li&gt;Authentication and access control configuration&lt;/li&gt;
&lt;li&gt;Integrating ntfy with Alertmanager, GitHub Actions, Home Assistant, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Basic familiarity with HTTP concepts (GET/POST requests)&lt;/li&gt;
&lt;li&gt;Comfortable with Linux command-line basics (curl)&lt;/li&gt;
&lt;li&gt;Docker fundamentals for the self-hosting section&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What It Is
&lt;/h3&gt;

&lt;p&gt;ntfy is an &lt;strong&gt;HTTP-based publish-subscribe (pub-sub) push notification service&lt;/strong&gt;. Its design is deliberately minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publisher&lt;/strong&gt;: send an HTTP request to a "topic"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscriber&lt;/strong&gt;: subscribe to that topic in the phone app or browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Topics require no pre-registration. The message body is the notification content. There are no extra layers of abstraction.&lt;/p&gt;

&lt;p&gt;The official &lt;code&gt;ntfy.sh&lt;/code&gt; public service is free to use (with rate limits). You can also self-host it entirely — both options run the same open-source code and expose identical features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Author
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Author&lt;/strong&gt;: Philipp C. Heckel (website: heckel.io)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background&lt;/strong&gt;: German software engineer and independent open-source author; also created the ZFS backup tool &lt;code&gt;znapzend&lt;/code&gt; among others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin&lt;/strong&gt;: Started in 2021 from a personal itch — "I just want a notification when my script finishes"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Stats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ GitHub Stars: &lt;strong&gt;33,800+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🍴 Forks: &lt;strong&gt;1,600+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;📄 License: Apache 2.0 + GPLv2 (dual license)&lt;/li&gt;
&lt;li&gt;🌐 Public service: &lt;a href="https://ntfy.sh" rel="noopener noreferrer"&gt;ntfy.sh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 Docs: &lt;a href="https://docs.ntfy.sh" rel="noopener noreferrer"&gt;docs.ntfy.sh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📱 Android: Google Play + F-Droid (both free)&lt;/li&gt;
&lt;li&gt;🍎 iOS: App Store&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Problem It Solves
&lt;/h3&gt;

&lt;p&gt;ntfy is a &lt;strong&gt;minimal message broker&lt;/strong&gt; that reduces "send a notification" to the simplest possible HTTP request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Publisher (anything that can make an HTTP request)
    curl / shell script / GitHub Actions / Prometheus Alertmanager ...
         ↓  POST/PUT  ntfy.sh/my-topic
ntfy server (public ntfy.sh or self-hosted instance)
         ↓  push delivery
Subscriber (any client subscribed to that topic)
    mobile app / browser / CLI ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The topic name doubles as both the "channel address" and the first layer of access control — there is no registration flow, no API key to manage. An unpredictable topic name is itself the first barrier to unauthorized access (formal authentication is available too).&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage Scenarios
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Script and job completion notifications&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long-running backup jobs, builds, or data pipelines push their result to your phone the instant they finish. No more staring at a terminal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Server monitoring alerts&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pair ntfy with Uptime Kuma, Prometheus Alertmanager, or Healthchecks.io to receive alerts the moment a service goes down.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CI/CD pipeline notifications&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions or GitLab CI pushes success or failure to your phone on completion. No polling the web UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Smart home events&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Home Assistant automation rules fire ntfy notifications: "Front door unlocked," "Washing machine done."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Personal automation scripts&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any shell script scenario — scheduled tasks, file downloads finishing, scrapers completing — one curl line handles the notification.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick Start
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Use the public service — zero configuration&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;&lt;span class="c"&gt;# Step 1: Install the app on your phone (iOS / Android)&lt;/span&gt;
&lt;span class="c"&gt;# Open app → Add subscription → enter a topic name, e.g. "my-alerts-abc123"&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Send a notification (from any device)&lt;/span&gt;
curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Hello from ntfy!"&lt;/span&gt; ntfy.sh/my-alerts-abc123

&lt;span class="c"&gt;# With a title&lt;/span&gt;
curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Title: Backup complete"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"All files successfully backed up to S3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ntfy.sh/my-alerts-abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Self-hosted server (Docker)&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;&lt;span class="c"&gt;# Minimal start&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="nt"&gt;-it&lt;/span&gt; binwiederhier/ntfy serve

&lt;span class="c"&gt;# With persistent storage&lt;/span&gt;
docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/cache/ntfy:/var/cache/ntfy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /etc/ntfy:/etc/ntfy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-it&lt;/span&gt; binwiederhier/ntfy serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;ntfy.sh&lt;/code&gt; with your server's address and everything else stays identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Message Priority (levels 1–5)&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;&lt;span class="c"&gt;# Urgent: heavy vibration + full-screen alert&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Priority: urgent"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Disk almost full!"&lt;/span&gt; ntfy.sh/alerts

&lt;span class="c"&gt;# Low: silent delivery&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Priority: low"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Scheduled backup complete"&lt;/span&gt; ntfy.sh/alerts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Android Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;urgent&lt;/td&gt;
&lt;td&gt;Heavy vibration + full-screen popup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;Long vibration + default sound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;Default behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;No vibration or sound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;min&lt;/td&gt;
&lt;td&gt;Collapsed under "other notifications"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. Tags and Emoji&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tag names that match emoji short codes are automatically converted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Tags: warning,computer"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"CPU usage exceeded 90%"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ntfy.sh/alerts
&lt;span class="c"&gt;# Phone shows: ⚠️ 💻 CPU usage exceeded 90%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Action Buttons (up to 3)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add tappable actions directly to the notification — no need to open the app:&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;# Button that triggers an HTTP request&lt;/span&gt;
curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Actions: http, Silence alert, https://myserver.com/api/silence, method=POST"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"CPU sustained high load. Silence alert?"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ntfy.sh/alerts

&lt;span class="c"&gt;# Action types:&lt;/span&gt;
&lt;span class="c"&gt;# view      → open a URL&lt;/span&gt;
&lt;span class="c"&gt;# http      → send an HTTP request&lt;/span&gt;
&lt;span class="c"&gt;# broadcast → Android broadcast intent&lt;/span&gt;
&lt;span class="c"&gt;# copy      → copy text to clipboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Delayed Delivery&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;&lt;span class="c"&gt;# Send in 30 minutes&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"In: 30min"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Time to drink some water"&lt;/span&gt; ntfy.sh/reminders

&lt;span class="c"&gt;# Send tomorrow at 9am&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"At: tomorrow, 9am"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Weekly standup reminder"&lt;/span&gt; ntfy.sh/reminders

&lt;span class="c"&gt;# Dead man's switch: if the script doesn't ping again within 1 hour, fire an alert&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"In: 1h"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Title: Script heartbeat lost"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"No heartbeat in over 1 hour — please investigate!"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ntfy.sh/watchdog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Attachments&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;&lt;span class="c"&gt;# Upload a local file (max 15 MB, expires after 3 hours)&lt;/span&gt;
curl &lt;span class="nt"&gt;-T&lt;/span&gt; screenshot.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Filename: screenshot.png"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ntfy.sh/alerts

&lt;span class="c"&gt;# Attach an external URL (no server storage used)&lt;/span&gt;
curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Attach: https://example.com/report.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Filename: monthly-report.pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Monthly report is ready"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ntfy.sh/reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Message Templates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ntfy ships with built-in templates for Alertmanager and Grafana. Configuring the webhook URL is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://ntfy.sh/my-alerts?template=alertmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom Go templates can process arbitrary JSON payloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Competitive Comparison
&lt;/h3&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;ntfy&lt;/th&gt;
&lt;th&gt;Telegram Bot&lt;/th&gt;
&lt;th&gt;Slack Webhook&lt;/th&gt;
&lt;th&gt;Pushover&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No account needed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ Requires Telegram&lt;/td&gt;
&lt;td&gt;❌ Requires Slack&lt;/td&gt;
&lt;td&gt;❌ Requires signup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hostable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full support&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free self-hosted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Completely free&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;❌ Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free Android app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ Paid app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action buttons&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delayed delivery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Publish method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Plain HTTP&lt;/td&gt;
&lt;td&gt;Bot API&lt;/td&gt;
&lt;td&gt;Webhook&lt;/td&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture: Minimal Pub-Sub
&lt;/h3&gt;

&lt;p&gt;ntfy's architecture is deliberately simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────┐
│                ntfy server                    │
│                                              │
│  HTTP API ──→ Message routing ──→ Topic      │
│                                    ↓         │
│                               Message cache  │
│                            (SQLite / PgSQL)  │
│                                    ↓         │
│              ┌─────────────────────┤         │
│              ↓                     ↓         │
│          WebSocket              FCM/APNs     │
│        (Web / CLI)           (Mobile push)  │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Message routing&lt;/strong&gt;: The topic name is the routing key. Incoming messages are broadcast to all active subscribers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message cache&lt;/strong&gt;: The server caches messages for 12 hours by default. Clients that reconnect can pull any messages they missed. This is a key advantage over pure webhook solutions — the recipient does not need to be online at the moment of delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push channels&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web / desktop: Server-Sent Events (SSE) or WebSocket&lt;/li&gt;
&lt;li&gt;Android: Firebase Cloud Messaging (FCM), or direct long-polling in the F-Droid build (no FCM dependency)&lt;/li&gt;
&lt;li&gt;iOS: APNs via the official &lt;code&gt;ntfy.sh&lt;/code&gt; relay&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;p&gt;The backend is written in &lt;strong&gt;Go&lt;/strong&gt; — a well-suited choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Produces a single binary with zero external dependencies&lt;/li&gt;
&lt;li&gt;Handles many concurrent WebSocket connections efficiently&lt;/li&gt;
&lt;li&gt;Cross-platform compilation covers Linux, macOS, and Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database defaults to &lt;strong&gt;SQLite&lt;/strong&gt;; switching to PostgreSQL is a single line in the config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/ntfy/server.yml&lt;/span&gt;
&lt;span class="na"&gt;database-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://user:pass@localhost/ntfy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLite's single-writer constraint means it cannot scale horizontally; use PostgreSQL for high-concurrency production deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Hosting Configuration
&lt;/h3&gt;

&lt;p&gt;A production-ready server configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/ntfy/server.yml&lt;/span&gt;

&lt;span class="c1"&gt;# Base settings&lt;/span&gt;
&lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://ntfy.example.com"&lt;/span&gt;
&lt;span class="na"&gt;listen-http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:80"&lt;/span&gt;
&lt;span class="na"&gt;listen-https&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:443"&lt;/span&gt;
&lt;span class="na"&gt;key-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/letsencrypt/live/ntfy.example.com/privkey.pem"&lt;/span&gt;
&lt;span class="na"&gt;cert-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/letsencrypt/live/ntfy.example.com/fullchain.pem"&lt;/span&gt;

&lt;span class="c1"&gt;# Storage&lt;/span&gt;
&lt;span class="na"&gt;cache-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/cache/ntfy/cache.db"&lt;/span&gt;
&lt;span class="na"&gt;cache-duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12h"&lt;/span&gt;
&lt;span class="na"&gt;attachment-cache-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/cache/ntfy/attachments"&lt;/span&gt;
&lt;span class="na"&gt;attachment-total-size-limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5G"&lt;/span&gt;
&lt;span class="na"&gt;attachment-file-size-limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15M"&lt;/span&gt;
&lt;span class="na"&gt;attachment-expiry-duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3h"&lt;/span&gt;

&lt;span class="c1"&gt;# Access control (close anonymous access — login required)&lt;/span&gt;
&lt;span class="na"&gt;auth-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/ntfy/user.db"&lt;/span&gt;
&lt;span class="na"&gt;auth-default-access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny-all"&lt;/span&gt;

&lt;span class="c1"&gt;# Declarative user provisioning (auto-created on startup)&lt;/span&gt;
&lt;span class="na"&gt;auth-users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice:$2a$10$hashhere:admin"&lt;/span&gt;
&lt;span class="na"&gt;auth-access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice:*:rw"&lt;/span&gt;        &lt;span class="c1"&gt;# alice can read/write all topics&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*:public-*:ro"&lt;/span&gt;     &lt;span class="c1"&gt;# everyone can subscribe to public-* topics&lt;/span&gt;

&lt;span class="c1"&gt;# Web push (browser notifications)&lt;/span&gt;
&lt;span class="na"&gt;web-push-public-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BNbxxx..."&lt;/span&gt;
&lt;span class="na"&gt;web-push-private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xxx..."&lt;/span&gt;
&lt;span class="na"&gt;web-push-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/ntfy/webpush.db"&lt;/span&gt;

&lt;span class="c1"&gt;# Email forwarding&lt;/span&gt;
&lt;span class="na"&gt;smtp-sender-addr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mail.example.com:587"&lt;/span&gt;
&lt;span class="na"&gt;smtp-sender-user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ntfy@example.com"&lt;/span&gt;
&lt;span class="na"&gt;smtp-sender-pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-smtp-password"&lt;/span&gt;
&lt;span class="na"&gt;smtp-sender-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ntfy@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Three authentication options are supported:&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;# Username + password&lt;/span&gt;
curl &lt;span class="nt"&gt;-u&lt;/span&gt; alice:password &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Private message"&lt;/span&gt; ntfy.example.com/private-topic

&lt;span class="c"&gt;# Bearer token (recommended — more secure than a raw password)&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer tk_AbcDefGhi..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Private message"&lt;/span&gt; ntfy.example.com/private-topic

&lt;span class="c"&gt;# URL parameter (for environments where setting headers is inconvenient)&lt;/span&gt;
curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"Private message"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"ntfy.example.com/private-topic?auth=dXNlcjpwYXNz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokens support expiry and can be revoked at any time — safer than embedding a plain password in scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Send build notification&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;curl \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Title: ${{ github.repository }} build ${{ job.status }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Priority: ${{ job.status == 'success' &amp;amp;&amp;amp; 'default' || 'high' }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Tags: ${{ job.status == 'success' &amp;amp;&amp;amp; 'white_check_mark' || 'x' }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-d "Branch: ${{ github.ref_name }}, commit: ${{ github.sha }}" \&lt;/span&gt;
      &lt;span class="s"&gt;${{ secrets.NTFY_URL }}/${{ secrets.NTFY_TOPIC }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prometheus Alertmanager:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# alertmanager.yml&lt;/span&gt;
&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ntfy"&lt;/span&gt;
    &lt;span class="na"&gt;webhook_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://ntfy.sh/my-alerts?template=alertmanager"&lt;/span&gt;
        &lt;span class="na"&gt;send_resolved&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Home Assistant:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# configuration.yaml&lt;/span&gt;
&lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rest&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ntfy&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://ntfy.sh/my-home-alerts&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST_JSON&lt;/span&gt;
    &lt;span class="na"&gt;title_param_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;title&lt;/span&gt;
    &lt;span class="na"&gt;message_param_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Project Links &amp;amp; Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Official Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/binwiederhier/ntfy" rel="noopener noreferrer"&gt;https://github.com/binwiederhier/ntfy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://docs.ntfy.sh" rel="noopener noreferrer"&gt;https://docs.ntfy.sh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Public service&lt;/strong&gt;: &lt;a href="https://ntfy.sh" rel="noopener noreferrer"&gt;https://ntfy.sh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;Android App (F-Droid)&lt;/strong&gt;: &lt;a href="https://f-droid.org/packages/io.heckel.ntfy" rel="noopener noreferrer"&gt;f-droid.org/packages/io.heckel.ntfy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Issues&lt;/strong&gt;: &lt;a href="https://github.com/binwiederhier/ntfy/issues" rel="noopener noreferrer"&gt;github.com/binwiederhier/ntfy/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt; — self-hosted monitoring that pairs naturally with ntfy alerts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://healthchecks.io" rel="noopener noreferrer"&gt;Healthchecks.io&lt;/a&gt; — cron job heartbeat monitoring with ntfy support&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/binwiederhier/ntfy-android" rel="noopener noreferrer"&gt;ntfy Android source&lt;/a&gt; — Android client in its own repository&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimal pub-sub model&lt;/strong&gt;: topic = address, HTTP request = publish, no account, no SDK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich message features&lt;/strong&gt;: priority, tags, action buttons, delayed delivery, attachments — covers virtually every notification use case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete self-hosting&lt;/strong&gt;: single Go binary, &lt;code&gt;docker run&lt;/code&gt; in one line, SQLite or PostgreSQL your choice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong integration ecosystem&lt;/strong&gt;: Alertmanager, GitHub Actions, Home Assistant, Uptime Kuma all work out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message cache&lt;/strong&gt;: 12-hour cache ensures offline subscribers don't miss messages — more reliable than pure webhooks&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Who This Is For
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ops and DevOps engineers&lt;/strong&gt;: server alerts, CI/CD notifications, cron job monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indie developers and solo founders&lt;/strong&gt;: notification needs for automation scripts — cheap, zero-dependency, no lock-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting enthusiasts&lt;/strong&gt;: full ownership of notification infrastructure, no third-party service dependency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart home builders&lt;/strong&gt;: event push from Home Assistant, Node-RED, and similar platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One-Line Verdict
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;ntfy is the best argument for "simple is best" — it reduces push notifications to a single curl command, yet has enough depth to handle serious production requirements when you need it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Check out &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find more useful knowledge and interesting products on my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>go</category>
      <category>http</category>
      <category>devops</category>
    </item>
    <item>
      <title>Code Agent Anatomy (13): Harness Design Part 3 — Tool Execution Pipeline</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:38:08 +0000</pubDate>
      <link>https://dev.to/wonderlab/code-agent-anatomy-13-harness-design-part-3-tool-execution-pipeline-1k1g</link>
      <guid>https://dev.to/wonderlab/code-agent-anatomy-13-harness-design-part-3-tool-execution-pipeline-1k1g</guid>
      <description>&lt;h2&gt;
  
  
  The Journey of a Tool Call
&lt;/h2&gt;

&lt;p&gt;The model outputs a tool_call — say &lt;code&gt;Edit("foo.py", ...)&lt;/code&gt; — and the result ends up written into history. What happens in between?&lt;/p&gt;

&lt;p&gt;This post walks that path end-to-end, disassembling two core modules: &lt;code&gt;ToolOrchestrator&lt;/code&gt; (the dispatch layer, managing concurrency and result budgets) and &lt;code&gt;ToolExecutor&lt;/code&gt; (the execution layer, managing permissions, optimistic locks, and circuit breakers).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Conclusion First
&lt;/h2&gt;

&lt;p&gt;The tool execution pipeline has two layers with completely separate responsibilities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Responsible for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dispatch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ToolOrchestrator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Concurrent grouping, ordering guarantees, result budget truncation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ToolExecutor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permission check → optimistic lock injection → circuit breaker check → tool.run()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model produces a batch of tool_calls; Orchestrator handles "how to run this batch"; Executor handles "how to execute one tool safely." There's a clean interface boundary between the two layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Dispatch Layer: Concurrent Grouping, Ordering Guarantees
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tools/orchestrator.py  ToolOrchestrator
&lt;/span&gt;&lt;span class="n"&gt;SAFE_TOOL_NAMES&lt;/span&gt;   &lt;span class="o"&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;Read&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;Grep&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;Glob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# read-only, safe to parallelize
&lt;/span&gt;&lt;span class="n"&gt;UNSAFE_TOOL_NAMES&lt;/span&gt; &lt;span class="o"&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;Edit&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;Bash&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;Task&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="c1"&gt;# side-effecting, must be serial
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model may request multiple tools in a single step — for example, &lt;code&gt;Read&lt;/code&gt; three files and then &lt;code&gt;Edit&lt;/code&gt; one. The Orchestrator's first job is &lt;strong&gt;batching&lt;/strong&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="c1"&gt;# partition_tool_calls()
# input:  [Read, Read, Edit, Grep, Grep, Edit, Read]
# output: [concurrent(Read,Read), serial(Edit), concurrent(Grep,Grep), serial(Edit), concurrent(Read)]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is simple: consecutive safe tools are merged into one concurrent batch; a write tool breaks the batch.&lt;/p&gt;

&lt;p&gt;Concurrent batches run via &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;, but there's a key detail — result ordering is preserved:&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;def&lt;/span&gt; &lt;span class="nf"&gt;_run_batch_concurrently&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;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="c1"&gt;# submit returns a Future immediately without blocking — all tools start nearly simultaneously
&lt;/span&gt;    &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&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;_execute_plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&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;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# .result() blocks until done; results stored by offset, preserving original position
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# rebuild list in offset order — thread completion order is non-deterministic, this forces the original
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&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;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thread B may finish before thread A, but in the returned list A always precedes B. The order the model requested is the order written into history.&lt;/p&gt;

&lt;p&gt;Serial batches are a plain for-loop: one completes before the next begins. Total time is the sum of all tool times, but there's no race condition.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Execution Layer: Four-Gate Pipeline
&lt;/h2&gt;

&lt;p&gt;Each tool's actual execution goes through &lt;code&gt;ToolExecutor.execute()&lt;/code&gt;, which is a linear pipeline — any gate failure short-circuits immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argument parsing → [Gate 1] permission check → [Gate 2] optimistic lock injection → [Gate 3] circuit breaker check → tool.run()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gate 1: Permission Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tools/permissions.py  RiskClassifier
# Decision priority:
# 1. Read/Grep/Glob → ALLOW (read-only, no risk)
# 2. Edit → check runtime_mode (read-only sub-agent → DENY)
# 3. Bash → regex blacklist → greylist → whitelist
# 4. unknown tool → DENY (fail-closed)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash is the most complex case. Blacklist hits are immediate DENY without asking the user:&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;_BASH_DENY_PATTERNS&lt;/span&gt; &lt;span class="o"&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;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sudo&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;sudo crosses the process privilege boundary&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;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rm(?:\s|$)&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;destructive delete command&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;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash\s+-c&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;nested shell execution bypasses command classification&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;re&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;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;`|\$\(&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;shell command substitution executes nested commands&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Greylisted commands (&lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;pip install&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;, etc.) go through the ASK policy, which in the current MVP implementation defaults to DENY when &lt;code&gt;ask_policy="deny"&lt;/code&gt; — meaning &lt;strong&gt;unknown-risk commands are not executed by default&lt;/strong&gt;, forcing the model to find another approach.&lt;/p&gt;

&lt;p&gt;The critical design principle: &lt;strong&gt;fail-closed&lt;/strong&gt;. Being in the tool Registry only means the model can "see" the tool. Execution rights still have to pass through the permission gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 2: Optimistic Lock Injection (Edit only)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tools/executor.py
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Edit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parameters&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject_optimistic_lock_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a Read tool executes, the framework caches the file's &lt;code&gt;mtime + size&lt;/code&gt;. Before an Edit executes, the framework automatically injects the cached &lt;code&gt;expected_mtime_ms&lt;/code&gt; into the parameters. If the file was externally modified between the Read and the Edit, the Edit detects the mtime mismatch and returns a &lt;code&gt;CONFLICT&lt;/code&gt; error instead of silently overwriting.&lt;/p&gt;

&lt;p&gt;This solves a subtle problem: the model reads a file and decides to modify it, but the file may have been changed by the user or another tool in the interim. The optimistic lock makes this class of "write clobber" detectable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate 3: Circuit Breaker Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tools/circuit_breaker.py
# Three states: CLOSED (normal) → OPEN (disabled) → HALF_OPEN (probe after cooldown)
&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_circuit_open_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After 3 consecutive failures (default threshold), the circuit breaker opens and the tool is temporarily disabled for 300 seconds. This prevents a broken tool from retrying repeatedly, consuming tokens and step budget. After the cooldown period, the breaker enters &lt;code&gt;HALF_OPEN&lt;/code&gt; — it lets one probe through: success restores &lt;code&gt;CLOSED&lt;/code&gt;, failure resets the timer and continues &lt;code&gt;OPEN&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  tool.run() and the Exception Safety Net
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool&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;parameters&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolResult&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# all uncaught exceptions are caught here, converted to EXECUTION_ERROR ToolResult
&lt;/span&gt;    &lt;span class="c1"&gt;# guarantees no exception propagates upward; loop always receives a ToolResult
&lt;/span&gt;    &lt;span class="c1"&gt;# a single tool crash cannot interrupt the entire agent
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ToolResult&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="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EXECUTION_ERROR&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;Any internal exception from the tool is caught here and converted into a standard &lt;code&gt;ToolResult&lt;/code&gt;. This is the tool pipeline's last safety net: &lt;strong&gt;a single tool crashing doesn't crash the whole loop&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Result Post-Processing: Two-Layer Byte Budget
&lt;/h2&gt;

&lt;p&gt;After a tool executes, its result passes through three post-processing steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;execution result
  → _normalize_empty_result()   empty output gets a placeholder text
  → _apply_observation_limit()  initial truncation by line/byte count
  → _apply_result_budget()      two-layer byte budget final truncation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two-layer budget&lt;/strong&gt; prevents tool output from overflowing the context window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1 (single-tool cap, default 50KB):
    single tool output &amp;gt; 50KB → force_truncate → full content spills to disk file
    result includes the file path; model can reference it on demand

Layer 2 (batch total cap, default 200KB):
    total after per-tool truncation still &amp;gt; 200KB → sort by size descending,
    force-truncate one by one until total is under the limit
    greedy strategy: truncate the largest first to minimize truncation count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results already truncated by Layer 1 are marked &lt;code&gt;replaced=True&lt;/code&gt; in metadata; Layer 2 skips them to avoid double-truncating the same result.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Lifecycle Events: Full Observability
&lt;/h2&gt;

&lt;p&gt;Every tool call passes through four lifecycle states, all emitted as events to the trace/transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requested → started → completed / failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;requested&lt;/code&gt;: model requested the tool (recorded before argument parsing, regardless of outcome)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;started&lt;/code&gt;: passed permission check, entering actual execution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;completed&lt;/code&gt;: execution succeeded (including partial status)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed&lt;/code&gt;: execution failed (including permission denial, circuit break, exception)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These four states let the trace fully reconstruct "the story of a tool call": was it requested? Was it denied or did it actually run? How long did it take? What was the result?&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Highlights
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Dispatch and execution are separated&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Orchestrator doesn't care how a single tool executes — only "how to schedule this batch." Executor doesn't care how many tools are concurrent — only "is this one tool safe." Separation of responsibilities makes both layers independently testable and evolvable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Writes are forced serial; order matches model request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After concurrent execution, results are force-reordered by original offset — this isn't just an ordering concern, it's a semantic one. When the model issues &lt;code&gt;[Edit A, Edit B]&lt;/code&gt;, it expects A before B. Wrong write order in history will confuse the model on the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multiple safety boundaries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Permission blacklist (rule layer) → optimistic lock (data layer) → circuit breaker (availability layer) → exception safety net (stability layer). Each layer solves one class of problem, with no overlap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design choice&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Engineering value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency strategy&lt;/td&gt;
&lt;td&gt;Read-only concurrent, writes serial&lt;/td&gt;
&lt;td&gt;Safe, no race conditions; Read/Grep/Glob concurrency improves throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering guarantee&lt;/td&gt;
&lt;td&gt;Offset-based reordering&lt;/td&gt;
&lt;td&gt;Model semantics not scrambled by concurrent execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission design&lt;/td&gt;
&lt;td&gt;Fail-closed + regex blacklist&lt;/td&gt;
&lt;td&gt;Dangerous commands never reach tool.run()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimistic lock&lt;/td&gt;
&lt;td&gt;Read caches mtime, Edit auto-injects&lt;/td&gt;
&lt;td&gt;Write clobber is detectable, not silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circuit breaker&lt;/td&gt;
&lt;td&gt;Three-state + cooldown&lt;/td&gt;
&lt;td&gt;Broken tools don't retry and drain step budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Byte budget&lt;/td&gt;
&lt;td&gt;Single-tool + batch total, two layers&lt;/td&gt;
&lt;td&gt;Tool output can't overflow the context window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  About the Source Code
&lt;/h2&gt;

&lt;p&gt;All analysis in this series is based on the open-source project &lt;a href="https://github.com/chendongqi/MyCodeAgent" rel="noopener noreferrer"&gt;MyCodeAgent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source code includes comments at key locations aligned with the walkthrough in each post — you can read the articles alongside the code, or clone it directly to run, modify, and extend, building your own agent on top of it.&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/chendongqi/MyCodeAgent
&lt;span class="nb"&gt;cd &lt;/span&gt;MyCodeAgent
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# fill in your LLM API key&lt;/span&gt;
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Explore &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills, each validated against real enterprise workflows. No hype, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;personal site&lt;/a&gt; for more insights and interesting products.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>harnessengineering</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>One Open Source Project a Day (No. 164): next-forge — A Production-Grade Next.js SaaS Starter</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:36:57 +0000</pubDate>
      <link>https://dev.to/wonderlab/one-open-source-project-a-day-no-164-next-forge-a-production-grade-nextjs-saas-starter-3bn1</link>
      <guid>https://dev.to/wonderlab/one-open-source-project-a-day-no-164-next-forge-a-production-grade-nextjs-saas-starter-3bn1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Stop wasting time on boilerplate. Start building."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the &lt;strong&gt;164th&lt;/strong&gt; article in the "One Open Source Project a Day" series.&lt;/p&gt;

&lt;p&gt;Two hundred is a number worth pausing on. Over these 200 projects we have covered infrastructure engines, AI frameworks, developer tooling, content platforms — the whole spectrum of open-source creativity. For article 200, I wanted to pick something that represents the journey from idea to product in the most direct way possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;next-forge&lt;/strong&gt; is exactly that.&lt;/p&gt;

&lt;p&gt;Its positioning is unambiguous: a &lt;strong&gt;production-grade Next.js SaaS starter template&lt;/strong&gt;. Not a toy demo. Not a "you still need to wire up 10 more libraries" half-finished scaffold. A battle-tested, full-stack foundation that is ready to deploy on day one. Created by Vercel engineer Hayden Bleasel, drawing on more than a decade of building web applications, with 7,700+ Stars and an MIT license.&lt;/p&gt;

&lt;p&gt;If you have ever built a SaaS or an indie product, you know the pain: just getting authentication, payments, email, a database, error monitoring, and analytics all configured can eat a full week — and none of it has anything to do with your actual business logic. That is exactly the problem next-forge solves.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Will Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The five design principles of next-forge (Fast / Cheap / Opinionated / Modern / Safe) and the engineering philosophy behind them&lt;/li&gt;
&lt;li&gt;How a Monorepo with 7 apps and 20+ packages is organized&lt;/li&gt;
&lt;li&gt;The design rationale behind key integrations: Clerk (auth), Stripe (payments), Prisma + Neon (database)&lt;/li&gt;
&lt;li&gt;How to initialize a production-ready SaaS project with a single command&lt;/li&gt;
&lt;li&gt;The essential difference between next-forge and other SaaS starter templates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Basic familiarity with Next.js (App Router knowledge is a plus)&lt;/li&gt;
&lt;li&gt;Comfortable reading TypeScript&lt;/li&gt;
&lt;li&gt;A rough idea of what a Monorepo is ("multiple projects in one repository") — no deep expertise required&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What It Is
&lt;/h3&gt;

&lt;p&gt;next-forge is a &lt;strong&gt;Turborepo Monorepo template&lt;/strong&gt; designed for developers who want to ship a SaaS or full-stack web application without spending weeks on infrastructure decisions.&lt;/p&gt;

&lt;p&gt;It is not just a Next.js app scaffold. It is a &lt;strong&gt;curated collection of technology decisions&lt;/strong&gt;, each integration pre-selected, pre-configured, and verified to work together. You get a working foundation; you supply the business logic.&lt;/p&gt;

&lt;p&gt;The target audience is precise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indie developers and one-person companies&lt;/strong&gt; who want to validate ideas fast and not drown in configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical leads on small teams&lt;/strong&gt; who need a proven architectural baseline rather than having to debate "which auth library should we use"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experienced full-stack engineers&lt;/strong&gt; who want a production-ready template they can actually deploy, not just reference&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Author / Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary author&lt;/strong&gt;: &lt;a href="https://haydenbleasel.com" rel="noopener noreferrer"&gt;Hayden Bleasel&lt;/a&gt;, a design engineer from Australia based in San Francisco&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organization&lt;/strong&gt;: Vercel (now at OpenAI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background&lt;/strong&gt;: 10+ years building web applications; also maintains Ultracite and several other open-source projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin&lt;/strong&gt;: Distilled from real SaaS development experience — not a teaching exercise&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Stats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ GitHub Stars: &lt;strong&gt;7,700+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🍴 Forks: &lt;strong&gt;712+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;📄 License: MIT&lt;/li&gt;
&lt;li&gt;🌐 Website: &lt;a href="https://next-forge.com" rel="noopener noreferrer"&gt;next-forge.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 Docs: &lt;a href="https://next-forge.com/docs" rel="noopener noreferrer"&gt;next-forge.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎮 Demo: &lt;a href="https://demo.next-forge.com" rel="noopener noreferrer"&gt;demo.next-forge.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Problem It Solves
&lt;/h3&gt;

&lt;p&gt;next-forge addresses the &lt;strong&gt;foundation problem&lt;/strong&gt; in SaaS development: all the "must-haves" you need to wire up before you write a single line of business logic.&lt;/p&gt;

&lt;p&gt;A typical SaaS needs user authentication, billing, a database, transactional email, error tracking, product analytics, documentation, and more. next-forge has all of these pre-integrated and ready to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without next-forge:
idea → pick auth library → configure Stripe → choose ORM → integrate email →
set up error tracking → scaffold docs → ... → two weeks later you finally
start on actual business logic

With next-forge:
npx next-forge@latest init → fill in environment variables → write business logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage Scenarios
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rapid SaaS launch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to validate a subscription product's market fit without spending two weeks on infrastructure. next-forge ships with subscription plans, a checkout flow, and user management — you can have a working "sign up → pay → dashboard" path in a day.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Indie side projects&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time after hours is precious. next-forge offloads all the "not worth doing yourself" configuration work so every available hour goes toward product differentiation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Team project architectural baseline&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need to bikeshed "which auth provider should we use" — next-forge's technology choices are battle-tested and can serve as the team's starting point for any new project.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Learning Next.js production patterns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For developers who want to understand what Next.js App Router looks like in a real production codebase, next-forge is a genuine reference — not a tutorial demo, but code that is actually deployable.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick Start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Initialize a new project with one command&lt;/span&gt;
npx next-forge@latest init

&lt;span class="c"&gt;# Requirements: Node.js 20+, Bun (recommended), Stripe CLI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After initialization:&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;# Install dependencies&lt;/span&gt;
bun &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Start all apps in development mode simultaneously&lt;/span&gt;
bun run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;localhost:3000&lt;/code&gt; — Main application (authenticated user interface)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost:3001&lt;/code&gt; — Marketing website (public-facing)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost:3002&lt;/code&gt; — API service&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost:3003&lt;/code&gt; — Email template preview server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost:3004&lt;/code&gt; — Project documentation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;localhost:6006&lt;/code&gt; — Storybook component library&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Core Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Full Authentication System (Clerk)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email login and social login (Google, GitHub, etc.) out of the box&lt;/li&gt;
&lt;li&gt;Routes automatically grouped by authentication state: &lt;code&gt;(authenticated)/&lt;/code&gt; and &lt;code&gt;(unauthenticated)/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Organization/team support for B2B SaaS scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Subscription Billing (Stripe)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscription plans, one-time payments, and usage-based billing supported&lt;/li&gt;
&lt;li&gt;Webhook handling pre-configured in &lt;code&gt;apps/api&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Customer Portal for self-service subscription management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Type-Safe Database (Prisma + Neon)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma ORM provides end-to-end type-safe queries&lt;/li&gt;
&lt;li&gt;Defaults to Neon (serverless PostgreSQL), pay only for what you use&lt;/li&gt;
&lt;li&gt;Callable directly from React Server Components — no separate API layer needed
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Query directly in a Server Component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/database&lt;/span&gt;&lt;span class="dl"&gt;'&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;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;&lt;strong&gt;4. Email System (Resend + React Email)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write email templates as React components — real JSX, not HTML strings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/email&lt;/code&gt; provides a live preview server&lt;/li&gt;
&lt;li&gt;Resend handles delivery with high deliverability rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Observability (Sentry + BetterStack)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sentry for error tracking and performance monitoring&lt;/li&gt;
&lt;li&gt;BetterStack for log aggregation and alerting&lt;/li&gt;
&lt;li&gt;Both pre-configured; effective from the moment you deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Product Analytics (PostHog + Google Analytics)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostHog for product analytics, feature flags, and A/B testing&lt;/li&gt;
&lt;li&gt;Google Analytics for traffic metrics&lt;/li&gt;
&lt;li&gt;Both wrapped in a unified &lt;code&gt;@repo/analytics&lt;/code&gt; package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;7. Security (Arcjet)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Bot detection&lt;/li&gt;
&lt;li&gt;Email validation to block fake signups&lt;/li&gt;
&lt;li&gt;Automatically disabled in development so local debugging is unaffected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;8. Real-Time Collaboration (Liveblocks)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live Cursors&lt;/li&gt;
&lt;li&gt;Avatar Stacks&lt;/li&gt;
&lt;li&gt;Useful for SaaS products with collaborative features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;9. Internationalization (Languine)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;i18n solution pre-integrated and ready to use&lt;/li&gt;
&lt;li&gt;Translation resources managed centrally in &lt;code&gt;@repo/internationalization&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;10. Push Notifications (Knock)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In-app notifications, email notifications, and SMS notifications unified in one service&lt;/li&gt;
&lt;li&gt;Visual notification workflow editor&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Competitive Comparison
&lt;/h3&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;next-forge&lt;/th&gt;
&lt;th&gt;create-t3-app&lt;/th&gt;
&lt;th&gt;Manual Setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integrations included&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20+ services pre-wired&lt;/td&gt;
&lt;td&gt;Core stack only, rest manual&lt;/td&gt;
&lt;td&gt;Everything manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-app architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 7 independent apps&lt;/td&gt;
&lt;td&gt;❌ Single app&lt;/td&gt;
&lt;td&gt;Design it yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Production-validated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Real SaaS background&lt;/td&gt;
&lt;td&gt;✅ Widely used&lt;/td&gt;
&lt;td&gt;Depends on your experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full docs site&lt;/td&gt;
&lt;td&gt;✅ Active community&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payment integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Stripe pre-configured&lt;/td&gt;
&lt;td&gt;❌ Not included&lt;/td&gt;
&lt;td&gt;Manual integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Sentry + BetterStack&lt;/td&gt;
&lt;td&gt;❌ Not included&lt;/td&gt;
&lt;td&gt;Manual integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Collaboration features&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Liveblocks&lt;/td&gt;
&lt;td&gt;❌ Not included&lt;/td&gt;
&lt;td&gt;Manual integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to first run&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One command&lt;/td&gt;
&lt;td&gt;One command&lt;/td&gt;
&lt;td&gt;Days to weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why choose next-forge?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero decision cost&lt;/strong&gt;: Every domain (auth / payments / database) has already been decided for you, with a solution that is verified to work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a demo — a production template&lt;/strong&gt;: Every integrated service is something real SaaS products actually use in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monorepo architecture&lt;/strong&gt;: Marketing site, app, docs, and email templates are separate and independently deployable&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture: Why a Monorepo
&lt;/h3&gt;

&lt;p&gt;next-forge's choice of Turborepo Monorepo over a single-app repository has clear engineering reasoning.&lt;/p&gt;

&lt;p&gt;A complete SaaS typically involves several distinct but related "applications":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;marketing website&lt;/strong&gt; (for potential users, SEO-first)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;main application&lt;/strong&gt; (for paying users, feature-first)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API service&lt;/strong&gt; (for webhooks, cron jobs, etc.)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;documentation site&lt;/strong&gt; (for users and developers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these in one repository while maintaining independent deployability and sharing auth logic, UI components, and type definitions is precisely the use case Monorepos were built for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;next-forge/
├── apps/
│   ├── app/         ← Main application (localhost:3000)
│   │   └── env.ts   ← Aggregates all env vars this app depends on
│   ├── web/         ← Marketing website (localhost:3001)
│   ├── api/         ← API + Webhook handling (localhost:3002)
│   ├── email/       ← Email template preview server (localhost:3003)
│   ├── docs/        ← Mintlify documentation (localhost:3004)
│   ├── storybook/   ← Component library (localhost:6006)
│   └── studio/      ← Prisma visual database editor (localhost:3005)
│
└── packages/
    ├── auth/              ← Clerk authentication wrapper
    ├── database/          ← Prisma + Neon client
    ├── payments/          ← Stripe wrapper
    ├── email/             ← React Email templates
    ├── analytics/         ← PostHog + GA unified entry point
    ├── observability/     ← Sentry + BetterStack
    ├── security/          ← Arcjet protection
    ├── collaboration/     ← Liveblocks real-time features
    ├── notifications/     ← Knock notifications
    ├── internationalization/ ← i18n management
    ├── feature-flags/     ← Vercel Flags + PostHog
    ├── storage/           ← Vercel Blob file storage
    ├── ui/                ← Shared UI components (built on shadcn/ui)
    ├── design-system/     ← Fonts, themes, global styles
    └── typescript-config/ ← Shared TypeScript configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key design rule&lt;/strong&gt;: apps must not depend on each other. &lt;code&gt;web&lt;/code&gt; cannot import modules from &lt;code&gt;app&lt;/code&gt;. Shared code lives exclusively in &lt;code&gt;packages/&lt;/code&gt;, referenced as &lt;code&gt;@repo/xxx&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Correct: shared logic via package&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/database&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Wrong: cross-app imports&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UserCard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../app/components/UserCard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This boundary is enforced in CI via Turborepo's &lt;code&gt;boundaries&lt;/code&gt; check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run boundaries  &lt;span class="c"&gt;# detects cross-app dependency violations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Environment Variable Management: the &lt;code&gt;env.ts&lt;/code&gt; Pattern
&lt;/h3&gt;

&lt;p&gt;next-forge uses an elegant solution to the "scattered environment variables" problem: every app has an &lt;code&gt;env.ts&lt;/code&gt; file at its root that aggregates and validates all the environment variables the app needs.&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;// apps/app/env.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;authEnv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/auth/env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;databaseEnv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/database/env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;paymentsEnv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@repo/payments/env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;authEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;databaseEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;paymentsEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;&lt;span class="o"&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;Each &lt;code&gt;packages/xxx/env.ts&lt;/code&gt; uses &lt;code&gt;@t3-oss/env-nextjs&lt;/code&gt; for runtime validation — if a required variable is missing, the app fails at startup rather than at runtime. This pattern answers the perennial question: &lt;strong&gt;"What environment variables do I need to configure?"&lt;/strong&gt; — just read &lt;code&gt;apps/app/env.ts&lt;/code&gt; and the answer is complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Design Philosophy: Self-Contained, Replaceable
&lt;/h3&gt;

&lt;p&gt;Every package in next-forge follows one principle: &lt;strong&gt;self-contained, exposing a clean interface, with an implementation that can be swapped at any time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;@repo/auth&lt;/code&gt; as an example:&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;// packages/auth/index.ts — the public interface&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./clerk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Consumers are completely unaware of Clerk's existence.&lt;/span&gt;
&lt;span class="c1"&gt;// If you ever need to replace Clerk, only this package changes.&lt;/span&gt;
&lt;span class="c1"&gt;// App code remains untouched.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design minimizes the cost of migrating between vendors. If Clerk stops working for you, switching to Auth.js or Supabase Auth is a change to one package, not a project-wide refactor.&lt;/p&gt;

&lt;h3&gt;
  
  
  End-to-End Type Safety
&lt;/h3&gt;

&lt;p&gt;next-forge maintains type safety from database schema to frontend UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prisma Schema (database structure definition)
    ↓ prisma generate
Prisma Client (type-safe query functions)
    ↓ import { database } from '@repo/database'
Server Component (direct call, no API layer needed)
    ↓ returns data to
Client Component (TypeScript types automatically inferred)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No type breakage anywhere along the chain. Change a database field and TypeScript will immediately tell you at compile time exactly where the rest of the code needs to follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Options
&lt;/h3&gt;

&lt;p&gt;next-forge supports three deployment approaches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; (recommended)&lt;/td&gt;
&lt;td&gt;Fast launch, zero ops overhead, pay-per-use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vercel-like experience with different preferences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker self-hosted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full infrastructure control or compliance requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vercel deployment is the simplest path — each app connects independently to its subdirectory in the GitHub repository, with &lt;code&gt;turbo.json&lt;/code&gt; already defining the correct build dependency graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Links &amp;amp; Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Official Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/vercel/next-forge" rel="noopener noreferrer"&gt;https://github.com/vercel/next-forge&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://next-forge.com/docs" rel="noopener noreferrer"&gt;https://next-forge.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://next-forge.com" rel="noopener noreferrer"&gt;https://next-forge.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎮 &lt;strong&gt;Demo&lt;/strong&gt;: &lt;a href="https://demo.next-forge.com" rel="noopener noreferrer"&gt;https://demo.next-forge.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Issues&lt;/strong&gt;: &lt;a href="https://github.com/vercel/next-forge/issues" rel="noopener noreferrer"&gt;github.com/vercel/next-forge/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Related Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://turbo.build/repo/docs" rel="noopener noreferrer"&gt;Turborepo Documentation&lt;/a&gt; — understanding the Monorepo build system&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app" rel="noopener noreferrer"&gt;Next.js App Router Docs&lt;/a&gt; — mastering file-system routing and React Server Components&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ui.shadcn.com" rel="noopener noreferrer"&gt;shadcn/ui&lt;/a&gt; — the component library underpinning next-forge's UI layer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Five design principles&lt;/strong&gt; (Fast / Cheap / Opinionated / Modern / Safe) guide every technology choice in the template&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 independent apps + 20+ shared packages&lt;/strong&gt; in a Monorepo — clear responsibilities, independently deployable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;env.ts&lt;/code&gt; pattern&lt;/strong&gt; provides centralized environment variable management with runtime validation, eliminating "what variables do I need?" as a question&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-contained package design&lt;/strong&gt; makes vendor replacement a single-package change, not a project-wide migration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One command to initialize&lt;/strong&gt; a complete SaaS skeleton with auth, payments, database, email, and monitoring already wired together&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Who This Is For
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indie developers validating SaaS ideas&lt;/strong&gt;: next-forge handles the foundation so you can spend 100% of your time on product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small teams that don't want to reinvent the wheel&lt;/strong&gt;: start from a proven architecture and focus on what makes your product different&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers learning Next.js production patterns&lt;/strong&gt;: next-forge is one of the rare "actually production-grade" reference architectures, not a tutorial&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One-Line Verdict
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are building a SaaS or an indie product, next-forge is not "an option worth considering" — it is "the starting point you should default to." Every hour you would have spent on configuration is already handled; everything left is genuinely yours to build.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Check out &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find more useful knowledge and interesting products on my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;Homepage&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>nextjs</category>
      <category>sass</category>
      <category>turborepo</category>
    </item>
    <item>
      <title>Code Agent Anatomy (12): Harness Design Part 2 — Context Engineering</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Thu, 27 Aug 2026 01:50:35 +0000</pubDate>
      <link>https://dev.to/wonderlab/code-agent-anatomy-12-harness-design-part-2-context-engineering-1m2p</link>
      <guid>https://dev.to/wonderlab/code-agent-anatomy-12-harness-design-part-2-context-engineering-1m2p</guid>
      <description>&lt;h2&gt;
  
  
  The Loose End from Last Time
&lt;/h2&gt;

&lt;p&gt;The previous post (Part 11) dissected the control flow: a single main loop, an immutable state machine, and completion gates. One line of code was glossed over:&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="c1"&gt;# loop.py _prepare_step_context()
&lt;/span&gt;&lt;span class="n"&gt;model_view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_model_view&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model_view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;code&gt;build_model_view()&lt;/code&gt; actually does is far more than "fetch the message history." A long-running agent accumulates an ever-growing conversation history — exceed the token limit and it errors out. The subtler point: even &lt;em&gt;before&lt;/em&gt; hitting the limit, the agent must decide each step — what exactly should get sent to the model? System prompt, history, tool outputs… cramming everything in is no better than careful curation.&lt;/p&gt;

&lt;p&gt;This post dissects that pipeline: from "sensing we're almost out of headroom" to "deciding what the model gets to see."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Conclusion First
&lt;/h2&gt;

&lt;p&gt;MyCodeAgent breaks this problem into three cleanly separated concerns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;One-liner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What History is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HistoryManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Append-only fact log — messages are never deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What the model sees&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ModelView&lt;/code&gt; + &lt;code&gt;ProjectionBuilder&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Read-time projection, collapsing old messages post-compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When to compact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ContextBudgetPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dual-source estimation + threshold decision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three layers, clear responsibilities, no cross-contamination. Compaction doesn't mean deleting history — it means reading history a different way.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. History Is a Fact Log — Never Modified
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# runtime/history.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HistoryManager&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;__init__&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="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;_messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# append-only, never deleted
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_user&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_assistant&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_tool_result&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="p"&gt;...)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_messages&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&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;_messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# returns a copy — caller cannot mutate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HistoryManager&lt;/code&gt; is a &lt;strong&gt;pure append-only fact list&lt;/strong&gt;. It has no knowledge of "compaction," and no message ever disappears because of it.&lt;/p&gt;

&lt;p&gt;This matters: it means that when an agent crashes, the complete history can be reconstructed from the transcript — nothing is ever lost to a compaction operation (see Part 10).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Compaction Decision: How to Tell "We're Almost Full"
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# runtime/context/budget.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContextBudgetPolicy&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;should_compact&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="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_usage_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CompactDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# default: 128000 × 0.8 = 102400 tokens
&lt;/span&gt;        &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context_window&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compression_threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Source 1: estimate from message content (character count // 3)
&lt;/span&gt;        &lt;span class="n"&gt;estimated_from_messages&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="nf"&gt;estimate_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Source 2: actual usage from last LLM call + new input
&lt;/span&gt;        &lt;span class="n"&gt;estimated_from_usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_usage_tokens&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending_input&lt;/span&gt; &lt;span class="ow"&gt;or&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="mi"&gt;3&lt;/span&gt;

        &lt;span class="c1"&gt;# take the more pessimistic estimate
&lt;/span&gt;        &lt;span class="n"&gt;estimated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimated_from_messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estimated_from_usage&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;message_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CompactDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages_not_enough&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;estimated&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CompactDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;below_threshold&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CompactDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;threshold_exceeded&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;Two design details worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Why not count tokens precisely?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Exact token counting requires calling a tokenizer, which is model-specific — different models tokenize differently, and calling it has overhead. &lt;code&gt;character count // 3&lt;/code&gt; is a conservative approximation (an average across mixed Chinese/English text). Better to compact early than to react after an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Why take the max of two sources?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Estimating purely from the message list underestimates — &lt;code&gt;tool_calls&lt;/code&gt; JSON, &lt;code&gt;tool_name&lt;/code&gt; fields, and other metadata all consume tokens but don't live in &lt;code&gt;content&lt;/code&gt;. Meanwhile, &lt;code&gt;last_usage_tokens&lt;/code&gt; is the actual number the LLM consumed on the previous step — more accurate, but it only knows the past, not the current new input. Taking &lt;code&gt;max&lt;/code&gt; is an engineering habit of pessimistic estimation: compact a little early rather than not at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How Compaction Works: Non-Destructive Checkpoints
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# runtime/context/compact.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContextCompactor&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;compact&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;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&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="c1"&gt;# 1. segment messages into turns at user-message boundaries
&lt;/span&gt;        &lt;span class="n"&gt;rounds&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;round_segmenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. retain the most recent min_retain_rounds turns verbatim (default: 10)
&lt;/span&gt;        &lt;span class="n"&gt;retain_start_round&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min_rounds&lt;/span&gt;
        &lt;span class="n"&gt;retain_start_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;retain_start_round&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;start_idx&lt;/span&gt;
        &lt;span class="n"&gt;messages_to_compact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_messages&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;retain_start_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. call LLM to summarize the "old" messages
&lt;/span&gt;        &lt;span class="n"&gt;summary&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="nf"&gt;summary_generator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages_to_compact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 4. store summary + split-point in CompactStore
&lt;/span&gt;        &lt;span class="n"&gt;checkpoint&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;compact_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;retain_start_idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retain_start_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# note: source_messages is never modified
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword here is &lt;strong&gt;non-destructive&lt;/strong&gt;: compaction does not modify any message in &lt;code&gt;HistoryManager&lt;/code&gt;. It only writes a checkpoint to &lt;code&gt;CompactStore&lt;/code&gt; recording "what the summary says" and "which index marks the start of verbatim history."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RoundSegmenter&lt;/code&gt; divides the message list into turns at user-message boundaries. Compaction granularity is always a complete turn — splitting a user and assistant message from the same exchange would break semantic continuity.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Read-Time Projection: What the Model Sees ≠ What's Stored
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# runtime/context/projection.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectionBuilder&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;project&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;source_messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProjectionResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;checkpoint&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;compact_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_checkpoint&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;checkpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# no compaction yet: projection = full history
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ProjectionResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projection_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# checkpoint exists: projection = [summary message] + source[retain_start_idx:]
&lt;/span&gt;        &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ProjectionResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;retain_start_idx&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt;
            &lt;span class="n"&gt;projection_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compact_checkpoint&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;This is the single most important step in the whole context engineering stack: &lt;strong&gt;read-time projection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;source_messages&lt;/code&gt; has 200 entries and the checkpoint's &lt;code&gt;retain_start_idx&lt;/code&gt; is 120, the projected result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[summary (covering the first 120 messages)] + [source[120:] — the most recent 80 messages verbatim]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model sees 81 messages (1 summary + 80 verbatim), but &lt;code&gt;HistoryManager&lt;/code&gt; still holds all 200 originals.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. MessageNormalizer: Format Conversion
&lt;/h2&gt;

&lt;p&gt;History stores &lt;code&gt;Message&lt;/code&gt; objects (the runtime's internal format); the LLM API expects a list of OpenAI-style dicts. &lt;code&gt;MessageNormalizer&lt;/code&gt; handles that conversion:&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="c1"&gt;# runtime/context/normalizer.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessageNormalizer&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;_normalize_one&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;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Message&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;list&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&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="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="nf"&gt;_assistant_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="c1"&gt;# restores tool_calls
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&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="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="nf"&gt;_tool_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;        &lt;span class="c1"&gt;# links tool_call_id
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# summary becomes a system message, prepended to history
&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;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;## Archived History Summary&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;summary&lt;/code&gt;-role message is converted to a &lt;code&gt;system&lt;/code&gt;-role message before being sent to the model — the model treats it as background context rather than conversational history. That's a subtle but intentional semantic choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. ModelView: What the Model Saw on This Step
&lt;/h2&gt;

&lt;p&gt;All of the above converges in &lt;code&gt;ContextEngine.build_model_view()&lt;/code&gt;, which produces a &lt;code&gt;ModelView&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="c1"&gt;# runtime/context/engine.py (core logic of build_model_view)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_model_view&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="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ModelView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Bounded history projection (projection + normalize)
&lt;/span&gt;    &lt;span class="n"&gt;source_messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;history_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_messages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;projection&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;projection_builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;history_messages&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;normalizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. System layer (agent persona + tool contracts + project rules)
&lt;/span&gt;    &lt;span class="n"&gt;system_messages&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;context_builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_system_messages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Dynamic system: Session Memory (cross-run summaries)
&lt;/span&gt;    &lt;span class="n"&gt;dynamic_messages&lt;/span&gt; &lt;span class="o"&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;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rendered&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_memory&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Assemble: system first, history last
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dynamic_messages&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ModelView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system_message_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;history_message_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;source_message_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_message_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;projection_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projection_mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final message order sent to the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Constitution][Tool Contracts][Code Law]   ← system layer (Part 04)
[Session Memory?]                           ← cross-run summary (Part 10)
[summary? + most recent N turns verbatim]  ← history projection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ModelView&lt;/code&gt; is more than a message list — it carries metadata: &lt;code&gt;projection_mode&lt;/code&gt; (was it compacted?), &lt;code&gt;source_message_count&lt;/code&gt; vs. &lt;code&gt;history_message_count&lt;/code&gt; (before vs. after compaction counts). These feed the trace, letting you observe "how much the model actually saw this step."&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Two Trigger Paths
&lt;/h2&gt;

&lt;p&gt;Two places in the loop can trigger compaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Per-step (proactive)              On model call (reactive)
        ↓                                  ↓
compact_if_needed()            classify exception → PROMPT_TOO_LONG
  → budget estimate &amp;gt; threshold  → reactive_compact()
  → compactor.compact()          → compactor.compact()
  → record checkpoint             → rebuild model view, inner continue retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proactive compaction&lt;/strong&gt; (&lt;code&gt;compact_if_needed&lt;/code&gt;) runs before each step, prior to assembling the model view. If the estimate says we're almost out of headroom, compact first, then assemble — the model gets the compacted version on the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reactive compaction&lt;/strong&gt; (&lt;code&gt;reactive_compact&lt;/code&gt;) is the fallback. If the estimate didn't trigger but the model actually returns &lt;code&gt;PROMPT_TOO_LONG&lt;/code&gt;, force-compact immediately, rebuild the model view, and &lt;code&gt;continue&lt;/code&gt; to retry the current step. There's a retry limit; if exhausted, the agent takes the &lt;code&gt;MODEL_RECOVERY_FAILED&lt;/code&gt; termination path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Highlights
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. History and ModelView are separate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HistoryManager&lt;/code&gt; is an immutable fact log; &lt;code&gt;ModelView&lt;/code&gt; is a per-step ephemeral view. Because they're separate, compaction never loses history, crashes can be fully recovered, and during debugging you can compare "what was stored" against "what the model actually saw."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read-time projection, not write-time deletion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compaction results are stored as a checkpoint (summary + split index), folded in dynamically each time history is read. This is safer than "delete messages on compact" — you can roll back at any time (clear the checkpoint and you're back to full history), and a compaction bug can never corrupt history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Dual-path safety net&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Proactive estimation + reactive exception catching: both can trigger compaction. Even if the estimation algorithm has error, the agent self-heals on a model error rather than crashing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design choice&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Engineering value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;History storage&lt;/td&gt;
&lt;td&gt;Append-only fact log&lt;/td&gt;
&lt;td&gt;Compaction never loses history; full crash recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction method&lt;/td&gt;
&lt;td&gt;Read-time projection + checkpoint&lt;/td&gt;
&lt;td&gt;Rollback-safe; compaction bugs can't corrupt history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token estimation&lt;/td&gt;
&lt;td&gt;Dual-source max&lt;/td&gt;
&lt;td&gt;Pessimistic; fires early rather than late&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger paths&lt;/td&gt;
&lt;td&gt;Proactive estimation + reactive exception catch&lt;/td&gt;
&lt;td&gt;Fallback when estimation fails; self-heals rather than crashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ModelView&lt;/td&gt;
&lt;td&gt;Message list + observable metadata&lt;/td&gt;
&lt;td&gt;Can compare "what was stored" vs. "what the model saw"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  About the Source Code
&lt;/h2&gt;

&lt;p&gt;All analysis in this series is based on the open-source project &lt;a href="https://github.com/chendongqi/MyCodeAgent" rel="noopener noreferrer"&gt;MyCodeAgent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source code includes comments at key locations aligned with the walkthrough in each post — you can read the articles alongside the code, or clone it directly to run, modify, and extend, building your own agent on top of it.&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/chendongqi/MyCodeAgent
&lt;span class="nb"&gt;cd &lt;/span&gt;MyCodeAgent
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# fill in your LLM API key&lt;/span&gt;
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Explore &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills, each validated against real enterprise workflows. No hype, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;personal site&lt;/a&gt; for more insights and interesting products.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>harnessengineering</category>
      <category>contextengineering</category>
      <category>llm</category>
    </item>
    <item>
      <title>Open Source Project #163: OpenWiki — LangChain's Self-Maintaining Codebase Wiki CLI, Built for Agents</title>
      <dc:creator>WonderLab</dc:creator>
      <pubDate>Thu, 27 Aug 2026 01:49:26 +0000</pubDate>
      <link>https://dev.to/wonderlab/open-source-project-163-openwiki-langchains-self-maintaining-codebase-wiki-cli-built-for-43eh</link>
      <guid>https://dev.to/wonderlab/open-source-project-163-openwiki-langchains-self-maintaining-codebase-wiki-cli-built-for-43eh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"The self-maintaining wiki. Built for agents, explored by humans."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is &lt;strong&gt;article #163&lt;/strong&gt; in the "One Open Source Project a Day" series. Today's project is &lt;strong&gt;OpenWiki&lt;/strong&gt; — LangChain AI's open-source CLI for automatic codebase documentation generation and maintenance, 15,600 Stars, MIT license.&lt;/p&gt;

&lt;p&gt;OpenWiki addresses a problem every engineering team has: written documentation goes stale, but undocumented codebases leave AI agents disoriented. OpenWiki's solution: let an agent write the docs, let an agent maintain them. The output is a Markdown wiki that lives in your repository — agents can read it as memory, and humans can explore it through an interactive visualization.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You'll Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Two modes: the difference between a &lt;code&gt;code&lt;/code&gt; wiki (for a repository) and a &lt;code&gt;personal&lt;/code&gt; wiki (for personal knowledge)&lt;/li&gt;
&lt;li&gt;Grounded Claims: how material facts are tracked back to exact code lines&lt;/li&gt;
&lt;li&gt;Coding-agent integrations with Claude Code, Codex, and OpenCode&lt;/li&gt;
&lt;li&gt;Support for 13 model providers and 9 knowledge-source connectors&lt;/li&gt;
&lt;li&gt;Interactive node-graph visualization and static site export&lt;/li&gt;
&lt;li&gt;CI auto-update (GitHub Actions / GitLab CI / Bitbucket Pipelines)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Basic command-line experience&lt;/li&gt;
&lt;li&gt;Familiarity with LLM APIs (API keys, model names)&lt;/li&gt;
&lt;li&gt;Git workflow basics are helpful&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;OpenWiki is built on &lt;a href="https://github.com/langchain-ai/deepagentsjs" rel="noopener noreferrer"&gt;Deep Agents&lt;/a&gt;, LangChain's deep agent framework. It's not a "generate once and forget" documentation tool — it's a &lt;strong&gt;documentation lifecycle management system&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize&lt;/strong&gt;: the agent reads source code and generates a structured Markdown wiki&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track&lt;/strong&gt;: binds each material fact (Grounded Claims) to precise code lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt;: when code changes, checks which facts' "evidence" changed and rewrites only affected pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate&lt;/strong&gt;: maintains &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; at the repo root so coding agents can find and read the wiki&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Author / Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Organization&lt;/strong&gt;: LangChain AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Created&lt;/strong&gt;: 2026-06-22&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Stats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⭐ GitHub Stars: &lt;strong&gt;15,600+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🍴 Forks: 1,133+&lt;/li&gt;
&lt;li&gt;📄 License: MIT&lt;/li&gt;
&lt;li&gt;📅 Created: 2026-06-22&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requires Node.js 22 or newer&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; openwiki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate a Wiki for the Current Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
openwiki &lt;span class="nt"&gt;--init&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run walks you through selecting a model provider (default: OpenAI + &lt;code&gt;gpt-5.6-terra&lt;/code&gt;), entering your API key, and choosing a model. Documentation is written to &lt;code&gt;openwiki/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update an Existing Wiki
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Detect code changes, update only affected pages&lt;/span&gt;
openwiki &lt;span class="nt"&gt;--update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Launch the Visualizer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Open an interactive node graph in your browser&lt;/span&gt;
openwiki visualize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Two Modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Documents&lt;/th&gt;
&lt;th&gt;Writes to&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;code&lt;/strong&gt; (default)&lt;/td&gt;
&lt;td&gt;Current repository source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;openwiki/&lt;/code&gt; (inside the repo)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openwiki --init&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;personal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connected sources (Notion/Gmail/Slack, etc.)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.openwiki/wiki/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openwiki personal --init&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code mode&lt;/strong&gt; is built for teams: the wiki is committed alongside code, CI keeps it current, and &lt;code&gt;AGENTS.md&lt;/code&gt; + &lt;code&gt;CLAUDE.md&lt;/code&gt; give coding agents a direct entry point to the documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personal mode&lt;/strong&gt; is built for individuals: aggregates knowledge scattered across tools (Notion, email, X/Twitter, Hacker News…) into a local wiki that an AI agent structures into a knowledge graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Mechanism: Grounded Claims
&lt;/h2&gt;

&lt;p&gt;This is what sets OpenWiki apart from ordinary documentation generators.&lt;/p&gt;

&lt;p&gt;A typical documentation tool only tracks "when was this page last generated." OpenWiki goes further: &lt;strong&gt;it tracks every material fact in the documentation, down to the exact source lines that back it up.&lt;/strong&gt;&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Claim's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;structure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(stored&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;under&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;openwiki/.claims/)&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;span class="err"&gt;id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claim-abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;proposition:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AuthMiddleware returns 401 on failure without rethrowing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;evidence:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo://src/middleware/auth.ts#L40-L82"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;evidence_version:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git-sha-of-that-commit"&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;Claims cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Function and module behavior and responsibilities&lt;/li&gt;
&lt;li&gt;Architectural relationships and data flows&lt;/li&gt;
&lt;li&gt;Invariants and failure semantics&lt;/li&gt;
&lt;li&gt;Configuration requirements and security boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before an update, OpenWiki checks the evidence for every Claim &lt;strong&gt;before even deciding whether anything needs regenerating&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;openwiki --update execution order:
1. Check whether each Claim's evidence (to exact line numbers) has changed
2. If evidence changed → that page needs rewriting, regardless of how much code changed
3. If evidence unchanged → the page's content probably doesn't need updating
4. After a page completes, Claims are atomically persisted (no partial updates)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles a subtle correctness problem: lots of code can change while a key fact stays correct, or a small refactor can silently invalidate a documented invariant. Line-level evidence tracking makes update decisions much more precise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resumable Page-Job Architecture
&lt;/h2&gt;

&lt;p&gt;OpenWiki's generation flow is not a single monolithic batch — it's a stateful page queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;begin → submit_plan → next_page → submit_page → ... → finish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;begin&lt;/code&gt;: starts a new generation run, records run state to &lt;code&gt;openwiki/.run.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;submit_plan&lt;/code&gt;: submits the page plan (which topics need documentation)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next_page&lt;/code&gt; / &lt;code&gt;submit_page&lt;/code&gt;: works page by page, atomically persisting Claims on each completion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;finish&lt;/code&gt;: final verification, deletes &lt;code&gt;.run.json&lt;/code&gt;, marks the run complete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resumability&lt;/strong&gt;: if generation is interrupted mid-run (e.g., CI timeout), rerunning on the same checkout picks up where it left off without redoing completed pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI note&lt;/strong&gt;: ephemeral CI runners that start fresh on every run don't retain &lt;code&gt;.run.json&lt;/code&gt; after failure, so a failed run restarts from the beginning. Resume capability only works on persistent checkouts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coding-Agent Integrations: Let Claude Code Write the Docs
&lt;/h2&gt;

&lt;p&gt;OpenWiki can run inside Claude Code, Codex, or OpenCode, delegating repository research and writing to the coding agent while OpenWiki manages the Claims lifecycle and persistence.&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;# Install the integration for your coding agent&lt;/span&gt;
openwiki integrations &lt;span class="nb"&gt;install &lt;/span&gt;claude
openwiki integrations &lt;span class="nb"&gt;install &lt;/span&gt;codex
openwiki integrations &lt;span class="nb"&gt;install &lt;/span&gt;opencode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installation, restart the coding agent, open the repository, and ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initialize this repository's OpenWiki from the current source and tests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or to update an existing wiki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update this repository's OpenWiki for changes since its last successful run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integration exposes five operations: &lt;code&gt;openwiki_begin&lt;/code&gt;, &lt;code&gt;openwiki_submit_plan&lt;/code&gt;, &lt;code&gt;openwiki_next_page&lt;/code&gt;, &lt;code&gt;openwiki_submit_page&lt;/code&gt;, and &lt;code&gt;openwiki_finish&lt;/code&gt;. The coding agent submits a complete intended Claim set with each page; OpenWiki internally creates, updates, preserves, and retracts Claims — and &lt;strong&gt;refuses to call finish until the final state is fully durable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The advantage of this integration: it uses the coding agent's already-authenticated model session, so separate OpenWiki provider credentials are not required.&lt;/p&gt;




&lt;h2&gt;
  
  
  9 Knowledge-Source Connectors (personal mode)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connector&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom-mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any HTTP/stdio MCP server&lt;/td&gt;
&lt;td&gt;MCP config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git-repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local git repositories&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;notion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Notion pages&lt;/td&gt;
&lt;td&gt;OAuth (hosted MCP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gmail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gmail&lt;/td&gt;
&lt;td&gt;Google OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slack conversations&lt;/td&gt;
&lt;td&gt;Slack OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;X/Twitter timeline, bookmarks&lt;/td&gt;
&lt;td&gt;X OAuth 2.0 (PKCE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web-search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Web search via Tavily&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TAVILY_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hackernews&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HN feed + search APIs&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;langsmith&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LangSmith run traces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPENWIKI_LANGSMITH_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The LangSmith connector&lt;/strong&gt; is an exception: it's for &lt;code&gt;code&lt;/code&gt; mode, not &lt;code&gt;personal&lt;/code&gt;. It pulls recent LangSmith traces (tool calls, outcomes, latency) for chosen projects and injects that runtime context into the codebase wiki — so documentation reflects &lt;strong&gt;how the code actually behaves at runtime&lt;/strong&gt;, not just what the source says.&lt;/p&gt;

&lt;p&gt;The same connector can be configured multiple times as separate instances (e.g., two Web Search sources: one tracking AI research, another tracking NBA news), stored as &lt;code&gt;web-search-1&lt;/code&gt; and &lt;code&gt;web-search-2&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  13 Model Providers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI (default)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI (ChatGPT login)&lt;/td&gt;
&lt;td&gt;Browser OAuth, uses ChatGPT plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini (AI Studio)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GEMINI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini Enterprise (Vertex AI)&lt;/td&gt;
&lt;td&gt;Google ADC, keyless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Bedrock&lt;/td&gt;
&lt;td&gt;IAM credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;GitHub CLI session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPENROUTER_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nebius / Fireworks / Baseten / NVIDIA NIM&lt;/td&gt;
&lt;td&gt;Provider API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI-compatible (LiteLLM/Ollama/LM Studio)&lt;/td&gt;
&lt;td&gt;Base URL + key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Local model example with Ollama:&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="nv"&gt;OPENWIKI_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openai-compatible
&lt;span class="nv"&gt;OPENAI_COMPATIBLE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama
&lt;span class="nv"&gt;OPENAI_COMPATIBLE_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:11434/v1
&lt;span class="nv"&gt;OPENWIKI_MODEL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;llama3.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Interactive Visualization
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Open an interactive node graph in your local browser&lt;/span&gt;
openwiki visualize

&lt;span class="c"&gt;# Export as a static site (GitHub Pages, MkDocs, etc.)&lt;/span&gt;
openwiki visualize openwiki &lt;span class="nt"&gt;--export&lt;/span&gt; docs/openwiki-visualizer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visualization is a &lt;strong&gt;live node graph + Markdown reader&lt;/strong&gt; side by side: nodes are wiki pages, edges are links between them, clicking a node loads that page in the reader on the right.&lt;/p&gt;

&lt;p&gt;The static export contains &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;client.js&lt;/code&gt;, &lt;code&gt;styles.css&lt;/code&gt;, and &lt;code&gt;graph.json&lt;/code&gt; — deploy directly to any static host.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI Auto-Update
&lt;/h2&gt;

&lt;p&gt;Copy the example workflow file into your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/openwiki-update.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Runs on a schedule, checks for code changes,&lt;/span&gt;
&lt;span class="c1"&gt;# opens a documentation PR if the wiki needs updating&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Official examples for GitHub Actions, GitLab CI, and Bitbucket Pipelines are all in the &lt;code&gt;examples/&lt;/code&gt; directory. The auto-update flow: detect changes → update wiki → open a PR. It doesn't merge automatically — the documentation change stays in a PR for human review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Wiki Stays Yours
&lt;/h2&gt;

&lt;p&gt;OpenWiki's core design principle: &lt;strong&gt;your documentation belongs to you&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wiki is plain Markdown files committed in your repository — no external service dependency&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;openwiki/INSTRUCTIONS.md&lt;/code&gt; is user-authored; normal update runs never overwrite it&lt;/li&gt;
&lt;li&gt;OpenWiki only touches its own block (&lt;code&gt;&amp;lt;!-- OPENWIKI:START --&amp;gt;…&amp;lt;!-- OPENWIKI:END --&amp;gt;&lt;/code&gt;) inside &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;, leaving the rest untouched&lt;/li&gt;
&lt;li&gt;Claims files live in &lt;code&gt;openwiki/.claims/&lt;/code&gt;, versioned alongside Markdown in git history&lt;/li&gt;
&lt;li&gt;No-op runs don't churn docs — only &lt;code&gt;.last-update.json&lt;/code&gt; is updated, page content is left alone&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/langchain-ai/openwiki" rel="noopener noreferrer"&gt;langchain-ai/openwiki&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/openwiki" rel="noopener noreferrer"&gt;openwiki&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔧 &lt;strong&gt;Deep Agents&lt;/strong&gt;: &lt;a href="https://github.com/langchain-ai/deepagentsjs" rel="noopener noreferrer"&gt;langchain-ai/deepagentsjs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;OKF spec&lt;/strong&gt;: &lt;a href="https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md" rel="noopener noreferrer"&gt;Open Knowledge Format v0.2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;OpenWiki represents a different way of thinking about documentation engineering: &lt;strong&gt;the documentation quality problem isn't that the writing is bad — it's that nothing maintains what was written&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three things worth noting:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grounded Claims is the key innovation.&lt;/strong&gt; Traditional documentation tools have "last updated" timestamps at the page level. OpenWiki takes granularity down to "proposition + code line": each fact knows which lines are its evidence, and when those lines change, the system knows exactly what to rewrite. This transforms updates from "regenerate the whole file" to "only rewrite the propositions whose evidence changed" — far more precise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using a coding agent as a writer, not a Q&amp;amp;A tool.&lt;/strong&gt; OpenWiki's Claude Code/Codex integration puts the coding agent in the role of "repository researcher and documentation author," not a one-shot text generator. The agent has full repository access, works through a page queue incrementally, persists each result, and the whole process is resumable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The OKF format is a bet on interoperability.&lt;/strong&gt; Emitting Google Open Knowledge Format v0.2 means the wiki isn't locked to this tool — any OKF-aware system can read it. This is an explicit anti-vendor-lock-in design choice.&lt;/p&gt;

&lt;p&gt;If you're building an AI-native engineering team and need living documentation that automatically tracks code changes, OpenWiki is the most complete open-source solution available today.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Explore &lt;a href="https://primeskills.store" rel="noopener noreferrer"&gt;PrimeSkills&lt;/a&gt; — a curated marketplace of AI agents and skills, each validated against real enterprise workflows. No hype, just what actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Visit my &lt;a href="https://home.wonlab.top/en" rel="noopener noreferrer"&gt;personal site&lt;/a&gt; for more insights and interesting products.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>agents</category>
      <category>llm</category>
      <category>langchain</category>
    </item>
  </channel>
</rss>
