<?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: gen99</title>
    <description>The latest articles on DEV Community by gen99 (@_d1ea2a1f71316e743f41).</description>
    <link>https://dev.to/_d1ea2a1f71316e743f41</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%2F3921053%2F930909ed-9e29-44b8-9008-a709e57320db.png</url>
      <title>DEV Community: gen99</title>
      <link>https://dev.to/_d1ea2a1f71316e743f41</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_d1ea2a1f71316e743f41"/>
    <language>en</language>
    <item>
      <title>I built a local-first AI desktop where chat turns into cron jobs, parallel batches, and editable .pptx</title>
      <dc:creator>gen99</dc:creator>
      <pubDate>Tue, 23 Jun 2026 11:59:12 +0000</pubDate>
      <link>https://dev.to/_d1ea2a1f71316e743f41/i-built-a-local-first-ai-desktop-where-chat-turns-into-cron-jobs-parallel-batches-and-editable-joc</link>
      <guid>https://dev.to/_d1ea2a1f71316e743f41/i-built-a-local-first-ai-desktop-where-chat-turns-into-cron-jobs-parallel-batches-and-editable-joc</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Praxia Desktop is what I wanted when I got tired of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pasting "do this every Monday" into ChatGPT and then forgetting to actually do it every Monday&lt;/li&gt;
&lt;li&gt;Manually running the same prompt against 50 PDFs because nothing scaled my one-PDF chat workflow&lt;/li&gt;
&lt;li&gt;Asking an LLM for "a deck" and getting markdown back when I needed an actual editable PowerPoint to ship to a stakeholder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a desktop app where &lt;strong&gt;chat is the only interface&lt;/strong&gt;, and the agent on the other side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schedules itself&lt;/strong&gt; — say "every Monday at 9 AM summarize the diffs in Documents/" and a POSIX cron job appears&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fans out in parallel&lt;/strong&gt; — say "for each of these 50 PDFs, extract the action items" and 50 agents run concurrently with live progress&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes native, editable files&lt;/strong&gt; — say "draft a Q3 retrospective deck" and a real &lt;code&gt;.pptx&lt;/code&gt; lands in your workspace (charts, takeaways, the lot — not screenshots, not Markdown)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs &lt;strong&gt;locally&lt;/strong&gt; — your files and chat history live in your Praxia folder, not on a server I control. LLM calls go directly from your machine to whichever provider you pick (OpenAI, Anthropic, Azure, Gemini, or your own Ollama / LM Studio instance). I never see your prompts.&lt;/p&gt;

&lt;p&gt;Free, open source (Apache 2.0), and Windows 10/11 x64.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.microsoft.com/detail/9P9LSR34HZF3" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fget.microsoft.com%2Fimages%2Fen-us%2520dark.svg" alt="Get it from Microsoft" width="161" height="44"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rest of this post walks through each of those three things in detail — what it looks like, why it matters, how it's built, and what's interesting about the implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Thing 1: "Schedule this for me, weekly"
&lt;/h2&gt;

&lt;p&gt;Cron is one of the great computing primitives and basically nobody outside of sysadmins uses it directly. The interface is hostile. The mental model ("five fields separated by spaces representing minute / hour / day / month / weekday, all of which can be &lt;code&gt;*&lt;/code&gt; or &lt;code&gt;1-5&lt;/code&gt; or &lt;code&gt;*/15&lt;/code&gt; …") is something I always have to re-derive from &lt;code&gt;crontab -e&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wanted: I say it, it gets scheduled.&lt;/p&gt;

&lt;p&gt;In Praxia, this works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me: every Monday at 9 AM, summarize what changed in Documents/team-notes/
    since the previous Monday, and write the summary to
    workspace/weekly-team-summary.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent on the other side reads that, infers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a recurring task (not a one-shot)&lt;/li&gt;
&lt;li&gt;The schedule is &lt;code&gt;0 9 * * 1&lt;/code&gt; in POSIX cron&lt;/li&gt;
&lt;li&gt;The action involves a diff over a watched folder + a write to the workspace&lt;/li&gt;
&lt;li&gt;The write needs a user approval gate at execution time, not at schedule time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A schedule gets registered. The Schedules tab is where it lives:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bk05ws9bi96ssj24cno.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bk05ws9bi96ssj24cno.png" alt="Schedules tab" width="800" height="450"&gt;&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;You can inspect the cron expression, the prompt, the next run time, the last run result. You can pause it, edit the cron, edit the prompt, delete it. The same agent that registered it can also modify it — "actually, change that to every weekday morning."&lt;/p&gt;

