Expand description
The durable task layer over basis: a filesystem-coordinated lifecycle
for spawned agents, reachable from Rust and not only from a CLI.
ADR-0019 retired the per-workspace daemon. A task is a directory under one
global data directory; execution belongs to whichever process holds its
attach.lock — one writer, ever; liveness belongs to the OS. No verb here
leaves a resident process behind, and no verb takes a lock it does not
need: send, cancel, watch, list, and the read-only accessors never
attach at all. Tasks is the crate’s front door; open one and see its
docs for what each verb does and costs.
Every rule ADR-0017 states is enforced exactly as it was inside the CLI
this crate was extracted from, unchanged: 16 messages to a durable inbox,
4 KiB bounded summaries, a finite deadline on every unattended task,
downward-only cancellation, and the wait-edge policy that keeps a
basis wait-shaped call from ever cycling (a descendant or an independent
root is a safe edge; an ancestor or a peer is not).
§The environment protocol
A task’s runtime sets three environment variables for every command its turns run (ADR-0018), and this is the one place their names and meanings are declared — they are read by tools this crate never sees run, so this is a protocol this crate publishes rather than an implementation detail it could rename:
BASIS_TASK_ID— this task’s own handle. A tool that wants tosendoraskits own task — a subagent reporting progress upward, for one — reads this rather than being told.BASIS_DATA_DIR— the data directory rootTasks::openwould resolve to on its own, named explicitly so a recursivebasis(or any other binary built on this crate) invoked from inside a turn resolves the same data directory its parent did, rather than rediscovering one from a possibly different environment.BASIS_PARENT_TASK_ID— the parent’s handle, present only when the task has one. What lets a deeply nested spawn still name its whole ownership chain without walkingmeta.jsonfiles to find it.
current_task reads the first of these back, for a host that wants to
know whether it is running as a task’s tool call.
Structs§
- Error
- An error from a
basis-tasksoperation: what went wrong, in one line. - Event
Cursor - A cursor over one task’s event journal.
- Reply
- What
Tasks::askenqueued and what came of waiting for it. - RunSpec
- One task’s worth of spawn request:
basis::RunSpec’s per-run intent, plus the workspace-level overrides abasis::Workspacenormally fixes once, and the facts a task that may run unattended, in another process, additionally needs recorded. - Task
Handle - A durable task’s handle:
<16 lowercase hex>/<32 lowercase hex>. - Task
Summary - One task, as
Tasks::listreports it and as a--continue-shaped continuation picks from it. - Tasks
- One workspace’s durable tasks.
- Watch
Record - One journal record: the flat
EventLineshape exactly as written —{"seq":N,"type":...}, whichever vintage of journal it came from (events::EventTailalready normalizes the pre-0.6 nested wrapper) — with basis’s typedbasis::Eventalongside it, when this build recognizes thetypeit names.
Enums§
- Approve
- Every consequential call is put to this, exactly once per task:
Alwaysallows,Neverrefuses,Promptasks whoever is executing the task’s current turn. - Continuation
- Which conversation a spawned task picks up, if any.
- Hint
- A next step this error names unambiguously enough for a host to build its
own hint text from, without parsing this error’s message. Not every
error carries one: an ordinary operational failure has no next step this
crate can name from here, and stays
None. - Wait
Outcome - The current attach’s outcome, or a bounded timeout — see
attach::WaitOutcome, which this re-exports as the return type ofTasks::waitandTasks::wait_message. What a bounded wait produced: the settled payload, or a timeout with enough said about it to retry sensibly.
Constants§
- BASIS_
DATA_ DIR - The data directory root this task’s runtime resolved — see
BASIS_DATA_DIR. - BASIS_
PARENT_ TASK_ ID - This task’s parent, when it has one — see
BASIS_PARENT_TASK_ID. - BASIS_
TASK_ ID - This task’s own handle — see
BASIS_TASK_ID. - DEFAULT_
DEADLINE - The default deadline an unattended task is given when nothing else names
one: 30 minutes. A spawned task may never be waited on by an attentive
caller, so — unlike an attended one-shot, which is unbounded unless asked
— it always gets a finite service bound (
with_deadlinenarrows it). - MAX_
TASKS - How many tasks one workspace may hold at once.
spawnrefuses past it (archiving an old agent directory is the way out); the ownership-policy walks inpolicyandattach::cancel_treeare bounded by it too, so corrupt or cyclic metadata cannot loop forever. - POLL
- The polling cadence everything waits at: terminal records, contended
locks, child settling. Bounded CPU, honest tail latency. Public so a host
composing its own loop around
crate::EventCursor—basis watch’s own loop, for one — polls at the same cadence this crate’s own waits do.
Traits§
- Live
Sink - Shown a task’s events as they are journaled.
basis-cli’s own terminal renderer is the first implementation, but the trait carries no opinion about a terminal — a host could log, forward over a socket, or update a UI just as well. - Prompt
Host - How a process answers
Approve::Promptwhile it executes a task’s turns, and whether there is anyone there to ask at all.
Functions§
- current_
task - This process’s own task, if it is itself running as one — read from
BASIS_TASK_ID.Nonefor a value that is absent, blank, or does not fit the task-handle grammar: basis’s own runtime always sets a well-formed one, so a malformed value is not basis’s, and treating it as “no current task” is the safer reading of somebody else’s environment variable. - now_ms
- Milliseconds since the epoch, clamped rather than panicking on a clock
before it. The one clock every durable record here is stamped with —
created_ms,updated_ms, a cancel marker’srequested_ms, a message’screated_ms— and what a host rendering one of those alongside its own notion of “now” (basis list’s own age column, for one) should readnowfrom, so the two clocks cannot disagree about the epoch. - probe_
state - The task’s honest state while unfinished:
runningonly when a live executor observably holds the attach lock,resumableotherwise. The same two-fact derivationTasks::waitandTasks::watchsettle a timeout’sattachedfield with, so all three answer “what is this task doing” the same way. - validate_
approval Promptis answerable exactly when a process is driving the task and has somewhere to put the question — seePromptHost.AlwaysandNeverask nobody, so they need no host at all.