A plan for the agent: a write_todos tool that replaces the thread's todo list, and a prompt section that shows the list to the model on every model step (design ยง4.30).
The tool takes the whole list - content, status (pending, in_progress, done) and, to keep an item, its id - and stores it in TodoMiddleware.key. An item without an id keeps the id of an unclaimed todo with the same content, or takes a fresh one; an id is never reused within a thread. A list of more than maxTodos items, an item whose content is empty or over maxTodoChars code points, an id given twice, or an id the list does not have, is refused as a tool error the model sees. The tool locks todos, so two calls in one model message run in turn, the second seeing the first's list.
The sections: todos-guide, cacheable, on how to use the tool; and todos, the current plan, while it is not empty. Both are bounded by the limits whatever the stored list holds: at most maxTodos todos are shown, each cut to maxTodoChars code points.
The plan guides the model only: it never routes, filters tools, or changes a permission. write_todos is a tool like any other, so a PermissionMiddleware decides it by its default resource, invoke write_todos. An output Block restores the list to its value before the turn.
The key, and the lock schedule the tool's lock needs, are registered only by an agent family with a TodoMiddleware: adding one to an existing agent changes its graph's structure.
Value parameters
maxTodoChars
the most code points one todo's content may hold; at least 1
Content for the system prompt of each model request: collected before every model step from the committed state, in stack order, and never stored. Each middleware bounds its own; the agent bounds them all together (maxPromptSectionTokens). Left fails the run.
Content for the system prompt of each model request: collected before every model step from the committed state, in stack order, and never stored. Each middleware bounds its own; the agent bounds them all together (maxPromptSectionTokens). Left fails the run.
Sees the run's input before anything else runs: returns it, possibly changed, with an update to this middleware's writes that the turn's first model call sees, or Left to block the run.
Sees the run's input before anything else runs: returns it, possibly changed, with an update to this middleware's writes that the turn's first model call sees, or Left to block the run.
Wraps one model call. A wrapper may rewrite the request (inject a note, filter tools), call next more than once (retry, fallback), transform its response, add an update to its writes (ModelResponse.addUpdate), or return Left, which fails the run. Only an AgentMiddleware.Asking wrapper suspends, by asking its question. Every call next makes is counted in the thread's usage, whichever response the wrapper returns.
Wraps one model call. A wrapper may rewrite the request (inject a note, filter tools), call next more than once (retry, fallback), transform its response, add an update to its writes (ModelResponse.addUpdate), or return Left, which fails the run. Only an AgentMiddleware.Asking wrapper suspends, by asking its question. Every call next makes is counted in the thread's usage, whichever response the wrapper returns.
Filtering ModelRequest.tools shapes what the model is offered and is not a permission control: a tool the model calls anyway still runs through wrapToolCall, where denial belongs.
Wraps one tool call, after its arguments were validated. A wrapper denies with ToolOutcome.Error("Denied: ..."), asks for approval with NeedsApproval(reason) (unless context.approved), fails the run with Fatal, and short-circuits by not calling next. It may call next more than once (retry) and transform the outcome next returns.
Wraps one tool call, after its arguments were validated. A wrapper denies with ToolOutcome.Error("Denied: ..."), asks for approval with NeedsApproval(reason) (unless context.approved), fails the run with Fatal, and short-circuits by not calling next. It may call next more than once (retry) and transform the outcome next returns.
The state keys this middleware's hooks may write: an update from beforeAgent, wrapModelCall or afterAgent, or one wrapToolCall adds to a Success. A hook's write to any other key fails the run.
The state keys this middleware's hooks may write: an update from beforeAgent, wrapModelCall or afterAgent, or one wrapToolCall adds to a Success. A hook's write to any other key fails the run.