&lt;p&gt;This isn't "natural language cron parsing as a feature." That's a regex with extra steps. What makes it useful is that the &lt;strong&gt;execution context&lt;/strong&gt; at schedule-fire time is the same one you'd get from a fresh chat: same memory, same connectors, same approval-gate semantics, same provider config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;: lots of LLM tools can &lt;em&gt;describe&lt;/em&gt; a workflow. Few can &lt;em&gt;execute&lt;/em&gt; one on a schedule with all the actual side effects (file writes, API calls) properly gated and observable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Thing 2: Fan out a chat across 50 files
&lt;/h2&gt;

&lt;p&gt;The first time I tried to use an LLM at non-trivial scale was when I had 50 candidate résumés to triage. ChatGPT was great for one résumé. For 50, my options were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open 50 tabs (this is what I actually did the first time, regrettably)&lt;/li&gt;
&lt;li&gt;Write a Python script (the right answer, but I was on a deadline)&lt;/li&gt;
&lt;li&gt;Use a SaaS that does parallel agent runs (paid, locks me in)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Praxia's answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me: for each PDF in Documents/candidates/, extract strengths, weaknesses,
    tech stack, and a recommended team. Put each candidate's analysis in
    workspace/candidates/&amp;lt;name&amp;gt;.md and write a comparison matrix to
    workspace/candidates/MATRIX.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lists the PDFs in the folder&lt;/li&gt;
