Sagents.Todo (Sagents v0.14.0)

Copy Markdown

TODO item structure for task tracking.

TODOs help agents break down complex tasks into manageable steps and track progress through multi-step workflows.

Identity

Each TODO's id is a positive integer that is unique within a single list. There is no global ID space — IDs only need to distinguish items within the same conversation's todo list. When building a list, prefer list_from_maps/1, which assigns positional IDs (1..N) for any item that doesn't supply one.

Status Values

  • :pending - Task not yet started
  • :in_progress - Currently being worked on
  • :completed - Task finished successfully
  • :cancelled - Task no longer needed

The statuses split into two groups. :pending and :in_progress are open — the item still needs work. :completed and :cancelled are resolved — the item has been dealt with, whether by doing it or by establishing that it no longer applies. Use resolved?/1 and open?/1 rather than comparing against individual statuses, so a list that ends in a mix of completed and cancelled items is recognized as finished.

Both predicates accept a bare status as well as a Todo struct, so code holding serialized todo data can ask about a "status" value without rebuilding a struct for the comparison.

Usage

# Create a single TODO (id is required)
{:ok, todo} = Todo.new(%{id: 1, content: "Implement user authentication"})

# Build a list, letting positional defaults assign IDs
{:ok, todos} = Todo.list_from_maps([
  %{"content" => "Task A", "status" => "pending"},
  %{"content" => "Task B", "status" => "pending"}
])
# todos[0].id == 1, todos[1].id == 2

Summary

Functions

Create a single TODO from a map (for deserialization).

Build a list of TODOs from an ordered list of maps.

Create a new TODO item with validation.

Create a new TODO item, raising on error.

Whether the TODO still needs work.

Whether the TODO has been dealt with.

Convert a TODO struct to a map for serialization.

Types

status()

@type status() :: :pending | :in_progress | :completed | :cancelled

t()

@type t() :: %Sagents.Todo{content: String.t(), id: integer(), status: status()}

Functions

from_map(map)

Create a single TODO from a map (for deserialization).

Coerces a stringified integer id to an integer. A missing or non-numeric id is an error from this entry point; callers handling a whole list should use list_from_maps/1 to get positional ID defaults.

Examples

{:ok, todo} = Todo.from_map(%{"id" => 1, "content" => "Task", "status" => "pending"})
{:ok, todo} = Todo.from_map(%{"id" => "1", "content" => "Task"})

list_from_maps(todos)

Build a list of TODOs from an ordered list of maps.

This is the canonical ingest point for any list of incoming todo data: tool calls, persisted state, conversation rehydrate. It guarantees every TODO has a positive integer id by assigning positional defaults:

  • Missing, nil, or non-numeric id → id = index + 1 (1-based).
  • Positive integer id → kept as is.
  • Numeric-string id (e.g. "5", "01") → coerced to integer.

The non-numeric fallback exists for backward compatibility with snapshots persisted before IDs were typed as integers (which used random base64 strings). Those legacy IDs lose their original identity on reload, but in-list order is preserved.

Returns {:ok, [Todo.t()]} if every item parses, or {:error, reason} on the first failure.

new(attrs \\ %{})

Create a new TODO item with validation.

Requires an explicit positive-integer id. There is no auto-generation — callers building a list should use list_from_maps/1 instead, which assigns positional IDs.

Stringified integer ids (e.g. "5") are coerced to integers so payloads arriving from JSON tool calls keep working.

Examples

{:ok, todo} = Todo.new(%{id: 1, content: "Write tests"})
{:ok, todo} = Todo.new(%{id: "1", content: "Write tests"})  # coerced

new!(attrs \\ %{})

Create a new TODO item, raising on error.

Examples

todo = Todo.new!(%{id: 1, content: "Deploy to production"})

open?(status)

@spec open?(t() | status() | String.t()) :: boolean()

Whether the TODO still needs work.

The complement of resolved?/1: true for :pending and :in_progress, and for any status outside the four known values.

Takes the same arguments as resolved?/1: a Todo struct, a status atom, or a status string.

Examples

Todo.open?(Todo.new!(%{id: 1, content: "Task", status: :in_progress}))
# => true

Todo.open?(:pending)
# => true

Todo.open?("completed")
# => false

resolved?(status)

@spec resolved?(t() | status() | String.t()) :: boolean()

Whether the TODO has been dealt with.

A TODO is resolved when it is :completed or :cancelled. Cancelling is a legitimate outcome: a step can turn out not to apply once earlier steps report their findings.

Takes a Todo struct or a bare status, as an atom or a string. Code holding serialized todo data can ask about a "status" value directly instead of rebuilding a struct for the comparison. A status outside the four known values reads as not resolved, matching how from_map/1 treats an unrecognized status.

Examples

Todo.resolved?(Todo.new!(%{id: 1, content: "Task", status: :cancelled}))
# => true

Todo.resolved?(:completed)
# => true

Todo.resolved?("cancelled")
# => true

Todo.resolved?("in_progress")
# => false

to_map(todo)

Convert a TODO struct to a map for serialization.

Examples

todo = Todo.new!(%{id: 1, content: "Task"})
map = Todo.to_map(todo)
# => %{"id" => 1, "content" => "Task", "status" => "pending"}