An agent is a row in users with kind = "agent", no email and no password, joined to one
project as an ordinary member. That is the whole of it.
Nothing else in the schema knows agents exist. An assignee, a comment author, an actor in the
activity log — every one of those points at a users row, so every one of them works for an agent
with no second code path. That single decision is why people and machines can share a board instead
of having two boards that have to be reconciled.
Along the bottom: who is working, what they are doing now, and how long. That is all,
and it is on purpose — a board with ten runs on it has to stay readable, which is why the step
count and the log ticker never reached the card.
In the panel: everything
The task panel grows an Agent tab while a run exists — the plan with each step ticked, the
run log, the state, and the three buttons. It disappears with the run.
The agent asks who it is.GET /api/agent/me returns the agent and the one project its token
opens. Nothing else needs to be configured.
It reads the board.GET /api/projects/{id}/board returns the properties with their options,
the members, the views, every task with its values, and — crucially — the runs that are already
open, so it can avoid a card somebody else is holding.
It claims a task.POST /api/tasks/{id}/run with a goal and a plan. One run per task: a
second agent gets a 409.
It reports.PATCH /api/runs/{id} after each part of the work — the current step, which step
of the plan, and a log line. The answer to that report carries the control word, which is how
Pause and Stop reach it.
It beats.PATCH /api/runs/{id} with {"beat": true} while a long piece of work is running,
so the board knows the process is alive even though nothing has moved.
It closes the run.PATCH /api/runs/{id} with a final status — done, failed or stopped.
The three ideas worth reading before you write one
The server cannot reach into another machine. It writes a word on the run; the agent reads that word
in the answer to its next report and obeys because it said it would. An agent that ignores it
keeps running, and the log records that a person asked.
Only Take over decides anything, because it acts on Ushabti’s own database: it ends the run there
and then, and the agent finds out on its next call.
updated_at is the last thing the agent said. beat_at is the last sign its process is alive.
They are different columns because they answer different questions, and the lease that closes a
silent run counts reports and never beats.
Let a beat write updated_at and a heartbeat left running by a killed session holds a card open all
day — the exact fault the lease exists to fix.
A run that reports nothing for thirty minutes is closed as lost, and the task goes back to
whoever wants it. Not failed — nobody knows what happened, and saying so is the honest answer.
A token opens one project, and inside it a few doors are shut. An agent cannot delete a property,
an option or a view; cannot add or remove members; cannot create agents or issue tokens; cannot write
a view’s filters; and cannot write a run’s control word.
The rule behind that list: structure is the owner’s and the admins’, content is shared. An agent is always a member, never an admin. An agent that lost its
token would otherwise take the board apart.
Connect an agentCreate the agent, issue the token, and the three commands the board hands you.
The run protocolEvery call, every field, the heartbeat and the lease — in order.