&lt;li&gt;Spawns N parallel sub-agents (where N respects the rate limits of whatever LLM provider you've configured — OpenAI's TPM/RPM, Anthropic's per-minute budget, etc.)&lt;/li&gt;
&lt;li&gt;Watches them concurrently in the Batches tab:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flswpl5tpkdba5zp89gdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flswpl5tpkdba5zp89gdw.png" alt="Batches tab" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collects results, writes per-candidate analyses, builds the comparison matrix&lt;/li&gt;
&lt;li&gt;Surfaces failures individually so you can retry the 3 that timed out without re-running the 47 that succeeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user-facing UX is "I asked the agent to do a thing and watched it happen." The agent-facing UX is a &lt;code&gt;fan_out&lt;/code&gt; tool that takes a list of input items and a sub-prompt template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;: parallel execution is the gap between "useful for one document" and "useful for my actual workload." Once you have it, you find use cases everywhere — translating a backlog of strings, classifying support tickets, extracting fields from a stack of invoices, comparing N proposals against a rubric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's hard to do well&lt;/strong&gt;: rate-limit awareness. Praxia configures concurrency based on the active LLM provider's published limits, so you don't have to remember that GPT-4o-mini's TPM ceiling is different from Claude Sonnet's, or that your own Ollama instance is bottlenecked on local GPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  Thing 3: Native, editable &lt;code&gt;.pptx&lt;/code&gt; from chat
&lt;/h2&gt;

&lt;p&gt;This is the one I'm most proud of and it's the most technically interesting bit.&lt;/p&gt;

&lt;p&gt;When LLMs "generate slides," what you typically get is Markdown, or an HTML preview, or a screenshot, or — at best — a Reveal.js page. What you almost never get is &lt;strong&gt;a real &lt;code&gt;.pptx&lt;/code&gt; file you can open in PowerPoint and edit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Praxia does the real thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me: draft a Q3 retrospective deck from Documents/sales/. Three charts,
    one summary slide, one next-actions slide. Corporate colors are
    navy and white.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens under the hood:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan&lt;/strong&gt; — the LLM drafts an outline: 5 slides, each with a title, content type (bullets / chart / text), and source citations from the indexed sales folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code-gen&lt;/strong&gt; — the LLM writes Python code that uses &lt;a href="https://python-pptx.readthedocs.io/" rel="noopener noreferrer"&gt;python-pptx&lt;/a&gt; to construct the deck. Not Markdown that gets converted. Actual python-pptx calls: &lt;code&gt;shapes.add_chart()&lt;/code&gt;, &lt;code&gt;shapes.add_text_box()&lt;/code&gt;, &lt;code&gt;text_frame.paragraphs[0].font.color.rgb = RGBColor(0x1f, 0x2a, 0x5e)&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt; — the bundled Python sidecar executes that code. A &lt;code&gt;.pptx&lt;/code&gt; file lands on disk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vision review&lt;/strong&gt; — the deck is converted to PNGs (one per slide) and sent to a vision-capable LLM (GPT-4o, Claude 3.5 Sonnet, etc.) with the prompt: "Look at these slides. Are titles legible? Do text boxes fit? Are colors consistent? Are charts misaligned?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Iterate&lt;/strong&gt; — if the vision pass flags issues ("slide 3 title overflows", "chart 2 has overlapping labels"), the LLM regenerates the offending parts and step 4 runs again. Usually one or two iterations is enough.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Approve&lt;/strong&gt; — Praxia surfaces the final deck in an approval dialog. You click Apply. The &lt;code&gt;.pptx&lt;/code&gt; lands in your workspace folder.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcspg0gmtnk6udp7b5cy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcspg0gmtnk6udp7b5cy4.png" alt="PowerPoint output" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result you can open in PowerPoint and edit normally. Text boxes are real text boxes. Charts are real charts (driven by embedded data, not pasted images). Color schemes are consistent because the vision pass explicitly checks for inconsistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is hard&lt;/strong&gt;: LLMs are bad at spatial reasoning. They cheerfully generate &lt;code&gt;text_box(left=Inches(5), top=Inches(3), width=Inches(8), …)&lt;/code&gt; on a 10-inch-wide slide and don't notice the box runs off the edge. The vision-review loop catches this — the LLM can't "see" the slide it just generated through code alone, but it &lt;em&gt;can&lt;/em&gt; see the rendered PNG. That second pass closes the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;: the difference between "I have a Markdown outline of a deck" and "I have a polished PowerPoint I can send to my CFO" is roughly an hour of fiddly work that AI tools have historically not closed. This closes it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shape of the app
&lt;/h2&gt;

&lt;p&gt;Three tabs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chat&lt;/strong&gt; — where you talk to the agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt; — folders Praxia watches and indexes (RAG-style retrieval, fully local)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspace&lt;/strong&gt; — where Praxia writes files for you, all gated by an approval dialog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6k5iq7tb3aujb8dgche.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6k5iq7tb3aujb8dgche.png" alt="Documents tab" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every disk-touching action (file write, file delete, file overwrite) goes through a per-operation approval dialog. The agent never silently writes. If it tries to overwrite an existing file, you see the diff and decide.&lt;/p&gt;

&lt;p&gt;This is one of the harder parts of building an agentic desktop app, and it's where I disagree most strongly with cloud-first AI tools. &lt;strong&gt;Trust comes from being able to say no.&lt;/strong&gt; A model that occasionally hallucinates a wrong file path is fine if you have a dialog telling you what it's about to do. A model with the same hallucination rate writing silently to disk is a disaster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local-first, for real
&lt;/h2&gt;

&lt;p&gt;Your documents and chat history live in &lt;code&gt;~/Praxia/&lt;/code&gt; (or wherever you point it). I do not operate a backend that holds your data. When you send a chat message, Praxia routes it to whichever LLM provider you configured — OpenAI, Anthropic, Azure, Gemini, Ollama, LM Studio — and that provider sees the request directly from your machine.&lt;/p&gt;

&lt;p&gt;If you want strict on-device operation: configure Ollama or LM Studio as your provider. No HTTPS calls leave your machine. The agent loop, retrieval, scheduling, batch fan-out, and &lt;code&gt;.pptx&lt;/code&gt; rendering all happen locally.&lt;/p&gt;

&lt;p&gt;This is a different stance than most "local AI" desktop apps, which still phone home for telemetry or model registry checks. Praxia does neither. The only outbound traffic is to the LLM provider &lt;em&gt;you&lt;/em&gt; selected; if you selected a local one, there's no outbound traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting bits under the hood
&lt;/h2&gt;

&lt;p&gt;Architecture (this is the only place I'll go nerdy):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tauri 2 shell (Rust + Svelte 4 + WebView2)
        ↓ spawn / localhost HTTP
PyInstaller-frozen Python sidecar
  └─ FastAPI + litellm + chromadb + python-pptx + matplotlib + pypdfium2 + ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell is a Tauri 2 app. The agent backend is a Python FastAPI server bundled as a single &lt;code&gt;praxia-server.exe&lt;/code&gt; via PyInstaller, started as a child process at app launch. They talk over localhost HTTP.&lt;/p&gt;

&lt;p&gt;Why this shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same Python code path runs as &lt;code&gt;pip install praxia&lt;/code&gt; for CLI users AND as the desktop sidecar. One codebase, two distribution channels.&lt;/li&gt;
&lt;li&gt;WebView2 is enormously lighter than Electron's Chromium.&lt;/li&gt;
&lt;li&gt;The Rust shell handles the system-integration parts (file dialogs, OAuth callbacks, OS notifications) where Tauri 2's plugin ecosystem already has the right abstractions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few specific things I think are worth flagging:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5-layer memory stack.&lt;/strong&gt; Personal memory (auto-extracted from chats) → Sleep-time consolidation → Shared org memory → Frozen Markdown layer (git-managed) → optional graph layer. Three independent promotion paths (frequency / outcome / self-eval) decide which personal observations get elevated to organizational knowledge. Most agent platforms paywall this. It ships in Praxia's OSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifier loop.&lt;/strong&gt; A &lt;code&gt;CommandedAgent&lt;/code&gt; mode wraps the free-running agent loop with pre-retrieval + grounding verification + bounded retry + an explicit &lt;code&gt;abstain&lt;/code&gt; path. Calibrated against an in-house multi-hop RAG harness. The difference between this and the un-verified &lt;code&gt;AutonomousAgent&lt;/code&gt; mode is roughly 20 points of factual accuracy on private-corpus QA — at the cost of slower responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user OAuth across 20+ SaaS connectors.&lt;/strong&gt; When Alice connects Notion to Praxia, Praxia stores Alice's OAuth token and queries Notion as Alice. Bob's Notion view is different because Bob's OAuth token is different. This sounds obvious; most AI tools use a single service account and leak data across users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP support, both stdio and HTTP/SSE.&lt;/strong&gt; Any &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server you wrote for Claude Desktop or Cursor works in Praxia unchanged.&lt;/p&gt;

&lt;p&gt;If any of those are interesting, the &lt;a href="https://github.com/praxia-dev/praxia" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; has architecture docs and the actual implementation. It's all Python with type hints, formatted with Ruff, ~780 tests passing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: how it got onto the Microsoft Store
&lt;/h2&gt;

&lt;p&gt;Short version: I wrapped the Tauri 2 build in MSIX, declared &lt;code&gt;runFullTrust&lt;/code&gt; with a five-bullet justification ("spawn Python sidecar, read user folders, localhost loopback, outbound HTTPS to user-configured LLM API, native document generation"), and submitted to Partner Center. Cert passed on the first try in 4 days. Microsoft re-signs the MSIX with the Store identity, so SmartScreen no longer flags installation. &lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apps.microsoft.com/detail/9P9LSR34HZF3" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fget.microsoft.com%2Fimages%2Fen-us%2520dark.svg" alt="Get it from Microsoft" width="161" height="44"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Store&lt;/strong&gt;: &lt;a href="https://apps.microsoft.com/detail/9P9LSR34HZF3" rel="noopener noreferrer"&gt;https://apps.microsoft.com/detail/9P9LSR34HZF3&lt;/a&gt; (Windows 10/11, free)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; (Apache 2.0): &lt;a href="https://github.com/praxia-dev/praxia" rel="noopener noreferrer"&gt;https://github.com/praxia-dev/praxia&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt; (CLI version): &lt;code&gt;pip install praxia&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4-minute demo&lt;/strong&gt;: &lt;a href="https://youtu.be/Z3DFa2saHJg" rel="noopener noreferrer"&gt;https://youtu.be/Z3DFa2saHJg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://praxia.tools/" rel="noopener noreferrer"&gt;https://praxia.tools/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions&lt;/strong&gt; (GitHub): &lt;a href="https://github.com/praxia-dev/praxia/discussions" rel="noopener noreferrer"&gt;https://github.com/praxia-dev/praxia/discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something interesting on top of it, please drop a note in Discussions or ping me on X at &lt;a href="https://x.com/praxia_dev" rel="noopener noreferrer"&gt;@praxia_dev&lt;/a&gt;. I want to know what people make.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>I spent 5 weeks building an open-source multi-agent orchestrator. The hard part wasn't the agents — it was the memory.</title>
      <dc:creator>gen99</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:36:47 +0000</pubDate>
      <link>https://dev.to/_d1ea2a1f71316e743f41/i-spent-5-weeks-building-an-open-source-multi-agent-orchestrator-the-hard-part-wasnt-the-agents--43j3</link>
      <guid>https://dev.to/_d1ea2a1f71316e743f41/i-spent-5-weeks-building-an-open-source-multi-agent-orchestrator-the-hard-part-wasnt-the-agents--43j3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the launch post in a series on building &lt;strong&gt;Praxia&lt;/strong&gt;, an Apache-2.0 multi-agent orchestrator. Later posts go deep on the TiDB Vector memory backend and a Japanese-specialized STT integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This spring I built and shipped &lt;strong&gt;Praxia&lt;/strong&gt;, a multi-agent orchestrator OS, from scratch in about 5 weeks of nights and weekends (Apache-2.0).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;PyPI&lt;/strong&gt;: &lt;code&gt;pip install praxia&lt;/code&gt; — &lt;a href="https://pypi.org/project/praxia/" rel="noopener noreferrer"&gt;https://pypi.org/project/praxia/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/praxia-dev/praxia" rel="noopener noreferrer"&gt;https://github.com/praxia-dev/praxia&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎬 &lt;strong&gt;60-second demo&lt;/strong&gt;: &lt;a href="https://youtu.be/o_6NbjJU1AA" rel="noopener noreferrer"&gt;https://youtu.be/o_6NbjJU1AA&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Landing&lt;/strong&gt;: &lt;a href="https://praxia.tools/" rel="noopener noreferrer"&gt;https://praxia.tools/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The differentiator is &lt;strong&gt;automatic personal → organizational memory promotion&lt;/strong&gt;. The "prompts that actually work," which a senior engineer painstakingly tunes, usually stay locked in that one person's head. Praxia tries to solve that with a &lt;strong&gt;5-layer memory stack + a 3-path promotion engine&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually started — the decision
&lt;/h2&gt;

&lt;p&gt;I'd been using LangChain / CrewAI / AutoGen at work since late 2025, and one structural discomfort kept nagging at me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What really separates a good agent from a bad one isn't the library or the model — it's the &lt;strong&gt;accumulated domain-specific trial and error&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that accumulation almost always lives in one senior person's head (their Cursor / VS Code / Obsidian). Tacit knowledge that evaporates the day they leave. General-purpose frameworks are &lt;strong&gt;powerless&lt;/strong&gt; against that.&lt;/p&gt;

&lt;p&gt;In April 2026 I started writing code between day-job hours. A month later I shipped &lt;code&gt;v0.1.0&lt;/code&gt; to PyPI and GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why existing frameworks didn't cut it
&lt;/h2&gt;

&lt;p&gt;Four walls I hit in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wall&lt;/th&gt;
&lt;th&gt;What was happening&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-3 days just to get something running. Can't make a production call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tacit knowledge doesn't propagate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The prompts that work stay in one person's private space.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No evidence for evaluation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"It runs" doesn't guarantee "it works."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agents stagnate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build it once, and there's no feedback loop.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second one was the killer. General agent frameworks give you strong primitives, but &lt;strong&gt;the process of accumulating knowledge into the organization is left entirely to the implementer&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core design — 5 layers + 3 paths
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The 5-layer memory stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L1 PersonalMemory   Per-user (6 backends: JSON / Mem0 / Letta / Zep / Hindsight / LangMem)
L2 PromotionEngine  Nightly batch. Decides L1 → L3 promotion
L3 SharedMemory     Org-wide. RBAC gating, time decay
L4 MarkdownStore    Git-managed, PR review required, immutable
L5 GraphLayer       Optional (Zep / Graphiti), relation extraction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is that &lt;strong&gt;L1 → L4 is not manual&lt;/strong&gt;. A sleep-time consolidator (nightly batch) scans personal memory and auto-promotes the right parts into organizational knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 3-path promotion engine
&lt;/h3&gt;

&lt;p&gt;Memory promotion is evaluated in parallel across &lt;strong&gt;three independent signals&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frequency&lt;/strong&gt; — facts repeated across N+ people&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome correlation&lt;/strong&gt; — co-occurrence with wins / approved PRs / passing tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM self-eval&lt;/strong&gt; — a 0..1 "org-knowledge candidacy" score&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final score is a weighted blend, and &lt;strong&gt;any single decisive path triggers promotion&lt;/strong&gt;. This deliberately avoids single-mechanism dependence (where one broken signal takes the whole thing down).&lt;/p&gt;




&lt;h2&gt;
  
  
  The design choices that made "I can build this myself" possible
&lt;/h2&gt;

&lt;p&gt;Shipping in 5 weeks came down to four design choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Seven extension points
&lt;/h3&gt;

&lt;p&gt;Every extension point is built on the same &lt;code&gt;praxia.extensions.Registry&lt;/code&gt; primitive:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Extension point&lt;/th&gt;
&lt;th&gt;~LoC&lt;/th&gt;
&lt;th&gt;Entry point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connector (Box / Notion / Slack …)&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.connectors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory backend&lt;/td&gt;
&lt;td&gt;~80&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.memory_backends&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File parser&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.parsers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output exporter&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.exporters&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth provider&lt;/td&gt;
&lt;td&gt;~20&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.oauth_providers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.skills&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flow&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;&lt;code&gt;praxia.flows&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"&lt;strong&gt;Extend via a &lt;code&gt;pyproject.toml&lt;/code&gt; entry point, never edit core files&lt;/strong&gt;" keeps the cognitive load low when you write your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Apache-2.0 with everything included (no paywall)
&lt;/h3&gt;

&lt;p&gt;SSO (Google / Microsoft Entra / Okta / GitHub / Keycloak), RBAC, audit logs, per-user OAuth (13 providers), KMS-backed token encryption (AWS / Azure / GCP / Vault / local) — &lt;strong&gt;all in the OSS core&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most of what commercial agent platforms paywall as an "Enterprise tier" ships here under Apache-2.0. That directly speeds up adoption decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 100+ providers via LiteLLM
&lt;/h3&gt;

&lt;p&gt;Provider quirks (Anthropic not supporting &lt;code&gt;response_format&lt;/code&gt;, GPT-5.x disallowing &lt;code&gt;temperature&lt;/code&gt;, Azure's deployment-name format, …) are &lt;strong&gt;absorbed at the LiteLLM layer&lt;/strong&gt;. No provider-specific API keys leak into Praxia core. Fully offline operation is possible too (Ollama + a local model + &lt;code&gt;backend=json&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A Streamlit UI that's easy to throw away
&lt;/h3&gt;

&lt;p&gt;The UI is Streamlit, but the &lt;strong&gt;backend also runs headless as &lt;code&gt;praxia serve&lt;/code&gt; (FastAPI)&lt;/strong&gt;. When I swap to Next.js or mobile later, I throw away only the UI layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What went into v0.1.0, and what didn't
&lt;/h2&gt;

&lt;p&gt;Shipped (deliberately a full set):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5-layer memory + 3-path promotion engine&lt;/li&gt;
&lt;li&gt;6 business skills (investment / sales / design / procurement / patent / legal)&lt;/li&gt;
&lt;li&gt;6 LTM backends + Composite/Routed parallel fusion&lt;/li&gt;
&lt;li&gt;Per-user OAuth for 13 providers&lt;/li&gt;
&lt;li&gt;SSO + RBAC + ACL + audit logs&lt;/li&gt;
&lt;li&gt;Autonomous agent (LLM-driven tool-use loop)&lt;/li&gt;
&lt;li&gt;Document Designer (sandboxed &lt;code&gt;python-pptx&lt;/code&gt; / &lt;code&gt;docx&lt;/code&gt; → designed file output)&lt;/li&gt;
&lt;li&gt;i18n in 8 languages (en / ja / zh-CN / ko / es / fr / de / pt-BR)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deliberately deferred (v0.2+):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant GUI&lt;/strong&gt; — OSS targets "single-org self-host"; SaaS-grade tenant isolation belongs in a future Open Core tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF output&lt;/strong&gt; — LibreOffice-based workflow recommended for now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Pinecone / Weaviate / Qdrant backends&lt;/strong&gt; — can be wrapped via &lt;code&gt;mem0&lt;/code&gt; through Composite/Routed, so low priority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "what NOT to build" list was as important as the "what to build" list.&lt;/p&gt;




&lt;h2&gt;
  
  
  The TOP 3 things that actually ate my time
&lt;/h2&gt;

&lt;p&gt;This was a "ship something working in 5 weeks" sprint, and the three things that genuinely ate time were all in the &lt;strong&gt;memory layer and promotion engine&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Blending three signals that live on completely different scales
&lt;/h3&gt;

&lt;p&gt;The PromotionEngine evaluates L1 → L3 promotion across &lt;strong&gt;Frequency / Outcome correlation / LLM self-eval&lt;/strong&gt; in parallel. This was trickier than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frequency&lt;/strong&gt; is an integer in 0..∞ (reference count for the same fact)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome correlation&lt;/strong&gt; is a ratio in 0..1 (success rate of tasks where the fact co-occurred)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM self-eval&lt;/strong&gt; is a continuous 0..1 — but &lt;strong&gt;non-deterministic&lt;/strong&gt;, and the score distribution differs per provider (GPT-5 strict, median ~0.4; Claude lenient ~0.7; Gemini bimodal)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first implementation was a plain weighted average. Because frequency is unbounded, "facts I just happened to touch a lot in L1" floated to the top. I ended up here:&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="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PromoteSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;       &lt;span class="c1"&gt;# raw count
&lt;/span&gt;    &lt;span class="n"&gt;outcome_corr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;    &lt;span class="c1"&gt;# 0..1
&lt;/span&gt;    &lt;span class="n"&gt;self_eval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;       &lt;span class="c1"&gt;# 0..1, median of N=3 LLM calls
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide_promotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PromoteSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PromoteConfig&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;# Z-score normalize on a rolling 30-day population, then sigmoid
&lt;/span&gt;    &lt;span class="n"&gt;z_freq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sigmoid&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frequency&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_std&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# OR logic with per-path threshold
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;z_freq&lt;/span&gt;              &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;freq_threshold&lt;/span&gt;       &lt;span class="c1"&gt;# 0.85
&lt;/span&gt;        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_corr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_threshold&lt;/span&gt;    &lt;span class="c1"&gt;# 0.70
&lt;/span&gt;        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;self_eval&lt;/span&gt;    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;self_eval_threshold&lt;/span&gt;  &lt;span class="c1"&gt;# 0.80
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Z-score normalization&lt;/strong&gt; tames the runaway frequency signal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OR logic with per-path thresholds&lt;/strong&gt; means "any single decisive path promotes" — deliberately avoiding single-mechanism dependence&lt;/li&gt;
&lt;li&gt;LLM self-eval uses the &lt;strong&gt;median of N=3 calls&lt;/strong&gt; to average out non-determinism (I tried N=5 — the benefit plateaued)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decide_promotion&lt;/code&gt; is a &lt;strong&gt;pure function&lt;/strong&gt;, so I can replay historical promotion logs to do parameter sensitivity analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It took ~5 redesigns to land on "Z-score → sigmoid → OR with thresholds." Lesson: &lt;strong&gt;don't start signal fusion with a weighted average. Per-path decisive thresholds + OR turned out to be the most robust.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reconciling "fan-out search × single-destination write" in the Composite backend
&lt;/h3&gt;

&lt;p&gt;The Composite backend sends search queries in parallel to multiple backends (JSON / Mem0 / TiDB / Letta…) and fuses results with Reciprocal Rank Fusion (RRF). But writes go to a single backend specified by &lt;code&gt;write_to=&lt;/code&gt;. This asymmetry created three pitfalls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(a) Add a backend later, and past data is invisible to it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;Composite(backends=[A, B], write_to="A")&lt;/code&gt; and then adding C means C has none of the past writes. Search fan-out becomes lopsided — 2 backends hit, 1 doesn't. I discovered this during dogfooding as "one backend just has lower search quality."&lt;/p&gt;

&lt;p&gt;→ Added a &lt;code&gt;replay_writes(source=A, target=C, since=...)&lt;/code&gt; admin API after the fact. Needed for rebalancing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(b) Graceful degradation when &lt;code&gt;write_to&lt;/code&gt; goes down&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;write_to&lt;/code&gt; is down, stopping writes but continuing reads looks attractive. But that can cause &lt;strong&gt;"a record that was hitting in search disappears on the next search"&lt;/strong&gt; (because the write never re-runs after recovery).&lt;/p&gt;

&lt;p&gt;I ended up &lt;strong&gt;dropping graceful degradation and raising instead&lt;/strong&gt;. Half-baked availability stacks a data-truthfulness problem on top of eventual consistency. "&lt;strong&gt;When it's down, honestly say it's down&lt;/strong&gt;" turned out to be the easiest to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(c) Tie-breaking in RRF fusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When multiple backends return the &lt;strong&gt;same record_id at rank 1&lt;/strong&gt;, the scores tie exactly. Without a tie-breaking rule, ordering depends on backend registration order, and CI tests go flaky.&lt;/p&gt;

&lt;p&gt;→ Introduced lexicographic tie-breaking on &lt;code&gt;(rrf_score, timestamp DESC, backend_priority)&lt;/code&gt;. Now ordering is reproducible even in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Idempotency of the sleep-time consolidator
&lt;/h3&gt;

&lt;p&gt;Since the nightly batch re-runs L1 → L3 promotion, &lt;strong&gt;not promoting the same fact twice&lt;/strong&gt; is the crux of idempotency. Strict &lt;code&gt;record_id&lt;/code&gt;-based dedup wasn't enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same user records "Customer X prioritizes ROI" and "X-san values ROI" as two different phrasings → different record_ids, semantically identical&lt;/li&gt;
&lt;li&gt;LLM self-eval is &lt;strong&gt;non-deterministic&lt;/strong&gt;: the same text scores 0.78 / 0.82 / 0.76, so near a threshold you get "didn't promote yesterday, promoted today"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added &lt;strong&gt;fuzzy dedup&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;is_duplicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l3_recent&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;Record&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;# Already-promoted check: any L3 record within recent window whose
&lt;/span&gt;    &lt;span class="c1"&gt;# embedding cosine-sim &amp;gt;= threshold is treated as duplicate
&lt;/span&gt;    &lt;span class="n"&gt;cand_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&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;return&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;cosine_sim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cand_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l3_recent&lt;/span&gt;  &lt;span class="c1"&gt;# already filtered to last 30 days
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard part was &lt;strong&gt;tuning the two thresholds (similarity + window)&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Problem it causes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;0.95 / 7 days&lt;/strong&gt; (tight)&lt;/td&gt;
&lt;td&gt;"Same fact" with different wording ends up duplicated across L3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;0.85 / 90 days&lt;/strong&gt; (loose)&lt;/td&gt;
&lt;td&gt;"New but similar facts" (e.g. a similar trend for customer Y) get suppressed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;0.92 / 30 days&lt;/strong&gt; (adopted)&lt;/td&gt;
&lt;td&gt;On a hand-labeled set of 50, both false-merge and false-split stayed &amp;lt; 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deciding factor was &lt;strong&gt;instrumenting both directions at once&lt;/strong&gt;. Not just "rate of missed duplicates" but also &lt;strong&gt;"rate of treating genuinely distinct facts as duplicates."&lt;/strong&gt; Track only one side and you inevitably tune lopsided. I later applied this to the PromotionEngine's whole calibration loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.1.0 by the numbers
&lt;/h2&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;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codebase&lt;/td&gt;
&lt;td&gt;~25,000 LoC (Python + tests + i18n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;431 passing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported LLM providers&lt;/td&gt;
&lt;td&gt;100+ via LiteLLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundled connectors&lt;/td&gt;
&lt;td&gt;19 (pull + push)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-user OAuth providers&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;30,000+ chars across EN + JA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev time&lt;/td&gt;
&lt;td&gt;~5 weeks (nights and weekends)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Next 3 months
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;v0.2: First-class TiDB Vector / pgvector backends (currently via &lt;code&gt;mem0&lt;/code&gt; wrap)&lt;/li&gt;
&lt;li&gt;v0.2: localhost loopback OAuth (drop the &lt;code&gt;praxia serve&lt;/code&gt; requirement)&lt;/li&gt;
&lt;li&gt;v0.3: Multi-tenant org features (the Open Core entry point)&lt;/li&gt;
&lt;li&gt;Docs: expanded English tutorials&lt;/li&gt;
&lt;li&gt;Community: Discord, more active Discussions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  For anyone in the same spot
&lt;/h2&gt;

&lt;p&gt;Three things I felt after sprinting for 5 weeks, for anyone debating whether to start a solo OSS project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship frequency over polish.&lt;/strong&gt; Shipping &lt;code&gt;v0.1.0&lt;/code&gt; teaches you far more than polishing &lt;code&gt;v0.0.4&lt;/code&gt; forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State your differentiator in one line.&lt;/strong&gt; For Praxia: "personal → org memory auto-promotion." If you can't say it, you can't write the post, can't pitch it, and PRs won't come.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real competitor for OSS is the commercial SaaS solving the same problem&lt;/strong&gt; — not LangChain or CrewAI, but the paywall on paid agent platforms. Bundling those features under Apache-2.0 is itself the differentiation.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;⭐ Stars / 🍴 Forks / Issues / PRs all welcome.&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/praxia-dev/praxia" rel="noopener noreferrer"&gt;github.com/praxia-dev/praxia&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you liked this, the 60-second demo is here: &lt;a href="https://youtu.be/o_6NbjJU1AA" rel="noopener noreferrer"&gt;https://youtu.be/o_6NbjJU1AA&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Connecting "individual brilliance × organizational continuity" with AI — that's the mission Praxia started with this spring.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in this series: the enterprise-platform features I put directly in the OSS core (SSO, RBAC, audit logs, KMS-envelope-encrypted OAuth tokens) and why none of them are behind an Enterprise tier.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
