<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>writing | awill.co</title>
    <link>https://awill.co/writing</link>
    <description>just a sampling of the latent space in my brain.</description>
    <language>en</language>
    <lastBuildDate>Tue, 22 Sep 2026 06:38:20 GMT</lastBuildDate>
    <atom:link href="https://awill.co/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>why you should use omp</title>
      <link>https://awill.co/writing/why%20you%20should%20use%20omp</link>
      <guid>https://awill.co/writing/why%20you%20should%20use%20omp</guid>
      <pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate>
      <description>there didn't seem to be great resources on why oh my pi (omp) is such a good harness, so i figured id write a short blog on why you should use it myself.</description>
      <content:encoded><![CDATA[<p>there didn't seem to be great resources on why <a href="https://omp.sh/">oh my pi</a> (omp) is such a good harness, so i figured id write a short blog on why you should use it myself.</p>
<p>TLDR: omp is a better harness because it reduces context usage and latency for multi-step work. it does this by letting the model operate like an ide and manage work as a program rather than in its own context.</p>
<h2>background</h2>
<p>first, i'll briefly review how a vanilla code agent codes in your terminal.</p>
<p>say you are an engineer at <a href="https://driveempower.com/">empower</a>(uber competitor). you are shipping a new privacy feature to ensure riders get in the right car.</p>
<p>you prompt claude code to come up with a plan to build this feature:<img src="https://awill.co/writing-images/Screenshot%202026-07-08%20at%205.30.56%20PM.png" alt="Screenshot 2026-07-08 at 5.30.56 PM.png"></p>
<blockquote>
<p>agents have access to commands like Read, Write, Edit, Grep, Glob, and Bash</p>
</blockquote>
<p>an agent like claude code(or codex) operating any series of tasks runs on a loop like this. it picks one(or many) tools, executes it, takes in result, reads and decides what to do next.</p>
<h2>"problems" with vanilla claude(or codex)</h2>
<h4>1. each bash call is a new shell</h4>
<p>a human in a terminal window running bash commands retains their shell state.</p>
<p>the agents bash does not - it throws it away every single loop:</p>
<pre><code>Bash("export API_KEY=sk-abc123")
Bash("curl -H \"Authorization: Bearer $API_KEY\" api.example.com")
# → 401 Unauthorized — $API_KEY is empty in the new shell
</code></pre>
<blockquote>
<p>this also applies to functions, a virtualenv, or an in memory object</p>
</blockquote>
<p>to carry state over the agent needs to serialize it to disk. it can do this for simple scenarios like test results- these are all just bytes and take milliseconds to write/read.</p>
<p>but its not always so simple, sometimes bash state:</p>
<ol>
<li>can't be serialized;
<ol>
<li>DB connection, auth session</li>
</ol>
</li>
<li>or its serializable but very expensive to rebuild:
<ol>
<li>a 5GB vector index</li>
</ol>
</li>
</ol>
<p>now continuing with the empower example — 12 test cases failed after you built the privacy feature. you hand it to claude and ask it to figure out why.</p>
<p>claude finds that the failing tests are because the matching endpoint returns zero waiting riders. so either the data has no riders, or the new privacy feature is hiding them....</p>
<p>to check, claude goes around the server request and instead queries the staging DB in postgres.</p>
<p>to run analysis, you need to auth in. but, a live tcp connection can't be serialized, so for each query, the agent needs to redo the auth:</p>
<pre><code class="language-bash">Bash("python -c \"
	conn = psycopg2.connect(...) # reconnect + re-auth
	print(conn.execute(\"SELECT count(*) FROM riders WHERE status='waiting'\").fetchone()) \"")
# → (57,)
</code></pre>
<blockquote>
<p>a human on the other hand could just open a repl in the terminal to maintain the tcp connection OR query directly in the DB.</p>
</blockquote>
<p>each reconnect is less than a second, but if you are running many queries, it adds up.</p>
<h4>2. tool use and code execution can't share a loop</h4>
<p>this means the only way to use a tool after running code is to run an additional loop, which bloats context and increases latency.</p>
<p>the model's context is the only place where code can influence the agents next tool call. this makes it difficult to weave computation and reasoning together.</p>
<p>now continuing the empower example, we are analyzing the 12 failed tests. this is the process you'd have to go through with a vanilla subagent:</p>
<pre><code>1. reads the last failed test and reasons about it (10k tokens)
2. performs a fix  (12k)
3. re-runs the test suite, reads output, reasons about bug(14k)
4. makes another fix (16k)
5. ....
each of these is a loop, and creates context bloat. so far its already used 52k tokens to just re-read the same conversation 4 times.
</code></pre>
<blockquote>
<p>note: an llm doesn't have memory in between loops, so it reads all the conversation history on each loop</p>
</blockquote>
<h3>3. vanilla claudes have no access to a debugger frame</h3>
<p>for certain tasks, this can cause latency, and compounding context, since it prints, runs processes, etc, all in the normal loop.</p>
<p><em>vanilla claude can still typecheck using the cli, but it has to remember to run the checker.</em></p>
<hr>
<p>in sum, in the vanilla claude code setup:</p>
<ul>
<li>each new shell we run is stateless - anything you want to keep must be saved to disk, and anything that can't be gets redone on every call.</li>
<li>in session context bloats overtime. each loop sends the entire convo that came before it, compounding the token bill overtime.</li>
<li>debugging is painful, and requires slower feedback loops since agents cannot run debug loops in an ide.</li>
</ul>
<p>these two issues interact with each other - since you need to redo certain operations, it contributes to additional context bloat, which further compounds the bill.</p>
<h2>the solution: omp(oh my pi)</h2>
<p>oh my pi keeps the agent loop, but replaces where its executed.</p>
<p>instead of it running gin a fresh shell, running, and exiting, it gives the agent a pair of persistent kernels:</p>
<ol>
<li>a javascript bun worker - the default. runs like a jupyter notebook and used for web/repo-native work.</li>
<li>a python subprocess - the main model uses it for data analysis.</li>
</ol>
<h4>1. the kernels stay alive for the whole session, which means entire session is stateful</h4>
<p>variables, imports, db connections can all be saved as state throughout many tool calls.</p>













<table><thead><tr><th>vanilla</th><th>omp</th></tr></thead><tbody><tr><td>loop 1 connects to db with auth, and runs a query for awaiting riders<br>loop 2 re-do auth, then run query</td><td>loop 1 connects to db with auth, and runs query<br>loop 2 just query - auth is still in RAM</td></tr></tbody></table>
<p>the persistent kernel keeps auth connections, and variables alive across calls so anything expensive to rebuild is only done once.</p>
<h4>2. in OMP, code running inside the kernel can call the agents own tools through a bridge!</h4>

















<table><thead><tr><th>vanilla</th><th>omp</th></tr></thead><tbody><tr><td>the model is the loop. it runs something, gets results, reasons about them, runs the next thing, and so on.</td><td>the main model just writes one program with a for loop that re-runs tests, and spawns a fresh subagent to debug each failure.</td></tr><tr><td></td><td></td></tr></tbody></table>
<p>this means additional iterations on test cases doesn't have to pass through the orchestrators token stream, and can instead be self contained to the subagents in kernel:</p>
<pre><code class="language-js">attempts = {}; // stores data from previous loops

while (true) {
  const { code, text } = await runTests();
  const failures = parseFailures(text);
  if (code === 0 &#x26;&#x26; failures.length === 0) break;
  const reports = await parallel(
    failures.map(f => () => agent(
      `fix ${f.name}: ${f.traceback}\n` +
      `previous attempts and why they failed:\n${(attempts[f.name] ?? []).join("\n")}`,
      { agent: "task", schema }
    ))
  );

  failures.forEach((f, i) => {
    (attempts[f.name] ??= []).push(reports[i].fix);
  });
}
</code></pre>
<p>omp actually doesn't remove context, it still creates a ledger inside the program(attempts). in our example, we have the subagent end with a concise report so future subagents have the relevant context.</p>
<p>for a single debug round, this isn't any different than just spawning a subagent to do it in vanilla.</p>
<p>but if multiple iterations are required, the subagents context bloat and latency starts to add up. omp uses significantly less tokens relative to a vanilla agent.</p>
<p>this is the power of combining more programmatic workflows with an agents reasoning!</p>
<h4>3. omp has access to the debugger!</h4>
<p>with vanilla claude, the typical loop for debugging is:</p>
<ul>
<li>guess where the bug is</li>
<li>add print statements</li>
<li>run</li>
<li>read stdout</li>
<li>process exits, lose state</li>
<li>back to top</li>
</ul>
<p>with omp its:</p>
<ul>
<li>attach breakpoint</li>
<li>run to it</li>
<li>process stops</li>
<li>inspect frame</li>
<li>step forward, inspect again</li>
<li>only re-run if it overshot breakpoint placement, or to verify fix</li>
</ul>
<p>with breakpoints, <strong>the cost of a guess is much lower.</strong> they can access all the data/context relating to the bug, step through it, reason about it, and test a fix. all without compounding context.</p>
<p><em>note: omp also has more access to more lsp tools than vanilla agents, but i did not find it important enough to warrant a full deep dive.</em></p>
<h4>4. hash-anchored edits</h4>
<p>before, in vanilla claude, a tool like edit would work by string matching. the model sends the exact old text it wants to replace, and the harness finds and replaces it. BUT sometimes the model misremembers what the old text was(e.g. it thinks theres a whitespace somewhere). each failure wastes an entire loop.</p>
<p>after, with hashlines, each line is prefixed with a hash. to edit, it just says "replace line at hash <code>ajk2</code> with this "..."</p>
<p>this saves tons of tokens, and is much more efficient. omp quotes 61% reduction in token usage(grok 4 fast).</p>
<h2>how omp works</h2>
<p>first you fire up a session from the terminal:<br>
<img src="https://awill.co/writing-images/Screenshot%202026-07-23%20at%2011.13.31%20PM.png" alt="Screenshot 2026-07-23 at 11.13.31 PM.png"></p>
<p>at this moment, we have initiated a session in the harness, which looks like this now:</p>
<pre><code>harness process memory
└─ session object
     ├─ shell (brush): cwd, env, vars
     ├─ messages: [...]  ← full chat transcript every query/loop
     └─ kernels:
         ├─ js:
         └─ py:
</code></pre>
<p>the main model operates at the harness level and has access to all message history. it can use tools on its own or writes programs for the kernels to run, which includes tool use and calling subagents.</p>
<p>for example, say i ask omp to to debug a test cases that failed:</p>
<pre><code>This repo has failing tests in both packages. Run bun test from backend/ and bun run test from frontend/ to see them. Find the root causes in the source code and fix them. Re-run both suites after each change and keep going until both are fully green. Then give me a summary: what each root cause was and exactly what you changed.

Use the eval tool for this: write a loop that re-runs both suites and captures failing output into variables (don't print full tracebacks). Each round, dispatch a subagent (tool.task) per failure group with its traceback to diagnose and fix at the source, keeping each report in a variable. Loop until both suites are green, then show one summary: rounds taken and what each fix was.
</code></pre>
<p><strong>what happens</strong></p>
<ol>
<li>the main model spins up a program to run in kernel:</li>
</ol>
<pre><code class="language-js">while (true) {
     const { code, text } = await runTests();
     const failures = parseFailures(text);
     if (code === 0 &#x26;&#x26; failures.length === 0) break;
     if (round >= MAX) break;
     round++;
     const reports = await parallel(entries.map(([file, fs]) =>
       () => agent(buildPrompt(file, fs), { agent: "task", schema })));
     roundLog.push({ round, failuresBefore, fixes });
}
</code></pre>
<blockquote>
<p>here the instructions is simple, run this program, dispatch subagents for each bug, have them go in, make a fix, post a summary of the change they made.</p>
</blockquote>
<ol start="2">
<li>it runs a separate subagent through the kernel for each test bug in parallel</li>
</ol>
<p>the js kernel boots up each subagent, which runs in memory inside the harness.  they go through their own loops, append a summary at the end. then the loop runs the test again, and if they don't pass, a fresh subagent goes through again.</p>
<p>the harness session now looks like this:</p>
<pre><code>harness process memory
└─ session object
	 ├─ shell (brush): cwd, env, vars
     ├─ messages: [...]
     ├─ kernels:
     │    ├─ js:  ← while-loop program running here
     │    └─ py: (idle)
     └─ subagent workers (spawned by the js program):
          ├─ task: fix test_a  ← own messages, own loop
          ├─ task: fix test_b
          └─ task: fix test_c
</code></pre>
<ol start="3">
<li>when they finish the main model gets a summary of results, context only increments by 42k tokens(4.2%)</li>
</ol>
<p>since most of the work was done with subagents, the main orchestrator didn't need to keep track of much context. if we had multiple rounds of fixing, the orchestrator agents context would not bloat like it does in vanilla claude code.</p>
<p>see data here for analysis on cost/token savings:</p>
<ul>
<li><a href="https://x.com/composio/status/2085330850300797394?s=20">https://x.com/composio/status/2085330850300797394?s=20</a></li>
<li>(as fyi, this eval is across a variety of tasks, when omp is generally best for coding)</li>
</ul>
<h4>4. a few other reasons to use omp</h4>
<p><strong>image processing</strong></p>
<ul>
<li>in vanilla claude, reading images can be very token-expensive to put into context. one high-res screenshot can cost 2,691 tokens everytime we go through loop.</li>
<li>omp has an inspect_image tool that sends the image to a separate vision model, which returns a short text description. this saves significant</li>
<li>note: sometimes sending image to claude is better if the full context is needed. there also is a <a href="https://stencil.so/blog/snapcompact">snapcompact product</a> where you can use an image that contains 10k tokens worth of text, and only be billed by anthropics pixel formula of 3279 pixel tokens.</li>
</ul>
<p><strong>model routing superpowers</strong></p>
<ul>
<li>you can switch orchestrator model mid session</li>
<li>you can also specify which models you want to use by giving them roles, for example:
<ul>
<li>default: fable</li>
<li>plan: opus 4.8</li>
<li>smol: sol 5.6</li>
<li>commit: writing commit messages</li>
</ul>
</li>
</ul>
<p><strong>more LSP tools</strong></p>
<ul>
<li>vanilla has 9 LSP tools through IDE. integrations. but thats it.</li>
<li>omp has 14. the biggest edge being a file rename tool.</li>
</ul>
<p><strong>convo forking and refreshing history</strong></p>
<ul>
<li>you can use <code>/tree</code> and literally look back and fork from different parts of ur convo</li>
<li>you can use <code>/resume</code> to view a list of recent sessions, and select one.</li>
</ul>
<h2>final thoughts</h2>
<p>overall, oh my pi reduces:</p>
<ol>
<li>context</li>
<li>latency</li>
<li>state rebuilds</li>
</ol>
<p>but something more exciting(which pi also does), is it gives us the ability to introduce more deterministic methods into our agent processes. rather than writing a skill, we can make it a program instead, and have more trust in our agents!</p>]]></content:encoded>
    </item>
    <item>
      <title>the intent problem and thoughts on solutions</title>
      <link>https://awill.co/writing/the%20intent%20problem%20and%20thoughts%20on%20solutions</link>
      <guid>https://awill.co/writing/the%20intent%20problem%20and%20thoughts%20on%20solutions</guid>
      <pubDate>Sun, 02 Aug 2026 00:00:00 GMT</pubDate>
      <description>proper interpretation of human queries by ai is still a largely unsolved problem today. no matter how much structure you give ai, there’s still gaps it needs to fill, and oftentimes the gaps are wr…</description>
      <content:encoded><![CDATA[<p>proper interpretation of human queries by ai is still a largely unsolved problem today. no matter how much structure you give ai, there’s still gaps it needs to fill, and oftentimes the gaps are wrong.</p>
<p>it misinterprets what you mean by the user experience, or what the actual bug you pointed out is.</p>
<p>even if ai reaches human levels of interpretation, it still won’t be perfect. two humans have different interpretations of someone’s intent all the time!</p>
<p>i think this will be solved in a two ways:</p>
<ol>
<li>better context. by giving ai more contex of our lives, and the work we are doing, it have more context necessary for sense making.</li>
<li>better intuition. this quality is ephemeral, but ai must develop better intuition for determining intent. when i interpret another humans intent, so much of it is rooted in a feeling, not what they said, or what i know about them.</li>
</ol>
<p>today you can already manually give ai additional context with quizzes, or do a brain dump. but, it’s not perfect. obviously if ai also had context over your whole life, no doubt it could fill the gaps in better.</p>
<p>but, solution #2 is clearly the more difficult one. how can we give ai the ability to intuit the correct gaps?</p>
<p>i don’t really know, and im not an ml researcher, but here’s some rough thoughts:</p>
<ol>
<li>intent reasoning. steps in inference process that forces ai to think about feeling, and what the query really means.
<ol>
<li>may already be happening? idk.</li>
</ol>
</li>
<li>training on latent goals instead of explicit ones.
<ol>
<li>model learns to infer intent through examples</li>
</ol>
</li>
<li>determining when something is uncertain, and flagging another question instead of going forward with an assumption
<ol>
<li>fable is def better at this, but still not rlly there.</li>
</ol>
</li>
<li>different training process altogether. autoregressive models don’t seem to be right for this. perhaps something similar to world models would allow for greater sensemaking?</li>
<li>continual learning for intent
<ol>
<li>as we see models actually get good at learning on the job, one improvement will be properly inferring intent.</li>
</ol>
</li>
</ol>
<p>if someone reading this has other thoughts, i’d love to hear them! reach out @</p>]]></content:encoded>
    </item>
    <item>
      <title>getting a tree to sense touch</title>
      <link>https://awill.co/writing/getting%20a%20tree%20to%20sense%20touch</link>
      <guid>https://awill.co/writing/getting%20a%20tree%20to%20sense%20touch</guid>
      <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
      <description>(but not actually)</description>
      <content:encoded><![CDATA[<p>(but not actually)</p>
<h2>beginnings</h2>
<p>i've been dreaming of a world where intelligence is no longer limited to our personal devices, and instead is everywhere. i shoudl be able to go to a cafe, sit at a table, and learn about who sat there before me.</p>
<p>i've been calling this "communal computing" and hope to write more on the topic.</p>
<p>but for now, i decided to do a simple experiment - how could i get a tree to tell when it has been touched by a human, and respond?</p>
<p>so, like all people in 2026, i asked claude.</p>
<p>turns out, with a simple ESP32 and a piece of copper tape, we could do it!</p>
<p>so i asked claude how we could do that, and began the experiment.</p>
<h2>process</h2>
<p>everything around us can hold some amount of charge - flowers, chairs, even your pencil!</p>
<p>to understand how much charge a given object can hold, we can measure capacitance. if a human touches an object like a tree, they "join forces" and become one electrode, increasing the capacitance being measured.</p>
<p>so, to tell when a tree has been touched, all we need to do is measure the change in its capacitance.</p>
<p>to do this, we hook an ESP32 up to some copper tape on a tree. the ESP32 removes electrons from the electrode(tree) until it hits a voltage of 2.4V. then, it adds them back until it matched ground.</p>
<p>the time it takes to go through this process(measured in computer ticks) is the trees capacitance.</p>
<p>when a person touches the tree, the capacitance increases.</p>
<p>but, we ran into a problem. initially, the change in capacitance was only ~500 ticks. this was difficult to detect, since the wind and other environmental effects naturally change the trees capacitance by ~500.</p>
<p>after debugging with claude, it turns out this was because we were not hooked up to a ground wire.</p>
<p>the ESP32 measures voltage of an electrode relative to its own boards 'ground' of 0V. so 2.4V is just the delta.</p>
<p>but the problem is that the board ground is also the destination of the pulled electrons from the electrode. this means that as we pull electrons from the electrode and put it onto the board, the ground charge decreases.</p>
<p>the time it takes to "fill up" is <strong>very</strong> quick since the boards ground charge is decreasing as we are simultaneously increasing the charge in the electrode.</p>
<p>because of this, we were only getting about a +500 difference in capacitance(ticks to fill up to 2.4v) when touching the tree.</p>
<p>so i had to connect the boards ground GPIO pin to a stake in the ground.</p>
<p>this meant the board could send the electrons it pulled from the electrode down into the ground, while the 0v reference remained the same. see gpt slop image:</p>
<p><img src="https://awill.co/writing-images/ChatGPT%20Image%20Sep%204%2C%202026%2C%2006_26_30%20PM.png" alt="ChatGPT Image Sep 4, 2026, 06_26_30 PM.png"></p>
<p>once we did this, we were getting about a +330k touch delta! very obviously easy to sense when someone touched it then.</p>
<h2>learnings on relationship between voltage, capacitance and charge</h2>
<p>charge is the actual difference between protons and electrons in a given electrode.</p>
<p>as each unit of charge is pulled from the electrode into the ground, the energy required to pull the next unit of charge out increases.</p>
<p>the voltage is the amount of energy required to move one unit of charge across the gap!</p>
<ul>
<li>measured per electron!</li>
</ul>
<p>capacitance is the amount of time, measured in ticks, that it takes to increase the voltage up to 2.4v(in our case) and back down to 0v(ground).</p>]]></content:encoded>
    </item>
    <item>
      <title>future of personal computing pt 1</title>
      <link>https://awill.co/writing/future%20of%20personal%20computing%20pt%201</link>
      <guid>https://awill.co/writing/future%20of%20personal%20computing%20pt%201</guid>
      <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
      <description>the year is 2035.</description>
      <content:encoded><![CDATA[<p>the year is 2035.</p>
<p>you are walking in an anthropology museum with your family and you come across at a display on exhibit:</p>
<p><img src="https://awill.co/writing-images/Museum%20Exhibit%20Concept%20Jul%201%202026.png" alt="Museum Exhibit Concept Jul 1 2026.png"></p>
<p>"ahhh yes, the iphone." you tell your kids. "that came before {BLANK}"</p>
<p>what was blank? how did our personal computing experiences change in the years leading up to this moment?</p>
<p>i'll be working backwards from what our ideal experiences should look like, what those experiences are, and the downstream impact on culture.</p>
<p>in this first post, i'll focus on problems with our current computing experiences, and ideal experiences. in later posts i'll dive deeper into specific mechanics, and the cultural impact.</p>
<p>throughout all personal computing tasks, there are two main problems we encounter:</p>
<ol>
<li>personalization - our devices lack sufficient knowledge to provide optimal experiences.</li>
<li>specialization - computing is trapped in a single medium. because our current computing devices have been designed for a variety of tasks, they don't meet us where we are.</li>
</ol>
<p>these two problems will require different solutions.</p>
<h2>personalization</h2>
<p>today, figuring out where to eat for lunch goes something like this:</p>
<ul>
<li>you pull out your phone</li>
<li>you ask siri for food nearby</li>
<li>you scroll the list and identify a place you'd like</li>
<li>then you order (likely by placing an order online, manually entering in payment info, etc)</li>
</ul>
<p>this process is broken. you eat everyday at 12:30. your tech should know you didn't bring a lunch, find a great place you should order from (based on past meals), suggest a good meal, and just pop up a quick confirmation where you can confirm the order.</p>
<p>it should be that simple.</p>
<p>but to get to this personalized experience, our devices need so much more context.</p>
<p>our phone might know food we ate if we ordered it online, but won't know any data otherwise (unless you are obsessively logging calories):</p>
<ul>
<li>how hungry you are</li>
<li>if you plan on getting lunch with others</li>
<li>meals you cooked at home</li>
<li>if you attended an event with free pizza</li>
<li>etc.</li>
</ul>
<p>so what fills in this missing gap? what are some of the ways we can provide near perfect context to ai's?</p>
<p>the answer is a personal device we wear with a camera and mic.</p>
<h4>personal context devices</h4>
<p>in a future where we get develop cheaper, smaller, and more efficient chips, a smaller device will be possible. it'll be worn daily, and capture all the context needed for a personalized experience.</p>
<p>i envision a near invisible device with a camera + mic. it will likely take many forms - a necklace, an earbud, glasses. the main point is it will need to be something ambient that you forget is even there.</p>
<p>since it can see and hear everything you can, it will have near perfect data to build up context about your habits, preferences, and personality to best provide assistant-like care to you.</p>
<p>imagine you are cooking a stir fry meal at home while listening to a podcast. the device stores this in its current context:</p>
<pre><code>- aaron is cooking a stir fry with broccoli, onions, carrots, soy sauce, chicken, and chives.
- aaron is listening to a podcast with tyler cowen and jackson dahl
</code></pre>
<p>and later even update the memory for "aaron cooked an asian stir meal"</p>
<p>later that day, you put food in storage container in the freezer.</p>
<p>then a week later when you eat it for dinner, AI can store this in its weekly data:</p>
<pre><code>## weekly diet

monday 6/29:
- 3 scrambled eggs, onion, tomatoe, arugula
- chicken sandwich with avocado, peppers, habanero cheese
- salmon with potatoes and baby brocolli (sp on broccoli? I can't remember how to spell it either)

tuesday 6/30:
- 3 scrambled eggs, onion, arugula, and cheese
- red lentil soup with onion, carrot, tomato paste, arugula and lemon juice
- **asian stir fry with broccoli, onions, carrots, soy suace, chicken, and chives**

wednesday 7/1:
- 3 scrambled eggs, onions, and arugula
-
</code></pre>
<p>now, the clock hits 12:30p, your assistant can check your weekly meals, and will know that you didn't bring a lunch, and that you worked out this morning, and had a lighter breakfast. to balance things out, it suggests you eat a carne asada burrito from the spot nearby, just with a yes/no confirmation message.</p>
<p>this is only possible with the perfect context that a device like this can create, otherwise there are too many guesses.</p>
<p>we can extend this across many different tasks:</p>
<ul>
<li>getting from point a to point b</li>
<li>making plans with friends</li>
<li>cooking dinner (better example)</li>
<li>etc.</li>
</ul>
<p>today, users have to explicitly provide intent. tomorrow, AI will confidently act on intent it already has it.</p>
<p>generally, the personalization problem is most prevalent in tasks that require lower cognitive effort. ai will order me dinner, but won't write a blogpost for me.</p>
<h2>specialization</h2>
<p>today, devices like the smartphone and PC are portals to computing experiences. you can do so much:</p>
<ul>
<li>watch youtube shorts</li>
<li>learn linear algebra</li>
<li>build software for your grandma</li>
<li>coordinate a party</li>
</ul>
<p>i love my computer, and it does a lot of things well, but it doesn't meet me where i am.</p>
<p>why is it that we feel more in the flow state when writing with a pen than on a keyboard? why does sketching on an ipad feel more creative than doing mock-ups on figma?</p>
<p>in many ways, the devices we use today constrain us:</p>
<ul>
<li>they have limited ability to meet us in the real world and interpret context</li>
<li>we are forced to meet them in a world filled with distractions — texts, tweets, and so much other noise.</li>
</ul>
<p>in the future tech will meet us where we are most productive, in the real world!</p>
<p>2 changes will be most relevant:</p>
<ol>
<li>augmented reality</li>
<li>intelligence in more devices</li>
</ol>
<p>imagine a painter. they have a paintbrush they use daily for their work, but its more than that. it's been tuned to millions of the painters strokes, and helps them do generative exploration at any time.</p>
<p>the painter just speaks possible paths it'd like to see, and the ai can just determine their intent and generate four different paths that they can take. this all happen through the physical world through augmented reality(AR).</p>
<p>they can toggle selection, color tuning, with different side button son the brush to further improve their options.</p>
<p>all of this can happen away from the computer, and directly in touch with the medium itself:<br>
<img src="https://awill.co/writing-images/ChatGPT%20Image%20Jul%209%202026%20from%20AR%20Painting%20Studio.png" alt="ChatGPT Image Jul 9 2026 from AR Painting Studio.png"></p>
<p>now extend this out to writing, building, or any creative work. this is the future we will live in — a world with more opinionated and personalized experiences, enabling people to live their best lives, and do their best work.</p>
<p>so, how will we get there?</p>
<h4>AR based experiences</h4>
<p>companies like meta, snap, and raven are already working in this space. as the tech improves, it will become the best place to do creative work.</p>
<p>you can finally bring ai into the same 3D environment as you! pointing, dragging, and interacting in ways you could never have before on a computer.</p>
<h4>new devices to wield intelligence</h4>
<p>instead of computers, i imagine a new class of devices that we embed/augment with intelligence. everyday objects like pens, headphones, notebooks, and more!</p>
<p>through these devices, intelligence will meet us where we are, in the medium.</p>
<p>its possible the device itself has intelligence, or perhaps it actually is just designed to feel as if we are interacting with it, but in reality a combination of your personal device and AR makes it seem that way.</p>
<ul>
<li>maybe we just have haptics in our fingers, and ar/other devices generate a feeling that we interact with paint brush when its normal?</li>
</ul>
<h2>conclusion</h2>
<p>while a device constantly monitoring your every move may sound scary, im quite excited for a future where i no longer need to think about frivolous things like what to eat for lunch, or how to get to my friends house.</p>
<p>this will free up so much of our time for the things that matter in the post-agi world — connections, experiences, and creative work.</p>
<p>for creative work, with advances like AR and cheaper chips, it will be easier than ever for people to do high quality creative work.</p>
<p>the idea that by 2035 we will see <strong>10x more people doing creative work than we do today</strong> is so beautiful. what a time to be alive!</p>
<hr>
<h2><em>endnotes</em></h2>
<h4><em>privacy</em></h4>
<p><em>in my opinion, this will largely go away.</em></p>
<p><em>in the 90s, putting your payment info on the internet felt weird. while a constantly on device is on another level, its inevitable that we will constantly be recorded for superior experiences with our tech.</em></p>
<p><em>perhaps ill dive into risks + potential futures in this regard in another blogpost!</em></p>
<h4><em>mind reading tech</em></h4>
<p><em>many people now use voice to communicate their intent to ai, but this is likely just a transition state until we (eventually) reach something even better - reading minds!</em></p>
<ul>
<li><em>meta is already <a href="https://ai.meta.com/blog/brain2qwerty-brain-ai-human-communication/">making progress</a> in this space. in the future, the notion of a keyboard or voice input will seem so odd....words are lossy!</em></li>
</ul>
<p><em>as invasive as it may seem, this is clearly where the future is heading. i plan to dig deeper into how hci will change once this hits in a future post.</em></p>]]></content:encoded>
    </item>
    <item>
      <title>oboe assignment</title>
      <link>https://awill.co/writing/most%20inneficient%20way%20to%20find%20needle%20in%20a%20haystack</link>
      <guid>https://awill.co/writing/most%20inneficient%20way%20to%20find%20needle%20in%20a%20haystack</guid>
      <pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate>
      <description>for a job application, i had to think about the most inefficient way to find a needle in a haystack, here is the solution and math proof(linked) i ended up with! it was such a good learnign experie…</description>
      <content:encoded><![CDATA[<p><em>for a job application, i had to think about the most inefficient way to find a needle in a haystack, here is the solution and math proof(linked) i ended up with! it was such a good learnign experience and was fun, so posted here</em></p>
<hr>
<p><strong>Prompt:</strong></p>
<p>With a budget of less than $100, come up with the most inefficient method imaginable to find a needle in a haystack. Briefly explain why it's so bad as well as its computational complexity.</p>
<hr>
<p>for the most inefficient method, i had a person performing a random walk across every piece from the haystack, and increasing the likelihood that they re-visit pieces.</p>
<p>materials:</p>
<ul>
<li><a href="https://www.amazon.com/DINOBROS-Shark-Toy-Grabber-Reacher/dp/B0DK6GJYF6?sr=8-11">roboclaw</a> - $6</li>
<li><a href="https://www.google.com/aclk?sa=L&#x26;ai=DChsSEwj8o_uJ4PCUAxXxaUcBHU4ND9AYACICCAEQDBoCcXU&#x26;co=1&#x26;ase=2&#x26;gclid=EAIaIQobChMI_KP7ieDwlAMV8WlHAR1ODQ_QEAQYBSABEgLcPvD_BwE&#x26;cid=CAAS0gHkaOqgARNTl7WrY_T-lFO4mvXckRgW1ZWz49soOhEV6xul1ec618_HzEvoR0UkXvQR5T8D4fbuq1h9Gi7seBUvTmV2olf60ySMMANgHgBRJFN1jVuzru5ICC2I2S8ulBDK-5y4C7ayqDisqYXMlYdvetSFKscQtFVxnPeVrZ_rrLJxVfay1RpktFtqzsPL3rQTi4Rk5XQKEfwQTFMQATz5imtul7YnIaHBnReSNpxok8fTs3FeUKjb29ZnonXpcPjPXgB_hX9Ak5mNfCv-twiHc7s&#x26;cce=2&#x26;category=acrcp_v1_32&#x26;sig=AOD64_39Fw6YT2rbBAqjooQi-tbJaP7YeQ&#x26;adurl=&#x26;ctype=5&#x26;q=">scissors</a> - $1.67</li>
<li><a href="https://www.walmart.com/ip/Sleep-mask-blindfold-for-night-eye-cover/20300057012?wmlspartner=wlpa&#x26;selectedSellerId=101684422">blindfold</a> - $1.29</li>
<li><a href="https://www.target.com/p/enday-bulk-box-of-2-pre-sharpened-wood-pencils/-/A-1000101938?sid=&#x26;TCID=PDS-20996572261&#x26;gad_campaignid=20996572261&#x26;gbraid=0AAAAAD-5dfZZS-9zmycajBPKT6THpkyhz">144 pack of pencils</a>- $19.99</li>
<li><a href="https://www.target.com/p/pencil-sharpener-2-hole-1ct-colors-may-vary-up-38-up-8482/-/A-16637246#lnk=sametab">5 pencil sharpeners </a>- $4.45</li>
<li><a href="https://www.google.com/aclk?sa=L&#x26;ai=DChsSEwjh-Nf87Y-VAxWDRP8BHSLNJ2IYACICCAEQCxoCbWQ&#x26;co=1&#x26;ase=2&#x26;gclid=Cj0KCQjwi8nRBhDhARIsAHZf_pYSl3IKDD2GDT9KrVsNoDjiUJU7r3N0WFjfACf74yjsA5zQzoRQaoIaApLOEALw_wcB&#x26;cid=CAAS0gHkaB_R4DQXqzA1Tjnl05zhzaCzkrDxGZgVhkOvkWC-ZwsWPaajvzHbq-Sbm6bzay6LN-1WPFEnbH8JcG2-bBtc8uFExvFVXdxhhr_gl2TdVYE2TrR2vQqW_Aipfu8DD7xa9JghrnfY-qhYTddGf9R982H1NZtkQT-6TCUbGB_lJir04fkDnkouymCAs9cbTRq723_JED8Od36D6vBoI-nm5Upu-80qcFFjKt21VyKw3e8_DN6MIDxq8ySrifNSyeFe7xKuAvT6IkW6MgQA30lUU_Q&#x26;cce=2&#x26;category=acrcp_v1_32&#x26;sig=AOD64_0V_22jINyMxCnaA9yGvQ4LdTS-HQ&#x26;ctype=5&#x26;q=&#x26;nis=4&#x26;ved=2ahUKEwj_49D87Y-VAxV1lYkEHXqMN8oQ5bgDKAB6BAgLEBo&#x26;adurl=">11 reams of paper</a> - <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>40</mn><mo stretchy="false">(</mo><mn>10</mn><mi>r</mi><mi>e</mi><mi>a</mi><mi>m</mi><mi>s</mi><mo stretchy="false">)</mo><mo>+</mo></mrow><annotation encoding="application/x-tex">40(10 reams) + </annotation></semantics></math></span>6 bucks(1 individual ream)</li>
<li><a href="https://share.google/nIkDDpVrZDQtiOV2J">calculator</a> - $11.48 dollars</li>
<li><a href="https://www.dicegamedepot.com/10-sided-opaque-dice-d10-black/?sku=CHXPQ1008-ST">10 sided dice</a> - $6.76(with shipping)</li>
<li>wood ruler - $0.89</li>
</ul>
<p>total of $98.53 spent</p>
<p>steps:</p>
<ol>
<li>open up all the reams of paper, take each singular piece of looseleaf paper and cut it into cards of 1cm x 0.5cm dimension
<ol>
<li>21cm x 55cm = 1,155 papers per sheet</li>
<li>1155 x 500 x 11 = 6,352,500 cards(more than average hay bale of 6 million)</li>
</ol>
</li>
<li>put a blindfold on</li>
<li>grab the roboclaw</li>
<li>begin the process of lining up pieces(from the haystack) and the cards:
<ol>
<li>pull out each piece of hay(or needle, you won't know) with roboclaw and place it on the ground</li>
<li>grab a card of paper and place it after the piece(its an edge). cards will live in between pieces.</li>
<li>continue until every piece is placed down in line.<br>
at this point, the line looks like this:<br>
<img src="https://awill.co/writing-images/Screenshot%202026-06-23%20at%206.19.24%20PM.png" alt="Screenshot 2026-06-23 at 6.19.24 PM.png"></li>
</ol>
</li>
<li>spin around with blindfold on and then point your finger. take blindfold off. you start to the piece closest to the direction your finger is pointing.</li>
<li>then we begin random walk:
<ol>
<li>when you land on a piece that has value of x on the right card, and value of y on the left card, compute odds for progressing to the right side with calculator:
<ol>
<li>going to the right is: <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>P</mi><mtext>right</mtext></msub><mo>=</mo><mfrac><mrow><mo stretchy="false">(</mo><mi>x</mi><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo></mrow><mrow><mo stretchy="false">(</mo><mi>x</mi><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo><mo>+</mo><mo stretchy="false">(</mo><mi>y</mi><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo></mrow></mfrac></mrow><annotation encoding="application/x-tex">P_{\text{right}} = \frac{(x+1)}{(x+1) + (y+1)}</annotation></semantics></math></span></li>
</ol>
</li>
<li>we roll a ten sided dice against the percentage odds to simulate random choice.</li>
<li>the first roll of the dice determines the tens decimal place of the odds, the second the ones, and so on.</li>
<li>use the following logic as we iterate through each decimal place:
<ol>
<li>if (dice > P<sub>right</sub> decimal): go to the left</li>
<li>else if (dice &#x3C; P<sub>right</sub> decimal): go to the right</li>
<li>else(its equal): continue rolling</li>
<li><strong>note</strong>: since calculator only computes odds up to 10 decimals, if we are still tied, we favor going to the right ensure it remains sublinear.</li>
</ol>
</li>
<li>whenever a step is made, the value of the card we crossed increments by 1. to calculate the weight we do (k+1) where k is the value on the card.
<ol>
<li>e.g. if the initial weight is <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn></mrow><annotation encoding="application/x-tex">1</annotation></semantics></math></span>, the new weight is <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mn>1</mn><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">(1+1)</annotation></semantics></math></span></li>
</ol>
</li>
<li>continue until you find the needle</li>
<li><em>note</em>: if you end up at the end of the line, you must go directly to the left/right(of the piece you just crossed)</li>
</ol>
</li>
<li>needle is found!</li>
</ol>
<p>this method is terrible because it:</p>
<ol>
<li>doesn't remove pieces of hay from pool</li>
<li>only allows steps of size 1, meaning you cannot easily leave where you started</li>
<li>favors pieces you have already seen, exponentially decreasing your odds of "escaping" to pieces you have not seen yet</li>
</ol>
<p>worst case compute complexity here is O(4<sup>n</sup>). here’s the <a href="https://awill.co/haystack-errw-proof">full proof</a> for more details.</p>
<ol>
<li>we know that at your first visit to a given piece i, the weight of the edge to your left is 2, and the weight of the edge to right is 1.</li>
<li>we determine a fixed escape probability given any attempt at vertex i. this is derived from Beta(1/2, 1).</li>
<li>from this probability, we write a function for the average number of failures(left) for one success(right).</li>
<li>we know the n-th vertex(needle in worst case) will be visited once. so we can find that the n-2 edge failures can be calculated by multiplying 1(successes) by average failures.</li>
<li>we continue doing this all the way down till the first piece.  we use the failures number since its approx the same as successes over n turns.</li>
<li>this gives us the total number of leftward crossings for neighboring vertices, which equates to 4^n.
<ol>
<li>although leftward crossings only represents ~half of all visits doubling this number will not impact compute complexity significantly, so it stays at O(4^n)</li>
</ol>
</li>
</ol>
<p><strong>note:</strong> i used oboe to learn about reinforced random walks, and beta distributions when going through this problem. <a href="https://projecteuclid.org/journals/annals-of-probability/volume-16/issue-3/Phase-Transition-in-Reinforced-Random-Walk-and-RWRE-on-Trees/10.1214/aop/1176991687.full">Pemantle(1988)</a>, proves that increasing weights linear(or less) guarantees that you will eventually visit each vertex.</p>]]></content:encoded>
    </item>
    <item>
      <title>thera copilot</title>
      <link>https://awill.co/writing/thera%20copilot</link>
      <guid>https://awill.co/writing/thera%20copilot</guid>
      <pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate>
      <description>for two weeks in april i had the pleasure of helping thera build an AI analyst agent.</description>
      <content:encoded><![CDATA[<p>for two weeks in april i had the pleasure of helping <a href="https://www.getthera.com/">thera</a> build an AI analyst agent.</p>
<p>thera's customers had to take painful steps to get questions answered - manually putting in long PDFs into chatgpt just to find out how much they spent on payroll in the last 6 months.</p>
<p>the idea was if we build an AI analyst agent, customers could get their data questions answered quicker.</p>
<p><em><strong>video of product here:</strong></em></p>
<p><a href="https://www.loom.com/share/f15b3c861068426ab9d98c675f232cb4">https://www.loom.com/share/f15b3c861068426ab9d98c675f232cb4</a></p>
<h2><strong>our process</strong></h2>
<p>since there were two of us working on it, we split the work as follows:</p>
<ol>
<li>tool calls, function creation, logging (my pal <a href="https://x.com/iamconormac">conor</a>)</li>
<li>frontend, bedrock integration, agentController (me)</li>
</ol>
<p>this meant we were only dependent on the data schema, everything else could be tested independently - function calls, agent conversations, etc.</p>
<p>we used the <a href="https://medium.com/@remind.stephen.to.do.sth/targeting-success-the-power-of-tracer-bullets-in-pragmatic-software-engineering-cd6c53758986">tracer bullet</a> approach with the goal of getting one tool call working end to end, then expand.</p>
<p><strong>week 1:</strong> we got the whole thing working end to end with one tool call.<br>
<strong>week 2:</strong> we worked out kinks with model decisions, latency, and additional tool calls.</p>
<h2>lessons learned</h2>
<h3>think hard about your database options up front</h3>
<p>thera was using a noSQL DB for their data storage which limited our query patterns. for example, queries outside the partition/sort key like "how much did we spend on payroll in the last 6 months? give me a list of all employees and their costs." was much more expensive.</p>
<p>our options were to create a new centralized SQL DB in snowflake, OR get scrappy and build an MVP with tool call functions.</p>
<p>since we only had two weeks of time, we got scrappy, and built custom tool calls for common questions we knew customers would want answered. these tool calls queried internal/external API's and interacted directly with our noSQL db.</p>
<p>but this solution was brittle, and meant customers would have to ask for "feature requests" to get new questions answered.</p>
<p>if we had the time to store all the data into a centralized SQL db, our agent would have been much more flexible.</p>
<h3>consider third party API limits</h3>
<p>thera uses a variety of third party payroll and payment API's like check and MT. these API's are rate limited, which impacted agent response time. for example, modern treasury had limits of 20 req/s in production.</p>
<p>on a question like "how much did we spend on payroll in the last two years?", the model broke it down into 8 quarters, 3 currencies and 2 pages(8 x 3 x 2), which meant 48 api calls all at the same time.</p>
<p>this blew past our production rate limits. to fix this, we added semaphore limits on our function that uses MT, which capped the functions to 3 concurrent requests, which averages out to between 6 and 15 api calls.</p>
<p>if we had more time, centralizing the data in a new SQL DB at the time of writes would have made it much faster to get questions answered.</p>
<h3>balance chat continuity with token limits</h3>
<p>we encountered many input/output token limits throughout the project. the amazon nova pro model had an input limit of 300k and an output limit of 5k.</p>
<p>due to very detailed API responses, we often ran into output limits when the agent was building a tables. to mitigate this we 1) stripped unimportant fields and 2) split tool calls into summary and detail.</p>
<p>this led to a reduction in ~2k tokens used when generating a response.</p>
<h3>be mindful of where deterministic code is better then LLM</h3>
<p>initially i had the agent writing out the .md syntax to build a table of data it received from an api.</p>
<p>later in the process, we realized it was not consistently rendering the table in the same way, and it would make more sense to make table generation programmatic.</p>
<p>replacing LLM reasoning with programmatic code helps reduce token output, and provide a more consistent user experience.</p>
<h2>how the agent works</h2>
<p><img src="https://awill.co/writing-images/Thera_blog_v3.png" alt="Thera_blog_v3.png"><br>
say a user sends a simple query like:</p>
<blockquote>
<p>"how much did we spend on payroll in the last 12 months?"</p>
</blockquote>
<p>below is the flow for how our system answers this question.</p>
<h3>1. sending message to bedrock</h3>
<p><em>agentService</em> adds the message to history, and sends it over to <em>bedrock</em>.</p>
<ul>
<li>bedrock is an api service aws runs for accessing foundation models. it ensures data is not used for training, and is model-agnostic.</li>
</ul>
<p>we used amazons nova pro model, since it appropriately balanced cost with quality.</p>
<p>the model reads from our <em>tool_config.json</em> on every request — a list of available tools and what they do:</p>
<pre><code class="language-json">{
  "tools": [
    {
      "toolSpec": {
        "name": "getSpendSummary",
        "description": "Returns aggregated spend totals or transaction counts over a date range, optionally grouped by month. Response includes: dateFrom, dateTo, aggregate......",
        "inputSchema": {
          "json": {
            "type": "object",
            "properties": {
              "dateFrom": {
                "type": "string",
                "format": "date",
                "description": "Inclusive start date in YYYY-MM-DD format."
              },
              "dateTo": {
                "type": "string",
                "format": "date",
                "description": "Inclusive end date in YYYY-MM-DD format."
              },
            },
            "required": ["dateFrom", "dateTo", "aggregate"],
            "additionalProperties": false
          }
        }
      }
    },
    //additional calls listed
  ]
}
</code></pre>
<p>the model then selects which tool to use based on description, and formulates the name of the tool, and any input parameters:</p>
<pre><code class="language-json">{ 
	"name": "getSpendSummary", 
	"input": {
      "dateFrom": "2026-01-01",
      "dateTo": "2026-03-31",
      "aggregate": "SUM"
  }
}
</code></pre>
<h3>2. invoking the tool</h3>
<p>this is sent back to <em>agentService</em>.</p>
<p>then, we execute methods from the tool list(in this case getSpendSummary) in toolCallService. the methods do a mix of:</p>
<ol>
<li>calling external apis</li>
<li>calling internal apis</li>
<li>directly querying data in our databases</li>
</ol>
<h3>3. sending data back to the model</h3>
<p>after getting a response, the <em>toolOrchestrator</em> passes the tool result through <em>agentService</em> back to the model via <em>bedrock</em>.</p>
<h3>4. model determines visualization</h3>
<p>the model then takes the response, and decides whether or not we should use a visualization and which type.</p>
<p>then it generates a vega-lite spec:</p>
<pre><code class="language-kotlin">ConverseResponse(
      stopReason = "end_turn",
      message = ConversationMessage(
          role = "assistant",
          content = [
              ContentBlock.Text("""
                  Over the last 24 months, your company's total payroll was $523,951 USD.

                  &#x3C;visualization>
                  {"$schema":"https://vega.github.io/schema/vega-lite/v5.json",
                   "mark":"bar",
                   "data":{"values":[
                     {"month":"Jan","amount":50000},
                     {"month":"Feb","amount":45000},
                     {"month":"Mar","amount":55000}
                     ....
                   ]},
                   "encoding":{
                     "x":{"field":"month","type":"nominal"},
                     "y":{"field":"amount","type":"quantitative"}
                   }}
                  &#x3C;/visualization>
              """)
          ]
      )
  )
</code></pre>
<blockquote>
<p>vega-lite is a declarative library that renders charts from JSON specs.</p>
</blockquote>
<h3>5. frontend renders response + visualization</h3>
<p>then, the frontend renders the text and the visualization using the vega-lite rendering library. boom - done.</p>
<h3>a note on error handling</h3>
<p>there were many possible errors that could occur through the flow, here's how we thought about it:</p>
<ol>
<li>model outage (Bedrock down)
<ol>
<li>bedrockClient.converse() throws an exception which bubbles up to the controller and sends an SSE event to the frontend which then shows to customer "there has been a model error, check back again later"</li>
</ol>
</li>
<li>tool call fails (API/data error)
<ol>
<li>the orchestrator catches the exception, returns {"error":"..."} with isError = true back to <em>Bedrock</em> as a tool result, and the model responds conversationally, e.g. "I wasn't able to retrieve that data."</li>
</ol>
</li>
<li>Unknown tool
<ol>
<li>Model hallucinates a tool name that doesn't exist in the handlers map — returns {"error":"Unknown tool: foo"} with isError = true.</li>
</ol>
</li>
</ol>
<p>by end of day 10 we had a working copilot: customers could ask natural language questions about their payroll, invoices, contracts, and payments, and get back answers and visuals(when relevant)!</p>]]></content:encoded>
    </item>
    <item>
      <title>friends</title>
      <link>https://awill.co/writing/friends</link>
      <guid>https://awill.co/writing/friends</guid>
      <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
      <description>someone asked me this past weekend what i thought makes a good friend. figured id do a quick write up on it.</description>
      <content:encoded><![CDATA[<p>someone asked me this past weekend what i thought makes a good friend. figured id do a quick write up on it.</p>
<p>a strong friend hits all 3 of these categories:</p>
<ol>
<li>common thread</li>
<li>trust</li>
<li>comfort</li>
</ol>
<h2>common thread</h2>
<p>maybe you both grew up in queens and were raised muslim, or you both read harry potter as a kid. <a href="https://open.substack.com/pub/christineist/p/todays-dating-apps-and-social-clubs?r=itwi7&#x26;selection=c4bb136d-5cef-44b0-8ae2-708f4bff6c72&#x26;utm_campaign=post-share-selection&#x26;utm_medium=web&#x26;aspectRatio=instagram&#x26;textColor=%23ffffff&#x26;bgImage=true">shared context is ideal here</a></p>
<p>i'd argue you can find a common thread with anyone. but, this is the key to starting a relationship. if you don't have anything in common, its harder to converse and develop kinship.</p>
<h2>trust</h2>
<p>trust is the foundation of any relationship. you should not be second guessing if they care for you.</p>
<p>if you are reading this and aren't sure if someone you consider 'friend' fits this criteria - i got news for you: they aren't a friend.</p>
<h2>comfort</h2>
<p>you shouldn't feel obligated to conform to their preferences. if anything, you should feel emboldened to be yourself. to share your darkest secrets, with no fear of judgement.</p>
<p>its natural to behave differently depending on the friends. but if you dont feel comfortable sharing your dreams, faults, or problems with friends, then something is wrong.</p>
<h2>vibes (i lied, theres a 4th)</h2>
<p>the intangibles. how you feel when you are with them. things you cant cleanly categorize.</p>]]></content:encoded>
    </item>
    <item>
      <title>building diaHistory with the macOS ax tree</title>
      <link>https://awill.co/writing/building%20diaHistory%20with%20the%20macOS%20ax%20tree</link>
      <guid>https://awill.co/writing/building%20diaHistory%20with%20the%20macOS%20ax%20tree</guid>
      <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
      <description>i've been using diaBrowser for over a year now. its perfect for learning and asking follow up questions about a new topic i am learning.</description>
      <content:encoded><![CDATA[<p>i've been using diaBrowser for over a year now. its perfect for learning and asking follow up questions about a new topic i am learning.</p>
<p>but, one problem i have is that the conversation history is not saved. theres no way to track my learning overtime.</p>
<p>so, i built diaHistory, a tool that automatically archives your diaConversation history using the macOS accessibility API.</p>
<h2>How it works</h2>
<p><img src="https://awill.co/writing-images/Screenshot%202026-04-20%20at%203.52.50%20PM.png" alt="Screenshot 2026-04-20 at 3.52.50 PM.png"><br>
see the full excalidraw <a href="https://multidraw.net/project/U5wjzGpdL_EEAmUufM0Ti#json=ea064558df5b44609715,xdYTIVAeYoWrStMaia14tQ">here</a></p>
<p>the architecture is simple:</p>
<ol>
<li>a while loop in chatWatcher polls to check if dia's is running(PID lookup)
<ol>
<li>this changes every single time you launch the app</li>
</ol>
</li>
<li>once we have dia's PID, we run a discovery poll every 15 seconds to scan the ax tree to discover any new chat panels with a message.</li>
<li>when a panel is discovered, we register an AXObserver to listen for new messages</li>
<li>whenever a message is sent/received - it goes through the whole pipeline:
<ol>
<li>accessibility reader gets the conversation that changed.</li>
<li>chatparser turns it into the message object</li>
<li>we append it OR create new convo in conversation tracker</li>
<li>THEN we write markdown file and save to documents</li>
</ol>
</li>
</ol>
<h2>macOS accessibility tree</h2>
<p>initially i hoped this tool could be a browser extension. but, dia's databases are (of course) encrypted, and dia's chat UI is native, meaning its not part of the DOM at all.</p>
<p>but, hope was not lost, the macOS accessibility tree ended up being what we needed. macOS has an accessibility API that lets you read the UI elements of any app.</p>
<p>its the same system that the voiceover feature uses for screen readers - every text field and label gets an accessibility role that describes what it is and what it contains. for example:</p>
<pre><code>AXSystemWide
  ├─ AXApplication "Dia"
  │  └─ AXWindow "Claude — claude.ai"
  │     ├─ AXToolbar
  │     │  └─ AXComboBox "Address"
  │     │      .value = "https://claude.ai/chat/abc-123"
  │     ├─ AXWebArea (webpage DOM — skipped by diaHistory)
  │     │  └─ ...
  │     └─ AXScrollArea (Dia's native chat panel)
  │        └─ AXList
  │           └─ AXList
  │              ├─ AXGroup
  │              │  └─ AXStaticText "what is 2+2?"
  │              ├─ AXGroup
  │              │  └─ AXStaticText "4"
  │              └─ AXGroup
  │                 └─ AXStaticText "how many r's in strawberry?"
  │
  ├─ AXApplication "Messages"
  │  └─ AXWindow "Sarah"
  │     └─ AXSplitGroup
  │        ├─ AXScrollArea
  │        │  └─ AXTable
  │        │     ├─ AXRow "Sarah"
  │        │     └─ AXRow "Mom"
  │        └─ AXScrollArea
  │           └─ AXList
  │              ├─ AXGroup
  │              │  └─ AXStaticText "hey i had a great time last night!"
  │              └─ AXGroup
  │                 └─ AXStaticText "i think we should just be friends"
</code></pre>
<p>to make sure the daemon ran anytime the computer restarted, or dia changed, we made the tool a macOS launch agent.</p>
<h2>Problems faced and how we solved them</h2>
<p><strong>no "new panel" AX notification exists</strong><br>
1. the AX Observer only had generic "AXValueChanged" events that fire on any change in the application:<br>
1. tab switch, keystroke in address bar, etc.<br>
2. even if we did decide to filter all these specifically for new chat events, sometimes these notifications can fail silently.<br>
3. <strong>fix:</strong> set up a poller to check for new populated chat panels every 15 seconds, THEN we register AXObserver to listen for new messages.</p>
<p><strong>the findChatGroups function was doing a recursive walk of the whole AX tree, causing a stack overflow on deep parts of the tree</strong>:</p>
<p>old:</p>
<pre><code class="language-swift">func findChatGroups(in element: AXUIElement) -> [AXUIElement]? {
      for child in children(of: element) {
          if role(child) == "AXScrollArea", let groups = checkForChat(child) {
              return groups
          }
          if let groups = findChatGroups(in: child) {  // recurse into everything
              return groups
          }
      }
      return nil
  }
</code></pre>
<p>here the old version recurses into every single node, including the rendered DOM.</p>
<p>to fix, i added a depth limit and skipped the DOM:</p>
<pre><code class="language-swift">func findChatGroups(in root: AXUIElement) -> [AXUIElement]? {
	var stack = [(element: root, depth: 0)]

	while let (element, depth) = stack.popLast() {
		guard depth &#x3C; 64 else { continue }          // depth limit
		for child in children(of: element) {
			if role(child) == "AXScrollArea", let groups = checkForChat(child) {
			  return groups
		  }
		  guard role(child) != "AXWebArea" else { continue }  // skip web DOM
		  stack.append((child, depth + 1))
	  }
  }
  return nil
}
</code></pre>
<p><strong>every AXObserver notification triggered a full rewrite to a new file.</strong></p>
<ol>
<li>this meant as a conversation continued, more and more files built up.</li>
<li><strong>fix:</strong> fingerprint each conversation with a SHA256 of the domain + first user message and stores the file path it was written to.</li>
</ol>
<p><strong>memory bloat</strong></p>
<ol>
<li>a week after using the tool myself, i discovered that the daremon was eating 1.2GB of memory.
<ol>
<li>since i had started the daemon, each conversation was being stored as state, with no way to way to evict.</li>
</ol>
</li>
<li><strong>fix:</strong> added a lastUpdatedAt timestamp to each record and scheduled a repeating timer that prunes stale convos every 24 hours.</li>
</ol>
<p>ive been using this tool as part of my learning loop and its been marvelous. a future where our agents have context on everything we do is coming - and maybe the AX tree will be a part of it.</p>
<p>i hope this post encourages one person to build a more ambitious project using macOS AX tree!</p>]]></content:encoded>
    </item>
    <item>
      <title>fine tuning sdxl to create particle art</title>
      <link>https://awill.co/writing/fine%20tuning%20sdxl%20to%20create%20particle%20art</link>
      <guid>https://awill.co/writing/fine%20tuning%20sdxl%20to%20create%20particle%20art</guid>
      <pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate>
      <description>prtkl is a fine tuned stable diffusion model that consistently reproduce abstract anthropomorphic figures made up of particles.</description>
      <content:encoded><![CDATA[<p><a href="https://prtkl.net">prtkl</a> is a fine tuned stable diffusion model that consistently reproduce abstract anthropomorphic figures made up of particles.</p>
<p>you type a word, and you get a beautiful particle image generated.</p>
<ul>
<li>i.e. 'love' would show the image below.</li>
</ul>
<p><img src="https://awill.co/writing-images/examplePerfect.png" alt="examplePerfect.png"></p>
<p>the below follows what i did and lessons learned along the way. <strong>if you are also planning to fine tune an SDXL with LORA to do abstract art, this should save you 10 hours+ of iteration.</strong></p>
<h2>high level architecture</h2>
<p><img src="https://awill.co/writing-images/Screenshot%202026-03-19%20at%208.02.04%20PM.png" alt="Screenshot 2026-03-19 at 8.02.04 PM.png"></p>
<p>see the excalidraw <a href="https://excalidraw.com/#json=1bArxuyw8u9jeeCiDQn5m,InAa4-gpYc4nEQPLiXOVqA">here</a> for more details</p>
<h2>setup</h2>
<p><strong>training framework:</strong> <a href="https://github.com/huggingface/diffusers/blob/v0.31.0/examples/advanced_diffusion_training/train_dreambooth_lora_sdxl_advanced.py">diffuser's <code>train_dreambooth_lora_sdxl_advanced.py</code></a> — hugging face's advanced dreambooth + LoRA + TI training script for SDXL.</p>
<ul>
<li><strong>TI:</strong> creates new token embeddings with no prior meaning</li>
<li><strong>LoRA</strong>: small trainable matrix we tack onto the SDXL transformer</li>
<li><strong>dreambooth</strong>: is a fine-tuning approach to bind a trigger token to the style you want. here we combine it with LORA + TI</li>
</ul>
<p><strong>compute:</strong> <a href="https://modal.com">Modal</a> — runs on an A10G GPU (24GB VRAM). $30 free credits(as of mar '26), which was more than enough for training + inference.</p>
<p><strong>setup steps:</strong></p>
<ol>
<li>install <a href="https://docs.astral.sh/uv/">uv</a> (python package manager)</li>
<li><code>uv run modal setup</code> — authenticates your modal account</li>
<li>clone the repo: <code>git clone https://github.com/aaronw122/particleArt</code>
<ol>
<li>or create ur own, up to you :)</li>
</ol>
</li>
</ol>
<p><strong>training data format:</strong></p>
<pre><code>images/curated/
  daily_life_001_v0.png        ← 1024x1024 training image
  daily_life_001_v0.txt        ← caption: "a figure lying on their side, curled in a sleeping position"
  gesture_and_action_005_v0.png
  gesture_and_action_005_v0.txt ← caption: "a figure bending down to pick something up"
</code></pre>
<p>each <code>.png</code> gets a matching <code>.txt</code> file with a scene description.<br>
use an image generator (GPT, Midjourney, Stable Diffusion) to create training data. you may only find 10% of images are acceptable, so generate a LOT to choose from.</p>
<p><strong>run training:</strong></p>
<pre><code>uv run modal run train_modal_adamw.py
</code></pre>
<p><strong>download weights:</strong></p>
<pre><code>modal volume get lora-output-adamw-v4 /results/ ./lora_output/
</code></pre>
<p><strong>run inference (generates images from multiple checkpoints):</strong></p>
<pre><code>uv run modal run generate_modal_sweep.py
</code></pre>
<h2>principles</h2>
<h3>1. generate as much high quality training data as possible</h3>
<p>for something abstract, 30 images isn't enough. you want at least 50, if not more.</p>
<p>see below for how to generate training data</p>
<h3>2. use gpt OR stable diffusion to create an image prompt for generating training data</h3>
<p>by doing this, you also prove to yourself if you even need to fine tune the model. ask yourself if the model consistently produce what you need with a prompt?</p>
<p>if the answer is yes, then you don't need to fine tune.</p>
<p>here's the prompt i used in chatGPT to generate the images:</p>
<blockquote>
<ul>
<li>"Sparse black particle flecks on a pure white background forming {scene}. Minimal, lots of negative space. No gray tones, no shading — just scattered black dots/flecks suggesting the form. Abstract, not realistic."</li>
</ul>
</blockquote>
<p>but, only 10% of the images met my standards.</p>
<p>since the hit rate was so low, i tried to get GPT to hone in on the exact style i was looking for:</p>
<blockquote>
<ul>
<li>"Sparse black particle flecks on a pure white background. Subject: an abstract human figure {scene}.  Only small scattered dots — no lines, no strokes, no outlines. Approximately 200-300 small dots total. Keep 70% of the canvas empty white space. All elements — figures and objects — equally sparse and light. No element bolder or denser than any other."</li>
</ul>
</blockquote>
<p>this(and a couple iterations after) actually produced <strong>fewer</strong> acceptable images.</p>
<p>some prompts need to stay vague for now, as gpt doesn't yet have the lexicon to appropriately generate images from.</p>
<h3>3. use TI(textual inversion)!</h3>
<p>Textual inversion adds new embeddings to the lookup table, that start with <strong>zero</strong> prior meaning.</p>
<p>similar to LLMs, SDXL has a lookup table that maps tokens to vectors:</p>

























<table><thead><tr><th>token ID</th><th>string</th><th>embedding</th></tr></thead><tbody><tr><td>0</td><td>!</td><td>[0.023, -0.156, -0.008, ...]</td></tr><tr><td>...</td><td></td><td></td></tr><tr><td>1542</td><td>dog</td><td>[0.192, -0.372, -0.291, ...]</td></tr></tbody></table>
<p>at the bottom, we add two(or more) fresh rows with random vectors that can be trained during fine tuning:</p>

























<table><thead><tr><th>tokenId</th><th>string</th><th>embedding</th></tr></thead><tbody><tr><td>...</td><td></td><td></td></tr><tr><td>49410</td><td><code>&#x3C;s0></code></td><td>[0.841, -0.725,  0.519, ...]</td></tr><tr><td>49411</td><td><code>&#x3C;s1></code></td><td>[-0.104, 0.667, -0.291, ...]</td></tr></tbody></table>
<p>these two token weights get adjusted during fine tuning for a fraction of total steps (controlled by <code>TI frac</code> — in my case, 0.5, so the first half of training). then they freeze and the LoRA matrices continue to update.</p>
<ul>
<li>in training, these tokens are mapped to the trigger word <code>TOK</code>. whenever the fine tuned model sees <code>TOK</code> in a prompt, it activates the freshly trained style embeddings.</li>
</ul>
<p>in sum:</p>
<ul>
<li><strong>without TI</strong>: the text encoder has no way to represent 'particle figure art' as a concept.
<ul>
<li>then the LoRA net we trained has to fight against existing meanings of a word and generate image</li>
</ul>
</li>
<li><strong>With TI:</strong> brand new embeddings are created and meaning is assigned to the word.</li>
</ul>
<h3>4. you will likely need to iterate on learning rate, rank, TI frac, and other params before you get the right outcome.</h3>
<p>here is a table with the params i finalized through many iterations with codex and claude:</p>























































<table><thead><tr><th>Param</th><th>Value</th><th>Why</th></tr></thead><tbody><tr><td>LR (UNet)</td><td>9e-5</td><td>Balances style binding vs overfitting. 7e-5 was too weak, 1e-4(standard) overfit.</td></tr><tr><td>text_encoder_lr</td><td>2.5e-4</td><td>Strong token-style attachment, slightly conservative for 50-image dataset.</td></tr><tr><td>scheduler</td><td>constant</td><td>Cosine let SDXL's priors creep back in late training. Constant maintains pressure.</td></tr><tr><td>warmup</td><td>100</td><td>Less warmup = learns style sooner.</td></tr><tr><td>TI frac</td><td>0.5</td><td>TI needed more steps to learn the full style concept, before was 0.3</td></tr><tr><td>max_steps</td><td>1900</td><td>Higher LR converges faster. Peak expected at 1450-1700.</td></tr><tr><td>noise_offset</td><td>0.0357</td><td>Fixes diffusion's luminance bias — needed for pure white/black.</td></tr><tr><td>mixed_precision</td><td>bf16</td><td>fp16 caused color rounding errors (blue dot artifacts). bf16 more stable.</td></tr><tr><td>rank</td><td>32</td><td>16 did not produce a large enough matrix for the model to effectively learn the new style</td></tr></tbody></table>
<blockquote>
<p>rank: determines the size of the LORA matrix we staple onto SDXL, the greater the rank, the more capacity to capture style nuance. BUT also more risk of overfitting.</p>
</blockquote>
<p>this was the outcome of 5+ rounds of iteration on the training runs. each of them i ran into different issues and had to optimize.</p>
<h3>5. modal is best rather than running locally OR using google colab</h3>
<p>i frequently ran into rate limiting issues with google colab, <strong>and</strong> there was not enough VRAM on Colab's T4 GPU (15GB) for SDXL LoRA training. Modal's A10G was much better (24GB VRAM). see setup section above for how to get started.</p>
<h3>6. develop consistent evaluation criteria before any model training</h3>
<ol>
<li>create a testing suite for tracking progress.
<ol>
<li>i would recommend 1-2 novel prompts that <strong>were not</strong> part of the training data to ensure the style generalizes well. then 4-5 more to stress test range once your confident in the style.</li>
</ol>
</li>
<li>add checkpoints in training and look at images at each one.
<ol>
<li>every 100 steps is usually a good baseline.</li>
<li>checkpoints also ensure you can resume training if something fails OR go back to older weights.</li>
</ol>
</li>
<li>run for more steps than you expect — it is not as simple as text models where you can optimize for loss. you care about perceptual qualities that loss can't capture.
<ul>
<li><img src="https://awill.co/writing-images/Screenshot%202026-03-12%20at%209.20.37%20PM.png" alt="Screenshot 2026-03-12 at 9.20.37 PM.png"></li>
<li><em>a loss curve like this is normal</em></li>
</ul>
</li>
<li>evaluating image models is more subjective, i evaluated myself with criteria i built up overtime:
<ol>
<li>does the overall style match what i want?</li>
<li>is the background correct? (mine kept drifting off-white)</li>
<li>are colors accurate? (got blue dots instead of black)</li>
<li>is the rendering style right? (3D when i wanted 2D)</li>
</ol>
</li>
</ol>
<h3>7. use negative prompts at inference</h3>
<p>negative prompts steer the model away from unwanted qualities. here's what i used at inference:</p>
<pre><code>prompt: "TOK, a figure [doing X], white background"
negative prompt: "photorealistic, detailed, shading, gradient, gray, color, dense, beige, tan, sepia, parchment,
  warm tones, blue, colored dots, 3D, lighting"
</code></pre>
<p>note: negative prompts are only used at inference, not during training. training captions are just scene descriptions we showed above.</p>
<h3>8. claude.md for the project you can use.</h3>
<p><a href="https://github.com/aaronw122/particleArt/blob/main/CLAUDE.md">https://github.com/aaronw122/particleArt/blob/main/CLAUDE.md</a></p>
<h2>brief summary of my journey:</h2>
<h3>1. generated + curated sample image data.</h3>
<ul>
<li>this was a &#x3C;10% hit rate. had to generate >300 images to produce 30 samples</li>
</ul>
<h3>2. tried fineTuning using LoRA + textual inversion(TI), produced mid images</h3>
<p><img src="https://awill.co/writing-images/Screenshot%202026-03-12%20at%209.18.57%20PM.png" alt="Screenshot 2026-03-12 at 9.18.57 PM.png"><br>
selection criteria was "does it match the aesthetic i am going for in the image above"</p>
<h3>3. claude/codex said the model overfit at 500 steps, and we should stop using TI</h3>
<ol>
<li>i regretfully followed their advice, and did pure LoRA. this produced disastrous outcomes:<br>
<img src="https://awill.co/writing-images/Screenshot%202026-03-13%20at%201.22.18%20AM.png" alt="Screenshot 2026-03-13 at 1.22.18 AM.png"></li>
</ol>
<h3>4. switched back to using TI, ran for 1000 steps with a rank of 32</h3>
<p><img src="https://awill.co/writing-images/Screenshot%202026-03-13%20at%202.47.16%20PM.png" alt="Screenshot 2026-03-13 at 2.47.16 PM.png"><br>
some of the images generated had the wrong background. this was because 32% of my training data had a slightly off-white background.</p>
<h3>5. train again on 2000 steps with the images fixed so they all had white backgrounds</h3>
<p>model quality peaked at 1300 but wanted more out of it<br>
<img src="https://awill.co/writing-images/Pasted%20image%2020260314103555.png" alt="Pasted image 20260314103555.png"><br>
1300 was identified as peak. see the white splothees in some areas.</p>
<h3>6. curated 20 more images for a 4 hr training run, ran it at 2am on March 14</h3>
<p>unfortunately i had a timeout set for 2 hours in my code, so it did not complete</p>
<h3>7. re-ran with a checkpoint at halfway</h3>
<p><img src="https://awill.co/writing-images/Screenshot%202026-03-14%20at%2012.25.42%20PM.png" alt="Screenshot 2026-03-14 at 12.25.42 PM.png"><br>
this felt good.</p>
<h3>8. better results, but the model was still pulling from its base styling:</h3>
<p><img src="https://awill.co/writing-images/Screenshot%202026-03-14%20at%202.39.13%20PM.png" alt="Screenshot 2026-03-14 at 2.39.13 PM.png"></p>
<blockquote>
<p>see how the figure here is 3d instead of 2d</p>
</blockquote>
<p>to resolve the issue, i worked with codex and increased the rank so it would capture more nuanced style differences.</p>
<h3>9. increased rank was better, more 2d, BUT was using blue dots instead of black/grey</h3>
<p><img src="https://awill.co/writing-images/Screenshot%202026-03-15%20at%203.51.08%20PM.png" alt="Screenshot 2026-03-15 at 3.51.08 PM.png"><br>
worked with codex/claude and identified two fixes:</p>
<ol>
<li>added noise_offset (0.0357) to fix diffusion's luminance bias — without it, the model couldn't produce true white/black.</li>
<li>BF16 instead of FP16
<ol>
<li>BF16 is more stable for extremes like pure white + black dots</li>
</ol>
</li>
</ol>
<h3>10. images finally looking good at step 1800!</h3>
<p><img src="https://awill.co/writing-images/Pasted%20image%2020260319202226.png" alt="Pasted image 20260319202226.png"></p>
<p><img src="https://awill.co/writing-images/Pasted%20image%2020260319202233.png" alt="Pasted image 20260319202233.png"></p>
<h3>Appendix:</h3>
<ul>
<li>github: <a href="https://github.com/aaronw122/particleArt">https://github.com/aaronw122/particleArt</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>product thinking</title>
      <link>https://awill.co/writing/product%20thinking</link>
      <guid>https://awill.co/writing/product%20thinking</guid>
      <pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate>
      <description>to be a good product thinker:</description>
      <content:encoded><![CDATA[<p>to be a good product thinker:</p>
<ol>
<li>market
<ol>
<li>who are you building for?</li>
<li>should you pivot? </li>
<li>what is your products core value prop?</li>
</ol>
</li>
<li>ideas/features
<ol>
<li>evaluating good vs bad ideas/hypotheses</li>
<li>is this feature worth building out? does it solve a painful problem? will it help enough of our customers? </li>
</ol>
</li>
<li>execution
<ol>
<li>making tradeoffs during development</li>
<li>asking yourself - is this experience intuitive, easy, good? </li>
</ol>
</li>
</ol>]]></content:encoded>
    </item>
    <item>
      <title>curation</title>
      <link>https://awill.co/writing/curation</link>
      <guid>https://awill.co/writing/curation</guid>
      <pubDate>Tue, 15 Oct 2024 00:00:00 GMT</pubDate>
      <description>you know that feeling when you know exactly what you want, but you just can't find it?</description>
      <content:encoded><![CDATA[<p>you know that feeling when you know exactly what you want, but you just can't find it?</p>
<p>endless time spent browsing amazon, but they just don't have the laundry basket you're looking for.</p>
<p>soon, the perfect <strong>anything</strong> will be at our fingertips.</p>
<p>instead of settling for something less than, you will find that perfect wicker laundry basket.</p>
<p>but shopping is just the tip of the iceberg. things you do in the physical world will be more curated as well.</p>
<p>imagine a world where it where novelty is effortless. you don't have to settle for the usual restaurant, but are nudged to try something new.</p>
<p>personally, a world where ai pushes us more out of our comfort zone couldn't come sooner. but, i do also wonder - will we miss the hunt? there's something cold about getting the best burrito of your life at a joint an ai recommended.</p>]]></content:encoded>
    </item>
    <item>
      <title>ask questions</title>
      <link>https://awill.co/writing/ask%20questions</link>
      <guid>https://awill.co/writing/ask%20questions</guid>
      <pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate>
      <description>when you disagree with someone, you need to shift mindset from a statement based (I do not like this idea, x would be better than Y) to question based.</description>
      <content:encoded><![CDATA[<p>when you disagree with someone, you need to shift mindset from a statement based (I do not like this idea, x would be better than Y) to question based.</p>
<p>for example:</p>
<ol>
<li>What are some of the downfalls of this strategy?</li>
<li>Can you walk me through the design decision you made?</li>
<li>What is the problem we are solving here?</li>
</ol>
<p>this does 2 things:</p>
<ol>
<li>Helps hone your own thinking on topics. a lot of times you may be wrong, and if you actually think through questions you have, your thinking will improve</li>
<li>forces the person to think through your questions and realize potential reasons a different approach/solution may better.</li>
</ol>
<p>approach things from curiosity, not disagreement. otherwise, you'll be wrong much more often.</p>]]></content:encoded>
    </item>
    <item>
      <title>lessons from nonpolar</title>
      <link>https://awill.co/writing/lessons%20from%20nonpolar</link>
      <guid>https://awill.co/writing/lessons%20from%20nonpolar</guid>
      <pubDate>Thu, 17 Feb 2022 00:00:00 GMT</pubDate>
      <description>in the winter of 2021, i had the crazy idea to build a news aggregation app to bridge the political divide. it failed. bigtime. but i still carry the learnings with me.</description>
      <content:encoded><![CDATA[<p>in the winter of 2021, i had the crazy idea to build a news aggregation app to bridge the political divide. it failed. bigtime. but i still carry the learnings with me.</p>
<p>these 5 lessons were scrawled in an old college notebook:</p>
<ol>
<li><strong>don't work with close friends unless you trust them well.</strong>
<ol>
<li>i would be working on the project, they'd be sleeping in. this made it difficult to hold folks accountable, and led to some resentment.</li>
</ol>
</li>
<li><strong>don't bring on more than one person to start</strong>
<ol>
<li>i decided to bring on 4 friends. this made it difficult to make quick decisions, and slowed the flow of information.</li>
</ol>
</li>
<li><strong>agency > smarts</strong>
<ol>
<li>i picked co-founders based on smarts, rather than their willingness to get shit done.</li>
</ol>
</li>
<li><strong>be decisive - pick a lane, test, iterate.</strong>
<ol>
<li>i kept thinking if i read one more research paper on polarization, all the dots would connect and i'd have the perfect solution. this lead to indecisiveness, and a perfectionist mindset.</li>
<li>it took us all summer to decide to pivot from an app to a newsletter. by then, we should have shipped 12 posts.</li>
</ol>
</li>
<li><strong>build something people want</strong>
<ol>
<li>people don't want to read things they disagree with, but yet i was stuck on the news aggregation idea for far too long.</li>
<li>it would have behooved me to ask 'is there a fun way we can help people be more aware of their cognitive biases?'</li>
</ol>
</li>
</ol>]]></content:encoded>
    </item>
  </channel>
</rss>
