AI-aware Software Design

Preface: I’ve been building Posthaste since March. It’s a modern, smart, fast mail workstation blah-blah-blah: check it out. It is my first big application since LLM dawn. Along the way I’ve rethought what mail should look like in an era of AI agents. Yes, this blog post will coincidentally advertise Posthaste further.

LLMs equip everyone with a pocket “computer guy”. It would be naive to think this doesn’t change things for software developers. I haven’t seen anyone make a convincing argument for what exactly this means; I haven’t seen, embarrassingly, many apps adopting a new paradigm either. Hopefully this blog post helps the situation a bit.

1. Users have a mediocre, tireless coder

1.1. Make your application text-editable

Put everything in a .toml, .json, .md or other plain text format. This will make it easier for users to debug and reconfigure your app, and you can expose arbitrary technical knobs without sacrificing usability. It doesn’t really matter if your schema will wind up complex, but make sure to provide documentation.

Put files in a predictable place to save your users some tokens. Don’t confuse the agents, and don’t create situations susceptible to hallucination. Put links to where each section of your config, registry and state is documented.

Info

Posthaste puts account registry in a .toml file, instead of using a more convenient SQLite. It points agents to documentation at https://theoryzhenkov.github.io/posthaste.

Ex: Though not designed with LLMs in mind, git, notmuch, Maildir, Tailscale are good implementations of this pattern. It is not a coincidence that these are mostly developer tools.

1.2. Provide a localhost-exposed API

How cool would it be to auto-add “zero inbox” event to your calendar when your inbound mail overflows a limit? Or display important mail on your TV? Or trigger an agent to do something on your VM when a mail with a specific tag arrives?

If I have my whole API stream on localhost, you can subscribe to listen to arbitrary events, fetch data by trigger, and so forth. You wouldn’t bother, but now it is mostly your agent that has to debug and read my API. Cool!

Danger

Just don’t forget to require authentication, and enable same origin by default, otherwise any website from your browser will be able to interact with your app with privileged rights. No, no users were compromised due to Posthaste, I caught this early.

Ex: Syncthing is almost a line-for-line match for what I described. It exposes a REST API on localhost:8384; you long-poll GET /rest/events, and create a subscription by adding an events=TypeA,TypeB filter for only the event types you want, plus a since=<lastSeenID> cursor so you resume exactly where you left off. Also look at Home Assistant.

1.2.1. Write an MCP server

If you expose the API, might as well make it easy for agents to work with it in a structured way. MCP has its own problems, including security ones, so users need to be careful — but for you it introduces no new vulnerability surfaces, so it is essentially free.

1.3. Make your software modular

Modularity always sounded nice. Now, since more users can exploit it, and it is cheaper for you, it is also worth it. Ex: Posthaste splits in three: a backend (talks to providers, holds canonical data), a runtime (holds client state), and a client (renders). They only ever talk over an abstract transport, which can be HTTP, WebSocket, loopback, whatever.

This is how LSPs already work. The design goal is precisely the same: the language server is transport-agnostic — it adheres to the protocol and does not know about the transport layer; a separate launcher component is what’s aware of the communication channel. That channel can be stdio, sockets, named pipes, or node IPC, and because of that transport-independence the server can run locally or in the cloud while you develop on your laptop.

1.3.1. Deployment modes

Because the parts don’t care how they’re wired, I assemble them differently at CD time, Ex:

  • Split: headless backend on the server; an app with bundled runtime + client connects to it over loopback. When you run the app on several devices, you’ve got shared smart mailboxes, filters, drafts, and backend-level cache.
  • Headless: backend and runtime bundled, no client, runs as a daemon on your machine.
  • Bundled: everything together, for a classic application experience.
  • etc.

Ex: The classic example is mpd (Music Player Daemon): a headless daemon holds the canonical library and playback state, and any number of thin clients (ncmpcpp, mpc, phone apps) connect over a socket or TCP to control the same daemon from different devices.

1.3.2. Swappable parts

The same seams let people bring their own components. Missing a feature in the client? Ask codex to fork it and add the feature — I already did the hard part in the runtime underneath, so you point the fork at it and you’re done.

Info

In the future Posthaste will take JS extensions to the frontend directly, so you won’t even need to rebuild.

Ex: notmuch. The library does the indexing and search, and the frontends are independent — emacs, vim, and mutt integrations, aerc, alot (Python/urwid), astroid (C++/GTK), bower (Mercury), meli (Rust), and web frontends written in Clojure, PHP, and JS.

2. You have a tireless, mediocre coder

Warning

This section is under construction. Come back later!

Footnotes