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 mocksWhere 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 withsaveHistory: falseorbranch.activate: falsedoes not). A later run of the thread whose history no longer holds thewrite_todoscalls (memory compacted them, or the history limit cut them) gets the list from there, shown as awrite_todoscall 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_todosresults, and when compaction of the conversation drops the newest one, the list comes back as awrite_todoscall and its result after the newest message, one however many compactions there are. Such a call (its id starts withcogitator_todos_) is part of the messages like any other, so a checkpoint, a fork or ahistorythat goes on from them keeps the list, inRunResult.todosandonTodostoo. 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.onTodosandRunObserver.onTodosget 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.