Skip to main content

basis_tasks/
lib.rs

1//! The durable task layer over [`basis`]: a filesystem-coordinated lifecycle
2//! for spawned agents, reachable from Rust and not only from a CLI.
3//!
4//! ADR-0019 retired the per-workspace daemon. A task is a directory under one
5//! global data directory; execution belongs to whichever process holds its
6//! `attach.lock` — one writer, ever; liveness belongs to the OS. No verb here
7//! leaves a resident process behind, and no verb takes a lock it does not
8//! need: `send`, `cancel`, `watch`, `list`, and the read-only accessors never
9//! attach at all. [`Tasks`] is the crate's front door; open one and see its
10//! docs for what each verb does and costs.
11//!
12//! Every rule ADR-0017 states is enforced exactly as it was inside the CLI
13//! this crate was extracted from, unchanged: 16 messages to a durable inbox,
14//! 4 KiB bounded summaries, a finite deadline on every unattended task,
15//! downward-only cancellation, and the wait-edge policy that keeps a
16//! `basis wait`-shaped call from ever cycling (a descendant or an independent
17//! root is a safe edge; an ancestor or a peer is not).
18//!
19//! # The environment protocol
20//!
21//! A task's runtime sets up to four environment variables for every command
22//! its turns run (ADR-0018), and this is the one place their names and meanings
23//! are declared — they are read by tools this crate never sees run, so this
24//! is a protocol this crate publishes rather than an implementation detail
25//! it could rename:
26//!
27//! - [`BASIS_TASK_ID`] — this task's own handle. A tool that wants to `send`
28//!   or `ask` its own task — a subagent reporting progress upward, for one —
29//!   reads this rather than being told.
30//! - [`BASIS_DATA_DIR`] — the data directory root [`Tasks::open`] would
31//!   resolve to on its own, named explicitly so a recursive `basis` (or any
32//!   other binary built on this crate) invoked from inside a turn resolves
33//!   the same data directory its parent did, rather than rediscovering one
34//!   from a possibly different environment.
35//! - [`BASIS_PARENT_TASK_ID`] — the parent's handle, present only when the
36//!   task has one. What lets a deeply nested spawn still name its whole
37//!   ownership chain without walking `meta.json` files to find it.
38//! - [`BASIS_HOP`] — the agent-to-agent hop the task's work arrived at,
39//!   present only when it is not 0 (see `RunSpec::with_hop`). What a client
40//!   run from inside a turn adds one to when it hands work to another agent
41//!   (ADR-0030 §9).
42//!
43//! [`current_task`] reads the first of these back, for a host that wants to
44//! know whether *it* is running as a task's tool call.
45
46mod answer;
47mod approve;
48mod attach;
49mod builders;
50mod client;
51mod data_dir;
52mod error;
53mod events;
54mod handle;
55mod inbox;
56mod live;
57mod lock;
58mod policy;
59mod spec;
60mod state;
61mod stop;
62mod tasks;
63mod watch;
64
65pub use answer::{ATTACH_FILE, AnswerFile, MAX_ANSWER_FILES_BYTES};
66pub use approve::{Approve, PromptHost, validate_approval};
67pub use attach::POLL;
68pub use builders::configure_builders;
69pub use client::{Reply, Tasks, WaitOutcome};
70pub use error::{Error, Hint};
71pub use handle::TaskHandle;
72pub use live::LiveSink;
73pub use spec::{Continuation, DEFAULT_DEADLINE, RunSpec};
74pub use state::{
75    InboxRecord, MAX_PROMPT, MAX_TASKS, MessageRecord, MessageReply, MessageState, Terminal,
76    TerminalRecord, now_ms,
77};
78pub use stop::Stop;
79pub use tasks::{TaskSummary, probe_state};
80pub use watch::{EventCursor, WatchRecord};
81
82/// This task's own handle — see [`BASIS_TASK_ID`].
83pub const BASIS_TASK_ID: &str = "BASIS_TASK_ID";
84/// The data directory root this task's runtime resolved — see
85/// [`BASIS_DATA_DIR`].
86pub const BASIS_DATA_DIR: &str = "BASIS_DATA_DIR";
87/// This task's parent, when it has one — see [`BASIS_PARENT_TASK_ID`].
88pub const BASIS_PARENT_TASK_ID: &str = "BASIS_PARENT_TASK_ID";
89/// The hop this task's work arrived at, when it is not 0 — see
90/// [`BASIS_HOP`].
91pub const BASIS_HOP: &str = "BASIS_HOP";
92
93/// This process's own task, if it is itself running as one — read from
94/// [`BASIS_TASK_ID`]. `None` for a value that is absent, blank, or does not
95/// fit the task-handle grammar: basis's own runtime always sets a well-formed
96/// one, so a malformed value is not basis's, and treating it as "no current
97/// task" is the safer reading of somebody else's environment variable.
98pub fn current_task() -> Option<TaskHandle> {
99    let value = std::env::var(BASIS_TASK_ID).ok()?;
100    if value.trim().is_empty() {
101        return None;
102    }
103    TaskHandle::parse(value).ok()
104}