<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>AI on Yarang's Tech Lair</title><link>https://blog.agentthread.dev/categories/ai/</link><description>Recent content in AI on Yarang's Tech Lair</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 08 May 2026 21:57:11 +0900</lastBuildDate><atom:link href="https://blog.agentthread.dev/categories/ai/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a Multi-LLM Distributed Orchestrator with NATS JetStream</title><link>https://blog.agentthread.dev/post/building-a-multi-llm-distributed-orchestrator-with-nats-jetstream/</link><pubDate>Fri, 08 May 2026 21:57:11 +0900</pubDate><guid>https://blog.agentthread.dev/post/building-a-multi-llm-distributed-orchestrator-with-nats-jetstream/</guid><description>&lt;p&gt;Part 1 discussed the model-specific limitations discovered while running four AIs—Claude, ZAI, Codex, and Gemini—concurrently on the same tasks. This part is about &amp;ldquo;how we made it possible&amp;rdquo;—the system design and implementation story.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="system-overview"&gt;System Overview
&lt;/h2&gt;&lt;p&gt;AgentForge consists of three components.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Task Publisher]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ NATS JetStream publish
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[NATS Broker] ─── af.worker.{id}.inbox
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ JetStream consume (independent streams per worker)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Worker Pollers] × N (poller.py × 18 instances)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ LLM CLI Execution (claude / codex / gemini)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Result Return] af.task.{task_id}.completed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When a publisher posts a task to NATS, each worker, which is independently subscribed, receives the message on its inbox and executes the LLM CLI. The result is then published back to a completion topic.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-nats-jetstream"&gt;Why NATS JetStream?
&lt;/h2&gt;&lt;p&gt;We considered several message broker options: Redis Streams, Kafka, RabbitMQ, and NATS JetStream.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reasons for choosing NATS JetStream:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Single Binary&lt;/strong&gt; — Operates with a single &lt;code&gt;nats-server&lt;/code&gt; without requiring separate runtimes. It has no dependencies like Kafka&amp;rsquo;s ZooKeeper or RabbitMQ&amp;rsquo;s Erlang/OTP.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Built-in Persistence&lt;/strong&gt; — JetStream is a streaming layer on top of NATS, storing messages to the filesystem. This ensures that unprocessed tasks are not lost even if a worker restarts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NKey-based Authentication&lt;/strong&gt; — We can issue independent Ed25519 key pairs for each worker. If one worker is compromised, the credentials of other workers remain valid.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lightweight&lt;/strong&gt; — Memory usage is around 30MB on a single server. Even with 18 workers connected, the broker load is minimal.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="the-core-backend-adapter-in-pollerpy"&gt;The Core: Backend Adapter in &lt;code&gt;poller.py&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The heart of the worker is &lt;code&gt;poller.py&lt;/code&gt;. This single file handles NATS subscriptions, LLM CLI execution, and result returns.&lt;/p&gt;
&lt;p&gt;Since LLMs have different execution methods, we separated them into a backend adapter dictionary.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;_BACKENDS: dict[str, dict] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;claude&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CLAUDE_BIN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/local/bin/claude&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;ALLOWED_TOOLS&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Read,Edit,Write,Glob,Grep&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CLAUDE_MODEL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;codex&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CODEX_BIN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/bin/codex&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CODEX_MODEL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;sandbox&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;CODEX_SANDBOX&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;read-only&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;gemini_cli&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;GEMINI_BIN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/usr/bin/gemini&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: os&lt;span style="color:#f92672"&gt;.&lt;/span&gt;environ&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;GEMINI_MODEL&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;MODEL_BACKEND&lt;/code&gt; environment variable determines which LLM to use. This allows the same &lt;code&gt;poller.py&lt;/code&gt; code to run different LLMs across 18 workers.&lt;/p&gt;
&lt;h3 id="claude-backend"&gt;Claude Backend
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;run_claude&lt;/span&gt;(instructions: str, task_id: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; tuple[int, str]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cfg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _BACKENDS[&lt;span style="color:#e6db74"&gt;&amp;#34;claude&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cmd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [cfg[&lt;span style="color:#e6db74"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;], &lt;span style="color:#e6db74"&gt;&amp;#34;--print&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;--allowedTools&amp;#34;&lt;/span&gt;, cfg[&lt;span style="color:#e6db74"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cmd &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;--model&amp;#34;&lt;/span&gt;, cfg[&lt;span style="color:#e6db74"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proc &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; asyncio&lt;span style="color:#f92672"&gt;.&lt;/span&gt;create_subprocess_exec(&lt;span style="color:#f92672"&gt;*&lt;/span&gt;cmd, stdin&lt;span style="color:#f92672"&gt;=&lt;/span&gt;PIPE, stdout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;PIPE, stderr&lt;span style="color:#f92672"&gt;=&lt;/span&gt;PIPE)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;--print&lt;/code&gt; flag is key. It runs Claude Code in non-interactive mode instead of conversational mode, ensuring the results are returned via stdout.&lt;/p&gt;
&lt;h3 id="zai-backend"&gt;ZAI Backend
&lt;/h3&gt;&lt;p&gt;ZAI offers an Anthropic API-compatible endpoint, so it doesn&amp;rsquo;t require a separate backend. Routing is handled by two environment variables.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/agentforge/cc-zai-high-dev-01.env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt;ZAI endpoint&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt;ZAI API key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By injecting this file using systemd&amp;rsquo;s &lt;code&gt;EnvironmentFile=&lt;/code&gt; directive, the &lt;code&gt;claude&lt;/code&gt; binary sends requests to the ZAI endpoint. This allows us to connect to a different LLM provider simply by changing environment variables, without altering the code.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="declarative-management-fleetyaml--serversyaml"&gt;Declarative Management: &lt;code&gt;fleet.yaml&lt;/code&gt; × &lt;code&gt;servers.yaml&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Manually managing 18 workers is impractical. We declaratively defined the entire infrastructure using two YAML files.&lt;/p&gt;
&lt;h3 id="serversyaml--server-inventory"&gt;&lt;code&gt;servers.yaml&lt;/code&gt; — Server Inventory
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;servers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;services&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;agentforge-worker, tunnel-arm1]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;services&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;nats-jetstream, postgres]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;services&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;agentforge-worker, tunnel-arm1]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="fleetyaml--worker-placement"&gt;&lt;code&gt;fleet.yaml&lt;/code&gt; — Worker Placement
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;workers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;worker_id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cc-go-dev-01&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;llm&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;model&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-sonnet-4-6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;developer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;create_pr&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;worker_id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;codex-py-dev-01&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;llm&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;codex&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;model&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gpt-5.5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;developer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;create_pr&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Changing just the &lt;code&gt;host&lt;/code&gt; field moves a worker to a different server. Setting &lt;code&gt;enabled: false&lt;/code&gt; stops the deployment script from starting that worker.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="worker-templating-system-provision_workerpy"&gt;Worker Templating System: &lt;code&gt;provision_worker.py&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Manually writing systemd unit files for each new worker is prone to errors. We automated this using Jinja2 templates and a provisioning script.&lt;/p&gt;
&lt;h3 id="template-structure"&gt;Template Structure
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;templates/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; systemd/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; claude.service.j2 # For claude-code and ZAI alike
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; codex.service.j2 # OpenAI Codex
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gemini.service.j2 # Google Gemini CLI
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The core part of &lt;code&gt;claude.service.j2&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;MODEL_BACKEND&lt;span style="color:#f92672"&gt;=&lt;/span&gt;claude
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CLAUDE_BIN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ claude_bin }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; claude_model &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CLAUDE_MODEL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ claude_model }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; env_file &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EnvironmentFile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ env_file }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;WORK_BASE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ work_base }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;WORK_DIR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ work_base }}&lt;span style="color:#f92672"&gt;/&lt;/span&gt;repo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;{{ &amp;#39;ALLOWED_TOOLS=&amp;#39; + allowed_tools }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CREATE_PR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ &lt;span style="color:#e6db74"&gt;&amp;#39;true&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; create_pr &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;false&amp;#39;&lt;/span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; create_pr &lt;span style="color:#f92672"&gt;and&lt;/span&gt; github_remote &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Environment&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;GITHUB_REMOTE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{{ github_remote }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#f92672"&gt;%&lt;/span&gt; endif &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For ZAI workers, the &lt;code&gt;env_file&lt;/code&gt; block is activated, adding the &lt;code&gt;EnvironmentFile&lt;/code&gt;. For PR creation workers, &lt;code&gt;github_remote&lt;/code&gt; is injected. Other settings use defaults.&lt;/p&gt;
&lt;h3 id="provision_workerpy-usage"&gt;&lt;code&gt;provision_worker.py&lt;/code&gt; Usage
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Preview (no actual deployment)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker new-worker-id --dry-run
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Actual deployment (including NATS creds issuance)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker new-worker-id --issue-creds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Bulk deployment for the entire fleet.yaml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --all
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Internal operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads worker entries from &lt;code&gt;fleet.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reads target hosts from &lt;code&gt;servers.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Renders Jinja2 templates.&lt;/li&gt;
&lt;li&gt;Deploys &lt;code&gt;/etc/systemd/system/{worker_id}-poller.service&lt;/code&gt; via SSH.&lt;/li&gt;
&lt;li&gt;Creates the working directory.&lt;/li&gt;
&lt;li&gt;Executes &lt;code&gt;systemctl daemon-reload &amp;amp;&amp;amp; enable --now&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;(Optional) Issues NATS NKey with &lt;code&gt;nsc add user&lt;/code&gt; → deploys creds → regenerates &lt;code&gt;auth.conf&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="distributed-hosting-adding-workers-to-a-second-server"&gt;Distributed Hosting: Adding Workers to a Second Server
&lt;/h2&gt;&lt;p&gt;Running all workers on a single server creates a single point of failure. We added Claude workers to a second host.&lt;/p&gt;
&lt;p&gt;The method for workers on the second host to connect to the NATS broker is via an autossh tunnel.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;NATS Broker Tunnel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;After&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;network-online.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ExecStart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/usr/bin/autossh -N \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -L 4222:127.0.0.1:4222 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -i /home/ubuntu/.ssh/id_ed25519 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Restart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RestartSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this configuration active, workers always connect to &lt;code&gt;nats://127.0.0.1:4222&lt;/code&gt;. They don&amp;rsquo;t need to know the broker host&amp;rsquo;s address. As long as the tunnel is alive, it works the same way from any host.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="nats-credential-operations-experience"&gt;NATS Credential Operations Experience
&lt;/h2&gt;&lt;p&gt;NATS NKey management was the most complex part of the implementation.&lt;/p&gt;
&lt;p&gt;NATS JetStream&amp;rsquo;s authentication structure is hierarchical.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Operator (Root Signing Authority)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── Account: SYS (System Account)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── Account: Services (Worker Account)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── User: cc-dev-01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── User: cc-go-dev-01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── User: codex-py-dev-01
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each worker has an independent User NKey and can publish/subscribe within the permissions scope (&lt;code&gt;af.&amp;gt;&lt;/code&gt;, &lt;code&gt;_INBOX.&amp;gt;&lt;/code&gt;, &lt;code&gt;$JS.&amp;gt;&lt;/code&gt;) of the Services account.&lt;/p&gt;
&lt;p&gt;Adding a new worker requires the Operator&amp;rsquo;s signing key. We initially made the mistake of not backing up this key, leading to its loss. Consequently, we had to regenerate the entire Operator and replace all worker credentials en masse. The service downtime was approximately 60 seconds.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Regeneration procedure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add operator AgentForge
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add account SYS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add account Services
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; worker in cc-dev-01 cc-go-dev-01 ...; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nsc add user --account Services --name $worker &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --allow-pub &lt;span style="color:#e6db74"&gt;&amp;#34;af.&amp;gt;,_INBOX.&amp;gt;,&lt;/span&gt;$JS&lt;span style="color:#e6db74"&gt;.&amp;gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --allow-sub &lt;span style="color:#e6db74"&gt;&amp;#34;af.&amp;gt;,_INBOX.&amp;gt;,&lt;/span&gt;$JS&lt;span style="color:#e6db74"&gt;.&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc generate config --mem-resolver --sys-account SYS &amp;gt; auth.new.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="adding-a-new-worker-the-full-procedure"&gt;Adding a New Worker: The Full Procedure
&lt;/h2&gt;&lt;p&gt;Since the completion of this system, adding a new worker is straightforward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Add an entry to &lt;code&gt;fleet.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;worker_id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;my-new-worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;llm&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;model&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;claude-haiku-4-5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;multi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;role&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;developer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;worker-node-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;enabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;create_pr&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Preview&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker my-new-worker --dry-run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Actual Deployment&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/provision_worker.py --worker my-new-worker --issue-creds
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Template rendering, SSH deployment, NATS credential issuance, and service registration are all handled by a single command.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="next-steps"&gt;Next Steps
&lt;/h2&gt;&lt;p&gt;The current system is structured such that workers process tasks independently. Future plans include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Routing Policies&lt;/strong&gt;: Automatically selecting the appropriate worker based on task characteristics (e.g., Go code → &lt;code&gt;claude-go-dev&lt;/code&gt;, cost-first → ZAI lightweight tier).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Results Comparison Dashboard&lt;/strong&gt;: A UI to display fan-out results side-by-side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost Tracking&lt;/strong&gt;: Aggregating API call costs per worker.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code is publicly available on GitHub.&lt;/p&gt;</description></item><item><title>I Sent the Same Coding Task to 4 AIs Simultaneously</title><link>https://blog.agentthread.dev/post/i-sent-the-same-coding-task-to-4-ais-simultaneously/</link><pubDate>Fri, 08 May 2026 21:55:39 +0900</pubDate><guid>https://blog.agentthread.dev/post/i-sent-the-same-coding-task-to-4-ais-simultaneously/</guid><description>&lt;p&gt;What happens when the same bug-fixing task is sent to Claude, ZAI (GLM), OpenAI Codex, and Google Gemini simultaneously?&lt;/p&gt;
&lt;p&gt;This question sparked the AgentForge project. We built a system that connects multiple LLM CLIs with the NATS JetStream message queue to process the same tasks in parallel, and in the process, we made some unexpected discoveries. This article focuses on the comparative experimental findings during the setup phase.&lt;/p&gt;
&lt;p&gt;The system&amp;rsquo;s design and implementation will be covered in Part 2.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="list-of-ais-tested"&gt;List of AIs Tested
&lt;/h2&gt;&lt;p&gt;The final configuration of 18 operational workers is as follows:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Family&lt;/th&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Notes&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;claude-sonnet-4-6&lt;/td&gt;
					&lt;td&gt;Main development worker&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Claude Code&lt;/td&gt;
					&lt;td&gt;claude-sonnet-4-5&lt;/td&gt;
					&lt;td&gt;Previous generation comparison&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Claude Code&lt;/td&gt;
					&lt;td&gt;claude-haiku-4-5&lt;/td&gt;
					&lt;td&gt;Lightweight &amp;amp; High-speed&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Claude Code&lt;/td&gt;
					&lt;td&gt;claude-opus-4-6&lt;/td&gt;
					&lt;td&gt;Top-tier&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Claude Code&lt;/td&gt;
					&lt;td&gt;claude-opus-4-5&lt;/td&gt;
					&lt;td&gt;Previous generation comparison&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;ZAI (GLM)&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;glm-5.1&lt;/td&gt;
					&lt;td&gt;High-tier&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ZAI (GLM)&lt;/td&gt;
					&lt;td&gt;glm-4.7&lt;/td&gt;
					&lt;td&gt;Mid-tier&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ZAI (GLM)&lt;/td&gt;
					&lt;td&gt;glm-4.5-air&lt;/td&gt;
					&lt;td&gt;Lightweight tier&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;OpenAI Codex&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;gpt-5.5&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Codex&lt;/td&gt;
					&lt;td&gt;gpt-5.4&lt;/td&gt;
					&lt;td&gt;1M context&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Codex&lt;/td&gt;
					&lt;td&gt;gpt-5.4-mini&lt;/td&gt;
					&lt;td&gt;400K context&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Codex&lt;/td&gt;
					&lt;td&gt;gpt-5.3-codex&lt;/td&gt;
					&lt;td&gt;272K context&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Google Gemini&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;gemini-2.5-flash&lt;/td&gt;
					&lt;td&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Gemini&lt;/td&gt;
					&lt;td&gt;gemini-2.5-pro&lt;/td&gt;
					&lt;td&gt;High-tier&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Gemini&lt;/td&gt;
					&lt;td&gt;gemini-2.5-flash-lite&lt;/td&gt;
					&lt;td&gt;Lightweight&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The list was much shorter when we first started. It grew as we experimented with which models were available.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-1-claude-3x-series-is-already-inaccessible"&gt;Discovery 1: Claude 3.x Series is Already Inaccessible
&lt;/h2&gt;&lt;p&gt;Those who have used Claude Code for a long time might recall Claude 3.7 Sonnet, 3.5 Sonnet, and 3.5 Haiku. We attempted to add these models as workers.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;claude --model claude-3-7-sonnet-20250219 --print &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → &amp;#34;may not exist or no access&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All three models returned the same error. The Claude 3 series reached its EOL in early 2026, and access via the Claude Code CLI has been blocked. Currently, only the 4.x series is available with a Claude Code subscription.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;: Claude workers were configured using only the 4.5/4.6 series.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-2-limited-model-selection-for-chatgpt-account-codex"&gt;Discovery 2: Limited Model Selection for ChatGPT Account Codex
&lt;/h2&gt;&lt;p&gt;The OpenAI Codex CLI authenticates with a ChatGPT Plus/Pro account or a separate API key. If authenticated via a ChatGPT account, the accessible models are limited.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;codex --model gpt-5.5-pro &lt;span style="color:#e6db74"&gt;&amp;#34;fix the bug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → &amp;#34;Model gpt-5.5-pro is not supported with ChatGPT account&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;codex --model gpt-5.5 &lt;span style="color:#e6db74"&gt;&amp;#34;fix the bug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → Works normally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Models available with a ChatGPT account:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Context&lt;/th&gt;
					&lt;th&gt;Inference Level&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;gpt-5.5&lt;/td&gt;
					&lt;td&gt;1M / 1M&lt;/td&gt;
					&lt;td&gt;High&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;gpt-5.4&lt;/td&gt;
					&lt;td&gt;1M / 1M&lt;/td&gt;
					&lt;td&gt;Medium&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;gpt-5.4-mini&lt;/td&gt;
					&lt;td&gt;400K / 400K&lt;/td&gt;
					&lt;td&gt;Medium&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;gpt-5.3-codex&lt;/td&gt;
					&lt;td&gt;272K / 400K&lt;/td&gt;
					&lt;td&gt;Medium&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All other models, including &lt;code&gt;gpt-5.5-pro&lt;/code&gt;, returned a &amp;ldquo;not supported with ChatGPT account&amp;rdquo; error. More models are available with an API key, but that&amp;rsquo;s a different approach.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-3-gemini-cli-only-supports-25-series"&gt;Discovery 3: Gemini CLI Only Supports 2.5 Series
&lt;/h2&gt;&lt;p&gt;We tested various models with the Gemini CLI (&lt;code&gt;gemini&lt;/code&gt; binary).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini -p &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt; -m gemini-2.0-flash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → ModelNotFoundError: models/gemini-2.0-flash is not found&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini -p &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt; -m gemini-1.5-pro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → ModelNotFoundError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemini -p &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt; -m gemini-2.5-flash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → Works normally&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gemini models accessible with the current account:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt; — Default recommended model&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-pro&lt;/code&gt; — High-tier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; — Lightweight&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Versions of Gemini 2.0 and below return &lt;code&gt;ModelNotFoundError&lt;/code&gt;. While this might vary based on account plan or API key type, based on the Gemini CLI, only the 2.5 series worked reliably.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovery-4-zai-can-be-bypassed-with-claude-sdk"&gt;Discovery 4: ZAI Can Be Bypassed with Claude SDK
&lt;/h2&gt;&lt;p&gt;ZAI is a service that provides an endpoint compatible with the Anthropic API. This allows us to use GLM models with the Claude Code CLI by changing just two environment variables.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ANTHROPIC_BASE_URL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;https://&amp;lt;ZAI endpoint&amp;gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ANTHROPIC_AUTH_TOKEN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&amp;lt;ZAI_KEY&amp;gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;claude --model glm-5.1 --print &lt;span style="color:#e6db74"&gt;&amp;#34;fix the bug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since Claude Code internally uses the Anthropic Python SDK, simply overriding &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; allows calling ZAI&amp;rsquo;s GLM models with the same format. It was interesting that we could reuse the existing &lt;code&gt;claude&lt;/code&gt; backend without any separate adapter code.&lt;/p&gt;
&lt;p&gt;The three GLM models used were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;glm-5.1&lt;/code&gt; — High-tier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glm-4.7&lt;/code&gt; — Cost-performance balance&lt;/li&gt;
&lt;li&gt;&lt;code&gt;glm-4.5-air&lt;/code&gt; — Lightweight &amp;amp; High-speed&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="4-way-fan-out-comparison-test"&gt;4-Way Fan-out Comparison Test
&lt;/h2&gt;&lt;p&gt;We simultaneously issued the same Go bug-fixing task to 4 representative workers out of the 18 (Claude Sonnet, GLM-5.1, Codex gpt-5.5, Gemini 2.5 Flash).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Task: &amp;#34;fix the off-by-one error in the binary search function&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Response times (wall clock):&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Worker&lt;/th&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Response Time&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;cc-go-dev-01&lt;/td&gt;
					&lt;td&gt;claude-sonnet-4-6&lt;/td&gt;
					&lt;td&gt;~8 seconds&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;cc-zai-high-dev-01&lt;/td&gt;
					&lt;td&gt;glm-5.1&lt;/td&gt;
					&lt;td&gt;~12 seconds&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;codex-py-dev-01&lt;/td&gt;
					&lt;td&gt;gpt-5.5&lt;/td&gt;
					&lt;td&gt;~15 seconds&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;gemini-py-dev-01&lt;/td&gt;
					&lt;td&gt;gemini-2.5-flash&lt;/td&gt;
					&lt;td&gt;~10 seconds&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;More interesting than the response times were the differences in their approaches. Claude tended to refactor the entire function, while Gemini preferred minimal modifications. Codex often included test code along with the fix.&lt;/p&gt;
&lt;p&gt;Of course, this is a single task result and has no statistical significance. It was a verification at the &amp;ldquo;does it actually work&amp;rdquo; level, not a benchmark.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="distributed-workers-adding-a-second-host"&gt;Distributed Workers: Adding a Second Host
&lt;/h2&gt;&lt;p&gt;If all workers are on the same server, the comparative experiment loses some of its meaning. Therefore, we added Claude workers to a second host.&lt;/p&gt;
&lt;p&gt;The method for workers to access the NATS broker (on the first host) from the second host is via an &lt;code&gt;autossh&lt;/code&gt; tunnel.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ExecStart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;autossh -N -L 4222:127.0.0.1:4222 broker-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By forwarding the local port 4222 to the broker, workers can connect to &lt;code&gt;nats://127.0.0.1:4222&lt;/code&gt; from any host without code changes.&lt;/p&gt;
&lt;p&gt;Advantage of this method: Workers don&amp;rsquo;t need to know where the broker is. They can always connect to &lt;code&gt;localhost:4222&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="most-panicked-moment-during-operation"&gt;Most Panicked Moment During Operation
&lt;/h2&gt;&lt;p&gt;The most distressing situation was losing the NATS operator signing key. NATS JetStream uses NKey-based authentication, and the operator/account&amp;rsquo;s signing key (nsc seed) is required to issue credentials for new workers.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nsc add user --account Services --name new-worker
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → &amp;#34;signing key not found&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There was no backup. Ultimately, we had to perform a large-scale cutover, regenerating the entire NATS operator and replacing all worker credentials with a new permission tree. Service downtime was approximately 60 seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Always create an offline backup of the NATS operator seed immediately after generation. If it&amp;rsquo;s lost, regeneration is the only option.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;Practical conclusions from this experiment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Claude 3.x is EOL&lt;/strong&gt; - Inaccessible via Claude Code CLI as of 2026. Use only 4.x.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codex ChatGPT Account Limited to 4 Models&lt;/strong&gt; - gpt-5.5, 5.4, 5.4-mini, 5.3-codex. Pro models require a separate API key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini Only 2.5 Series&lt;/strong&gt; - Previous versions inaccessible via CLI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ZAI Integrable via Claude SDK Environment Variable Override&lt;/strong&gt; - No separate adapter needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NATS NKey Must Be Backed Up&lt;/strong&gt; - Losing the signing key means reissuing everything.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The next installment will cover how these workers are connected, discussing system design and implementation.&lt;/p&gt;</description></item><item><title>Building a Blog AI Auto-Comment System (1/3) — Architecture and Implementation</title><link>https://blog.agentthread.dev/post/ai-auto-comment-system-part1-architecture/</link><pubDate>Sun, 03 May 2026 01:00:00 +0900</pubDate><guid>https://blog.agentthread.dev/post/ai-auto-comment-system-part1-architecture/</guid><description>&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;I built a system where AI automatically responds to blog comments. When a reader leaves a comment on a blog post, the AI assistant analyzes the post context and automatically generates a technically accurate yet friendly reply.&lt;/p&gt;
&lt;p&gt;This series consists of three parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Part 1 (This post)&lt;/strong&gt;: Overall architecture design and core code implementation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2&lt;/strong&gt;: File-based authentication, permission management, security hardening&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3&lt;/strong&gt;: systemd deployment, nginx proxy, troubleshooting&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="system-architecture"&gt;System Architecture
&lt;/h2&gt;&lt;h3 id="overall-data-flow"&gt;Overall Data Flow
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Reader writes a comment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[giscus] → Creates a comment on GitHub Discussions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[GitHub Webhook] → Sends HTTP POST
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[nginx reverse proxy] → Header forwarding
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Flask Worker] → Signature verification → Comment analysis
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Claude Code CLI] → Generates AI response
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[GitHub GraphQL API] → Posts reply to Discussion
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[giscus] → Displays reply on blog
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The key point in this architecture is that &lt;strong&gt;giscus uses GitHub Discussions as a comment storage&lt;/strong&gt;. Therefore, we can receive new comment events via GitHub Webhook and post responses using the same GitHub API.&lt;/p&gt;
&lt;h3 id="components"&gt;Components
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Component&lt;/th&gt;
					&lt;th&gt;Role&lt;/th&gt;
					&lt;th&gt;Technology&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;giscus&lt;/td&gt;
					&lt;td&gt;Blog comment widget&lt;/td&gt;
					&lt;td&gt;Based on GitHub Discussions&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GitHub Webhook&lt;/td&gt;
					&lt;td&gt;Event delivery&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;discussion_comment&lt;/code&gt; event&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;nginx&lt;/td&gt;
					&lt;td&gt;Reverse proxy&lt;/td&gt;
					&lt;td&gt;Header forwarding, SSL termination&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Flask Worker&lt;/td&gt;
					&lt;td&gt;Webhook receiving and processing&lt;/td&gt;
					&lt;td&gt;Python, Flask, Flask-Limiter&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Claude Code&lt;/td&gt;
					&lt;td&gt;AI response generation&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;--print&lt;/code&gt; mode CLI call&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GitHub GraphQL&lt;/td&gt;
					&lt;td&gt;Response posting&lt;/td&gt;
					&lt;td&gt;Mutation API&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="giscus-configuration"&gt;giscus Configuration
