Skip to content

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 a ready task from a CreateTaskInput (title required), returning its UUID; an omitted priority defaults to low (stored tasks keep their stored priority; no default is applied)
  • getTask(id): returns a task by id, or undefined
  • resolveId(idOrPrefix): resolves a full id or a unique prefix of at least MIN_ID_PREFIX_LENGTH characters (default 8) to a task; exact matches win, ambiguous prefixes report all candidate ids, shorter input reports too-short
  • getTasks(): all tasks
  • getAvailableTasks(): tasks that are not done and whose dependencies are all finished
  • getUnfinishedDependencyIds(taskId): the dependency ids of a task that still block it
  • updateTask(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 are in_progress or done, and rejects removal of an id the task does not depend on
  • clear(): 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:

  1. Constructor calls super(name, description, params) with a ToolParameters instance
  2. onExecute() validates parameters (typeof guards), performs the operation, and returns PartialToolResult
  3. 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:

abstract class ToolPackage {
    tools(): Tool[];
}

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 providing Tool, ToolParameters, etc.
  • async-mutex: serializes pool mutations
  • json-file-store: the ObjectStore backend every pool is backed by (in-memory by default, or a JsonFileStore when dir is configured)