<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Scc_hy</title>
    <description>The latest articles on DEV Community by Scc_hy (@scchy).</description>
    <link>https://dev.to/scchy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4059119%2F44320eda-f8f5-4276-b7b5-e10351b263a8.jpg</url>
      <title>DEV Community: Scc_hy</title>
      <link>https://dev.to/scchy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scchy"/>
    <language>en</language>
    <item>
      <title>My Personal Knowledge Management Loop: Obsidian + Blog + DeepTutor</title>
      <dc:creator>Scc_hy</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:28:22 +0000</pubDate>
      <link>https://dev.to/scchy/my-personal-knowledge-management-loop-obsidian-blog-deeptutor-4m87</link>
      <guid>https://dev.to/scchy/my-personal-knowledge-management-loop-obsidian-blog-deeptutor-4m87</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The goal of knowledge management isn't "store more," it's "use it well." This post shares how I strung collecting, processing, output, and reviewing into a &lt;strong&gt;sustainable loop&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why a loop
&lt;/h2&gt;

&lt;p&gt;As a developer, I've been writing blogs, building projects, and learning new things. But for a long time, my knowledge management was &lt;strong&gt;fragmented&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blog posts were written and never revisited&lt;/li&gt;
&lt;li&gt;Things I learned were scattered everywhere, hard to find when needed&lt;/li&gt;
&lt;li&gt;Output and input were disconnected—writing blogs felt like squeezing toothpaste, not natural flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The root cause: &lt;strong&gt;my workflow was inverted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I spent most of my energy directly on writing (output) while skipping the "collect → process" stages. The result: my blog became a junk drawer where everything got stuffed, instead of a finished-goods warehouse for things I'd actually thought through.&lt;/p&gt;

&lt;p&gt;The loop I built with three tools finally solved this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The loop at a glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Obsidian (local: collect + process)
   │  distill into articles when mature
   ▼
Blog (output: site + community distribution)
   │  one-click sync script
   ▼
DeepTutor (server: RAG Q&amp;amp;A + personal mentor review)
   │  deeper review, new understanding
   ▼
(feed back into Obsidian, loop closes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of the three components has a distinct role:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&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;Core value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Obsidian&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Processing workshop&lt;/td&gt;
&lt;td&gt;Collect fragments, recombine with links, organize via PARA, build reusable assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Finished-goods warehouse&lt;/td&gt;
&lt;td&gt;Publish thought-through content, SEO accumulation, multi-platform distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepTutor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Review coach&lt;/td&gt;
&lt;td&gt;RAG Q&amp;amp;A grounded in your notes, mentor persona guides deeper understanding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Ring 1: Obsidian — the processing workshop
&lt;/h2&gt;

&lt;p&gt;Obsidian is the &lt;strong&gt;starting point and foundation&lt;/strong&gt; of the whole loop. I use the classic &lt;strong&gt;PARA structure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KnowledgeBase/
├── 0-Inbox        (fragments land here first)
├── 1-Projects     (ongoing work)
├── 2-Areas        (long-term responsibility areas)
├── 3-Resources    (permanent notes, reusable assets)
├── 4-Archive      (finished content)
└── _templates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The core mechanic is bidirectional links.&lt;/strong&gt; For example, one of my Agent retrospective notes links to 4 permanent notes (Agent skeleton, SSE streaming, context compaction, tool safety). In the graph view, knowledge isn't isolated files—it's a &lt;strong&gt;web&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Key principle: &lt;strong&gt;Obsidian stores "processed" assets, not raw copies.&lt;/strong&gt; From a 70KB blog post, I distilled 5 core sentences plus 4 structured notes. That's real sedimentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ring 2: Blog — the finished-goods warehouse
&lt;/h2&gt;

&lt;p&gt;The Blog receives content that's &lt;strong&gt;already been thought through&lt;/strong&gt;. My blog is built with Astro (see &lt;a href="https://dev.tofirst-post"&gt;this post&lt;/a&gt;) and uses a &lt;strong&gt;dual-track content strategy&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main hub&lt;/strong&gt;: my site (Astro + GitHub Pages), for SEO and long-term content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribution channels&lt;/strong&gt;: Dev.to, Juejin, for immediate reach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All distributed articles carry a &lt;code&gt;canonical_url&lt;/code&gt; pointing back to the main site, avoiding duplicate-content penalties and funneling SEO weight to the hub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key shift&lt;/strong&gt;: I went from "write whatever comes to mind" to "distill into an article once it's matured in Obsidian." Writing went from squeezing toothpaste to a natural overflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ring 3: DeepTutor — the review coach
&lt;/h2&gt;

&lt;p&gt;This is the ring that actually keeps the loop &lt;strong&gt;spinning&lt;/strong&gt;—&lt;strong&gt;review&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I upload my Obsidian knowledge base to a DeepTutor server via its API and index it with &lt;strong&gt;llamaindex&lt;/strong&gt; for RAG retrieval. DeepTutor can then answer questions grounded in my notes instead of speaking in generalities.&lt;/p&gt;

&lt;p&gt;More importantly, I configured a &lt;strong&gt;mentor persona&lt;/strong&gt; for it, whose prompt specifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prioritize referencing already-sedimented content in the knowledge base, connecting new questions to existing knowledge&lt;/li&gt;
&lt;li&gt;Socratic guidance, but give direct answers when asked&lt;/li&gt;
&lt;li&gt;Tie back to my real experience (knowing I've read 2000 lines of Agent code and which pitfalls I hit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So every question I ask in DeepTutor becomes a &lt;strong&gt;deeper review grounded in existing knowledge&lt;/strong&gt;, and the new understanding feeds back into Obsidian.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sync: Obsidian → DeepTutor
&lt;/h3&gt;

&lt;p&gt;I wrote a sync script with this core logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Detect local note changes (MD5 hash comparison)
   ├─ no change → skip
   └─ changed → delete old KB → re-upload all → wait for index → save state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I just write in Obsidian, run the script, and DeepTutor's knowledge base updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepTutor integration notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge base&lt;/strong&gt;: upload markdown notes, index with llamaindex (works out of the box, no API key)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona&lt;/strong&gt;: custom "personal mentor" persona that guides based on the knowledge base&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow&lt;/strong&gt;: login for token → upload files → poll until index is ready&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pitfalls I hit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inverted workflow&lt;/strong&gt;: the biggest one. I used to write directly, skipping sedimentation, so content never formed a system. Build the Obsidian foundation first, then talk about output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opening the wrong vault&lt;/strong&gt;: Obsidian defaulted to an empty vault, making me think the knowledge base was empty. Point Obsidian's default path at the real knowledge base directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server can't read local files&lt;/strong&gt;: DeepTutor runs on a server and can't see local paths. Solution: upload files via API instead of having the server read local directories directly.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What this loop changed for me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge went from dead to alive&lt;/strong&gt;: notes connect into a web via links, no longer isolated files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output went from squeezing to natural&lt;/strong&gt;: once matured in Obsidian, Blog is just distillation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review went from occasional to daily&lt;/strong&gt;: DeepTutor turns every question into a deepening&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core takeaway: &lt;strong&gt;knowledge management isn't hoarding, it's flow.&lt;/strong&gt; Collect, process, output, review—every step makes knowledge more useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Wrap the sync script into a one-command shortcut&lt;/li&gt;
&lt;li&gt;Keep iterating on DeepTutor's mentor persona so it understands my knowledge system better&lt;/li&gt;
&lt;li&gt;Keep writing, and let the loop spin on its own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're doing personal knowledge management too, I'd love to hear how you've structured your system.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>knowledgemanagement</category>
      <category>obsidian</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Building a Pi Agent from Scratch (7-Day Retrospective)</title>
      <dc:creator>Scc_hy</dc:creator>
      <pubDate>Wed, 05 Aug 2026 18:16:35 +0000</pubDate>
      <link>https://dev.to/scchy/building-a-pi-agent-from-scratch-7-day-retrospective-7g2</link>
      <guid>https://dev.to/scchy/building-a-pi-agent-from-scratch-7-day-retrospective-7g2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I spent 7 days building a Pi Agent from scratch and understood the universal skeleton behind every agent:&lt;br&gt;
&lt;strong&gt;loop + tools + context management&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full implementation: &lt;a href="https://github.com/scchy/My_Learn/tree/master/PI_Agent/pi_agent" rel="noopener noreferrer"&gt;mini-PI-Agent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Wrote This Post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Motivation&lt;/strong&gt;: I didn't want to remain just an API caller. I wanted to truly understand the internals of an Agent, so I could do better Agent development and RL-related research.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome&lt;/strong&gt;: ~2,000 lines of code, 205 unit tests passing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audience&lt;/strong&gt;: Developers who want to understand how Agents work under the hood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview&lt;/strong&gt;: Each module = one engineering problem + my solution, ending with a distilled universal skeleton.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Big Picture: Agent System Architecture
&lt;/h2&gt;

&lt;p&gt;Think of an Agent as a computer: LLM + Tools = CPU, recent conversation = RAM, compressed summaries = virtual memory, session tree = disk, CLI = operating system. Once these components are wired together into a reusable system, you can run different apps on top — coding, research, data analysis. Swap the app without changing the skeleton: that's the essence of generalizing to any Agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│                                                                        │
│   ┌────────────────────────────────────────────────────────────────┐  │
│   │   OS / CLI                                                       │  │
│   │   ── Agent terms: Harness / Agent loop scheduler               │  │
│   │   Scheduling · Config precedence · Interaction/steering          │  │
│   │   Meta-commands (/compact /status)                             │  │
│   └─────────────────────────────┬──────────────────────────────────┘  │
│                                 │ Assemble into a reusable system       │
│                                 ▼                                      │
│   ┌────────────────────────────────────────────────────────────────┐  │
│   │                                                              │  │
│   │   ┌─────────────── Agent Loop (the heart) ────────────────┐ │  │
│   │   │                                                       │ │  │
│   │   │    ┌──────────┐      ┌──────────────┐      ┌────────┐ │ │  │
│   │   │    │ Reason   │─────►│   Act         │─────►│Observe │ │ │  │
│   │   │    │ (LLM)    │      │ (call Tools)  │      │(result)│ │ │  │
│   │   │    └────┬─────┘      └──────┬───────┘      └───┬────┘ │ │  │
│   │   │         │                   │                  │      │ │  │
│   │   │         └───────────────────┴──────────────────┘      │ │  │
│   │   │                     ▲ Loop until done                 │ │  │
│   │   │    (no tool_call / max_turns / user interrupt)        │ │  │
│   │   └───────────────────────────────────────────────────────┘ │  │
│   │                                                              │  │
│   │   ┌────────────────────────┐   ┌──────────────────────────┐  │  │
│   │   │  CPU (compute)         │   │  RAM (working memory)    │  │  │
│   │   │  ── Agent: LLM + Tools │◄─►│  ── Agent: Context      │  │  │
│   │   │  Reasoner + toolset    │   │  Recent msgs + state     │  │  │
│   │   └────────────────────────┘   └────────────┬─────────────┘  │  │
│   │                                             │ Swap out when full│  │
│   │                                             ▼                │  │
│   │   ┌──────────────────────────────────────────────────────┐  │  │
│   │   │  Virtual memory (swap)                             │  │  │
│   │   │  ── Agent: Compaction / retrieval                  │  │  │
│   │   │  Compressed summaries + retrieval (swap in/out)  │  │  │
│   │   └──────────────────────────────┬───────────────────────┘  │  │
│   │                                  │ Persist                  │  │
│   │   ┌──────────────────────────────▼───────────────────────┐  │  │
│   │   │  Device (persistent storage)                         │  │  │
│   │   │  ── Agent: Session / file tracking                   │  │  │
│   │   │  Session tree / JSONL / file tracking                │  │  │
│   │   └─────────────────────────────────────────────────────┘  │  │
│   │                                                              │  │
│   └────────────────────────────────────────────────────────────────┘  │
│                                                                        │
│   ┌────────────────────────────────────────────────────────────────┐  │
│   │   APP (concrete tasks running on the system)                   │  │
│   │   Coding · Research · Data analysis · File ops ...             │  │
│   │   ── Swap the app, keep the skeleton ──                        │  │
│   └────────────────────────────────────────────────────────────────┘  │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mapping table&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Computer&lt;/th&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;LLM + Tools&lt;/td&gt;
&lt;td&gt;Compute unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;Working memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual memory&lt;/td&gt;
&lt;td&gt;Compaction / retrieval&lt;/td&gt;
&lt;td&gt;Swap in/out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Session / file tracking&lt;/td&gt;
&lt;td&gt;Persistent storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Harness / CLI&lt;/td&gt;
&lt;td&gt;Scheduling + config + interaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;Concrete Agent app&lt;/td&gt;
&lt;td&gt;Tasks running on the system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Module 1: LLM Client
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do you stably call an OpenAI Chat Completions-style streaming LLM API?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part&lt;/strong&gt;: Streaming doesn't return the full response at once; it pushes chunks as they are generated. Network jitter and rate limits (429) can interrupt at any moment. Stable calling = correct SSE parsing + graceful failure handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module role&lt;/strong&gt;: The LLM client is a &lt;strong&gt;generic layer&lt;/strong&gt; — every Agent calls an LLM. Keep only "mechanics" here (how to parse, retry, estimate tokens), not "policy" (when to compress, how much to keep). Policy belongs to the Agent-specific context manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[x] SSE streaming parse (&lt;code&gt;async for&lt;/code&gt;, line by line)

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data: &amp;lt;JSON&amp;gt;&lt;/code&gt; + blank-line separators&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[DONE]&lt;/code&gt; marks the end of the stream&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delta&lt;/code&gt; is incremental, not a full snapshot
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                    LLM server (e.g. OpenAI API)                      │
│                                                                     │
│   Generated text: "The weather is nice, let's" → "go to the park"    │
│                                                                     │
│   Server cuts content into chunks and pushes them over SSE:          │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              │ HTTP response stream (SSE)
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Raw SSE byte stream (network layer)              │
│                                                                     │
│   data: {"choices":[{"delta":{"content":"The"}}]}                    │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" weather"}}]}                │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" is"}}]}                    │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" nice"}}]}                  │
│                                                                     │
│   data: {"choices":[{"delta":{"content":","}}]}                     │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" let's"}}]}                 │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" go"}}]}                   │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" to"}}]}                   │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" the"}}]}                  │
│                                                                     │
│   data: {"choices":[{"delta":{"content":" park"}}]}                  │
│                                                                     │
│   data: [DONE]                                                      │
│                                                                     │
│   (Lines separated by \n\n; data: prefix + JSON body)                 │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              │ Read line by line (async for)
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Client parse (llm.py)                            │
│                                                                     │
│   async for line in response:          # read SSE line by line      │
│       if line.startswith("data:"):     # only handle data: lines    │
│           data = line[5:].strip()      # strip "data:" prefix        │
│           if data == "[DONE]":         # stream end marker           │
│               break                    # stop the loop               │
│           chunk = json.loads(data)     # parse JSON                │
│           text = chunk["choices"][0]["delta"]["content"]             │
│           yield text                   # emit text fragment          │
│                                                                     │
│   Emitted sequence: "The" → " weather" → " is" → ... → " park"     │
│                                                                     │
│   Client concatenates: "The weather is nice, let's go to the park"  │
└─────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] Exponential backoff retry (network jitter / 429)