&lt;/h2&gt;&lt;p&gt;To integrate giscus into a Hugo blog, the following configuration is required in &lt;code&gt;hugo.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;comments&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;enabled&lt;/span&gt; = &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;provider&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;giscus&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;comments&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;giscus&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;repo&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;yarang/blogs&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;repoId&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;YOUR_REPO_ID&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;category&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;General&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;categoryId&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;YOUR_CATEGORY_ID&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;mapping&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;pathname&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;strict&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;reactionsEnabled&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;emitMetadata&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;inputPosition&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;bottom&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;ko&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;theme&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;noborder_gray&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;mapping = &amp;quot;pathname&amp;quot;&lt;/code&gt; maps Discussions based on the post URL path. This creates an independent Discussion for each blog post.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="github-webhook-configuration"&gt;GitHub Webhook Configuration
&lt;/h2&gt;&lt;p&gt;In the GitHub repository Settings &amp;gt; Webhooks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Payload URL&lt;/strong&gt;: &lt;code&gt;https://your-domain/webhook&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content type&lt;/strong&gt;: &lt;code&gt;application/json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret&lt;/strong&gt;: Secret for HMAC-SHA256 signature (covered in detail in the security post)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt;: Select &lt;code&gt;Discussion comments&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Webhook sends a &lt;code&gt;discussion_comment&lt;/code&gt; event to the Flask worker whenever a comment is created.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="flask-worker-implementation"&gt;Flask Worker Implementation
&lt;/h2&gt;&lt;h3 id="project-structure"&gt;Project Structure
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;auto-comment-worker/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── scripts/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── auto-comment-worker.py # Main worker
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── deploy/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── auto-comment-worker.service # systemd service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── venv/ # Python virtual environment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── logs/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── audit.log # Audit log
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="core-dependencies"&gt;Core Dependencies
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; flask &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Flask, request, jsonify
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; flask_limiter &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Limiter
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; flask_limiter.util &lt;span style="color:#f92672"&gt;import&lt;/span&gt; get_remote_address
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; marshmallow &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Schema, fields, validate, ValidationError
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flask&lt;/strong&gt;: Provides Webhook endpoints as a lightweight web framework&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flask-Limiter&lt;/strong&gt;: Prevents abuse with rate limiting (10 times per minute)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;marshmallow&lt;/strong&gt;: Safe data parsing via request schema validation&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="webhook-endpoint"&gt;Webhook Endpoint
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.route&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/webhook&amp;#39;&lt;/span&gt;, methods&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@limiter.limit&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;10 per minute&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;github_webhook&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Receives GitHub Webhook&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 1. Signature verification&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; signature &lt;span style="color:#f92672"&gt;=&lt;/span&gt; request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;headers&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;X-Hub-Signature-256&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; verify_webhook_signature(request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;data, signature):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_audit(&lt;span style="color:#e6db74"&gt;&amp;#39;SIGNATURE_INVALID&amp;#39;&lt;/span&gt;, {&lt;span style="color:#e6db74"&gt;&amp;#39;ip&amp;#39;&lt;/span&gt;: request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;remote_addr})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;unauthorized&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;401&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 2. Request schema validation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schema &lt;span style="color:#f92672"&gt;=&lt;/span&gt; WebhookSchema()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; payload &lt;span style="color:#f92672"&gt;=&lt;/span&gt; schema&lt;span style="color:#f92672"&gt;.&lt;/span&gt;load(request&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; ValidationError &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; err:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;invalid&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 3. Extract and filter comment information&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; comment &lt;span style="color:#f92672"&gt;=&lt;/span&gt; payload&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;comment&amp;#39;&lt;/span&gt;, {})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; discussion &lt;span style="color:#f92672"&gt;=&lt;/span&gt; payload&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;discussion&amp;#39;&lt;/span&gt;, {})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; original_author &lt;span style="color:#f92672"&gt;=&lt;/span&gt; comment&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;user&amp;#39;&lt;/span&gt;, {})&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;login&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;User&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Ignore if it&amp;#39;s the blog owner&amp;#39;s comment&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; _is_blog_owner(original_author):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;owner_comment_ignored&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Ignore if AI generated comment (prevent infinite loop)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; _is_ai_generated_comment(comment&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#39;body&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;)):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; jsonify({&lt;span style="color:#e6db74"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;ai_comment_ignored&amp;#39;&lt;/span&gt;}), &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 4. Generate and post AI response&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; discussion_graphql_id &lt;span style="color:#f92672"&gt;=&lt;/span&gt; get_discussion_graphql_id(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; repo_owner, repo_name, discussion_number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Title: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;discussion_title&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Content: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;discussion_body&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reply &lt;span style="color:#f92672"&gt;=&lt;/span&gt; analyze_comment(context, comment_body)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; post_reply_graphql(discussion_graphql_id, comment_body, original_author, reply)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The core flow consists of 4 steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Signature verification&lt;/strong&gt;: Verifies if the request came from GitHub using HMAC-SHA256&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema validation&lt;/strong&gt;: Validates payload structure with marshmallow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filtering&lt;/strong&gt;: Ignores comments from the blog owner and the AI itself (prevents infinite loops)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;: Generates AI response with Claude Code and posts via GraphQL API&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="preventing-infinite-loops"&gt;Preventing Infinite Loops
&lt;/h3&gt;&lt;p&gt;If AI responds to a comment created by AI, it will fall into an infinite loop. To prevent this, we use marker-based detection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_is_ai_generated_comment&lt;/span&gt;(body: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Identifies if the comment was generated by AI&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ai_markers &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;🤖 AI Assistant&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;AI Assistant&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;AgentForge&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Automatically generated by Claude Code&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;was automatically generated&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body_lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; body&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; any(marker&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower() &lt;span style="color:#f92672"&gt;in&lt;/span&gt; body_lower &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; marker &lt;span style="color:#f92672"&gt;in&lt;/span&gt; ai_markers)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When posting the AI response, you must include one of these markers in the body:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;---
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;**🤖 AI Assistant**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;safe_reply&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;*This comment was automatically generated by AgentForge + Claude Code.*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="calling-claude-code-cli"&gt;Calling Claude Code CLI
&lt;/h3&gt;&lt;p&gt;Use Claude Code&amp;rsquo;s &lt;code&gt;--print&lt;/code&gt; mode to generate an AI response non-interactively:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;analyze_comment&lt;/span&gt;(context: str, comment: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Run Claude Code with AgentForge settings&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; prompt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;## Blog Post Context
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;context[:&lt;span style="color:#ae81ff"&gt;2000&lt;/span&gt;]&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;## Reader Comment
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;comment&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;Please write a response to this comment.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;- As a technical blog assistant, be professional yet friendly
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;- Concise within 200 characters
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;- Provide additional info or links if needed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cmd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CLAUDE_CODE_PATH,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;--settings&amp;#39;&lt;/span&gt;, AGENTFORGE_CONFIG,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;--print&amp;#39;&lt;/span&gt;, prompt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run(cmd, capture_output&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, text&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;returncode &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;and&lt;/span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stdout&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stdout&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Thanks for your opinion! I think it would be great to discuss the technical aspects further. 🙏&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Specify the AgentForge dedicated configuration file with the &lt;code&gt;--settings&lt;/code&gt; flag. You can manage the model, token limits, etc. in this configuration file.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--print&lt;/code&gt; flag runs Claude Code in non-interactive mode and outputs the result to stdout. Unlike interactive mode, it terminates after a single prompt-response.&lt;/p&gt;
&lt;h3 id="github-graphql-api-integration"&gt;GitHub GraphQL API Integration
&lt;/h3&gt;&lt;p&gt;Since giscus uses GitHub Discussions, the response must also be posted via the GitHub GraphQL API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retrieve Discussion GraphQL ID:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;get_discussion_graphql_id&lt;/span&gt;(repo_owner, repo_name, discussion_number):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; query &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; query($owner: String!, $name: String!, $number: Int!) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; repository(owner: $owner, name: $name) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; discussion(number: $number) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; variables &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;owner&amp;#34;&lt;/span&gt;: repo_owner,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: repo_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;number&amp;#34;&lt;/span&gt;: discussion_number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; response &lt;span style="color:#f92672"&gt;=&lt;/span&gt; requests&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; GITHUB_API_URL,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; json&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;query&amp;#34;&lt;/span&gt;: query, &lt;span style="color:#e6db74"&gt;&amp;#34;variables&amp;#34;&lt;/span&gt;: variables},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; headers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Bearer &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;GITHUB_TOKEN&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; response&lt;span style="color:#f92672"&gt;.&lt;/span&gt;status_code &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; response&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; data[&lt;span style="color:#e6db74"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;][&lt;span style="color:#e6db74"&gt;&amp;#34;repository&amp;#34;&lt;/span&gt;][&lt;span style="color:#e6db74"&gt;&amp;#34;discussion&amp;#34;&lt;/span&gt;][&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Webhook payload only includes the Discussion&amp;rsquo;s REST API ID. However, to post a comment, the GraphQL Node ID is required. Therefore, we first retrieve the Discussion&amp;rsquo;s Node ID via a GraphQL query, then use it to post the comment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post Response (Mutation):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;post_reply_graphql&lt;/span&gt;(discussion_graphql_id, original_comment, original_author, reply):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; query &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mutation($discussionId: ID!, $body: String!) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; addDiscussionComment(input: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; discussionId: $discussionId, body: $body
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; comment { id, databaseId }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ... (Send request)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;addDiscussionComment&lt;/code&gt; mutation adds a new comment to the entire Discussion. It is a Discussion-level comment, not a reply to a specific comment.&lt;/p&gt;
&lt;h3 id="input-sanitization"&gt;Input Sanitization
&lt;/h3&gt;&lt;p&gt;Since user comments are external input, they must be sanitized:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitize_comment&lt;/span&gt;(body: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;User input sanitization&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; body:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; body
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; re&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sub(&lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;[^&amp;gt;]+&amp;gt;&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;, body) &lt;span style="color:#75715e"&gt;# Remove HTML tags&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; html&lt;span style="color:#f92672"&gt;.&lt;/span&gt;escape(body) &lt;span style="color:#75715e"&gt;# Escape HTML entities&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body &lt;span style="color:#f92672"&gt;=&lt;/span&gt; body[:&lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt;] &lt;span style="color:#75715e"&gt;# Length limit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To prevent XSS attacks, HTML tags are removed, remaining special characters are escaped, and the length is limited to 1000 characters.&lt;/p&gt;
&lt;h3 id="audit-logging"&gt;Audit Logging
&lt;/h3&gt;&lt;p&gt;We keep audit logs to track security events:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;log_audit&lt;/span&gt;(event_type: str, details: dict):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Record security event audit log&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; open(AUDIT_LOG, &lt;span style="color:#e6db74"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; f&lt;span style="color:#f92672"&gt;.&lt;/span&gt;write(json&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dumps({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;timestamp&amp;#39;&lt;/span&gt;: datetime&lt;span style="color:#f92672"&gt;.&lt;/span&gt;utcnow()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isoformat(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;event&amp;#39;&lt;/span&gt;: event_type,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;details&amp;#39;&lt;/span&gt;: details
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Event types recorded:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SIGNATURE_INVALID&lt;/code&gt;: Webhook signature verification failed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INVALID_PAYLOAD&lt;/code&gt;: Invalid request payload&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WEBHOOK_RECEIVED&lt;/code&gt;: Webhook received successfully&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AI_RESPONSE_SENT&lt;/code&gt;: AI response posted successfully&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;In this Part 1, we covered the overall architecture and core implementation code connecting giscus, GitHub Webhook, Flask worker, Claude Code CLI, and GitHub GraphQL API.&lt;/p&gt;
&lt;p&gt;In the upcoming Part 2, we will cover &lt;strong&gt;security hardening&lt;/strong&gt; for this system — file-based authentication management, file permission verification, HMAC-SHA256 signature verification, and more in detail.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This post is Part 1 of the AgentForge blog automatic comment system series.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Multi-Model AI Agent Team Design: Composed Architecture and 5-Team Hierarchy</title><link>https://blog.agentthread.dev/post/multi-model-ai-agent-team-design-composed-architecture-and-5-team-hierarchy/</link><pubDate>Mon, 30 Mar 2026 00:33:34 +0900</pubDate><guid>https://blog.agentthread.dev/post/multi-model-ai-agent-team-design-composed-architecture-and-5-team-hierarchy/</guid><description>&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;For building a blog system, I designed a multi-model agent team consisting of &lt;strong&gt;14 AI specialists, 5 teams, and 4 LLM models&lt;/strong&gt;. The core innovations are two:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Composed Agent&lt;/strong&gt;: Separating role definitions from execution profiles for maximum reusability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hierarchical Bridge Leadership&lt;/strong&gt;: Dual membership of tech leads between upper and lower teams to resolve communication bottlenecks&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This post covers the final structure, model distribution strategy, and the composed architecture design process.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="background-why-multi-model"&gt;Background: Why Multi-Model?
&lt;/h2&gt;&lt;p&gt;Using a single LLM for all tasks creates two problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Running 14 specialists on a Claude Opus-level model makes costs uncontrollable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fit&lt;/strong&gt;: Design needs fast reasoning, security analysis needs deep logic, implementation needs stable coding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we distributed models based on task characteristics.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="final-team-structure"&gt;Final Team Structure
&lt;/h2&gt;&lt;p&gt;5 teams, 14 specialists, 4 models.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 subgraph UPPER["Upper — Steering Team · consensus"]
 ORC["Orchestrator&lt;br/&gt;relay:steering-orchestrator"]
 DES["Architect&lt;br/&gt;gemini:gemini-2.5-flash"]
 SEC["Security Reviewer&lt;br/&gt;codex:gpt-4o"]
 STL["Backend Tech Lead&lt;br/&gt;relay:developer-zai"]
 FTL["Frontend Tech Lead&lt;br/&gt;relay:developer-zai"]
 DTL["Desktop Tech Lead&lt;br/&gt;relay:developer-zai"]
 INF["Infra Network&lt;br/&gt;gemini:gemini-2.5-flash"]
 SAD["Server Admin&lt;br/&gt;relay:developer-zai"]
 end

 subgraph LOWER_BE["Backend Team · leader_decides"]
 BTL["Backend Tech Lead"]
 BDEV["Backend Developer"]
 end

 subgraph LOWER_FE["Frontend Team · leader_decides"]
 FTL2["Frontend Tech Lead"]
 FDEV["Frontend Developer"]
 FUX["UX Designer"]
 end

 subgraph LOWER_DT["Desktop Team · leader_decides"]
 DTL2["Desktop Tech Lead"]
 DDEV["Desktop Developer"]
 DUX["UX Designer"]
 end

 subgraph LOWER_INFRA["Infra Team · leader_decides"]
 SAD2["Server Admin (Leader)"]
 INET["Cloud Network"]
 DBA["DB Architect"]
 end

 UPPER -.-&gt;|bridge| LOWER_BE
 UPPER -.-&gt;|bridge| LOWER_FE
 UPPER -.-&gt;|bridge| LOWER_DT
 UPPER -.-&gt;|bridge| LOWER_INFRA

 BTL --&gt; BDEV
 FTL2 --&gt; FDEV
 FTL2 --&gt; FUX
 DTL2 --&gt; DDEV
 DTL2 --&gt; DUX
 SAD2 --&gt; INET
 SAD2 --&gt; DBA&lt;/pre&gt;&lt;h3 id="team-details"&gt;Team Details
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Team&lt;/th&gt;
					&lt;th&gt;Type&lt;/th&gt;
					&lt;th&gt;Decision-Making&lt;/th&gt;
					&lt;th&gt;Leader&lt;/th&gt;
					&lt;th&gt;Members&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Steering Team&lt;/td&gt;
					&lt;td&gt;upper&lt;/td&gt;
					&lt;td&gt;consensus&lt;/td&gt;
					&lt;td&gt;Orchestrator&lt;/td&gt;
					&lt;td&gt;8 (including bridges)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Backend Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Backend Tech Lead&lt;/td&gt;
					&lt;td&gt;2&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Frontend Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Frontend Tech Lead&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Desktop Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Desktop Tech Lead&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Infra Team&lt;/td&gt;
					&lt;td&gt;lower&lt;/td&gt;
					&lt;td&gt;leader_decides&lt;/td&gt;
					&lt;td&gt;Server Admin&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="the-infra-team-separation-decision"&gt;The Infra Team Separation Decision
&lt;/h2&gt;&lt;p&gt;In the initial design, DB Architect and Server Admin were part of the backend team. But we separated them based on &lt;strong&gt;workspace&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 subgraph BackendTeam["Backend Team"]
 B["API Code Writing&lt;br/&gt;FastAPI, Python&lt;br/&gt;workspace: VS Code / SSH"]
 end

 subgraph InfraTeam["Infra Team"]
 S["Server Management&lt;br/&gt;Docker, Ubuntu, Nginx&lt;br/&gt;workspace: SSH Terminal"]
 N["Cloud Network&lt;br/&gt;Cloudflare Dashboard&lt;br/&gt;workspace: Web Console"]
 D["DB Management&lt;br/&gt;PostgreSQL, Migrations&lt;br/&gt;workspace: psql / SSH"]
 end

 B -.-&gt;|API deployment| S
 B -.-&gt;|Query optimization| D&lt;/pre&gt;&lt;p&gt;When workspaces differ, separation is more natural than keeping them together.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="model-distribution-strategy"&gt;Model Distribution Strategy
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;pie title Specialists by Model
 "relay:developer-zai (GLM)" : 10
 "gemini:gemini-2.5-flash" : 2
 "codex:gpt-4o" : 1
 "zai:glm-4" : 1&lt;/pre&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Model&lt;/th&gt;
					&lt;th&gt;Specialists&lt;/th&gt;
					&lt;th&gt;Purpose&lt;/th&gt;
					&lt;th&gt;Why Chosen&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;relay:developer-zai&lt;/td&gt;
					&lt;td&gt;10&lt;/td&gt;
					&lt;td&gt;Implementation, ops, leads&lt;/td&gt;
					&lt;td&gt;Cost-effective, stable coding&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;gemini:gemini-2.5-flash&lt;/td&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;Design, infra network&lt;/td&gt;
					&lt;td&gt;Fast response, easy external API calls&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;codex:gpt-4o&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;Security review&lt;/td&gt;
					&lt;td&gt;High reasoning, OWASP knowledge&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;zai:glm-4&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;Context compression&lt;/td&gt;
					&lt;td&gt;Free tier, text summarization specialized&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;By assigning 10 implementation specialists to GLM, we achieved &lt;strong&gt;60-70% total cost reduction&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="composed-agent-architecture"&gt;Composed Agent Architecture
&lt;/h2&gt;&lt;p&gt;The core innovation is &lt;strong&gt;separating role definitions (Expert) from execution profiles (Definition)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With the traditional approach, role and execution logic are coupled — any change requires a full rewrite, and reuse is impossible.&lt;/p&gt;
&lt;h3 id="composed-approach"&gt;Composed Approach
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 DEF["Definition&lt;br/&gt;Backend Developer"]
 DEF --&gt; BASE["Base: backend-core"]
 DEF --&gt; CAP["Capabilities:&lt;br/&gt;rest-api, crud, auth-jwt"]
 DEF --&gt; PLAT["Platform: fastapi"]
 DEF --&gt; POL["Policy: blog-default"]

 BASE --&gt; |"compose"| RUN["Runtime Agent"]
 CAP --&gt; |"compose"| RUN
 PLAT --&gt; |"compose"| RUN
 POL --&gt; |"compose"| RUN&lt;/pre&gt;&lt;h3 id="module-structure"&gt;Module Structure
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;agent-library/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── definitions/ ← 14 agent definitions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── modules/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── base/ ← 6 base modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── capabilities/ ← 15 capability modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── platforms/ ← 5 platform modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── policies/ ← 1 policy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── runs/ ← execution history
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="advantages"&gt;Advantages
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reusability&lt;/strong&gt;: &lt;code&gt;rest-api&lt;/code&gt; capability module shared by backend developer and backend tech lead&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform swap&lt;/strong&gt;: Change &lt;code&gt;platform: fastapi&lt;/code&gt; to &lt;code&gt;platform: django&lt;/code&gt; for instant switching&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capability extension&lt;/strong&gt;: Add a new capability module and connect it to the Definition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy unification&lt;/strong&gt;: All agents follow the same &lt;code&gt;blog-default&lt;/code&gt; policy&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="expert-definition-mapping"&gt;Expert-Definition Mapping
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Expert&lt;/th&gt;
					&lt;th&gt;Definition&lt;/th&gt;
					&lt;th&gt;Base&lt;/th&gt;
					&lt;th&gt;Capabilities&lt;/th&gt;
					&lt;th&gt;Platform&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Backend Developer&lt;/td&gt;
					&lt;td&gt;backend-developer&lt;/td&gt;
					&lt;td&gt;backend-core&lt;/td&gt;
					&lt;td&gt;rest-api, crud, auth-jwt&lt;/td&gt;
					&lt;td&gt;fastapi&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Backend Tech Lead&lt;/td&gt;
					&lt;td&gt;backend-tech-lead&lt;/td&gt;
					&lt;td&gt;backend-core&lt;/td&gt;
					&lt;td&gt;rest-api, crud, code-review&lt;/td&gt;
					&lt;td&gt;fastapi&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Frontend Developer&lt;/td&gt;
					&lt;td&gt;frontend-developer&lt;/td&gt;
					&lt;td&gt;frontend-core&lt;/td&gt;
					&lt;td&gt;markdown-renderer, list-filter-sort&lt;/td&gt;
					&lt;td&gt;nextjs&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Server Admin&lt;/td&gt;
					&lt;td&gt;server-administrator&lt;/td&gt;
					&lt;td&gt;server-core&lt;/td&gt;
					&lt;td&gt;docker-management, nginx-config, postgres-admin&lt;/td&gt;
					&lt;td&gt;ubuntu&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Infra Network&lt;/td&gt;
					&lt;td&gt;infra-network-admin&lt;/td&gt;
					&lt;td&gt;infra-core&lt;/td&gt;
					&lt;td&gt;dns-management, ssl-certificates, rate-limiting&lt;/td&gt;
					&lt;td&gt;cloudflare&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Security Reviewer&lt;/td&gt;
					&lt;td&gt;security-auditor&lt;/td&gt;
					&lt;td&gt;specialist-core&lt;/td&gt;
					&lt;td&gt;security-audit&lt;/td&gt;
					&lt;td&gt;fastapi&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Context Compressor&lt;/td&gt;
					&lt;td&gt;context-compressor&lt;/td&gt;
					&lt;td&gt;specialist-core&lt;/td&gt;
					&lt;td&gt;context-compression&lt;/td&gt;
					&lt;td&gt;markdown&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="tls-certificate-strategy-cloudflare-origin-ca"&gt;TLS Certificate Strategy: Cloudflare Origin CA
&lt;/h2&gt;&lt;p&gt;For production TLS certificates, we chose &lt;strong&gt;Cloudflare Origin CA&lt;/strong&gt; over Let&amp;rsquo;s Encrypt.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant Client as Visitor
 participant CF as Cloudflare (Proxy)
 participant Nginx as Nginx (Origin)
 participant API as FastAPI

 Client-&gt;&gt;CF: HTTPS request
 CF-&gt;&gt;CF: Terminate with Cloudflare-managed cert
 CF-&gt;&gt;Nginx: Encrypt with Origin CA cert
 Nginx-&gt;&gt;API: HTTP (local)
 API--&gt;&gt;Nginx: Response
 Nginx--&gt;&gt;CF: Encrypted with Origin CA
 CF--&gt;&gt;Client: Response&lt;/pre&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Item&lt;/th&gt;
					&lt;th&gt;Let&amp;rsquo;s Encrypt&lt;/th&gt;
					&lt;th&gt;Cloudflare Origin CA&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Validity&lt;/td&gt;
					&lt;td&gt;90 days (renewal required)&lt;/td&gt;
					&lt;td&gt;15 years (no renewal)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Issuance&lt;/td&gt;
					&lt;td&gt;ACME automation required&lt;/td&gt;
					&lt;td&gt;Manual from Dashboard&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Complexity&lt;/td&gt;
					&lt;td&gt;certbot setup&lt;/td&gt;
					&lt;td&gt;Copy cert files only&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Production architecture:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Oracle Cloud ARM (4 OCPU, 24GB)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── PostgreSQL (installed directly on host)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Docker Compose
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── blog-api (FastAPI)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── blog-frontend (Next.js standalone)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── MinIO (S3-compatible storage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── Nginx (Cloudflare Origin CA)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── Cloudflare Proxy (Full Strict SSL)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="relay-plugin-agent-invocation-mechanism"&gt;Relay Plugin: Agent Invocation Mechanism
&lt;/h2&gt;&lt;p&gt;The team structure runs in Claude Code through the &lt;strong&gt;Relay plugin&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;sequenceDiagram
 participant User as User
 participant Claude as Claude Code
 participant Plugin as Relay Plugin
 participant MCP as MCP Server
 participant LLM as External LLM

 User-&gt;&gt;Claude: /relay:invoke-agent
 Claude-&gt;&gt;Plugin: Load definition by expert slug
 Plugin-&gt;&gt;Plugin: Compose Definition (base + capabilities + platform + policy)
 Plugin-&gt;&gt;Plugin: Check backed_by

 alt relay:developer-zai
 Plugin-&gt;&gt;Claude: Run internal agent
 else gemini:*
 Plugin-&gt;&gt;MCP: Call gemini_mcp server
 MCP-&gt;&gt;LLM: Gemini API
 LLM--&gt;&gt;MCP: Response
 MCP--&gt;&gt;Plugin: Result
 else codex:*
 Plugin-&gt;&gt;MCP: Call codex_mcp server
 MCP-&gt;&gt;LLM: OpenAI API
 LLM--&gt;&gt;MCP: Response
 MCP--&gt;&gt;Plugin: Result
 end

 Plugin--&gt;&gt;Claude: Final result
 Claude--&gt;&gt;User: Response&lt;/pre&gt;&lt;h3 id="backed_by-namespaces"&gt;backed_by Namespaces
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Namespace&lt;/th&gt;
					&lt;th&gt;MCP Server&lt;/th&gt;
					&lt;th&gt;Purpose&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;relay:developer-zai&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;internal agent&lt;/td&gt;
					&lt;td&gt;Implementation, ops (low cost)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;relay:steering-orchestrator&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;internal agent&lt;/td&gt;
					&lt;td&gt;Coordination, final decisions&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;gemini:gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;gemini_mcp&lt;/td&gt;
					&lt;td&gt;Design, external APIs&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;codex:gpt-4o&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;codex_mcp&lt;/td&gt;
					&lt;td&gt;Security analysis&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;zai:glm-4&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;zai_mcp&lt;/td&gt;
					&lt;td&gt;Context compression&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="design-decision-log"&gt;Design Decision Log
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Decision&lt;/th&gt;
					&lt;th&gt;Alternative&lt;/th&gt;
					&lt;th&gt;Rationale&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Separate infra team&lt;/td&gt;
					&lt;td&gt;Include in backend&lt;/td&gt;
					&lt;td&gt;Different workspaces (SSH vs IDE)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Cloudflare Origin CA&lt;/td&gt;
					&lt;td&gt;Let&amp;rsquo;s Encrypt&lt;/td&gt;
					&lt;td&gt;15-year validity, no renewal&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;PostgreSQL on host&lt;/td&gt;
					&lt;td&gt;Docker container&lt;/td&gt;
					&lt;td&gt;Memory efficiency on single server&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Composed agent&lt;/td&gt;
					&lt;td&gt;Single-definition agent&lt;/td&gt;
					&lt;td&gt;Module reusability, easy platform swap&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Assign many GLM&lt;/td&gt;
					&lt;td&gt;Assign many Claude&lt;/td&gt;
					&lt;td&gt;60-70% cost reduction&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="retrospective-what-i-learned"&gt;Retrospective: What I Learned
&lt;/h2&gt;&lt;h3 id="1-executable-structure-over-perfect-structure"&gt;1. &amp;ldquo;Executable Structure&amp;rdquo; over &amp;ldquo;Perfect Structure&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;Trying to design everything perfectly can prevent you from ever starting. It&amp;rsquo;s better to compromise and improve as you execute.&lt;/p&gt;
&lt;h3 id="2-workspace-defines-team-boundaries"&gt;2. Workspace Defines Team Boundaries
&lt;/h3&gt;&lt;p&gt;People who write code and people who manage servers have different physical work environments — that&amp;rsquo;s a natural team boundary.&lt;/p&gt;
&lt;h3 id="3-the-value-of-composed-architecture"&gt;3. The Value of Composed Architecture
&lt;/h3&gt;&lt;p&gt;In an environment with 14 specialists, 5 teams, and 4 models interacting, module separation is essential. It minimizes the scope of changes and maximizes reusability.&lt;/p&gt;
&lt;h3 id="4-cost-is-determined-at-design-time"&gt;4. Cost is Determined at Design Time
&lt;/h3&gt;&lt;p&gt;Asking &amp;ldquo;does this task really need a high-cost model?&amp;rdquo; every time naturally optimizes costs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="next-steps"&gt;Next Steps
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Start Phase 1 implementation: DB, Auth, Post/Category CRUD, Docker&lt;/li&gt;
&lt;li&gt;Share team operation experience: Problems encountered during actual execution&lt;/li&gt;
&lt;li&gt;Performance monitoring: Response time per model, cost vs quality analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;

 &lt;blockquote&gt;
 &lt;p&gt;This post summarizes my experience building an AI agent team using Claude Code + Relay plugin.
It reflects learnings from applying this to a real project — different approaches may be more suitable depending on your situation.&lt;/p&gt;

 &lt;/blockquote&gt;</description></item><item><title>[LLM] How to Write Effective Prompts</title><link>https://blog.agentthread.dev/post/2026-02-21-002-llm-prompt-guide/</link><pubDate>Sat, 21 Feb 2026 20:40:00 +0900</pubDate><guid>https://blog.agentthread.dev/post/2026-02-21-002-llm-prompt-guide/</guid><description>&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;Writing effective prompts is essential for getting the most out of LLMs (Large Language Models). This article summarizes key principles and practical patterns of prompt engineering.&lt;/p&gt;
&lt;h2 id="core-principles-of-good-prompts"&gt;Core Principles of Good Prompts
&lt;/h2&gt;&lt;h3 id="1-clarity"&gt;1. Clarity
&lt;/h3&gt;&lt;p&gt;Avoid ambiguous expressions and be specific.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bad Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Write good code
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Good Example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Implement a binary search tree in Python.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Include insert, search, and delete methods,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;with time complexity of O(log n).
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-provide-context"&gt;2. Provide Context
&lt;/h3&gt;&lt;p&gt;Give background information needed for the LLM to understand the task.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;I&amp;#39;m a React beginner.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Explain the difference between useState and useEffect
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;with example code.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-role-playing"&gt;3. Role Playing
&lt;/h3&gt;&lt;p&gt;Set up responses from a specific expert&amp;rsquo;s perspective.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You are a senior backend developer with 10 years of experience.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Explain the pros and cons of microservices architecture.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-specify-output-format"&gt;4. Specify Output Format
&lt;/h3&gt;&lt;p&gt;Explicitly state the desired response format.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Summarize the following in a markdown table:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Language features
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Pros and cons
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Use cases
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="prompt-patterns"&gt;Prompt Patterns
&lt;/h2&gt;&lt;h3 id="chain-of-thought"&gt;Chain of Thought
&lt;/h3&gt;&lt;p&gt;Guide step-by-step thinking for complex problems.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Let&amp;#39;s think through this problem step by step:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1. First analyze the problem
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2. Consider solutions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3. Write the final answer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="few-shot-learning"&gt;Few-Shot Learning
&lt;/h3&gt;&lt;p&gt;Provide examples to teach the desired format.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Summarize in the following format:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Input: &amp;#34;The weather is nice today&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Output: Positive, Weather
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Input: &amp;#34;The meeting was too long&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Output: Negative, Work
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Input: &amp;#34;Started a new project&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Output: ?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="structured-prompts"&gt;Structured Prompts
&lt;/h3&gt;&lt;p&gt;Divide complex tasks into sections.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;## Goal
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Design a user authentication API
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;## Requirements
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Use JWT tokens
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Refresh token rotation
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- Apply rate limiting
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;## Output
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;1. API endpoint specification
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2. Sequence diagram
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3. Security considerations
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="common-mistakes"&gt;Common Mistakes
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Mistake&lt;/th&gt;
					&lt;th&gt;Problem&lt;/th&gt;
					&lt;th&gt;Solution&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Too long prompts&lt;/td&gt;
					&lt;td&gt;Key points get lost&lt;/td&gt;
					&lt;td&gt;Keep it concise&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Ambiguous instructions&lt;/td&gt;
					&lt;td&gt;Unexpected results&lt;/td&gt;
					&lt;td&gt;Provide specific examples&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Missing context&lt;/td&gt;
					&lt;td&gt;Inaccurate answers&lt;/td&gt;
					&lt;td&gt;Add background info&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Unspecified format&lt;/td&gt;
					&lt;td&gt;Poor readability&lt;/td&gt;
					&lt;td&gt;Specify output format&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="practical-checklist"&gt;Practical Checklist
&lt;/h2&gt;&lt;p&gt;Check before writing your prompt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Is the goal clear?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Included necessary context?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Specified output format?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Stated constraints?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Would examples help?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Good prompts are clear, specific, and provide necessary context. Practice to improve your prompt writing skills.&lt;/p&gt;
&lt;h2 id="references"&gt;References
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://platform.openai.com/docs/guides/prompt-engineering" target="_blank" rel="noopener"
 &gt;OpenAI Prompt Engineering Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.anthropic.com/claude/docs/prompt-engineering" target="_blank" rel="noopener"
 &gt;Anthropic Claude Prompt Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Korean Version:&lt;/strong&gt; &lt;a class="link" href="https://blog.agentthread.dev/ko/post/2026-02-21-002-llm-prompt-guide/" &gt;한국어 버전&lt;/a&gt;&lt;/p&gt;</description></item></channel></rss>