Agent Update

Example agent

Calendar.

Eight tools in three lanes, and only the lane that reaches another person stops to ask.

What it does

Almost everything a calendar agent does reaches another human. Moving a meeting sends four people mail. Cancelling one sends mail nobody can unsend. Accepting an invitation puts your name on an answer you did not write. There is no undo for any of it, and the mistake is discovered by someone else.

The naive fix is to gate everything, and it fails worse than not gating at all: an agent that asks twenty times a day trains you to tap Allow without reading, and by the time it asks about something that matters the reflex is built.

So the gate goes on one lane and nowhere else. It protects deep work, absorbs the scheduling back-and-forth, and tells you the evening before when tomorrow looks wrong — and it spends your attention roughly twice a week.

Where the line is

The rule is about people, not about the calendar: if a change reaches another human, ask first. If it touches only your own time, do it.

That is not a size threshold and it is not about importance. Shortening a client call by five minutes reaches someone. Deleting a three-hour deep-work block does not.

It does this aloneIt stops and asks
Create, move and release your own focus, prep and travel blocksInvite anyone to anything
Read anything — your calendars, free/busy, the shared onesMove, shorten or cancel a meeting that has attendees
Add the prep time and travel buffers you never think to ask forRSVP on your behalf

The never-list is specific rather than principled — even by five minutes, even when you clearly want it — because a principle the model has to apply is a principle it can reason its way around.

The tools

Eight, in three lanes. Splitting read from write puts the gate on the smallest action; splitting write as well is what makes the gate rare enough to still be read, because half the writes a calendar agent makes are not addressed to anybody.

LaneToolsGate
Readcalendars, agenda, freebusynone
Your own timeholdnone
Another personbook, reschedule, cancel, rsvpeffect: 'write'

hold is ungated, which makes it the obvious tool to reach for when a gated one refuses. So it cannot:

async function mineAlone(ctx: Ctx, eventId: string): Promise<RawEvent> {
  const event = await getEvent(ctx, calendarId(), eventId);
  const others = guests(event);
  if (others > 0) {
    throw new Error(
      `"${event.summary ?? eventId}" has ${others} other people on it, so hold will not touch it. ` +
        'Changing it notifies them. Use reschedule or cancel, which ask first.',
    );
  }
  return event;
}

The refusal names the tool that is allowed, because an error that only says no leaves the agent to invent a workaround. Stripping the attendees off an event to make it editable would also uninvite them, so that door is closed in the north star by name.

A second guard, clash, runs inside hold, book and reschedule and refuses an overlap — ignoring all-day events and anything marked free, which are context rather than occupancy. In book it runs before the API call, so a refusal costs a round trip rather than an invitation somebody has to recall. It is a guard, not a guarantee: it reads one calendar, and hold takes an anyway escape, which is a boolean the model sets and nothing reviews. The north star says both, because an agent that thinks it cannot double-book will stop checking.

What it needs

One Google OAuth client — id, secret, refresh token — all gated: held by the supervisor and kept out of the container’s environment, released to a tool only after you approve that tool holding that credential. So the model can decide to skip the tools and use curl, and find it has nothing to authenticate with. The supervisor runs inside the container, so this stops the shell and the model — not a compromise of the container itself.

Plus CALENDAR_ID, which is deliberately not gated. It is an address, not a credential, and gating it would leave the model unable to read the thing it has to name in every request. Gate what spends money or speaks to a person, not everything that arrives from a setup wizard.

The gate is on the tool call, not on the agent. Inside its container it has a shell, curl and python3, with no prompts in front of any of them. What a gated tool buys is that the credential is withheld until you release it and the effect is previewed before it happens — which is why the credentials matter more than the guards.

How it reaches you

Four of the eight tools stop and put a question block in front of you — in the terminal and on your phone at the same moment, whichever you answer first winning. They are answered away from the desk, so the preview has to stand on its own:

// event_id says which event. title rides alongside it purely so this line
// reads on a lock screen, and span() formats in the human's timezone.
preview: (args) =>
  `move "${args.title}" to ${span(args.start, args.end)} and tell everyone on it — ${args.why}`
on your phone, with the options as tappable answers
Approve · reschedule

move "Client review" to Thu 20 Aug 15:00–16:00 and tell everyone on it — clash with the board call

Tap an answer. No reply means no, and nothing runs.
in the terminal, at the same moment
approve · reschedule
move "Client review" to Thu 20 Aug 15:00–16:00 and tell everyone on it — clash with the board call
1 Allow once   2 No
pick one · no reply means no · also on your phone

A block with fixed options, answerable from either place, has three consequences. The options are the only answers: a reply of sure, go ahead reports back as did not run, along with what you actually said. Silence is a refusal — after an hour it comes back as no and the agent is told not to retry. And none of the four is repeatable, because “Allow all session” would approve that tool for any arguments until the process dies.

That is the agent runtime’s approval block, not an unanswered question over the API, which never expires and never declines itself. The two look identical on a phone and do not mean the same thing.

The unprompted message is the other half. One line the evening before when tomorrow is wrong — no gap between calls, no lunch, four stacked, a 9am after travel — and nothing at all when tomorrow is fine. A nightly note that usually says looks fine is a nightly note you stop reading, and then the one that mattered is unread too.

The decision worth stealing

The third lane.

Read-versus-write is the obvious split, and it is not the useful one. The lane worth looking for is the write that reaches nobody: a focus block here, a saved draft in an inbox agent, a local ledger entry in a bookkeeping one. Those are real changes with no audience.

Leave that lane ungated and the gate on the lane that does reach someone stays rare enough to be read. Then make the ungated tool structurally incapable of crossing into it — a refusal in code, not a sentence in a prompt — because that is the only thing standing between the two once you have stopped asking.

Build it

The build lives in Temper — a terminal agent runtime, sandboxed in Docker, with memory, schedules, custom tools and a line to your phone already there. One guide, and it writes every file: the interview, the north star, all eight tools, both guards, the nightly schedule, and the tests that prove the gate holds.

Build the calendar agent · Temper on GitHub