Architecture
Overview
llm-chat-task provides a store-backed task pool with dependency tracking plus
the tools agents use to manage it. A task is a small, well-specified unit of
work: a short title plus structured fields that together form an embedded plan.
It follows the conventions shared across all llm-chat tool packages.
Design
Task model
Every task has a required short title and an optional set of structured plan
fields. See the full field table in the API Reference.
String-array items are trimmed and must be non-empty; links must be valid URLs.
Milestones are trimmed too; an empty or whitespace-only value means "no
milestone", so passing '' to updateTask clears the field. A non-empty
milestone is identifier-style: it must contain no whitespace characters and
consist of printable ASCII only (code points 0x20-0x7E).
The plan arrays mirror the guidance from LLM-agent task-spec literature: state
the goal, provide context and constraints, define done, and call out edge cases.
Task status
Every task has a status with one of four values:
| Status | Meaning |
|---|---|
pending |
has at least one unfinished dependency |
ready |
no unfinished dependencies, not started |
in_progress |
currently being worked on |
done |
completed |
pending and ready are derived from dependencies and cannot be set directly:
creating a task makes it ready; adding an unfinished dependency demotes it to
pending; once all its dependencies are done it returns to ready
automatically. Only ready, in_progress and done can be set via updateTask.
Configuration
The persistence directory and task limits live in a TaskConfiguration
passed to TaskPool.create(config?) (defaulting to a fresh instance). Each
setting is resolved at construction time: an explicit option wins, then a
LLM_CHAT_TASK_* environment variable, then the DEFAULT_* constant.
Environment-derived values are clamped to a minimum of 1. See the full
defaults table in the API Reference.
The configuration is captured by the pool, the three tool classes, and the package tutorial at construction, so a pool enforces its limits consistently for its whole lifetime. Invalid env values (empty, non-numeric, too small) fall back to the default (or clamp to 1).
TaskPool
TaskPool is an in-memory Record<id, Task> guarded by an async-mutex:
createTask(input): validates and creates areadytask from aCreateTaskInput(titlerequired), returning itsUUID; an omitted priority defaults tolow(stored tasks keep their stored priority; no default is applied)getTask(id): returns a task by id, orundefinedresolveId(idOrPrefix): resolves a full id or a unique prefix of at leastMIN_ID_PREFIX_LENGTHcharacters (default 8) to a task; exact matches win, ambiguous prefixes report all candidate ids, shorter input reportstoo-shortgetTasks(): all tasksgetAvailableTasks(): tasks that are not done and whose dependencies are all finishedgetUnfinishedDependencyIds(taskId): the dependency ids of a task that still block itupdateTask(id, changes): sets a status (ready/in_progress/done), refines any structured field, appends a timestamped entry to the progress log, and/or adds or removes a dependency; rejects missing ids, self-dependencies, duplicate dependencies, and any cycle, refuses new dependencies on tasks that arein_progressordone, and rejects removal of an id the task does not depend onclear(): resets the pool (also empties the backing store)
The pool always has a backing {@link ObjectStore} from
@johannes.latzel/json-file-store. TaskPool.create builds it: without a
configured dir it is a fresh in-memory store, with dir a JsonFileStore
on that directory. When a dir is configured, createTask and updateTask
persist the (changed) task inside the pool mutex (writes stay ordered) and
clear() also empties the store.
A pool backed by a dir pre-loads any stored tasks on TaskPool.create
(option or LLM_CHAT_TASK_DIR env var), validating each stored task
strictly, pruning dangling dependency ids, and keeping the pool bound to that
store so later writes persist back into it. Legacy save/loadFromFile/
TaskPool.load(path)/loadStored are gone: persistence is always store-backed.
Tasks reference their dependencies by id, so a single Task never carries the
whole dependency tree and task lists serialize cleanly. Dangling dependency ids
in a hand-edited file are pruned on load. Files carry the status field; a file
with an invalid or missing status is rejected on load. updateTask refuses a
task depending on itself, and a BFS over the dependency graph rejects any other
cycle before the edge is added.
Titles are normalized (trimmed) and validated on create, update, and load:
non-empty and at most maxTitleLength (default 100) characters. Descriptions
must be non-empty and at most maxDescriptionLength. All structured fields are
validated the same way on create, update, and load: enum membership, URL
validity for links, and count + item-length limits for arrays. history is an
append-only progress log: every updateTask(..., { history }) call prepends an
ISO timestamp and appends the entry, and appends beyond maxHistoryLength
(default 10,000) are rejected. All updateTask validation runs before any field
is mutated, so a rejected update never leaves a task half-changed. Task listings
preview long text fields at historyPreviewLength (default 200) characters;
single-task reads return full fields.
Tool classes
Each tool extends Tool from @johannes.latzel/llm-chat:
- Constructor calls
super(name, description, params)with aToolParametersinstance onExecute()validates parameters (typeofguards), performs the operation, and returnsPartialToolResult- All errors are caught and returned as plain-string messages; tools never throw
The tool descriptions double as usage coaching: create_task explains how to
fill each structured field productively, and update_task reminds the caller to
record a completion summary when marking a task done.
Package classes
The ToolPackage abstract class (from @johannes.latzel/llm-chat) groups related tools for registration:
TaskToolPackage extends ToolPackage and bundles the three task tools around a shared TaskPool.
| Class | Tools | Constructor |
|---|---|---|
TaskToolPackage |
create_task, read_task, update_task | required TaskPool |
Tools
| Tool | Description |
|---|---|
create_task |
Create a task from a title plus optional structured fields: description, milestone, acceptance_criteria, priority, type, links, and the six plan arrays. All values validated against the configured limits. |
read_task |
Read a task by id (full structured fields and progress log; shortened ids of at least 8 characters accepted when unique), list all tasks that are not done, list available tasks (available flag), filter listings by status, priority, type or exact milestone, and search task fields with a case-insensitive JavaScript regex (query + optional strict flag), with matches annotated via matchedFields. Listings preview long text fields at historyPreviewLength. |
update_task |
Set a task status, refine any structured field (title, description, milestone, empty string clears it, priority, type, and the array fields; arrays replace the whole list), append a progress-log entry, and/or add or remove a dependency. Accepts shortened ids for id, dependency_id and remove_dependency_id. |
Dependencies
llm-chat: framework providingTool,ToolParameters, etc.async-mutex: serializes pool mutationsjson-file-store: theObjectStorebackend every pool is backed by (in-memory by default, or aJsonFileStorewhendiris configured)