<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Gauresh.log]]></title><description><![CDATA[I'm Gauresh, an AI Engineer building with local LLMs, RAG pipelines, and agentic AI. I write honest build logs on what works, what breaks, and what I learn alon]]></description><link>https://blog.gauresh.art</link><image><url>https://cdn.hashnode.com/uploads/logos/69ee1d2d3d6a492cdd0b6bbe/fd27a1d6-d294-4d9f-a7b1-5ff0a46f0166.png</url><title>Gauresh.log</title><link>https://blog.gauresh.art</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 06 Oct 2026 11:34:42 GMT</lastBuildDate><atom:link href="https://blog.gauresh.art/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building Sadhan: My First AI Harness]]></title><description><![CDATA[After going through a lot of harness repos and research papers, I finally decided to build my own. I thought it would be simple, and the scaffolding part really was. A minimal working demo came togeth]]></description><link>https://blog.gauresh.art/building-sadhan-my-first-ai-harness</link><guid isPermaLink="true">https://blog.gauresh.art/building-sadhan-my-first-ai-harness</guid><category><![CDATA[llm]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[Python]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Gauresh Tambe]]></dc:creator><pubDate>Sat, 19 Sep 2026 14:44:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/6e7bbc34-1ffb-4a9a-a249-a94a68fb2e80.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After going through a lot of harness repos and research papers, I finally decided to build my own. I thought it would be simple, and the scaffolding part really was. A minimal working demo came together quickly. But the more time you spend on it, the trickier it gets.</p>
<p>In this post I'll cover where Sadhan is right now, the difficulties I hit, how I solved them, what I learned along the way, and what I plan to do next.</p>
<hr />
<h3>Version 1: The Simple Loop</h3>
<p>The first version of the harness was small. I gave it a bash tool that ran a single command, and the bash process closed after each one, so nothing persisted between commands.</p>
<p>The flow worked like this:</p>
<ol>
<li><p>The agent loop starts and initializes the state.</p>
</li>
<li><p>The user's query is added to the state and passed to the LLM.</p>
</li>
<li><p>The LLM returns a message with <code>&lt;reasoning&gt;</code> and <code>&lt;action&gt;</code> tags.</p>
</li>
<li><p>A parser method pulls out the reasoning and action and returns them as JSON.</p>
</li>
<li><p>The action part is executed as a bash command.</p>
</li>
<li><p>The return code and output from the bash tool go back to the LLM, and the loop continues.</p>
</li>
</ol>
<p>That is the whole idea. The model thinks, picks an action, the harness runs it, and the result goes back so the model can decide what to do next. This worked well for a while. I used it for single-file coding tasks and it handled them fine, so at this point I felt the hard part was behind me. It wasn't.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/6d8b678f-acb8-4e4a-837f-c12048114460.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Problem 1: Bash With No Memory</h3>
<p>Single-command bash breaks down quickly on real projects. Anything that depends on earlier commands, like working inside a Python venv or moving through nested directories, became really difficult without a persistent shell.</p>
<p>Think about a normal workflow. You <code>cd</code> into a folder, activate a virtual environment, and then run your tests. Each of those steps depends on the one before it. When every command runs in a fresh shell, the agent keeps losing its place, and the model has to work around a limitation that a human developer never has to think about.</p>
<p>So I replaced it with an asyncio-based persistent shell, where one shell process stays alive for the whole session and every command runs inside it.</p>
<h3>Problem 2: How Do I Know a Command Has Finished?</h3>
<p>A persistent shell created a new problem. Before, a command ended when its process ended, so knowing it was done was free. Now the shell never exits. When the LLM sends a command, how do I know it has completely finished and isn't still running? Commands like <code>pip install</code> make this obvious, since they produce output over a long stretch of time. If I return too early, the model sees half the output and makes decisions on incomplete information.</p>
<p>My fix was a sentinel marker. After every LLM-given command, I append an echo of a unique marker plus the exit code:</p>
<p>python</p>
<pre><code class="language-python">marker = f"__SADHAN_DONE_{token}_"
payload = f"{command}\necho {marker}$?\n"
</code></pre>
<p>The shell runs the command, and then prints the marker followed by the exit code of that command. I read the output stream until the marker shows up. When it does, I know the command is fully done, and I get the return code from the same line.</p>
<p>This change gave the agent much better power. It can now move around nested directories and work across multi-file projects, because the shell keeps its state from one command to the next.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/fbe12bd8-f5bd-4196-9879-5eb1715675c4.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Problem 3: Tag-Based Output and Small Models</h3>
<p>The tag-based LLM output gave me a lot of headaches. Small models like Qwen 3.5 9B would often write the opening <code>&lt;action&gt;</code> tag but never the closing <code>&lt;/action&gt;</code>. That caused a lot of parsing errors, and many times the model just gave me broken results instead of something the harness could run.</p>
<p>I could have kept making the parser more forgiving, but that felt like patching a symptom. So I switched to tool-based calling. I know small models are not great at tool calling, but I tried it as an experiment and it gave me better results. It still isn't always correct, but it is a lot better than the tag-based approach.</p>
<h3>Supporting More Models</h3>
<p>Until this point I had only tried local models, and there was no model registry for anything else. I added LiteLLM as the provider layer so I can switch models without touching the core loop. It doesn't cover every model yet, but the popular ones like Gemini, OpenAI, and Anthropic are in.</p>
<h3>Sessions and Modes</h3>
<p>I also added session management. Conversations are stored by directory path and time inside <code>.sadhan/sessions/</code>. This makes it easy to pick up a project I started last week without starting over, and sessions can be renamed for convenience.</p>
<hr />
<h3>How Sadhan Works Today</h3>
<ol>
<li><p>The user installs it in a venv with <code>pip install sadhan</code> and runs <code>sadhan</code>.</p>
</li>
<li><p>A TUI opens, and the user types the task.</p>
</li>
<li><p>The state's message array gets the system prompt first, then the user's task.</p>
</li>
<li><p>A <code>while True</code> loop runs. The messages go to the LLM, which reasons over them and returns a tool call if needed.</p>
</li>
<li><p>The tool call is executed in the persistent shell and the output goes back to the LLM.</p>
</li>
<li><p>This repeats until the task is done.</p>
</li>
</ol>
<p><strong>Stopping conditions:</strong> the loop can't run forever. If the same kind of error happens 5 times in a row, the agent terminates automatically and kills all child processes, so nothing is left running in the background. It also terminates when the default step limit is exhausted.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/7208e2b7-cd2c-407d-a371-4a69ea7ce11c.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>What I Learned</h3>
<p><strong>The loop is the easy part.</strong> The first version was basically LLM, tool, result, repeat, and it came together fast. Almost every real problem showed up around the loop: shell state, knowing when a command is done, the model's output format, and how to stop safely.</p>
<p><strong>Persistence changes everything.</strong> Moving from one-shot bash to a persistent shell wasn't a small upgrade. It made the agent usable on real projects, but it also brought a new class of problems, like detecting completion, that I never had to think about before.</p>
<p><strong>Keep the model layer swappable.</strong> Using LiteLLM means the loop doesn't care which model is behind it. That matters when you are experimenting with local models and hosted ones side by side.</p>
<p><strong>Small usability features matter.</strong> Sessions look minor, but being able to come back to a project a week later without starting over changes how the tool feels to use.</p>
<p><strong>An agent needs a way to stop.</strong> Without the 5 consecutive errors rule and the step limit, a confused model can keep looping and leave processes running. Termination and cleanup need to be designed in from the start.</p>
<p><strong>Building it is not the same as knowing it works.</strong> I've been testing by feel so far. That is why evaluation is the next big thing on my list.</p>
<hr />
<h3>What's Next</h3>
<ul>
<li><p><strong>Evaluation.</strong> I haven't properly evaluated the agent yet. In a couple of weeks I'll run it on 3 to 4 real GitHub repos to fix bugs or add features, and share the results.</p>
</li>
<li><p><strong>Plan mode.</strong> Along with an evaluator mode that runs in the backend and isn't used directly by the user.</p>
</li>
<li><p><strong>Multimodal input.</strong> Right now it only understands text. I want to make it handle images and other data later.</p>
</li>
</ul>
<p>Sadhan is still a work in progress, and I'll keep sharing what I learn as it grows. More updates soon.</p>
]]></content:encoded></item><item><title><![CDATA[Demystifying AI Harnesses ]]></title><description><![CDATA[An LLM can write code.
But writing code isn't the hard part.
The hard part is getting the model to keep working when the task takes hundreds of steps, the context gets messy, the code doesn't behave a]]></description><link>https://blog.gauresh.art/demystifying-ai-harnesses</link><guid isPermaLink="true">https://blog.gauresh.art/demystifying-ai-harnesses</guid><category><![CDATA[AI]]></category><category><![CDATA[ai-agent]]></category><category><![CDATA[llm]]></category><category><![CDATA[AI Engineering]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Gauresh Tambe]]></dc:creator><pubDate>Sun, 09 Aug 2026 16:49:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/062dc7c1-1d34-4ba7-a269-c9df4502782e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/1a0b5072-b03f-49ff-a412-cad73fe7cacf.png" alt="" style="display:block;margin:0 auto" />

<p>An LLM can write code.</p>
<p>But writing code isn't the hard part.</p>
<p>The hard part is getting the model to keep working when the task takes hundreds of steps, the context gets messy, the code doesn't behave as expected, and the model confidently tells you everything is fine.</p>
<p>That's where the <strong>harness</strong> comes in.</p>
<p>At least, that's what I thought going in.</p>
<p>I've spent the last couple of days trying to actually understand this thing. Read through a handful of articles and a research paper, went through some GitHub repos around it too.</p>
<p>This isn't a definitional blog on harnesses. It's just my understanding of what a harness is, so feel free to correct me if I got something wrong.</p>
<h2>What Anthropic actually built</h2>
<p>Digging into Claude Code, I found that they originally had an <strong>initializer agent</strong>. It set the starting point, wrote out a <code>feature_list.md</code>, handled system prompts, and then handed off to a <strong>coding agent</strong> that actually picked up the task and completed it, moving to the next task only once the current one satisfied the evidence for being done.</p>
<p>This worked, but context anxiety and the model's own bias toward rating its work favorably made it fall apart on longer tasks. So they introduced a three-agent setup: Planner, Generator, Evaluator, inspired by GAN architecture.</p>
<p>So: is that above setup a "harness," by Anthropic's own claim? Yes. The system built around the model to get better results than a bare LLM call, that's what a harness is.</p>
<p>It's genuinely different from prompt and context engineering. I do think prompt and context engineering are <em>part</em> of a harness, but not the whole thing. Not the full body of it. There are other requirements, laid out well in this paper: <a href="https://arxiv.org/html/2606.10106v1">What Makes a Harness a Harness (paper)</a></p>
<h2>The four conditions</h2>
<p>They lay out four minimum requirements for a system to be called a harness:</p>
<ol>
<li><p><strong>Agent loop.</strong> The core reasoning-action-observation cycle: query the model, act on what it returns, feed the result back in, repeat until the task's done or a stop condition fires.</p>
</li>
<li><p><strong>Tools / environment.</strong> This can be anything: web, bash tools, MCPs, etc.</p>
</li>
<li><p><strong>Context management.</strong> How much, and which, tokens get passed to the LLM to generate output. (Same point as above, context is <em>part</em> of a harness, not the whole of it.)</p>
</li>
<li><p><strong>Control mechanism.</strong> Step limits, stop-on-error conditions, retry limits, guardrails.</p>
</li>
</ol>
<p>If all four are satisfied, it's a harness. Miss any one, and it isn't.</p>
<p>There are extras on top of this too: model-switch retries, memory, and other things that make a harness more capable and complex. They're just not part of the minimum bar.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/2bc512f1-b69c-40c3-bfc1-c9a9b5ae779c.png" alt="" style="display:block;margin:0 auto" />

<h2>Checking it against mini-swe-agent</h2>
<p>A simple harness that satisfies all four is <strong>mini-swe-agent</strong>. Let's see how.</p>
<p><strong>1. Loop:</strong></p>
<pre><code class="language-python">def run(self, task: str = "", **kwargs) -&gt; dict:
    """Run step() until agent is finished. Returns dictionary with exit_status, submission keys."""
    self.extra_template_vars |= {"task": task, **kwargs}
    self.messages = []
    self.add_messages(
        self.model.format_message(role="system", content=self._render_template(self.config.system_template)),
        self.model.format_message(role="user", content=self._render_template(self.config.instance_template)),
    )
    while True:
        try:
            self.step()
            self.n_consecutive_format_errors = 0  # reset on any clean step
        except FormatError as e:
            self.cost += e.messages[0].get("extra", {}).get("cost", 0.0)
            self.n_consecutive_format_errors += 1
            if 0 &lt; self.config.max_consecutive_format_errors &lt;= self.n_consecutive_format_errors:
                self.add_messages(
                    *e.messages,
                    {
                        "role": "exit",
                        "content": "RepeatedFormatError",
                        "extra": {"exit_status": "RepeatedFormatError", "submission": ""},
                    },
                )
            else:
                self.add_messages(*e.messages)
        except InterruptAgentFlow as e:
            self.add_messages(*e.messages)
        except Exception as e:
            self.handle_uncaught_exception(e)
            raise
        finally:
            self.save(self.config.output_path)
        if self.messages[-1].get("role") == "exit":
            break
    return self.messages[-1].get("extra", {})
</code></pre>
<p><code>run()</code> has a <code>while True</code> loop that calls <code>step()</code>. <code>step()</code> calls <code>query()</code>, which passes the message history to the LLM and gets output back, usually a tool call, and that tool call gets executed.</p>
<p><strong>2. Tools / environment:</strong></p>
<pre><code class="language-python">def execute_actions(self, message: dict) -&gt; list[dict]:
    """Execute actions in message, add observation messages, return them."""
    outputs = [self.env.execute(action) for action in message.get("extra", {}).get("actions", [])]
    return self.add_messages(*self.model.format_observation_messages(message, outputs, self.get_template_vars()))
</code></pre>
<p><code>execute_actions()</code> takes the actions from the model's message and executes them based on whatever environment is configured: local subprocess, Docker exec, Singularity/Apptainer. Swapping environments only means swapping what <code>self.env.execute()</code> points to, nothing else in the code needs to change.</p>
<p><strong>3. Context management:</strong></p>
<pre><code class="language-python">def query(self) -&gt; dict:
    """Query the model and return model messages. Override to add hooks."""
    if 0 &lt; self.config.step_limit &lt;= self.n_calls or 0 &lt; self.config.cost_limit &lt;= self.cost:
        raise LimitsExceeded(...)
    if 0 &lt; self.config.wall_time_limit_seconds &lt;= int(time.time() - self._start_time):
        raise TimeExceeded(...)
    self.n_calls += 1
    message = self.model.query(self.messages)
    self.cost += message.get("extra", {}).get("cost", 0.0)
    self.add_messages(message)
    return message
</code></pre>
<p><code>self.messages</code> is the context passed to the model. At this level, mini-SWE-agent maintains the conversation history as the agent progresses; whether that constitutes the paper's stronger notion of <em>active context management</em> depends on how the model/history is actually selected, trimmed, or transformed during execution.</p>
<p><strong>4. Control:</strong></p>
<p>There's no dedicated method for this. It's enforced through the same if/else checks already visible in <code>query()</code> above:</p>
<pre><code class="language-python">if 0 &lt; self.config.step_limit &lt;= self.n_calls or 0 &lt; self.config.cost_limit &lt;= self.cost:
    raise LimitsExceeded(...)
if 0 &lt; self.config.wall_time_limit_seconds &lt;= int(time.time() - self._start_time):
    raise TimeExceeded(...)
</code></pre>
<p>Plus:</p>
<pre><code class="language-python">if self.messages[-1].get("role") == "exit":
    break
</code></pre>
<p>Together, these don't make the agent deterministic, but they make its execution bounded and give the harness deterministic stopping conditions: hitting a limit, timing out, or actually finishing the work.</p>
<p>By this definition, mini-SWE-agent satisfies all four conditions, at least on a loose reading of context management, so I'd classify it as a harness.</p>
<h2>Is it a <em>good</em> harness?</h2>
<p>Depends on the model underneath it. On the mini-swe-agent leaderboard, Claude Opus 4.5 (high) scores <strong>76.80%</strong>, with Gemini 3 Flash (high) close behind at <strong>75.80%</strong>. GPT-4o sits far lower, around <strong>21.62%</strong>. The model clearly matters. But these numbers are also a reminder that we're not measuring the model in isolation, we're measuring the model running inside a specific agent system, since SWE-bench Verified uses mini-SWE-agent as the common harness for these evaluations. That makes the leaderboard useful for comparing model-plus-harness systems, but it doesn't cleanly separate how much of the performance comes from the model versus how much comes from the harness itself.</p>
<p>I tested it myself with a smaller model, Qwen3.5 4B, running locally on Ollama. It handled simple tasks fine. It failed on something slightly harder: building a Flappy Bird game. It failed on the physics, the bird crashed while passing through the poles, even though it should have made it through cleanly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/f4574a41-f202-479d-831d-3bb4876fbc2c.png" alt="" style="display:block;margin:0 auto" />

<p>But I don't think the failure was purely a model problem. Part of it was the harness.</p>
<p>You could argue: if Gemini 3 does fine with the same harness, why doesn't Qwen3.5? Fair enough, it's a much larger model, obviously it'll do better. But we're not here to compare models. We're here to look at the harness. And a bash-only harness like this one is missing something specific: <strong>vision</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69ee1d2d3d6a492cdd0b6bbe/e4f1a0c8-6cc8-417c-b144-28689fe871c1.webp" alt="" style="display:block;margin:0 auto" />

<p>or more precisely, any real observation channel into what the application actually does once it's running.</p>
<p>Right now the harness can tell the model "the script ran without an error." It can't tell the model whether the bird actually cleared the pipe or crashed into it. That's the gap. If the model gets fed real evidence that its code is failing, not just confirmation that it executed, it can likely course-correct into something much better than one-shotting the physics blind. Something like Puppeteer or Playwright MCP would let the model test its own output, understand a screenshot, see where it actually failed, and fix it with that in hand.</p>
<p>Harnesses aren't limited to coding agents, either. They can wrap around anything an agent is capable of doing. But the harness itself has to shift a lot depending on the application. The right harness depends on where the model fails.</p>
<p>If the model loses track of long tasks, give it better context management. If it can't verify its own work, give it an evaluator. If it can't see the application's output, give it an observation channel. If it runs forever, give it control mechanisms.</p>
<p><strong>The interesting part isn't building a bigger harness. It's figuring out exactly what the model needs help with, and engineering only that part around it.</strong></p>
<h2>What's next</h2>
<p>I'm going to try building my own harness from scratch, leaning heavily on mini-swe-agent as a base since it's the simplest real harness out there. I'll also look at Pi and OpenHands for inspiration, though both are more specialized toward particular use cases.</p>
<p>Then I'll try the Flappy Bird task again, one-shot, with a harness, on the same small model (Qwen3.5 4B). My focus going forward is two things:</p>
<ol>
<li><p>Getting better results out of smaller models through the harness itself.</p>
</li>
<li><p>Building a focused "friendly neighborhood harness," one that handles a specific problem and does that job properly, rather than trying to be general-purpose.</p>
</li>
</ol>
<p>This is my first proper technical post, so a quick introduction: I'm a fresher AI engineer, and I'm using this blog to document what I actually build, test, and break while learning in public. I'm still figuring a lot of this out, so if I've misunderstood something, I'd genuinely like to hear about it. Follow along on X, and subscribe to the blog if that sounds useful. More of these coming.</p>
<h2>References</h2>
<ul>
<li><p><a href="https://github.com/SWE-agent/mini-swe-agent/tree/main">mini-swe-agent repo</a></p>
</li>
<li><p><a href="https://www.anthropic.com/engineering/harness-design-long-running-apps">Harness Design for Long-Running Applications (Anthropic)</a></p>
</li>
<li><p><a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents">Effective Harnesses for Long-Running Agents (Anthropic)</a></p>
</li>
<li><p><a href="https://arxiv.org/html/2606.10106v1">What Makes a Harness a Harness (paper)</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>