&lt;ul&gt;
&lt;li&gt;Exponential backoff borrows the "backoff" idea from CSMA/CD, but the scenario is different — one avoids collisions before sending, the other waits after failure.&lt;/li&gt;
&lt;li&gt;Exponential backoff: 1s, 2s, 4s, 8s ... after each failure&lt;/li&gt;
&lt;li&gt;CSMA/CD (Ethernet): wait $2^n × slot_time$ after n collisions
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                    Client (llm.py)                                  │
│                                                                     │
│   ┌─────────────┐                                                   │
│   │ 1st request │───► fails (network jitter / 429)                  │
│   └─────────────┘        │                                          │
│                          ▼                                          │
│                   wait 1s (2^0 × base)                              │
│                          │                                          │
│   ┌─────────────┐        │                                          │
│   │ 2nd request │◄───────┘                                          │
│   └─────────────┘───► still 429                                     │
│                          │                                          │
│                          ▼                                          │
│                   wait 2s (2^1 × base)                              │
│                          │                                          │
│   ┌─────────────┐        │                                          │
│   │ 3rd request │◄───────┘                                          │
│   └─────────────┘───► still 429                                     │
│                          │                                          │
│                          ▼                                          │
│                   wait 4s (2^2 × base)                              │
│                          │                                          │
│   ┌─────────────┐        │                                          │
│   │ 4th request │◄───────┘                                          │
│   └─────────────┘───► success!                                      │
│                          │                                          │
│                          ▼                                          │
│                   reset backoff counter                             │
│                                                                     │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  wait = 2^(n-1) × base + random jitter                    │   │
│   │  n = retry count, base = initial delay (e.g. 1s)          │   │
│   │  cap: max_retries (e.g. 5) or max_wait (e.g. 60s)         │   │
│   └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Token estimation&lt;/strong&gt;: classify characters — CJK ≈ 1 token, other chars 4 ≈ 1 token, round up, add ~4 tokens metadata overhead per message. Conservative estimate to avoid overflow.&lt;/li&gt;
&lt;li&gt;[x] &lt;strong&gt;LLM self-summarization&lt;/strong&gt;: provide prompt templates and wrappers for summaries.

&lt;ul&gt;
&lt;li&gt;Mechanics (how to summarize) + LLM call → &lt;code&gt;llm.py&lt;/code&gt;, reusable&lt;/li&gt;
&lt;li&gt;Policy (when to summarize, how much to keep, how to splice back) → &lt;code&gt;context.py&lt;/code&gt;, Agent-specific&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Boundary cases in streaming parse&lt;/strong&gt; (everyone who writes SSE parsing will hit these):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Truncated JSON&lt;/strong&gt;: network jitter may deliver only half a JSON object in one chunk, causing &lt;code&gt;json.loads&lt;/code&gt; to throw.

&lt;ul&gt;
&lt;li&gt;Fix: catch &lt;code&gt;json.JSONDecodeError&lt;/code&gt;, skip the chunk, wait for the next chunk to complete it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken stream&lt;/strong&gt;: server drops mid-stream, &lt;code&gt;async for&lt;/code&gt; ends early without &lt;code&gt;[DONE]&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Fix: treat "stream ended early" as an error and retry (with exponential backoff).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty / comment lines&lt;/strong&gt;: SSE may include blank lines or lines starting with &lt;code&gt;:&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Fix: skip them; don't try to parse them as data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty delta&lt;/strong&gt;: some chunks have &lt;code&gt;delta&lt;/code&gt; without &lt;code&gt;content&lt;/code&gt; (e.g. only &lt;code&gt;role&lt;/code&gt; or &lt;code&gt;tool_calls&lt;/code&gt;).

&lt;ul&gt;
&lt;li&gt;Fix: check before accessing, otherwise &lt;code&gt;KeyError&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data:&lt;/code&gt; prefix stripping&lt;/strong&gt;: &lt;code&gt;data:&lt;/code&gt; is protocol, not payload. Strip with &lt;code&gt;line[5:]&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Pitfall: &lt;code&gt;json.loads(line)&lt;/code&gt; will try to parse &lt;code&gt;data:&lt;/code&gt; as JSON and fail.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reflection&lt;/strong&gt;: SSE parsing is easy in the happy path; the hard part is boundary cases. Half JSON, broken streams, empty deltas — you only encounter these in real runs. &lt;strong&gt;Defensive programming&lt;/strong&gt; (null checks, exception handling, treating broken streams as errors) is mandatory for streaming parsing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reflection
&lt;/h3&gt;

&lt;p&gt;The LLM client is the foundation of every Agent. Without stable streaming and retries, everything above collapses. Separating mechanism from policy is what makes the client reusable — it doesn't care how the Agent uses context; it only needs to reliably return results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Module 2: Tool System
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do you let an LLM call functions safely?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part&lt;/strong&gt;: An LLM only emits text; it cannot execute code directly. The tool system must do two things — &lt;strong&gt;expose functions to the LLM&lt;/strong&gt; (so it knows what tools exist and how to call them) and &lt;strong&gt;protect the system&lt;/strong&gt; (prevent the LLM from invoking dangerous operations). The former uses schemas; the latter uses defensive lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module role&lt;/strong&gt;: The tool protocol is the Agent's "limbs and hands". It determines what the Agent can and cannot do. The core design is &lt;strong&gt;declarative&lt;/strong&gt; — developers declare tools with an &lt;code&gt;@tool&lt;/code&gt; decorator, the system auto-generates schemas, and the LLM calls according to the schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Decorator registration (&lt;code&gt;@tool&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A developer decorates a normal function with &lt;code&gt;@tool&lt;/code&gt;; the system registers it automatically.&lt;/li&gt;
&lt;li&gt;Benefit: &lt;strong&gt;declarative&lt;/strong&gt; — adding a tool means adding a function, similar to &lt;code&gt;@mcp.tool&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Query the weather for a city&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is sunny today, 25°C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[x] &lt;strong&gt;Auto-generated schema&lt;/strong&gt; (JSON Schema from function signature)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the function signature with &lt;code&gt;inspect.signature&lt;/code&gt;, convert parameter types, defaults, and docstring into JSON Schema.&lt;/li&gt;
&lt;li&gt;Benefit: single source of truth — the signature is the only truth; the schema is generated automatically, so you never "update the signature but forget the schema".&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[x] &lt;strong&gt;Dangerous confirmation&lt;/strong&gt; (dangerous ops require human approval)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark a tool &lt;code&gt;dangerous=True&lt;/code&gt;; a confirmation dialog pops before execution.&lt;/li&gt;
&lt;li&gt;Benefit: LLMs may misjudge or be tricked (prompt injection); dangerous ops must have human oversight.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dangerous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Delete a file (dangerous, requires confirmation)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deleted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Timeout cutoff&lt;/strong&gt; (prevent tools from running forever)

