Agent
A named, durable worker owned by one child project. It holds a task board, an event log and a transcript, and it hibernates between wakes.status is what it is doing; attention is whether it needs you. They are
separate fields because an agent can be idle and still be waiting on you. The
five values the column is permitted to hold, subject to the warning above about
which of them you can observe today:
Task — and why there is no separate “job”
Every unit of work is a task, whatever door it came through. The board is the queue, so there is one noun and one table.source is platform-set and no request body can change it. A caller-settable
source would make the observability column a lie the first time someone passed
"schedule" from an API call.Job vs message: one route, two ways to read it
They are the same call. What differs is whether you want a stream back.POST /:id/tasks — and the Accept
header decides. Accept: text/event-stream streams; anything else returns
202 and the task record.
202 means durably queued. The row is inserted and the alarm armed in one
transaction before the call returns, so a runtime outage is a
503 you can see
and never a silent delay.Parked tasks, and how to un-park them
An agent can stop and ask you something. The task goes towaiting with
blocked_reason: "needs_input", and it stays there until you answer.
409 task_not_replyable.
Proof status. The park-and-resume round trip — the agent calling
message_user, the task landing at waiting, replyTask un-parking it and the
run continuing — is exercised in-process against the real router and the real
core. It is not yet part of what has been run against a deployed runtime, so
treat it as working-and-unproven-in-the-large rather than as a load-bearing
production path.Cancelling is cooperative
bash takes ten
minutes to stop, and bills them. cancel_requested_at is what lets you render
“cancelling — waiting on bash (4m12s)” instead of appearing hung.
Idempotency
Idempotency-Key header, and both are
real.
Completion window
One pinned model, three schedules. It selects how a turn is executed, never which model runs.
Fan-out is a latency knob and the batch hint a cost knob, moved in
opposite directions on purpose. On GLM-5.2 the window also selects the vendor’s
own price tier, which is the larger of the two effects.
Measured over 142 graded trials on terminal-bench 2 (
long-horizon-8,
GLM-5.2-FP8, n=2), as ratios rather than dollars: flex costs 0.70 per solved
task against asap’s 1.00, standard 0.94. Six of eight instances scored
identically under all three windows, and the two that moved
(compile-compcert, configure-git-webserver) failed their tests rather than
running out of time.
Wall clock moves with the window and is published alongside: asap 24.7 min per
trial and 43.8 s per model call, standard 50.8 min and 58.2 s/call, flex
45.1 min and 79.5 s/call.
The standard and flex arms ran at 2.5x and 3x the agent wall-clock
cap the asap arm ran at — deliberately, because a slower window against a fixed
cap fits fewer steps and fails for a reason unrelated to price. That makes the
cost column a fair comparison and the solve column one at unequal time budgets;
only the asap row is cap-matched.
A harness comparison against a fixed cap measures the cap too — and it measures
the instance list before that. Scored on the seven instances all five harnesses
ran (n=14 each), raw solves put Vetta’s harness first at 12/14 and claude-code
second at 11/14, but conditional on not hitting the wall-clock cap claude-code is
100% (10/10) and first; 4 of its 14 ran out of clock. The cap is part of the
mechanism and not all of it: the arms differ by 1.8x in seconds per model call
(15.7 on hermes to 28.7 on bare
pi), and hermes is the fastest per step of the
five while still solving fewer than claude-code. Both readings, the clean uncapped
3-instance subset that leads the comparison, why the eighth instance is not in the
denominator, and the reason cursor cannot be measured in this cell are on
Pricing.Budget
Required at create. An agent with no cap is not creatable, because a null cap means no cap and no per-agent spend record at all.period: "total" is the long-horizon statement — “spend $50 on this job, ever” —
and it is the one period a project-level cap has no way to express.
How the cap actually binds
Before each model call the runtime prices a reserve for that call against observed rates and checksspent + reserve > cap. If it would cross, the task
stops at braked with blocked_reason: "budget". It does not half-run a turn it
cannot pay for.
budget on update is a full replacement, not a merge — a partial budget
where period moved and cap_micro_usd did not is a cap nobody meant to set.
How the per-task ceiling binds
max_task_micro_usd is checked at the same moment and against the same reserve,
but over that task’s own spend — its root run plus every sub-agent it
delegated to, so a task cannot delegate its way around its ceiling.
The two ceilings mark a task differently, and the difference is deliberate. A
period cap resets, so a task that crosses it is parked at braked. A
per-task ceiling never resets, so a task that crosses it ends: status: "failed", non-retryable, with the spend, the reserve and the ceiling named in
error. When one call would cross both, the cap wins, because parking is the
recoverable answer of the two — though see above for what braked costs you
today.
Both numbers are on the event stream every turn: budget carries
spentMicroUsd/capMicroUsd and taskMicroUsd/maxTaskMicroUsd.
Reading spend
inference, sandbox,
web_search, browser, storage, wakes.
Money is decimal strings. Nothing in the client does arithmetic on it.
Two totals, and they are not the same number:
Every group carries
source: "meter" or source: "ledger", saying which record
it came from. Never add the two totals: they are two records of one agent’s
money, not two pots of it.
task.spent_micro_usd is the runtime’s own synchronous counter, which the
pre-call gate needs — and spend() reads that same meter, so the two now agree
instead of describing different worlds.wakes always reports rate_configured: false today: the axis exists and has no
meter. It prints as a zero with the reason rather than being omitted.Schedule
A cron expression, a timezone, and the work to send when it fires.- Five fields —
min hour dom mon dow. Seconds are not a field, and a 6-field expression is refused. textis required. A schedule with no work is a bug that fires forever.enableddefaults totrue.windowis accepted and dropped — every schedule-fired task runsasap.- A fire that lands while the agent is busy is collapsed, and the collapsed
count is written into the task’s own text (“This schedule fired N times while
you were unavailable… You are late.”). The
missed_firesfield on a task is hard-coded to0: collapses are counted against the schedule, not the task.
Tools
Which naïve primitives the agent may reach. Every flag narrows and none widens.requested / effective pair:
A flag cannot widen the Account Kit.
web_search: true resolves to naïve’s
search primitive under the same child project’s kit, so a kit with search
turned off removes the tool whatever the flag says. Read effective, never
requested, when a run does not use a tool you granted.Delegation
delegate_models is the only switch. An empty array — the default — means
delegation is off and the delegate tool is absent from the schema list
entirely.
Sandbox
An agent withsandbox set gets a Linux micro-VM it can write files in, run
commands in and test against.
"none"— no sandbox. The default."auto"— the agent gets one provisioned on demand."<workspace id>"— pin it to a specific workspace so state survives between tasks.
Limits — the bounded wake
Eleven numbers, and they are materialised onto the agent at create.get()
shows the values it will actually run with, never a pointer to a default that
could move under it between two wakes six weeks apart.
slice_wall_ms that
was silently dropped is an agent running on defaults while you believe otherwise.
On update, limits and tools merge field by field; everything else
replaces.
Event log
One append-only log per agent. It is written whether or not anyone is watching.That list is what the SDK’s
AgentEventKind union names, and the log is
wider. Nothing filters the read path, so a stream can also carry
batch_planned, parallel_dispatch, speculation and cost_disagreement.
Treat kind as open and keep a default branch.Deliverable
The output of work, with a manifest.kind—report|doc|code|dataset|image|video|pr|deploy|other- Up to 20 per task, 25 MB each.
download_urlis minted fresh on every read, expires in an hour, and is unauthenticated once issued — never cache it, call again instead.download_urlisnullwhen the deployment has no storage sink configured, and thencontentcarries the whole artifact. That is a deployment state, not an error.
🔴 A
final deliverable must carry an artifact or say why it does not. The
server refuses final: true with no text, no storage_key and no
no_artifact_reason. Without that refusal, “deliver” degrades into a status
update.Proof status. The full fetch-back loop — the agent’s own
deliver writing an
artifact, and deliverables()/deliverable() reading it back with a fresh
download_url — is exercised in-process against the real router and the real
core. It is not yet part of what has been run against a deployed runtime.
The manifest write and read routes themselves are; the artifact round trip
through a storage sink is the part that has not been.Errors
Every refusal arrives as naïve’s envelope with a stablecode.
window_unavailable is sent: standard or flex on any model other than
zai-org/GLM-5.2-FP8 is refused with it rather than downgraded, at the first
model call rather than at create. Handle it if you set a non-default window.
window_cannot_stream appears in the published type and is never sent — every
window streams.