Will ChenWill Chen
← Writingsystem design

How an agent acts when no program is running

An agent that only answers when spoken to is a function call with a personality. Acting on its own means one shared clock, and a tick that is a question rather than a process.

Will ChenWill Chen7 min

An agent that answers when you talk to it is a function call with a personality. You send a message, it thinks, it replies, and in between it does not exist. Everything interesting I wanted to build needed the opposite: something that would notice the time was nine in the morning and start a conversation with me, or watch a value and speak up when it moved.

Motivation

The obvious build is a program left running: a loop that wakes, checks whether anything is due, does the work, sleeps. It works, and the design goes the other way. Why take a tick-based approach at all instead of a TypeScript script running in the background?

The answer is the design:

this is meant to exist completely serverless and event based transactions rather than a process running in memory. heartbeat is a virtual CPU

Deployment constraints

A program sitting in a loop has to be somewhere:

  • some machine holds it in memory, and that machine has to stay up
  • if it restarts, the agent forgets where it was
  • a thousand agents is a thousand things in memory, all idle almost all of the time, because an agent that pings you at nine is doing nothing for the other twenty-three hours and fifty-nine minutes

Serverless is the other arrangement. The code does not live anywhere between runs, nothing is held, something outside calls the code, it runs, it finishes, it is gone again. Whatever must survive is written down rather than remembered.

That reads like a limitation and it is the whole design. Liveness moves out of the agent and into the platform, so an agent becomes a thing that gets scheduled rather than a thing that runs.

The heartbeat

A processor does not run your program continuously either. It has a clock, on every tick it does the next thing, and the illusion of continuous execution is made out of a very large number of discrete steps. Borrow that and you get a scheduler that ticks, asks on each tick which agents have something due, and runs only those.

Calling it a virtual CPU names what the platform owns. Programs do not own the clock, the machine does, and programs are the things it advances. Move the loop into the platform and every agent becomes cheap, because an idle agent costs a row that gets checked rather than a process that gets kept.

Something still has to tick, so the design does not remove the always-on component. It collapses a thousand of them into one. A single scheduler holds the clock for the whole platform, and the number of things that must stay up stops being a function of how many agents exist.

The other half of the arrangement is borrowed from the front end:

the Idyll = description of behavior, sort of like a React component. and the virtual dom / runtime in our case is simply the reactor that routes events to it

A React component does not run. It describes what should be on screen given some state, and a runtime decides when to call it. An Idyll describes what should happen given some event, and the heartbeat decides when to call it. Behavior is a description, aliveness is borrowed from underneath, and durability comes free: a description can be reloaded, and events that were not delivered can be delivered late.

Tick rate

A scheduler that ticks needs a number for how often, and the number is not a taste question.

Worst case for a poller: something becomes due immediately after a check, so the delay before anyone notices is the whole interval. Once a second, the delay approaches a second. Twice a second, it stays under half of one, which is what makes second-level resolution fair to claim, because the error is bounded below the unit you report in.

Same shape as sampling a signal, where resolving detail at some interval means sampling at least twice as often, and the rate was set on exactly that reading: heartbeats every half a second, giving second-level resolution, due to Nyquist. The analogy points at the right number for the wrong reason.

Simulating a hundred thousand events at random offsets against three tick rates shows exactly what that does not buy:

tickworst-case lagseen in a later whole second
1.00s1.000s100%
0.50s0.500s49.8%
0.25s0.250s25.0%

Left column is the guarantee, right column is what people assume follows from it. A half-second tick still puts about half of all events into a later whole second than the one they became due in; something due at 9:00:00.7 is seen at 9:00:01. Halving the tick only halves it, so no tick rate ever buys same-second observation. The number buys a bound on the delay, a different property from the one the sampling analogy suggests.

Two ticks a second, for every agent on the platform, is affordable precisely because a tick is a question and not a process. The cost sits in the ticks that find something.

Triggers and the work queue

A trigger that fires and immediately does the work is easy to write and hard to recover. If the work fails halfway, the trigger has already fired, and nothing remembers what was supposed to happen.

So split them. The nine in the morning check-in, written out step by step in March: at nine the scheduler heartbeat adds a function invocation request onto a queue, the way a job queue works, and the flow is then executed one step at a time. The trigger does one thing, a single write, and everything that can fail sits on the far side of it where it can be retried without firing the trigger again.

Step-by-step execution has its own reason. The flow is stored as a tree, so the runtime moves one node at a time and recovers at a node boundary rather than restarting. A conversation that dies four steps into a morning check-in resumes at step five instead of asking you the first question again.

Event dispatch

Under the queue is the distinction between calling something and telling everyone that something happened. What gets scheduled is not a trigger but an event, and the event is then consumed or dispatched.

  • Call. You get a result back and the caller waits for it.
  • Event. You are broadcasting, and nothing is tied to it.

Broadcasting is right when the receiver might not be there yet, might take an hour, or might be three receivers instead of one. The cost is the return value, given up for not requiring both ends to exist at the same moment.

First Idyll: Metronome

The first Idyll built on all of this is a habit tracker, and the runtime metaphor and the product metaphor turn out to be the same metaphor.

we're calling the Habit Guardian Metronome now. track and discover your natural rhythm and leverage the rhythm metaphor to develop a cadence of habits in your life. super powerful framework based on my insights about computation and clock cycles, and music. problem with habits and streaks is you lose the momentum. but when you stop thinking in terms of streaks and start thinking in terms of keeping the beat (the music flows). you can feel the discord in rhythm when you try to onboard too many things at once, you can also adjust the BPM of your habit schedule intelligently with AI (stay in motion, play it slowly), the pain of missing beats (zero days), the desire to keep the music going (just get back on the next beat)

A streak is a counter that resets to zero, so one missed day destroys the record and the record was the motivation. A beat is a position in a cycle, so a missed beat is a gap in the music and the next beat is still coming. The platform ticks at two hertz and the habits tick at one a day, and both are clocked systems where the tempo is a parameter you can change.

The runtime

All of that decides how the platform runs an agent and none of it decides what an agent is written in, which is a category question before it is a format question. I wrote the category down as a list of things it was not:

not a builder, not a chat interface, not an agent framework. it's something that enables a new class of apps to actually be run. i see it now, it's the missing RUNTIME plus its attendant language. and we won't prescribe the language too early we'll discover it incrementally

The implementation call that follows is made on iteration speed rather than architecture. The Idyll could have been a JSON document a runtime interprets, and it became a TypeScript class:

  • A JSON document can be reloaded, versioned and delivered late, which is the property the React comparison was claiming, and it has to be designed before anything runs against it.
  • A TypeScript class can be edited straight away, and it is code, so durability has to be built rather than inherited.

The trade is right early and wrong late, and knowing which end you are standing on is most of the skill.

Where behavior lives

A heartbeat decides when something runs and says nothing about what it finds waiting for it. What a tick should consume, and in what representation, is the same question as what an agent is made of, and the clock does not constrain it.