Tasks — the dynamic work-unit model¶
A Task is a first-class, durable unit of work an agent can create, track, delegate, and depend on — created and managed dynamically, mid-flow, by the LLM through ordinary tool calls. It replaces the former upfront planner (a single tool that pre-declared a fixed 2–7 step plan): instead of committing to a plan before starting, an agent decomposes as it goes, adds dependencies when it discovers them, and delegates sub-tasks to other sessions.
Why dynamic, not upfront¶
The planner asked the LLM to pre-select an orchestration tool and emit a complete plan before any work — a shape a weaker model rarely produces well, and that can't adapt once execution reveals the real structure. The task model exposes small, composable ops the LLM reaches for when the need appears: create a sub-task, order two of them, mark one done, abort a sub-tree. Adoption is driven by the system prompt's routing guide ("multi-target / multi-step work → decompose into sub-tasks") plus the catalog, not by a forced mode.
The model¶
- Work-unit identity. Each Task has a
task_id, aname, an optionaldescription, and astatus(lifecycle:unassigned→ready→running→done/failed;blockedwhile deps are unmet;abortedon abort.archived_atis a retention field on the task record — set alongsideabortedby abort — not a lifecycle state). - Requester vs assignee. The requester is the session that created the
Task (the notify-target). The assignee is the single worker session and is
immutable for the Task's life (no hand-off).
assigneedefaults to the caller (a self-task); a different value delegates the Task cross-session. - Single-writer CAS. Only the assignee session may write a Task's status
— enforced by a fixed-equality compare-and-set (
assignee == caller session id) in the backend. The caller session id is theOpContext.session_idrouting key, threaded by the OS (never an op field). A terminal Task rejects all further writes (the cooperative-terminal guard). Topology writes (dependencies, abort) are owned by the requester. - Dependency DAG.
depsare depends-on edges. A Task born with unmet deps is OS-derivedblocked; readiness is recomputed (never written directly) as deps complete. Edges are existence- and cycle-checked. The requester canrepointa dependent at a substitute — the primary recovery move. - Child link type and completion-join. A decomposition child carries a
link_type(awaitedorbackground).awaitedmeans the parent needs that child's result and blocks on it — it gates the parent'srunning → donetransition.backgroundmeans the parent continues in parallel and never waits. A task transitionsrunning → doneonly when bothawaitedandbackgroundopen-child counts reach zero (the completion-join gate). - Backend as external state master. The task backend (sqlite by default) is the
external master of task state — it holds each task's
status, DAG, and content. Reyn acts as a client: it sends state-change requests to the backend and subscribes to the state-change events the backend publishes. The task↔session binding (assignee,requester) is Reyn-internal and lives in the WAL (StateLog), not in the backend — that binding is what gets rewound on time-travel, while the backend's task-state is re-read as current external truth.
The ops¶
The 11 ops are callable both from a phase's control-IR and, since the dynamic
wiring, from the chat router via invoke_action (task__create,
task__update_status, …). The router path enforces the same assignee CAS as
the phase path — keyed on the real caller session id, with no bypass (the bridge
refuses rather than run a session-less context that would mask the gate).
| Op | Role gate | Purpose |
|---|---|---|
task.create |
requester = self | Create a (sub-)task; deps order it, assignee delegates; sub-task ownership is OS-derived from execution context (§16) |
task.update_status |
assignee (CAS) | Declare a status transition (single writer) |
task.get / task.list |
— | Read one record / list (by assignee / requester / status); requester=<task-id> lists sub-tasks owned by that task |
task.add_dependency / task.remove_dependency |
requester | Add / drop a depends-on edge |
task.repoint_dependency |
requester | Atomically repoint an edge to a substitute (cycle-checked first) |
task.abort |
requester | Move a Task and its sub-tree to aborted and set archived_at (cooperative-terminal, down-cascade) |
task.heartbeat |
assignee | Liveness + unblock-predicate evaluation trigger |
task.register_unblock_predicate |
assignee | Register a deterministic (no-LLM) unblock predicate |
task.comment |
— | Append to the Task's thread (inter-agent / human-in-the-loop) |
The ToolDefinitions are derived single-source from the IROp models
(model_json_schema() minus the kind discriminator), so the LLM-facing schema
never drifts from the runtime contract.
When to use¶
- Multi-target / iteration ("do X for each Y", "process N files"): one
sub-task per target plus a final aggregate task that
depson the rest. - Multi-step work worth tracking: create sub-tasks and update their status so progress is durable across turns and crashes.
- Delegation: create a sub-task with another session as
assigneeto hand work to a peer agent (the worker is the single writer of its status).
See also¶
- Workspace — the single source of truth for data passed between phases
- Events — the runtime's per-run audit trail
- Permission model — the gate layer the ops resolve through