Cogitator
Core

Todo Lists

Let an agent plan a long task as a todo list with write_todos, kept in the thread across runs and compaction, reported to your code and to Cogitator Studio.

Overview

An agent working through a task of many steps loses track of them as the conversation grows. With todos: true it gets the built-in write_todos tool: it writes the plan as a todo list, keeps one item in progress and marks items completed as it goes.

import { Agent, Cogitator, LocalWorkspace, workspaceTools } from '@cogitator-ai/core';

const workspace = new LocalWorkspace({ root: './app' });
const agent = new Agent({
  name: 'migrator',
  model: 'anthropic/claude-sonnet-5-5',
  instructions: 'You migrate code bases. Plan the work before you start.',
  tools: workspaceTools(),
  workspace,
  todos: true,
});

const cog = new Cogitator({ memory: { adapter: 'memory' } });
const result = await cog.run(agent, {
  input: 'Move every test from Jest to Vitest',
  threadId: 'migration',
  onTodos: (todos) => console.table(todos),
});

console.log(result.todos);

The tool

write_todos({
  todos: [
    { id: 'inventory', content: 'List every Jest test file', status: 'completed' },
    { id: 'config', content: 'Add vitest.config.ts', status: 'in_progress' },
    { id: 'rewrite', content: 'Rewrite the mocks', status: 'pending' },
  ],
});

Each call replaces the whole list. The ids must be unique, at most one item may be in_progress and a list holds at most 100 items: a call that breaks a rule fails with a validation error the model reads, and the list stays as it was. The model reads the new list back after each change:

Todo list (1 of 3 completed):
[x] inventory: List every Jest test file
[>] config: Add vitest.config.ts
[ ] rewrite: Rewrite the mocks

Where the list lives

The list is read from the conversation itself: the newest successful write_todos call in the messages a run goes on from. So a resumed run, a fork and an agent the run hands over to all see it. On top of that:

  • Thread metadata: each change is copied to the metadata of the run's thread, under todos, as { items, entryId }: the list and the newest entry of the run's branch when it was written. Only a run that saves to memory on the active branch writes it (a run with saveHistory: false or branch.activate: false does not). A later run of the thread whose history no longer holds the write_todos calls (memory compacted them, or the history limit cut them) gets the list from there, shown as a write_todos call and its result after the new input, but only when that entry is on its branch: a run that goes on from an earlier entry (branch.fromEntryId, to regenerate or edit a message) does not take the list a later turn of another branch wrote.
  • Compaction: clearing tool outputs never clears the run's write_todos results, and when compaction of the conversation drops the newest one, the list comes back as a write_todos call and its result after the newest message, one however many compactions there are. Such a call (its id starts with cogitator_todos_) is part of the messages like any other, so a checkpoint, a fork or a history that goes on from them keeps the list, in RunResult.todos and onTodos too.
  • RunResult.todos: the list as the run left it, when the agent keeps one and it was written in this run or an earlier run of the thread.
  • onTodos: RunOptions.onTodos and RunObserver.onTodos get the whole list each time it changes, and once when a run starts from a list it found in the conversation or the thread metadata. A listener that throws is logged, as is a failed metadata write: the list the model wrote stays applied.

Cogitator Studio shows the list of a run in a Todos panel, live while the run works.

Reading lists yourself

todosIn(messages) returns the list a conversation last wrote, parseKeptTodos(value) reads what the thread metadata keeps (TODOS_METADATA_KEY), parseTodos(value) checks a list, and renderTodos(todos) formats one as the model reads it.

On this page