agents #
a subagent is a session with a parent. a message is mail. both are daemon primitives; the mail and agents extensions (on by default) give the model its python surface.
python #
m = await agents.models() # required once before spawning
kid = await agents.self.spawn("map every wake path", name="scout", model=m[0],
deliverable="notes/wakes.md",
verifier="test -s notes/wakes.md") # returns at once
await mail.submit(kid, "also cover schedules") # or "parent", a name, a session id
kids = await agents.self.children() # snapshots: running, closed
await kid.switch("provider/model") # {model, pending}; next request, same turn
await kid.cancel() # stop its turn, keep everything
await kid.cancel(tree=True) # ...and the turns of everything beneath it
await agents.cancel_all() # every child and descendant (tree=True by default)
await kid.close() # done: keep messages + files, free the kernel
other = await agents.get("a1b2c3") # "parent", a family name, or any session id
page = await other.messages(seq=0) # page of its rows: content, next_offset
one = await other.messages(7, through=7) # exactly row 7; limit/offset count characters
hits = await kid.search_messages("error") # rows by seq, kind and preview
older = await kid.search_messages("error", before_seq=7) # also after_seq
await agents.progress("on turn.gleam now") # shows in /agents, starts no turn
running=Falsemeans no active turn. A new child can be waiting for kernel preparation with an empty transcript; neither observation proves spawn failed. End the parent turn and wait for mail instead of cancelling on that snapshot.- spawn never returns the child's answer: the answer arrives later as
<mail>and starts the parent's next turn. a child about to end its turn without answering is reminded once (a hook); one that still ends without answering forwards its last message, markedunreviewed. verifieris an optional shell command stored on the child'ssession_familyrow (up to 4096 characters). When set, normalmail.submitto the parent andStoppingwhile a reply is owed run the command in the child's workspace (in its own process group, 300 s timeout) and refuse with its exit status and output until it exits 0. To ask questions or clarify without completing, the child usesawait mail.request("parent", ...); to report an unavoidable blocker when the verifier fails,await mail.submit("parent", ..., blocker=True). Mail to siblings or other sessions is unaffected.- names resolve inside the family (children, siblings, parent); anything else takes a session id.
- any session may look up and read any other:
get,messages, andsearch_messagesresolve names and ids the way mail does, and closed agents stay readable. cancel and close work on your own children only.cancel(tree=True)andcancel_all()stop parents before their children, so a stopped agent cannot answer by spawning, and answer the sessions that were running; they never close or delete anything. the model cannot delete an agent; it asks the user. switch(model)accepts a bare model id orprovider/model, like spawn, but refuses any id no profile lists. It returns{model: "provider/model", pending: bool}: idle sessions change now; a running request and its tools finish on the old model, and the next request uses the new one. It never cancels the turn, replaces the kernel, or changes the default for new sessions. A second switch replaces the pending one. Unsupported current effort falls back to a supported profile default, then the catalog default, as for a normal session switch. History is checked before acceptance and projected again at the boundary. If new output cannot transfer, the old model stays selected, no switch note is written, and the caller receives mail explaining the failure. At application, configuration and a user note from originmodel switchedcommit together. The note follows the finished step's tool results and is visible to context, forks and transcript reloads. Switch, cancel and close share the ownership rule (your own children or root peers).- limits: 3 deep and 12 open children per parent by default, set in the daemon's
subagentssettings section (maxDepth,maxOpenChildren; -1 for no limit; see settings), and fixed at 1 MiB per letter and 1000 undelivered letters per session.
root peers, roles, and snapshots #
agents.start(task, name=None, model=None, cwd=None, role=None) creates a root peer
rather than a child. The task is delivered as a request, and only the starter may
close or cancel that peer. agents.set_role(text, id=None) stores optional free-form
role text (trimmed to 512 characters). agents.sessions reports role,
started_by, pinned, and bounded owed/awaiting request counts.
mail delivery #
a letter is stored first and marked delivered in the same transaction that writes it into the recipient's transcript. an idle recipient accepts it for a turn (kernel preparation may still be pending); a running one reads it at its next step.
the Python receipt delivered means accepted by an idle recipient, not that a provider request has started; queued means it waits behind a turn, and pending means it is stored for retry. undelivered letters retry every 15 seconds, so a restart or a session that is not running yet loses nothing. Closed children keep their undelivered letters but do not run them; close also prevents pending kernel preparation from admitting them. webhook deliveries are letters from outside, and wait for an idle session.
the orchestrator view #
ctrl+o or /agents in a session shows its whole tree live: running agents pulse with their token rate, mail travels the edges as blocks. tab picks an agent, enter opens it (typing there is you, as the user), text + enter sends to it, /spawn <name> <task> starts a child under it.
The selected agent's card shows its status, latest delivered task or message from its parent, and latest progress. Request and progress previews are bounded to 512 characters. A new parent request clears the previous progress; peer mail does not replace the request. Progress starts no turn and is replaced in place. Idle means idle, including after restart; it does not imply the task is complete. Narrow terminals show the card below the graph. Mail animation deduplicates IDs with a bounded cache, while distinct messages with identical text remain visible.
The view reads GET /sessions?scope=family&root_id={root_id} and subscribes
to the same collection with Accept: text/event-stream. Child creation and
messages use the core session and input resources. The
HTTP contract
defines activity replacements, mail animation metadata, and invalidations.
The Go adapter decodes known events before delivery. Malformed known events
fail the stream; unknown event kinds are ignored. The daemon supplies bounded
tool progress under tool_progress: 1. Clients render it without reconstructing
arguments or guessing Python intent. For Python cells, progress carries a derived title (from the leading # title comment or first code line, up to 160 scalars) as soon as the first line completes, so long cells retain their label even after the streaming code preview scrolls past its 512-codepoint window. Settled tool history entries and stream tool events also carry this derived title in tool.title (derived in bounded O(1) time from the first 512 scalars of arguments, up to 160 scalars), so clients no longer derive titles or need full arguments when payloads exceed stream limits; if reaching the first code or comment line takes more than 512 scalars, title is null and clients fall back to line 1.
Stream overflow and refresh #
The initial batch contains ready and reset controls. Subscriber byte and event limits apply before payloads enter mailboxes; wakes are coalesced. Publication does not wait for a client to read its socket.
Overflow discards queued deltas, emits an overflow control when the socket can accept it, and closes the stream. The client resubscribes and refreshes the authoritative family. That refresh replaces names, relationships, closed state, status, and activity. Replies from an older attachment cannot repopulate the view. Disconnect releases subscriber state without another publication. Drafts and pending action results survive recovery. Terminal failures remain visible; ctrl+l starts a fresh attachment.
swarm overhead #
kernel boots queue behind four slots. each boot owns only its session's composition, and each kernel's host callback captures only its routes, store handle, and session id—not a snapshot of the other agents. run waits on process-exit notifications, not periodic exit checks; descendant cleanup and deadlines still apply.
session replay retains at most 256 events or 992 kib, evicting incrementally rather than copying the full window per token. missing events require a transcript reset. the orchestrator feed batches every 100 ms and skips activity serialization when nobody is watching.
a child's or peer's provider and model are resolved before the registry is asked
(daemon/child_models.gleam, called from the agents seam and from
PUT /sessions/{id}), and agents.models() never reaches the registry: listing a
generic profile's models can wait on its endpoint, and the registry answers every
request in the daemon. resolution lists only the profiles a choice needs.
run jobs start at once, at low priority (nice, and utility QoS on macOS), so a
busy swarm yields to the person at the machine. jobs never pause for admission,
and their deadlines count wall time after the program starts.
deletion failures #
Direct parent deletion requires a successful child lookup. Tree deletion collects the child-first walk before deleting any session. A failed lookup returns an error and leaves the sessions intact. A failure during deletion still reports how many sessions were already deleted.
requests, roles, and peers #
await mail.request(to, body) creates a request; the recipient owes the sender until it sends any letter. Peer messages do not create obligations; a child still owes its parent after a task or ordinary message. await mail.open() returns bounded owed and awaiting entries. await agents.set_role(text, id=None) stores optional trimmed metadata for workspace views; it is not prompt context.
Peers are root sessions started with await agents.start(task, name, model, cwd=None, role=None). They have no family parent and do not count against child limits. The starter alone may cancel or close its peers. Session snapshots expose role, provenance, pinned state, and open request counts.