Task Tools¶
Eleven tools manage the task lifecycle. Tasks live in SQLite (.lithos/coordination.db) and coordinate multi-agent work through aspect claims — TTL-based advisory locks on named facets of a task.
The task record¶
Every task-returning tool uses the same record shape:
{
"id": "…", "title": "…", "description": "…",
"status": "open", "task_type": "task",
"created_by": "…", "created_at": "…",
"resolved_at": null, "updated_at": "…",
"tags": ["…"], "metadata": {}, "outcome": null
}
- Lifecycle:
open→completed|cancelled→ (vialithos_task_reopen) →open. task_type:task(default),epic(roll-up container), orgate(external wait) — see Task Graph.resolved_atis set on both terminal transitions (complete and cancel);nullwhile open.outcomeholds the free-text completion summary;nulluntil completed with one.updated_at(since v0.5.0) is bumped by every task-row write — create, update (even a no-opmetadata={}merge), complete, cancel, reopen. Claim/renew/release never bump it, so lease heartbeats can't masquerade as edits. Mutating responses echo the stamp the write produced; detect "edited since X" by comparing stamps for equality, not ordering.
All task_id parameters accept an unambiguous ≥6-char short ID prefix; an unknown 6–35-char prefix returns task_not_found on every task tool.
lithos_task_create¶
lithos_task_create(title: str, agent: str, description: str | None = None,
tags: list[str] | None = None, metadata: dict | None = None,
task_type: str = "task", depends_on: list[str] | None = None,
parent_task_id: str | None = None)
| Name | Type | Description |
|---|---|---|
title |
string | Task title |
agent |
string | Creating agent |
description |
string | Task description |
tags |
string[] | Task tags |
metadata |
object | Arbitrary JSON persisted at insert time (an initial set, not a merge). Must not contain depends_on/blocked_on → invalid_metadata_key; dependencies are edges. |
task_type |
string | task | epic | gate (default task; other values → invalid_task_type). A gate requires gate metadata — see Gates. |
depends_on |
string[] | Predecessor task IDs; each creates a blocks edge predecessor → this task. Predecessors must exist (task_not_found). |
parent_task_id |
string | Creates a parent_child edge parent → this task (structural; never blocks). |
Returns: {"task_id": "…", "title": "…", "updated_at": "…"} (plus resolved depends_on/parent_task_id when supplied), or an error envelope (invalid_metadata_key, invalid_task_type, task_not_found). Emits task.created.
lithos_task_update¶
Update mutable task fields. Works on terminal tasks too — useful for annotating an archived task without reviving it; task_not_found genuinely means missing.
lithos_task_update(task_id: str, agent: str, title: str | None = None,
description: str | None = None, tags: list[str] | None = None,
metadata: dict | None = None,
add_tags: list[str] | None = None, remove_tags: list[str] | None = None,
expected_updated_at: str | None = None)
At least one of title, description, tags, metadata, add_tags, remove_tags must be provided (invalid_input otherwise).
Tag set operations (since v0.5.0): add_tags appends without duplicates and remove_tags drops entries, preserving order — applied read-modify-write inside the write transaction, so incremental tag edits from stale reads compose instead of clobbering. They are mutually exclusive with the wholesale tags replace and must not overlap each other (invalid_input).
Compare-and-set (since v0.5.0): pass expected_updated_at — the updated_at from a prior read or update echo, compared byte-for-byte. On mismatch nothing is written, no event is emitted, and the tool returns {"status": "error", "code": "version_conflict", "message": "…", "current_updated_at": "…"} so you can retry from the current stamp without re-reading. Without the token, behaviour is last-writer-wins as before. Note the dialect difference from notes: the task-side conflict is an error envelope, while lithos_write/lithos_note_update keep version_conflict as a top-level status. Every task-row mutation commits a stamp strictly greater than the previous one, so a token is invalidated by any intervening write even if the clock repeats.
Metadata is an additive per-key merge: non-null values overwrite, {"key": null} deletes that key, unmentioned keys are preserved. metadata={} preserves everything (though it still writes the row and bumps updated_at). There is no wholesale clear. The merge runs in a single transaction, so concurrent writers on different keys never clobber each other. depends_on/blocked_on keys → invalid_metadata_key.
Returns: {"success": true, "message": "…", "task_id": "…", "title": "…", "updated_at": "…"} or an error envelope (invalid_input, invalid_metadata_key, task_not_found, version_conflict). Emits task.updated.
lithos_task_get¶
Fetch a single task without claims — with an explicit not-found envelope, unlike lithos_task_status.
Returns: {"task": {…}} (the task record), or {status: "error", code: "task_not_found", message}.
lithos_task_list¶
lithos_task_list(agent: str | None = None, status: str | None = None,
tags: list[str] | None = None, since: str | None = None,
resolved_since: str | None = None, with_claims: bool = False,
metadata_match: dict | None = None, task_type: str | None = None)
| Name | Type | Description |
|---|---|---|
agent |
string | Filter by creating agent |
status |
string | open | completed | cancelled |
task_type |
string | task | epic | gate |
tags |
string[] | Tasks containing all listed tags |
since |
string | created_at >= since (ISO) |
resolved_since |
string | resolved_at >= resolved_since — tasks resolved either way in the window; open tasks excluded |
with_claims |
bool | Inline each task's active claims as claims: [{agent, aspect, expires_at}] (one batched query — avoids an N+1 of lithos_task_status calls). Default false. |
metadata_match |
object | Same semantics as lithos_list.metadata_match; pushed into SQLite via json_extract/json_each, never a Python scan |
Returns: {"tasks": […]} of task records (plus claims when with_claims=true).
lithos_task_status¶
Full record of one task with its active claims.
Returns: {"tasks": [{…, "claims": [{"agent": "…", "aspect": "…", "expires_at": "…"}]}]}. Returns {"tasks": []} for an unknown full-length task id (historical behaviour — use lithos_task_get for an explicit not-found envelope). Expired claims are filtered out lazily at query time.
lithos_task_claim¶
Claim an aspect of a task. Claims are advisory TTL locks — one agent per (task, aspect); the aspect string is free-form ("research", "implementation", "review", …).
ttl_minutes defaults to 60, max 480 (configurable via coordination.*).
Returns: {"success": true, "expires_at": "…", "task_id": "…", "title": "…"} or {status: "error", code: "claim_failed"} (task closed, or aspect already claimed). A prefix that matches no task → task_not_found. Emits task.claimed.
lithos_task_renew¶
Extend a claim you hold. Only the claim's agent can renew it.
Returns: {"success": true, "new_expires_at": "…", "task_id": "…", "title": "…"} or {status: "error", code: "claim_not_found"}.
lithos_task_release¶
Release a claim without resolving the task.
Returns: {"success": true, "task_id": "…", "title": "…"} or {status: "error", code: "claim_not_found"}. Emits task.released.
lithos_task_complete¶
Complete a task, optionally closing the LCMA reinforcement loop.
lithos_task_complete(task_id: str, agent: str, outcome: str | None = None,
cited_nodes: list[str] | None = None,
misleading_nodes: list[str] | None = None,
receipt_id: str | None = None)
| Name | Type | Description |
|---|---|---|
outcome |
string | Free-text completion summary, persisted on the row and in the task.completed event |
cited_nodes |
string[] | Note IDs from a lithos_retrieve that were genuinely useful — boosts their salience |
misleading_nodes |
string[] | Note IDs that misled — penalizes their salience |
receipt_id |
string | Bind feedback to a specific retrieve receipt (default: the latest for this (task_id, agent)) |
Returns: {"success": true, "unblocked": ["…"], "task_id": "…", "title": "…", "updated_at": "…"} — unblocked lists dependents this completion just made ready, so an orchestrator can pick them up without re-polling lithos_task_ready. Errors: task_not_found (missing or not open), receipt_not_found.
Behavior: sets status=completed, resolved_at = updated_at = now, stores outcome, releases all claims. Feedback is validated against the bound receipt; with no findable receipt and no explicit receipt_id, feedback is silently dropped and the task still completes. Emits task.completed.
lithos_task_cancel¶
Cancel a task and delete all its claims.
Returns: {"success": true, "task_id": "…", "title": "…", "updated_at": "…"} or task_not_found. Sets status=cancelled and resolved_at = now. reason is accepted but not persisted (it does appear in the task.cancelled event payload).
Warning
A cancelled task leaves its blocks/waits_on_gate dependents permanently blocked (blocker_unsatisfiable), not spuriously ready. Reopening the cancelled task un-strands them — see below and Task Graph.
lithos_task_reopen¶
Move a terminal (completed/cancelled) task back to open — the inverse of complete/cancel.
Returns: {"success": true, "reblocked": ["…"], "task_id": "…", "title": "…", "updated_at": "…"}, or an error envelope (task_not_found; task_not_resolved when the task is already open).
Behavior:
- Clears
resolved_atandoutcome, setsstatus=open, bumpsupdated_at. - Posts a durable
[Reopened]finding recording the prior terminal status, and emitstask.reopened(payload carriesprior_status/prior_outcome). - Claims were released at complete/cancel time and are not restored.
reblockedlists open dependents this reopen put back under the task's block — non-empty only when reopening a completed blocker/gate. Reopening a cancelled blocker instead un-strands its dependents (blocker_unsatisfiable→ waiting) and reblocks nothing.