&lt;ul&gt;
&lt;li&gt;Add a timeout (e.g. 30s) to each tool call; force-interrupt if it exceeds, so a stuck tool cannot hang the whole Agent.&lt;/li&gt;
&lt;li&gt;Benefit: a single tool failure should not crash the loop; the Agent can continue or report the error.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Schema generation: all parameters typed as &lt;code&gt;string&lt;/code&gt; (simplification in current implementation)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the LLM passes parameters according to the schema, it sends ints as strings (e.g. &lt;code&gt;"30"&lt;/code&gt; instead of &lt;code&gt;30&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Therefore the built-in tools are full of manual conversions like &lt;code&gt;int(offset)&lt;/code&gt; and &lt;code&gt;float(timeout_sec)&lt;/code&gt; — the cascading cost of the "schema simplification".
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# All parameters are treated as string
&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;param_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: the schema is a contract between LLM and function. An imprecise contract (everything marked string) makes the LLM pass wrong types, so the function must do fallback conversions. Contract precision = amount of fallback code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. What counts as "dangerous"?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the current implementation only &lt;code&gt;bash&lt;/code&gt; is marked &lt;code&gt;dangerous=True&lt;/code&gt;; other tools (write, edit, delete) are not. But:

&lt;ul&gt;
&lt;li&gt;write / edit modify files — are they dangerous?&lt;/li&gt;
&lt;li&gt;bash can execute arbitrary commands, including &lt;code&gt;rm -rf&lt;/code&gt; — definitely dangerous.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: danger classification has no absolute standard; it's an engineering judgment. The principle is "better over-mark than under-mark" — mark uncertain tools as dangerous and let the user confirm, rather than under-mark and cause accidental deletion. Marking only bash is conservative; whether write/edit should also be marked is debatable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Timeout cutoff: cooperation of &lt;code&gt;asyncio.wait_for&lt;/code&gt; and &lt;code&gt;to_thread&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The bash tool additionally uses &lt;code&gt;subprocess.run(timeout=...)&lt;/code&gt; to actually kill the child process.&lt;/li&gt;
&lt;li&gt;But for other tools (e.g. reading a large file), the thread may keep running in the background after timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: &lt;code&gt;asyncio.wait_for&lt;/code&gt; only stops waiting; it cannot kill a thread. Real termination requires internal timeout support in the tool itself (e.g. &lt;code&gt;subprocess.run(timeout=)&lt;/code&gt;). Timeout is cooperative, not forcible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Output truncation: prevent tool output from blowing up context&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3900&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;... [truncated, original output &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chars]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Tool output (e.g. &lt;code&gt;ls -R&lt;/code&gt; from bash) can be huge; stuffing it directly into context can blow the token budget. Truncate to 4000 chars and annotate the original length.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: tool output is an important context source but uncontrolled. Truncation is the first line of defense in context management — limiting output size at the tool layer is cheaper than compressing at the context layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reflection
&lt;/h3&gt;

&lt;p&gt;The tool system is the Agent's limbs and hands; four lines of defense (&lt;code&gt;dangerous flag&lt;/code&gt; / &lt;code&gt;confirmation&lt;/code&gt; / &lt;code&gt;timeout&lt;/code&gt; / &lt;code&gt;error wrapping&lt;/code&gt; / &lt;code&gt;output truncation&lt;/code&gt;) are standard in production. But deeper design philosophies emerge from the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Contract" determines everything&lt;/strong&gt;: The schema is the contract between LLM and function. An imprecise contract (all strings) makes the LLM pass wrong types, so the function must do fallback conversions. Contract precision = amount of fallback code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors are "data", not exceptions&lt;/strong&gt;: &lt;code&gt;ToolResult&lt;/code&gt; wraps errors as return values so the LLM can see the error and adjust. This is unique to Agent scenarios — errors should be handed to the LLM for judgment rather than crashing the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout is cooperative&lt;/strong&gt;: &lt;code&gt;asyncio.wait_for&lt;/code&gt; can only stop waiting, not kill threads. Real termination needs internal tool support. Timeout is not a silver bullet; tools must be able to exit gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security is layered&lt;/strong&gt;: dangerous flag (static declaration) + human confirmation (runtime gate) + timeout (anti-hang) + output truncation (anti-bloat). A single layer is not enough; layers must stack.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Module 3: Agent Loop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do you make an LLM act autonomously?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part&lt;/strong&gt;: An LLM is stateless — it generates output based only on the current input and doesn't "do things" by itself. The Agent loop wires "thinking" and "acting" together: LLM thinks one step → executes tools → observes results → thinks again... until the task is done. This "think → act → observe → think again" loop is the heart of the Agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module role&lt;/strong&gt;: The Agent loop is the &lt;strong&gt;skeleton of every Agent&lt;/strong&gt;. Whether the upper layer is ReAct, Plan-then-Execute, or Reflexion, the bottom layer is this loop. Its core design decision is the &lt;strong&gt;termination condition&lt;/strong&gt; — when to stop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;ReAct loop (Reason → Act → Observe)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Each turn: LLM streams "thought" (text) + "action" (tool_call) → execute tools → feed results (tool messages) back to LLM → next turn&lt;/li&gt;
&lt;li&gt;Until the LLM stops calling tools (gives final answer directly), or &lt;code&gt;max_turns&lt;/code&gt; is reached (default 50)
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Check steering (user interjection mid-run)
&lt;/span&gt;    &lt;span class="n"&gt;steering_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_drain_steering&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;steering_msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[User steering]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;steering_msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Context compression (check token usage every turn)
&lt;/span&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress_if_needed&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Stream LLM call, collect text + tool_calls
&lt;/span&gt;    &lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_stream_and_collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. No tool_calls → task complete
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;final_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="c1"&gt;# 5. Has tool_calls → execute in parallel, inject results
&lt;/span&gt;    &lt;span class="n"&gt;tool_msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_execute_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_msgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Parallel tool calls (&lt;code&gt;asyncio.gather&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;When the LLM emits multiple tool_calls at once, execute them concurrently rather than serially.&lt;/li&gt;
&lt;li&gt;Key: &lt;code&gt;return_exceptions=True&lt;/code&gt; — ensures all tools run to completion; one failure doesn't drag down the others.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Streaming render&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;LLM replies are displayed as they generate (typewriter effect), so users don't wait idle.&lt;/li&gt;
&lt;li&gt;Key: single panel + delayed tool-result printing — avoids nested &lt;code&gt;Live&lt;/code&gt; conflicts.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refresh_per_second&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;live_text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live_text&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;▌&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Steering interrupt&lt;/strong&gt; (&lt;code&gt;asyncio.Queue&lt;/code&gt; injects user messages)

&lt;ul&gt;
&lt;li&gt;Users can interject while the Agent is running to change direction.&lt;/li&gt;
&lt;li&gt;Implementation: &lt;code&gt;steer()&lt;/code&gt; puts a message into &lt;code&gt;_steer_queue&lt;/code&gt; and sets &lt;code&gt;_cancel_event&lt;/code&gt; to interrupt the current LLM stream; next loop &lt;code&gt;_drain_steering()&lt;/code&gt; extracts and injects it.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;steer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_steer_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cancel_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# interrupt current LLM stream
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Termination condition design (most overlooked, most critical decision)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Agent loop must be able to stop, otherwise it loops forever and burns money. The code has four layers of termination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;tool_call&lt;/code&gt;: LLM no longer calls tools, gives final answer directly → normal end&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_turns&lt;/code&gt;: max turns reached (default 50) → forced end, prevents infinite loops&lt;/li&gt;
&lt;li&gt;User interrupt: &lt;code&gt;StreamCancelledError&lt;/code&gt; or &lt;code&gt;_aborted&lt;/code&gt; → immediate end&lt;/li&gt;
&lt;li&gt;LLM error: &lt;code&gt;LLMError&lt;/code&gt; → log error, end current turn&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: termination is not "one thing" but "multiple layers stacked". Relying only on "no tool_call" is insufficient — the LLM might keep calling tools (e.g. reading the same file repeatedly), so &lt;code&gt;max_turns&lt;/code&gt; is mandatory. Termination conditions are the most overlooked and most critical decision in loop design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Result replay order for parallel tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;asyncio.gather&lt;/code&gt; executes multiple tools concurrently, but when replaying results into &lt;code&gt;messages&lt;/code&gt;, the order must match the &lt;code&gt;tool_call&lt;/code&gt; order. The key practice in the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use zip to pair task_info with results one-to-one
&lt;/span&gt;&lt;span class="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tool_msgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: &lt;code&gt;asyncio.gather&lt;/code&gt; returns results in submission order (not completion order), so as long as results are replayed by &lt;code&gt;tool_call_id&lt;/code&gt;, order is preserved. The key is to explicitly maintain the mapping between tool_call and result; don't assume "whoever finishes first gets replayed first".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Tool results must map back to the correct &lt;code&gt;tool_call_id&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The OpenAI protocol requires: tool results must carry &lt;code&gt;tool_call_id&lt;/code&gt;, and every &lt;code&gt;tool_call&lt;/code&gt; must have a corresponding result. Missing one will cause the LLM to error or behave strangely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: tool results are not "just stuffed into messages"; they must map precisely by &lt;code&gt;tool_call_id&lt;/code&gt;. This is a protocol requirement and a prerequisite for correct LLM reasoning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Tool errors should be fed back to the LLM, not crash&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The design of &lt;code&gt;return_exceptions=True + ToolResult(is_error=True)&lt;/code&gt; (the comment says "tool errors are fed back to the LLM with isError semantics rather than crashing"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ToolResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: there are two philosophies for tool errors — throw and crash the flow, or return an error flag and let the LLM handle it. The current implementation chooses the latter because in Agent scenarios, errors should be judged by the LLM so it can adjust and retry. The code also has an &lt;code&gt;all_failed&lt;/code&gt; check — when all tools fail, it notes "the LLM will attempt to recover".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;5. Dangerous tool confirmation (&lt;code&gt;confirm_dangerous&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before executing dangerous tools (e.g. bash), the code prints a warning, but auto-confirms in non-interactive mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confirm_dangerous&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_dangerous_tools&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[yellow]⚠ Dangerous op: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...[/yellow]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[dim]Auto-confirming dangerous operation[/dim]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# non-interactive auto-confirm
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: dangerous confirmation should prompt the user in interactive mode, but in non-interactive mode (scripts, CI) there is no one to wait for, so it must auto-confirm or reject. The "confirmation" action itself must be mode-aware.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reflection
&lt;/h3&gt;

&lt;p&gt;The Agent loop is the heart of the Agent, and its design philosophy is &lt;strong&gt;"loop + termination + resilience"&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loop = repetition of think → act → observe&lt;/strong&gt;: the LLM only does one step per turn; the loop strings multi-step tasks together. This is the core of ReAct — breaking complex tasks into multiple simple decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Termination condition is the soul of the loop&lt;/strong&gt;: without it, the loop is infinite. Four-layer termination (no &lt;code&gt;tool_call&lt;/code&gt; / &lt;code&gt;max_turns&lt;/code&gt; / user interrupt / LLM error) is standard in production — it must end normally, have a forced fallback, and be interruptible by humans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience means feedback, not crash&lt;/strong&gt;: tool errors are wrapped as &lt;code&gt;ToolResult(is_error=True)&lt;/code&gt; and fed back to the LLM so it can recover. This is unique to Agent scenarios — errors are "data", handed to the LLM for judgment rather than crashing the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism saves time but is not required&lt;/strong&gt;: parallel tool calls are an optimization, not the core. &lt;strong&gt;The core is correct replay — order and &lt;code&gt;tool_call_id&lt;/code&gt; mapping&lt;/strong&gt; — these details determine whether the LLM can reason correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steering is the key to human-in-the-loop collaboration&lt;/strong&gt;: an Agent is not a fully automatic black box; users must be able to interject and change direction. &lt;code&gt;asyncio.Queue&lt;/code&gt; + &lt;code&gt;cancel_event&lt;/code&gt; is the implementation of human-in-the-loop — it can inject new instructions and interrupt current actions.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Module 4: Context Management
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do you prevent long conversations from "forgetting"?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part&lt;/strong&gt;: LLM context windows are limited (128K or less), while a single coding task can generate thousands of messages — reading files, editing code, running tests, fixing bugs, back and forth for dozens of turns. When the total token count approaches the window limit, you must &lt;strong&gt;swap old content out&lt;/strong&gt; to leave space for recent conversation. But swapping out is not simply discarding — the discarded context may contain key decisions, file paths, and error information; lose them and the Agent "forgets".&lt;/p&gt;

&lt;p&gt;The difficulty of swapping out lies in three conflicts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep vs discard&lt;/strong&gt;: which messages should be swapped out? On what basis?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summary vs original&lt;/strong&gt;: when swapping out, do you discard directly or compress into a summary? How much detail does the summary lose?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First vs repeated&lt;/strong&gt;: the first compression into a summary is fine, but long conversations may be compressed 5–10 times — should you fully rewrite the old summary every time, or incrementally update?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Module role&lt;/strong&gt;: Context management is the Agent's "virtual memory" — when RAM (recent conversation) is full, old pages are swapped out to disk (compressed into summaries) and swapped back in when needed (injected as summary messages). It determines the Agent's "memory span" — can it remember decisions from 50 turns ago, or only the last 5?&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;The overall flow: at the start of each Agent loop turn, &lt;code&gt;compress_if_needed()&lt;/code&gt; is called and does 5 things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compress_if_needed(messages, context_limit)
  │
  ├── Step 1: Estimate tokens → trigger only above threshold
  │     threshold = context_limit - reserve_tokens
  │     e.g. 128K - 16K = 112K, current 115K → trigger
  │
  ├── Step 2: Split system / non-system messages
  │     system messages (prompts) → always keep, never compress
  │     non_system → proceed
  │
  ├── Step 3: Walk backward to find a valid cut point
  │     Keep the most recent keep_recent_tokens of raw messages
  │     Cut point must be valid (user / tool_call start / non-orphan assistant)
  │     ↓
  │     Determine is_split_turn: does the cut fall in the middle of a Turn?
  │
  ├── Step 4: Generate summary
  │     ├── Normal cut (Turn boundary) → single summary (full / incremental)
  │     └── Split Turn → dual summary (history summary + turn prefix summary)
  │
  └── Step 5: Merge summary + file-operation tracking → rebuild message list
        system + [summary messages] + kept recent messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Absolute token budgets (&lt;code&gt;reserve_tokens&lt;/code&gt; + &lt;code&gt;keep_recent_tokens&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Two budgets, separate concerns:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reserve_tokens = 16384&lt;/code&gt;: space reserved for model generation + safety margin. &lt;code&gt;context_limit - reserve_tokens&lt;/code&gt; is the trigger threshold — compression starts only above this value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keep_recent_tokens = 20000&lt;/code&gt;: how many of the most recent tokens of raw messages to keep (uncompressed).&lt;/li&gt;
&lt;li&gt;Key distinction: &lt;code&gt;reserve_tokens&lt;/code&gt; decides &lt;strong&gt;when to trigger&lt;/strong&gt;; &lt;code&gt;keep_recent_tokens&lt;/code&gt; decides &lt;strong&gt;how much to keep&lt;/strong&gt;. They are independent and must not be the same value.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompactionConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reserve_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16384&lt;/span&gt;       &lt;span class="c1"&gt;# trigger threshold = context_limit - reserve_tokens
&lt;/span&gt;    &lt;span class="n"&gt;keep_recent_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;   &lt;span class="c1"&gt;# keep the most recent N tokens of raw messages
&lt;/span&gt;    &lt;span class="n"&gt;summary_max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;    &lt;span class="c1"&gt;# history summary length cap
&lt;/span&gt;    &lt;span class="n"&gt;turn_prefix_summary_max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;  &lt;span class="c1"&gt;# split-turn prefix summary length cap
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Cut-point search (walk backward + Turn boundary protection)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Idea: start from the newest message, accumulate token count backward until exceeding &lt;code&gt;keep_recent_tokens&lt;/code&gt; → find the first valid cut point after that position.&lt;/li&gt;
&lt;li&gt;Valid cut-point rules (&lt;code&gt;is_valid_cut_point&lt;/code&gt;):
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role               │ Valid cut?   │ Reason
───────────────────┼──────────────┼─────────────────────────────
user               │ ✅ yes       │ Turn start, natural boundary
assistant + tool_calls │ ✅ yes   │ Tool-call unit start, followed by tool results
assistant (plain)  │ ⚠️ depends   │ Cannot cut if previous is tool (would break tool unit)
tool               │ ❌ no        │ Would create orphan tool result
system             │ ❌ no        │ At the beginning, not involved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_valid_cut_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# tool alone creates orphan
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# user is Turn start
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# assistant with tool_calls = unit start
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="c1"&gt;# Plain assistant: previous cannot be tool
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;After finding the cut point, determine whether it is a &lt;strong&gt;split turn&lt;/strong&gt;: find the nearest &lt;code&gt;user&lt;/code&gt; message before the cut as the Turn start; if cut ≠ Turn start, it's a split turn → dual summary.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message sequence (old → new):
[user: "read file"] [assistant: tool_call(read)] [tool: file content] [assistant: "file content is..."]
←─── Turn start                               ↑ cut lands here → split turn!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Dual summaries (&lt;code&gt;SUMMARIZATION_PROMPT&lt;/code&gt; + &lt;code&gt;UPDATE_SUMMARIZATION_PROMPT&lt;/code&gt;)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two prompt templates cover two scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full summary&lt;/strong&gt; (&lt;code&gt;SUMMARIZATION_PROMPT&lt;/code&gt;): first compression, no old summary. Ask the LLM to produce a structured summary from scratch: Goal → Constraints → Progress (Done / In Progress / Blocked) → Key Decisions → Next Steps → Critical Context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental summary&lt;/strong&gt; (&lt;code&gt;UPDATE_SUMMARIZATION_PROMPT&lt;/code&gt;): 2nd+ compression, old summary exists. Inject the old summary text via &lt;code&gt;{previous_summary}&lt;/code&gt; and ask the LLM to append new progress, update status, and preserve key information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key logic at call time — detect whether an old summary exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_previous_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cut_index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Scan backward for [Context Summary] marker and extract old summary text.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cut_index&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Context Summary]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Context Summary]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;   &lt;span class="c1"&gt;# return old summary text + index
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;              &lt;span class="c1"&gt;# no old summary → full mode
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With an old summary, only pass &lt;strong&gt;new messages after the summary&lt;/strong&gt; to the LLM (&lt;code&gt;history_start = prev_summary_idx + 1&lt;/code&gt;), rather than re-reading all history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Incremental mode: pass only new messages + old summary
&lt;/span&gt;&lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_previous_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;non_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cut_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;history_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev_idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;previous_summary&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;new_messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;non_system&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;history_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cut_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# only new messages
&lt;/span&gt;
&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# non-None → UPDATE_SUMMARIZATION_PROMPT
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn prefix summary&lt;/strong&gt; (&lt;code&gt;TURN_PREFIX_SUMMARIZATION_PROMPT&lt;/code&gt;): unique product of a split turn. When a Turn is cut in half — first half compressed, second half kept — the turn prefix summary carries the "context" of the first half to the second half.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Split Turn diagram:
┌──────────────────────────────────────────────────────┐
│  Compressed part                │  Kept part         │
│                                  │                    │
│  [user: task request]            │  [assistant: plan] │
│  [assistant: tool_call(read)]   │  [assistant: edit] │
│  [tool: file content]           │  ← second half needs│
│                                  │    to know what    │
│  ← generate turn prefix summary ─→│    first half read │
│  "User asked to read file X;    │                    │
│   content shows issue Y..."      │                    │
└──────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;File operation tracking (&lt;code&gt;fileops.py&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;From all &lt;code&gt;tool_calls&lt;/code&gt; in the compressed messages, extract paths for &lt;code&gt;read&lt;/code&gt; / &lt;code&gt;grep&lt;/code&gt; / &lt;code&gt;find&lt;/code&gt; / &lt;code&gt;ls&lt;/code&gt; (read类) and &lt;code&gt;write&lt;/code&gt; / &lt;code&gt;edit&lt;/code&gt; / &lt;code&gt;bash&lt;/code&gt; (modify类).&lt;/li&gt;
&lt;li&gt;Format the file list as a Markdown block appended under the summary, helping the LLM know "which files were read/modified historically".
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_file_operations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;reads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;find&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
                    &lt;span class="n"&gt;reads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
                    &lt;span class="n"&gt;modified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reads&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modified_files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modified&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final summary message format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[Context Summary]

&lt;span class="gu"&gt;## Goal&lt;/span&gt;
Fix database connection error in config.py

&lt;span class="gu"&gt;## Progress&lt;/span&gt;
&lt;span class="gu"&gt;### Done&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [x] Read config.py, found db_host points to localhost:3306
...

&lt;span class="gu"&gt;## Files Read&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; config.py
&lt;span class="p"&gt;-&lt;/span&gt; db.py

&lt;span class="gu"&gt;## Files Modified&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; config.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Turn boundary protection (don't break tool-call pairing)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The validity check (&lt;code&gt;is_valid_cut_point&lt;/code&gt;) is the "constraint cornerstone" of the compression system — if you cut at the wrong position, the LLM receives isolated &lt;code&gt;tool&lt;/code&gt; messages (without corresponding &lt;code&gt;assistant&lt;/code&gt; tool_calls), causing protocol errors.&lt;/li&gt;
&lt;li&gt;The rule in one sentence: &lt;strong&gt;only cut at "natural conversation boundaries"&lt;/strong&gt; — either a Turn start (user) or a tool-call unit start (assistant + tool_calls), never in the middle of a tool-call chain.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. The "connection" problem of incremental summary — the most important gap to fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UPDATE_SUMMARIZATION_PROMPT&lt;/code&gt; and &lt;code&gt;generate_summary(previous_summary=...)&lt;/code&gt; were written from the start, but &lt;code&gt;compress_if_needed()&lt;/code&gt; always passed &lt;code&gt;None&lt;/code&gt; — causing every compression to fully rewrite the old summary. A conversation compressed 5 times = 5 full rewrites, wasting tokens and possibly losing details.&lt;/p&gt;

&lt;p&gt;The fix centers on &lt;code&gt;_extract_previous_summary()&lt;/code&gt; — scan backward for &lt;code&gt;[Context Summary]&lt;/code&gt; marker messages, extract the old text, then pass only &lt;strong&gt;new messages after the summary&lt;/strong&gt; to the LLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before fix: always full rewrite
&lt;/span&gt;&lt;span class="n"&gt;history_summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history_messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After fix: detect old summary, incrementally update
&lt;/span&gt;&lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_previous_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;non_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cut_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;history_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev_idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;previous_summary&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;history_summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;non_system&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;history_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cut_index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# only new messages
&lt;/span&gt;    &lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;previous_summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# old summary → UPDATE_SUMMARIZATION_PROMPT
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: the incremental summary "interface" was ready, but the "call" was not wired. This is a classic "code present, logic not connected" — the prompt template and parameter definitions were correct; what was missing was the step to "discover" the old summary from the message list. &lt;strong&gt;Interface correctness ≠ functional completeness&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. The necessity of "forward search" after finding the threshold&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After walking backward and finding position i where token budget is exceeded, you cannot cut directly at i. Because i may be an invalid cut point (e.g. inside a tool message). You must search &lt;strong&gt;forward&lt;/strong&gt; from i for the first valid point — this expands the kept range but preserves message integrity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;estimate_message_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;keep_recent_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;    &lt;span class="c1"&gt;# search forward for valid point
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_valid_cut_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;cut_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: "finding the threshold position" and "finding a valid cut point" are two separate steps. Forward search means keeping slightly more messages than budget — this is an intentional trade-off: &lt;strong&gt;better to spend a few extra tokens preserving complete messages than to cut a tool-call chain for budget precision&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Dual-summary concatenation logic for split turns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the cut lands in the middle of a Turn, two summaries are needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;History summary: compression of all complete Turns (history before the Turn start)&lt;/li&gt;
&lt;li&gt;Turn prefix summary: the first half of the current Turn that was cut away&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But concatenating these two summaries is not a simple &lt;code&gt;+&lt;/code&gt; — the merged message must clearly distinguish "historical context" from "the first half of the current Turn", otherwise the LLM gets confused.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge_summaries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turn_prefix_summary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;history_summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

---

**Turn Context (split turn):**

&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;turn_prefix_summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: split turn is a "boundary case" of compression, but it happens frequently in long conversations. The core of dual summaries is &lt;strong&gt;using formatting to distinguish context layers&lt;/strong&gt; — history is "background", turn prefix is "continuation of the current task". Blurring the layers causes the LLM to treat history as the current task.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Identity of compressed summary messages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compressed summaries in the message list are special — they are neither from the user nor a normal LLM reply; they are system-injected. The &lt;code&gt;[Context Summary]&lt;/code&gt; prefix exists precisely for identifiability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_is_compaction_summary()&lt;/code&gt; uses it to identify summary messages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_extract_previous_summary()&lt;/code&gt; uses it to find old summaries&lt;/li&gt;
&lt;li&gt;It is also the watershed between incremental and full mode&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: when injecting "meta-messages" (summaries) into the message stream, you must give them an identifiable marker. Otherwise later compression logic cannot tell which messages are summaries and which are raw conversation, and incremental updates become impossible. &lt;strong&gt;Markers carry metadata&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reflection
&lt;/h3&gt;

&lt;p&gt;Context management is &lt;strong&gt;the hardest part of productionizing an Agent&lt;/strong&gt; — not because the code is complex (~550 lines total), but because &lt;strong&gt;strategy choices directly affect the quality and cost of long conversations&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token budget vs turn budget&lt;/strong&gt;: Pi uses absolute token budgets (&lt;code&gt;reserve_tokens&lt;/code&gt; / &lt;code&gt;keep_recent_tokens&lt;/code&gt;), not "keep last N turns". Turn budgets fail because turns vary wildly (from 10-token "continue" to 5000-token code reviews); fixed turn counts either waste space or blow the window. &lt;strong&gt;Token budgets match model windows more precisely&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Forward search after threshold is a trade-off&lt;/strong&gt;: finding the threshold then searching forward for a valid point means keeping more messages than budget. This is a deliberate compromise on "message integrity" — &lt;strong&gt;better to spend a few extra tokens preserving tool-call pairing than to save tokens but create orphan messages&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Interface + call" two-layer design of incremental summaries&lt;/strong&gt;: &lt;code&gt;UPDATE_SUMMARIZATION_PROMPT&lt;/code&gt; is the interface layer ("how to incrementally update"); &lt;code&gt;_extract_previous_summary&lt;/code&gt; is the call layer ("when to go incremental"). Correct interface does not mean complete function — &lt;strong&gt;between code present and logic connected lies the step of "discovering the old summary"&lt;/strong&gt;. This is the easiest engineering detail to overlook.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Split turn is a compromise on "atomicity"&lt;/strong&gt;: ideally compression cuts at Turn boundaries, but a long Turn may have one user request followed by 20 tool-calls — 20 turns that may far exceed &lt;code&gt;keep_recent_tokens&lt;/code&gt;. Split turn acknowledges "this Turn is too long, must cut in the middle". Dual summaries compensate by distinguishing context layers via formatting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File tracking is the first building block of "long-term memory"&lt;/strong&gt;: &lt;code&gt;fileops.py&lt;/code&gt; extracts read/modified file lists from historical tool_calls, so the summary contains not only "what was said" but also "which files were touched". This information exists independently of the summary — even if the LLM-generated summary loses file paths (uncontrollable), the file list remains. &lt;strong&gt;Dual encoding (summary + structured list) is more reliable than relying solely on LLM summaries&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The computer analogy is particularly apt here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context = RAM&lt;/strong&gt;: recent conversation is working memory, fast but limited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compaction = virtual memory&lt;/strong&gt;: old conversation is "swapped out" into summaries and "swapped back in" via summary messages when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental summary = incremental checkpoint&lt;/strong&gt;: instead of dumping all RAM to disk every time, only write dirty pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File tracking = page table&lt;/strong&gt;: maintains metadata of "which files were touched", independent of content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The essence of context management is one sentence: &lt;strong&gt;use limited space to remember infinitely long conversations&lt;/strong&gt;. Compression strategy (full vs incremental), cut-point choice (Turn boundary vs split turn), and metadata preservation (file tracking) are all concrete implementations of this goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Module 5: Session Persistence
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do you save and restore conversations?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part&lt;/strong&gt;: An Agent conversation can last hours and span multiple sessions. Code written today should be resumable tomorrow — you cannot start from scratch every time. So the conversation must be &lt;strong&gt;persisted to disk&lt;/strong&gt; and &lt;strong&gt;restored&lt;/strong&gt; on next launch. But persistence is more than "dump messages to a file" — it involves three deeper issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structural problem&lt;/strong&gt;: conversation is not a straight line. A user might &lt;code&gt;/save&lt;/code&gt; a checkpoint at any time, then branch from that point into different exploration directions. What is saved is not a file but a &lt;strong&gt;conversation tree&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance problem&lt;/strong&gt;: if every &lt;code&gt;save()&lt;/code&gt; fully rewrites the whole file (possibly hundreds of KB), frequent saves are slow. Can we append only new data?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency problem&lt;/strong&gt;: if the program crashes mid-write (e.g. power loss), the file may be corrupted. How do we guarantee &lt;strong&gt;atomic writes&lt;/strong&gt;?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Module role&lt;/strong&gt;: Session persistence is the Agent's "disk" (Device). It maps to the lowest layer of the computer analogy — whatever is in RAM must eventually land on disk. Without it, the Agent has the memory of a fish — every launch is a fresh start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;JSONL storage + full in-memory load&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Each line is a JSON object (one session node), easy to append and parse line by line.&lt;/li&gt;
&lt;li&gt;On startup, load everything into &lt;code&gt;dict[str, SessionNode]&lt;/code&gt; for O(1) lookup.&lt;/li&gt;
&lt;li&gt;Write back on exit.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SessionNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;   &lt;span class="c1"&gt;# in-memory index
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_dirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_new_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;             &lt;span class="c1"&gt;# newly added nodes
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_modified_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="c1"&gt;# modified nodes
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Load everything on startup.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SessionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                   &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bookmark&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bookmark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Conversation tree (&lt;code&gt;parent_id&lt;/code&gt; linked list)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Each node points to its parent via &lt;code&gt;parent_id&lt;/code&gt;, forming a tree.&lt;/li&gt;
&lt;li&gt;Branching: the same parent can have multiple child nodes → users can explore different directions from a checkpoint.&lt;/li&gt;
&lt;li&gt;Queries: &lt;code&gt;get_branch(node_id)&lt;/code&gt; walks from leaf to root; &lt;code&gt;get_children(parent_id)&lt;/code&gt; lists all branches.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversation tree:
                    ┌──────────────┐
                    │  root (abc1) │  ← first conversation
                    │  "Build an API"│
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │  node (abc2) │  ← user /save checkpoint1
                    │  "API done"  │
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
     ┌────────▼───┐  ┌─────▼──────┐  ┌─▼──────────┐
     │ node (abc3)│  │ node (abc4)│  │ node (abc5) │  ← three branches
     │ Add cache  │  │ Make async │  │ Add tests   │
     └────────────┘  └────────────┘  └────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[x] &lt;strong&gt;Bookmarks (restore points)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can bookmark any node: &lt;code&gt;store.bookmark_node(node_id, "before-refactor")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restore by bookmark: &lt;code&gt;store.get_by_bookmark("before-refactor")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bookmarks are "human-readable restore points" — much friendlier than memorizing &lt;code&gt;abc123def456&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[x] &lt;strong&gt;Atomic write + append-only optimization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two write strategies, chosen automatically:&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only new nodes&lt;/strong&gt; (&lt;code&gt;_new_ids&lt;/code&gt; non-empty, &lt;code&gt;_modified_ids&lt;/code&gt; empty) → &lt;strong&gt;append-only&lt;/strong&gt;: append new lines to the end, O(new nodes), zero rewrite cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modified nodes exist&lt;/strong&gt; (&lt;code&gt;_modified_ids&lt;/code&gt; non-empty) → &lt;strong&gt;full atomic rewrite&lt;/strong&gt;: write to a temp file → &lt;code&gt;os.replace(tmp, target)&lt;/code&gt;. &lt;code&gt;os.replace&lt;/code&gt; is atomic on POSIX: either fully succeeds or leaves the original file untouched.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_modified_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_full_rewrite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;# full atomic rewrite
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_new_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_append_new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# fast append
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_full_rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write to temp file → atomic rename, never corrupts original.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkstemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.sessions_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fdopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# atomic replace
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Message serialization / deserialization&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Message&lt;/code&gt; ↔ &lt;code&gt;dict&lt;/code&gt; conversion: &lt;code&gt;_message_to_dict()&lt;/code&gt; / &lt;code&gt;_dict_to_message()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Session nodes store &lt;code&gt;list[dict]&lt;/code&gt; (JSON-compatible), restored to &lt;code&gt;list[Message]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Tree vs linear: when do you need branches?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JSONL one node per line + &lt;code&gt;parent_id&lt;/code&gt; links = naturally supports trees. But "tree" brings complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On restore, you restore &lt;strong&gt;one node&lt;/strong&gt; (one path), not the whole tree.&lt;/li&gt;
&lt;li&gt;Branch child nodes exist but do not affect the current path — this is both an advantage (isolation) and a potential confusion ("why can't I find my recent edit? Because it's in another branch").&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: tree structure models "exploration" — users can return to checkpoints and try different approaches. But &lt;strong&gt;most conversations do not need branching&lt;/strong&gt; — users just progress linearly. Tree is a capability, but linear is the default. &lt;strong&gt;Default linear, branch when needed&lt;/strong&gt; is the pragmatic strategy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. State reconstruction on restore (compactor state, token counts)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The current implementation only restores the &lt;code&gt;messages&lt;/code&gt; list, but the Agent has runtime state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ContextCompactor._last_stats&lt;/code&gt;: compression statistics&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Agent._turn_count&lt;/code&gt;: current turn count (resets to 0 after restore)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Agent._steer_queue&lt;/code&gt; / &lt;code&gt;_cancel_event&lt;/code&gt;: runtime control signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These states are not persisted — the restored Agent is "partially reconstructed". Losing compactor state means the first compression after restore may be less precise (no previous statistics reference).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: persistence granularity determines restore "completeness". Current is "message-level persistence" (only messages), not "state-level persistence" (all runtime states). Full restore would require serializing all Agent internal state — complexity doubles, but completeness doubles too. &lt;strong&gt;This is a trade-off: simple vs complete&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Empty-write guard: necessity of &lt;code&gt;ensure_loaded()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the user calls &lt;code&gt;save()&lt;/code&gt; without first calling &lt;code&gt;load()&lt;/code&gt;, it causes an &lt;strong&gt;empty overwrite&lt;/strong&gt; — empty &lt;code&gt;_nodes&lt;/code&gt; overwrites the existing JSONL file. Fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SessionStore has not loaded data, save() forbidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: persistence operations need an &lt;strong&gt;explicit state machine&lt;/strong&gt; — "loaded" is a precondition. Writing without checking state is catastrophic. &lt;strong&gt;Defensive programming in I/O is not optional, it is mandatory&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Append-only "dirty data" risk&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the same node is appended twice (e.g. &lt;code&gt;_new_ids&lt;/code&gt; not cleared after &lt;code&gt;create_node&lt;/code&gt;, then &lt;code&gt;save()&lt;/code&gt; again), the JSONL file will have duplicate lines. On next &lt;code&gt;load()&lt;/code&gt;, later lines overwrite earlier ones — result is fine, but the file bloats.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: append-only is performant but state-dependent — you must precisely track "what has already been written". The cleanup timing of &lt;code&gt;_new_ids&lt;/code&gt; and &lt;code&gt;_modified_ids&lt;/code&gt; (at the end of &lt;code&gt;save()&lt;/code&gt;) is critical. Cleaning too early or too late both cause problems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reflection
&lt;/h3&gt;

&lt;p&gt;Session persistence is the most durable layer of the Agent's "three-tier memory":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Short-term memory = &lt;code&gt;agent.messages&lt;/code&gt;&lt;/strong&gt; (RAM): full message list of current conversation, disappears when the program exits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium-term memory = conversation tree&lt;/strong&gt; (Disk): JSONL-persisted conversation history, exists across sessions. Can branch, backtrack, restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-term memory = file tracking + summaries&lt;/strong&gt; (Archive): file operation records extracted by &lt;code&gt;fileops.py&lt;/code&gt; and compaction summaries — not tied to a specific conversation, can be referenced in entirely different sessions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Design philosophy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSONL &amp;gt; SQLite&lt;/strong&gt;: for "hundreds of nodes, occasional queries", JSONL + full in-memory load is far simpler than SQLite. No SQL, no ORM, no migration scripts. &lt;strong&gt;Simple scenarios use simple solutions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic write &amp;gt; direct write&lt;/strong&gt;: &lt;code&gt;tempfile + os.replace&lt;/code&gt; is the classic POSIX atomic-write pattern. The cost is one extra file copy; the benefit is "original file is never corrupted". &lt;strong&gt;I/O safety beats I/O performance&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append-only optimizes the common path&lt;/strong&gt;: most save operations only need to append new nodes (normal conversation → exit → save), not modify existing nodes. Optimize the common path, not the rare one — &lt;strong&gt;the 80/20 rule applies to engineering optimization too&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Module 6: CLI / Harness
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do you turn the Agent into a usable product?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part&lt;/strong&gt;: The first five modules build the Agent kernel — LLM client, tool system, loop, context management, session persistence. But users cannot write Python scripts every time they want to use the Agent. A &lt;strong&gt;"Harness"&lt;/strong&gt; is needed to wrap the kernel, providing a command-line entry point, configuration management, and interactive experience.&lt;/p&gt;

&lt;p&gt;Harness's core challenge is &lt;strong&gt;"multi-scenario adaptation"&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One-shot vs continuous conversation&lt;/strong&gt;: ask one question and leave, or keep interacting?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration source precedence&lt;/strong&gt;: users may provide API Key / model / base_url via CLI args, environment variables, or config files — how to merge them?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interrupt recovery&lt;/strong&gt;: last conversation was interrupted; how to resume quickly?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Module role&lt;/strong&gt;: Harness is the Agent's "operating system" — it starts, configures, and schedules the Agent, providing the user interface. The Agent kernel is the "engine"; the Harness is the "steering wheel + dashboard".&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;typer commands (&lt;code&gt;chat&lt;/code&gt; + &lt;code&gt;resume&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pi-agent chat&lt;/code&gt;: start a new conversation. Can take a &lt;code&gt;prompt&lt;/code&gt; argument for one-shot Q&amp;amp;A; omit to enter interactive mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pi-agent resume &amp;lt;bookmark&amp;gt;&lt;/code&gt;: resume a previous conversation from a saved point.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--base-url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--max-turns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;no_confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Configuration precedence (CLI &amp;gt; env &amp;gt; config file)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three sources, strict ordering. &lt;code&gt;resolve_kwargs()&lt;/code&gt; uses an &lt;code&gt;or&lt;/code&gt; chain for fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_kwargs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cli_kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# load from config.yaml
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;cli_kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cli_kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.deepseek.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;cli_kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Precedence chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLI args  &amp;gt;  environment variables  &amp;gt;  config file (config.yaml)  &amp;gt;  hard-coded defaults
  --model      OPENAI_API_KEY        model: deepseek-chat           "deepseek-chat"
  deepseek-v4  OPENAI_BASE_URL       base_url: https://...          "https://api.deepseek.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Interactive / non-interactive dual mode&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-interactive&lt;/strong&gt; (&lt;code&gt;chat "write me a function"&lt;/code&gt;): one sentence in, result out, auto-save, exit. Good for scripts/CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive&lt;/strong&gt; (&lt;code&gt;chat&lt;/code&gt; without args): enter REPL loop, continuous conversation, supports meta-commands. Good for development and debugging.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_run_non_interactive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_run_interactive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;[x] &lt;strong&gt;Meta-command system (&lt;code&gt;/save&lt;/code&gt; &lt;code&gt;/exit&lt;/code&gt; &lt;code&gt;/stats&lt;/code&gt; &lt;code&gt;/bookmarks&lt;/code&gt; &lt;code&gt;/help&lt;/code&gt;)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In interactive mode, inputs starting with &lt;code&gt;/&lt;/code&gt; are interpreted as meta-commands rather than sent to the LLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/exit        save current session and exit
/save &amp;lt;name&amp;gt; bookmark current node (no new node created)
/bookmarks   list all bookmarks (★ marks current node)
/stats       show context stats (turns, message count, token usage)
/help        show help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implementation: in &lt;code&gt;_run_interactive&lt;/code&gt; main loop, first check &lt;code&gt;user_input.startswith("/")&lt;/code&gt;; if a command matches, &lt;code&gt;continue&lt;/code&gt; to skip LLM call.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[x] &lt;strong&gt;Rich terminal rendering&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Startup info: &lt;code&gt;Panel.fit()&lt;/code&gt; shows model, API address, max turns, recent bookmarks.&lt;/li&gt;
&lt;li&gt;Agent output: &lt;code&gt;Markdown()&lt;/code&gt; renders code-block highlighting.&lt;/li&gt;
&lt;li&gt;Tool results: &lt;code&gt;Panel(body, title=f"tool: {name}")&lt;/code&gt; colored panels distinguish success/failure.&lt;/li&gt;
&lt;li&gt;Resume hint: displays bookmark name and corresponding node ID.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[x] &lt;strong&gt;Smart &lt;code&gt;resume&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First lookup by bookmark name, then by node ID — bookmarks are human-friendly.&lt;/li&gt;
&lt;li&gt;Restore runtime params (model, max_turns) from node &lt;code&gt;metadata&lt;/code&gt;, but CLI explicit args override.&lt;/li&gt;
&lt;li&gt;After restore, enter interactive mode directly without reconfiguration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. The &lt;code&gt;or&lt;/code&gt; chain trap of configuration precedence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;or&lt;/code&gt; chain for fallback looks elegant, but has a pitfall — &lt;strong&gt;empty string vs None handling&lt;/strong&gt;. If &lt;code&gt;OPENAI_API_KEY=""&lt;/code&gt; (set but empty), &lt;code&gt;or&lt;/code&gt; treats it as falsy and skips it. If the user passes &lt;code&gt;--api-key ""&lt;/code&gt; (typer gives empty string), it is also skipped — the user may think they cleared the key, but fallback is used.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: the &lt;code&gt;or&lt;/code&gt; chain cannot distinguish "user didn't pass" from "user passed empty". For fields like API Key that "must have value" this is fine, but for fields "allowed to be empty" (e.g. &lt;code&gt;base_url&lt;/code&gt;), an empty string may be intentional. Python's &lt;code&gt;or&lt;/code&gt; is concise but imprecise fallback — &lt;strong&gt;if you need to distinguish "unset" from "set to empty", use &lt;code&gt;is None&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Exception handling in the interactive loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_run_interactive&lt;/code&gt; wraps &lt;code&gt;agent.run()&lt;/code&gt; in a &lt;code&gt;try/except&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[red]✗ Runtime error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;[/red]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if &lt;code&gt;agent.run()&lt;/code&gt; throws an uncaught exception internally (e.g. LLM API down), this &lt;code&gt;except&lt;/code&gt; catches it — yet the Agent's internal state (&lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;_turn_count&lt;/code&gt;) may already be partially modified. The next &lt;code&gt;run()&lt;/code&gt; starts from a half-modified state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: not crashing on exceptions = good interactive UX (user doesn't lose conversation), but state consistency = possibly broken. The current implementation chooses the former — &lt;strong&gt;"resilience over consistency"&lt;/strong&gt; — which is reasonable for interactive tools, but production-grade Agents need finer-grained state rollback.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;3. Partial state recovery with &lt;code&gt;resume&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Restore only recovers &lt;code&gt;agent.messages&lt;/code&gt; and partial config, but the following are lost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compactor state (&lt;code&gt;compressor._last_stats&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Turn count (restarts from 0)&lt;/li&gt;
&lt;li&gt;Current &lt;code&gt;current_node_id&lt;/code&gt; binding (if a new conversation restores an old node)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These losses mean post-restore behavior is not fully consistent with pre-interruption — e.g. compression threshold judgment may be less accurate (last compression stats lost).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: restore "completeness" is a sliding scale — from "only messages" (simplest) to "full snapshot" (most complete). Current chooses the simplest because full snapshots require serializing all Agent internal state, and snapshots from different Agent versions are incompatible. &lt;strong&gt;"Restore 80% of state" is more practical than "restore 100% but version-incompatible"&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Config file location and format&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two locations supported: &lt;code&gt;./config.yaml&lt;/code&gt; (project-level) and &lt;code&gt;~/.pi-agent/config.yaml&lt;/code&gt; (user-level). Current implementation merges with &lt;code&gt;config.update()&lt;/code&gt;, later-loaded overrides earlier-loaded (i.e. user-level overrides project-level). But YAML nested structures can produce unexpected merge results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sk-xxx&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deepseek-chat&lt;/span&gt;
&lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.deepseek.com&lt;/span&gt;
&lt;span class="na"&gt;max_turns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Essence of the pitfall: simple flat configs work fine, but once nested configs are supported (e.g. &lt;code&gt;compaction.reserve_tokens: 8192&lt;/code&gt;), &lt;code&gt;dict.update&lt;/code&gt; shallow merge breaks. &lt;strong&gt;Shallow merge is enough for flat configs; nested configs need deep merge&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reflection
&lt;/h3&gt;

&lt;p&gt;Harness is the "last mile" of the Agent — no matter how well the kernel is written, users won't use it without a good CLI. Several design philosophies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual mode = dual users&lt;/strong&gt;: one-shot mode for scripts/CI (composable, automatable), interactive mode for humans (explorable, interruptible). Both modes share the same Agent initialization logic (&lt;code&gt;_build_agent_and_store&lt;/code&gt;), diverging only at the "input loop". &lt;strong&gt;Share core, fork interface&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configuration precedence = user freedom&lt;/strong&gt;: CLI overrides the most (can differ per call), config file overrides the least (global defaults). The longer the precedence chain, the more user freedom, but also harder to debug — "which value actually took effect?" is the classic config-precedence problem. Current implementation lacks a &lt;code&gt;--show-config&lt;/code&gt; command to print the effective config, a UX gap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Meta-commands vs natural language&lt;/strong&gt;: the &lt;code&gt;/&lt;/code&gt; prefix is a clear "command vs conversation" boundary. Why not natural language (e.g. "please save the session")? Because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Determinism: &lt;code&gt;/save&lt;/code&gt; 100% triggers save; natural language may be misinterpreted by the LLM.&lt;/li&gt;
&lt;li&gt;Efficiency: one character &lt;code&gt;/&lt;/code&gt; distinguishes, no extra LLM round-trip.&lt;/li&gt;
&lt;li&gt;Privacy: meta-commands do not go through the LLM (no token cost, no conversation leakage).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Harness is a "replaceable shell"&lt;/strong&gt;: the same Agent kernel can wear different Harnesses — CLI, Web UI, IDE plugin, API service. Harness decides "how to interact"; Agent decides "what it can do". &lt;strong&gt;Decoupling Harness from Agent is the first step to productization&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;resume&lt;/code&gt; is key to human-machine trust&lt;/strong&gt;: a user spends 30 minutes debugging a bug with the Agent, exits or crashes, and returns seamlessly next time — this builds more trust than any new feature. &lt;strong&gt;Interrupt recovery is not "nice to have", it is "must have"&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Closing: From Pi Agent to Any Agent
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Universal skeleton
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Loop + Tools + Context Management + Memory + Control&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Swappable parts (the key to generalization)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Pi's implementation&lt;/th&gt;
&lt;th&gt;Other variants&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decision paradigm&lt;/td&gt;
&lt;td&gt;ReAct&lt;/td&gt;
&lt;td&gt;Plan-then-Execute / Reflexion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool protocol&lt;/td&gt;
&lt;td&gt;Local functions&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context compression&lt;/td&gt;
&lt;td&gt;Dual summaries&lt;/td&gt;
&lt;td&gt;5-stage progressive pipeline / RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Conversation tree&lt;/td&gt;
&lt;td&gt;Vector store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Agent&lt;/td&gt;
&lt;td&gt;Single Agent&lt;/td&gt;
&lt;td&gt;Sub-agent delegation / orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. The computer analogy (the finishing touch)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Think of an Agent as a computer: LLM + Tools = CPU, Context = RAM,&lt;br&gt;
Compaction = virtual memory, Session = disk, CLI = operating system.&lt;br&gt;
Wire these components into a reusable system, and you can run different apps on top — coding, research, data analysis.&lt;br&gt;
&lt;strong&gt;Swap the app, keep the skeleton — that is the essence of generalizing to any Agent.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. What's next
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sub-agent delegation (spawn independent-context child agents)&lt;/li&gt;
&lt;li&gt;Richer meta-command system (&lt;code&gt;/compact&lt;/code&gt;, &lt;code&gt;/status&lt;/code&gt;, &lt;code&gt;/undo&lt;/code&gt; handled by the Agent layer)&lt;/li&gt;
&lt;li&gt;MCP tool protocol support (replacing local function tools)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>agents</category>
      <category>aiengineering</category>
      <category>contextmanagement</category>
    </item>
    <item>
      <title>Building a Self-Hosted Developer Blog with Astro: The Complete Guide</title>
      <dc:creator>Scc_hy</dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:09:35 +0000</pubDate>
      <link>https://dev.to/scchy/di-pian-yong-astro-da-jian-ge-ren-kai-fa-zhe-bo-ke-wan-zheng-guo-cheng--1mf9</link>
      <guid>https://dev.to/scchy/di-pian-yong-astro-da-jian-ge-ren-kai-fa-zhe-bo-ke-wan-zheng-guo-cheng--1mf9</guid>
      <description>&lt;h2&gt;
  
  
  Why I Started a Blog
&lt;/h2&gt;

&lt;p&gt;As a developer, &lt;strong&gt;writing things down and sharing them is the best way to learn&lt;/strong&gt;. But &lt;em&gt;where&lt;/em&gt; you write and &lt;em&gt;how&lt;/em&gt; you organize it determines whether your content can compound in value over the long term.&lt;/p&gt;

&lt;p&gt;After comparing platforms like WeChat Official Accounts, Zhihu, and Juejin, I ultimately chose a &lt;strong&gt;self-hosted blog as my home base&lt;/strong&gt;. Three core reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full content ownership&lt;/strong&gt;: Not subject to platform algorithms, bans, redesigns, or commercialization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-term SEO compounding&lt;/strong&gt;: Valuable content keeps earning organic search traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero server cost&lt;/strong&gt;: GitHub Pages hosts for free, paired with Astro to generate a purely static site&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Dual-Track Content Strategy
&lt;/h2&gt;

&lt;p&gt;This is the core methodology behind this blog — a &lt;strong&gt;"self-hosted home base + community distribution"&lt;/strong&gt; dual-track model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Home base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;This site (Astro)&lt;/td&gt;
&lt;td&gt;Compounding SEO &amp;amp; long-term content&lt;/td&gt;
&lt;td&gt;Content ownership, SEO accumulation, portability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Distribution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dev.to, Juejin&lt;/td&gt;
&lt;td&gt;Immediate feedback &amp;amp; traffic&lt;/td&gt;
&lt;td&gt;Community exposure, fast interaction, cold-start&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key principle: canonical URL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every article distributed to communities must point back to the original on this site via &lt;code&gt;canonical_url&lt;/code&gt;, to avoid search engines flagging duplicate content. This way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Communities drive immediate traffic&lt;/li&gt;
&lt;li&gt;Search engine authority eventually accrues to this site&lt;/li&gt;
&lt;li&gt;Even if a platform changes, the content asset is never lost&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Selection: Why Astro
&lt;/h2&gt;

&lt;p&gt;Comparing several mainstream static site options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Astro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero JS by default, Islands architecture, extreme performance, native MDX&lt;/td&gt;
&lt;td&gt;Relatively young ecosystem&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Content sites (blogs)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;Powerful, React ecosystem&lt;/td&gt;
&lt;td&gt;Heavy, slower builds&lt;/td&gt;
&lt;td&gt;Complex applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hexo / Jekyll&lt;/td&gt;
&lt;td&gt;Many themes, mature ecosystem&lt;/td&gt;
&lt;td&gt;Less flexible&lt;/td&gt;
&lt;td&gt;Traditional blogs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugo&lt;/td&gt;
&lt;td&gt;Extremely fast builds&lt;/td&gt;
&lt;td&gt;Go template learning curve&lt;/td&gt;
&lt;td&gt;Large documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Several Astro features make it especially well-suited for blogging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero JS by default&lt;/strong&gt;: pages load no JavaScript by default, blazing-fast first paint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Islands architecture&lt;/strong&gt;: loads JS only where interaction is needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Markdown / MDX support&lt;/strong&gt;: writing a post is just writing Markdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Collections&lt;/strong&gt;: built-in type-safe content management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐   git push   ┌─────────────────────┐   build &amp;amp; deploy   ┌─────────────────┐
│   Local Markdown│ ────────────► │  GitHub Repository  │ ──────────────────► │   GitHub Pages  │
│  (src/content)  │              │  (scc-blog)         │                     │   (static site) │
└─────────────────┘              └─────────────────────┘                     └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tech stack&lt;/strong&gt;: Astro + Node.js 22 + GitHub Actions + GitHub Pages&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/content/blog/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Markdown article storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Astro config (site, base, integrations)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitHub Actions auto build &amp;amp; deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Static assets (favicon, robots.txt, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build output directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Complete Setup Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Initialize the Astro project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In your project directory&lt;/span&gt;
npm create astro@latest scc-blog &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; blog &lt;span class="nt"&gt;--no-install&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;scc-blog
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates the official Astro blog template with the homepage, article list, article detail page, BaseHead component, and other basic structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure GitHub Pages deployment
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to GitHub Pages&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-pages-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./dist&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-pages&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/deploy-pages@v4&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure &lt;code&gt;astro.config.mjs&lt;/code&gt; for the GitHub Pages subpath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://scchy.github.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/scc-blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;mdx&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;site&lt;/code&gt; is the GitHub Pages domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;base&lt;/code&gt; must be the repo name, since a project site lives at &lt;code&gt;https://scchy.github.io/scc-blog/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Localize content and site info
&lt;/h3&gt;

&lt;p&gt;Convert the default English template to Chinese:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/consts.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE_TITLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SCC 的博客&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE_DESCRIPTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;记录技术、思考与成长的开发者博客&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the homepage &lt;code&gt;lang&lt;/code&gt; to &lt;code&gt;zh-CN&lt;/code&gt;, translate the Hero copy, and add a &lt;code&gt;canonical_url&lt;/code&gt; field to article frontmatter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Add README and writing workflow
&lt;/h3&gt;

&lt;p&gt;The README documents local development, the writing workflow, deploy URL, and the content distribution strategy, making future maintenance (and possible collaboration) easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Connect the remote and push
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/scchy/scc-blog.git
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After pushing to &lt;code&gt;main&lt;/code&gt;, GitHub Actions automatically triggers the build and deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Author creates a new Markdown file under &lt;code&gt;src/content/blog/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fill in the Frontmatter (title, description, pubDate, tags, canonical_url)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git commit&lt;/code&gt; + &lt;code&gt;git push&lt;/code&gt; to the main branch&lt;/li&gt;
&lt;li&gt;GitHub Actions triggers: checkout → setup-node → npm ci → build → deploy-pages&lt;/li&gt;
&lt;li&gt;GitHub Pages serves the static files from &lt;code&gt;dist/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real Pitfalls Encountered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. GitHub username mismatch
&lt;/h3&gt;

&lt;p&gt;Initially configured with &lt;code&gt;scc.github.io&lt;/code&gt;, but the actual GitHub account is &lt;code&gt;scchy&lt;/code&gt;. This caused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;site&lt;/code&gt; and &lt;code&gt;base&lt;/code&gt; in &lt;code&gt;astro.config.mjs&lt;/code&gt; needed correcting to &lt;code&gt;scchy.github.io&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;canonical_url&lt;/code&gt; in the README and articles needed updating too&lt;/li&gt;
&lt;li&gt;The git remote also had to change to &lt;code&gt;github.com/scchy/scc-blog.git&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: confirm your GitHub username before starting; manage all URLs with placeholders to avoid scattering them around.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. GitHub Pages 404
&lt;/h3&gt;

&lt;p&gt;The first visit to &lt;code&gt;https://scchy.github.io/scc-blog/&lt;/code&gt; returned 404. Troubleshooting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The GitHub Actions workflow may have failed&lt;/li&gt;
&lt;li&gt;Pages &lt;strong&gt;Source must be &lt;code&gt;GitHub Actions&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;Deploy from a branch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;First deployment has a delay; wait 5–10 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Unstable push network
&lt;/h3&gt;

&lt;p&gt;Local &lt;code&gt;git commit&lt;/code&gt; succeeded, but &lt;code&gt;git push&lt;/code&gt; failed multiple times due to network flakiness. Solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry multiple times with a tokenized remote URL&lt;/li&gt;
&lt;li&gt;Reset the remote to a clean URL (no token) immediately after a successful push&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. SEO details added later
&lt;/h3&gt;

&lt;p&gt;After setup, I added SEO-related files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;public/robots.txt&lt;/code&gt;: allow search engines to crawl, declare the sitemap&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/pages/rss.xml.js&lt;/code&gt;: generate an RSS feed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BaseHead.astro&lt;/code&gt;: fix OG image URLs, add RSS auto-discovery&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Blog Evolution &amp;amp; Current State
&lt;/h2&gt;

&lt;p&gt;After the initial build, the blog went through several more iterations. Current features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dante theme&lt;/strong&gt;: migrated from the default blog template to the Dante theme — blog + portfolio in one, with dark mode and View Transitions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 topic categories&lt;/strong&gt;: Reinforcement Learning, LLM + Agent, ML &amp;amp; Deep Learning, Parenting &amp;amp; Growth, Experience Sharing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading time &amp;amp; TOC&lt;/strong&gt;: auto-estimated reading duration and a jumpable table of contents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSS &amp;amp; Sitemap&lt;/strong&gt;: auto-generated RSS feed and site map&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Search Console&lt;/strong&gt;: verified the domain and submitted the sitemap, with indexing monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO optimization&lt;/strong&gt;: Open Graph, Twitter Card, canonical URL, robots.txt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-platform distribution (bilingual)&lt;/strong&gt;: every post is maintained in both Chinese and English, auto-synced via GitHub Actions — Chinese goes to this site + Juejin (as draft), English goes to Dev.to (published directly), all with canonical URLs pointing back here to avoid SEO duplication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments (Giscus)&lt;/strong&gt;: a lightweight comment system built on GitHub Discussions — free, no ads, Chinese-friendly, and you own the comment data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multi-Platform Distribution Architecture
&lt;/h2&gt;

&lt;p&gt;Content follows the &lt;strong&gt;"self-hosted home base + community distribution"&lt;/strong&gt; dual-track strategy, partially automated through GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ┌──────────────────────────────────────────────┐
        │           src/content/blog/                   │
        │  first-post.md (Chinese)  first-post.en.md (English) │
        └──────────────────────────────────────────────┘
                          │ git push
                          ▼
              ┌───────────────────────┐
              │   GitHub Actions       │
              └───────────────────────┘
            ┌───────────┼───────────┐
            ▼           ▼           ▼
      ┌──────────┐ ┌─────────┐ ┌─────────┐
      │ This site │ │ Juejin  │ │ Dev.to  │
      │ (Chinese) │ │(Chinese)│ │(English)│
      │ auto-deploy│ │ draft   │ │ published│
      └──────────┘ └─────────┘ └─────────┘
          all with canonical_url pointing back here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Distribution rules&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This site&lt;/strong&gt;: deploys the Chinese version (&lt;code&gt;.md&lt;/code&gt;); the English version (&lt;code&gt;.en.md&lt;/code&gt;) is excluded from the content collection so it doesn't generate a page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Juejin&lt;/strong&gt;: syncs the Chinese version (&lt;code&gt;.md&lt;/code&gt;) as a &lt;strong&gt;draft&lt;/strong&gt;, published manually by the author in the backend (avoids platform risk-control against AI-batch-published content)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to&lt;/strong&gt;: syncs the English version (&lt;code&gt;.en.md&lt;/code&gt;) and &lt;strong&gt;publishes it directly&lt;/strong&gt; (Dev.to has an official API, fully automated)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Juejin has no official public API; it's accessed via its web &lt;code&gt;api.juejin.cn&lt;/code&gt; endpoints + login cookie (same source as community MCP solutions)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;curl&lt;/code&gt; instead of Node &lt;code&gt;fetch&lt;/code&gt; for cookie-bearing requests — &lt;code&gt;fetch&lt;/code&gt; handles cookies with special characters unreliably&lt;/li&gt;
&lt;li&gt;Juejin's draft-list endpoint is unavailable, so matching is done against &lt;strong&gt;published articles&lt;/strong&gt;: update if published, otherwise create a new draft&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comments (Giscus)
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Giscus&lt;/strong&gt; comment system at the bottom of each post was chosen after comparing several options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Data Ownership&lt;/th&gt;
&lt;th&gt;Ads&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Chinese Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Giscus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yours (GitHub Discussions)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;utterances&lt;/td&gt;
&lt;td&gt;Yours (GitHub Issues)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Waline/Twikoo&lt;/td&gt;
&lt;td&gt;Third-party backend&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Needs backend&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disqus&lt;/td&gt;
&lt;td&gt;Third-party&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Free/Paid&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why Giscus&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comment data lives in GitHub Discussions — &lt;strong&gt;you own it&lt;/strong&gt;, consistent with this blog's "content ownership" philosophy&lt;/li&gt;
&lt;li&gt;No extra server or database needed; it's a lightweight front-end script that doesn't hurt performance&lt;/li&gt;
&lt;li&gt;Chinese UI, and readers comment with their GitHub account&lt;/li&gt;
&lt;li&gt;Automatically follows the blog's dark mode&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep writing — at least one post per week&lt;/li&gt;
&lt;li&gt;Add real portfolio projects&lt;/li&gt;
&lt;li&gt;Consider a custom domain&lt;/li&gt;
&lt;li&gt;Improve Juejin distribution: semi-automate the draft publishing flow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This blog documents my thinking on technology, products, and personal growth. Feel free to stop by.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>githubpages</category>
      <category>seo</category>
      <category>blogging</category>
    </item>
  </channel>
</rss>
