Files
openworker/coworker/tools/todo.py
T
Rohit C Prasad dc8e7b6fe8 todo_write: rename the items param to todos
A top-level arguments key named "items" shadows minijinja's .items() in Together's GLM-5.2 chat template — every replayed turn 400s.
Old key still executes; GUI renders both; regression tests added.
2026-07-21 15:45:26 -07:00

88 lines
3.2 KiB
Python

"""Todo / plan tool — a structured task list the agent maintains and the UI renders.
Most of the "organized agent" feel in interactive work. Low risk, auto-approved. The list
is held in a `TodoList` the surface can read; `todo_write` replaces it.
"""
from __future__ import annotations
from dataclasses import dataclass, field
import aisuite as ai
_STATUSES = {"pending", "in_progress", "done"}
# Explicit schema — the array-of-objects shape can't be auto-generated reliably, and
# providers reject a bare `list` annotation. Registered via `__coworker_schema__`.
#
# The parameter is `todos`, NOT `items`: a top-level argument key named "items" shadows
# minijinja's `.items()` map method in at least one hosted chat template (Together's
# GLM-5.2, 2026-07-21 — "object is not callable"), 400-ing every request that replays
# the call. Any key name that isn't a minijinja map method is safe; never rename back.
_TODO_SCHEMA = {
"type": "function",
"function": {
"name": "todo_write",
"description": "Replace the task list. Provide the full list of todos each call.",
"parameters": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "done"],
},
},
"required": ["content", "status"],
},
}
},
"required": ["todos"],
},
},
}
@dataclass
class TodoList:
items: list[dict] = field(default_factory=list)
def todo_tools(todo: TodoList) -> list:
def todo_write(todos: list = None, items: list = None) -> dict:
"""Replace the task list. Each todo is an object with `content` and a `status`
of pending, in_progress, or done."""
# `items` stays accepted (models that free-style the old name; queued replays).
normalized = []
for entry in (todos if todos is not None else items) or []:
if isinstance(entry, dict):
status = entry.get("status", "pending")
if status == "completed": # common model alias for our "done"
status = "done"
normalized.append(
{
"content": str(entry.get("content", "")),
"status": status if status in _STATUSES else "pending",
}
)
else:
normalized.append({"content": str(entry), "status": "pending"})
todo.items = normalized
return {"count": len(normalized), "todos": normalized}
wrapped = ai.tool(
todo_write,
metadata=ai.ToolMetadata(
category="planning",
risk_level="low",
capabilities=["todo"],
),
)
wrapped.__coworker_schema__ = _TODO_SCHEMA
return [wrapped]