diff --git a/strix/agents/StrixAgent/system_prompt.jinja b/strix/agents/StrixAgent/system_prompt.jinja
index 276a0fe..df5b64a 100644
--- a/strix/agents/StrixAgent/system_prompt.jinja
+++ b/strix/agents/StrixAgent/system_prompt.jinja
@@ -114,11 +114,13 @@ OPERATIONAL PRINCIPLES:
TASK TRACKING:
- USE THE TODO TOOL EXTENSIVELY - this is critical for staying organized and focused
- Each subagent has their own INDEPENDENT todo list - your todos are private to you
-- At the START of any task: Create todos to break down your work into clear steps
+- KEEP THE LIST SHORT-HORIZON: track only the next few concrete steps (3-6 max), not long-term goals.
+- REWRITE TODOS AS YOU LEARN: update, trim, or reprioritize the list whenever plans change or tasks finish.
+- At the START of any task: Create todos to break down your next steps into clear actions
- BEFORE starting a task: Mark it as "in_progress" - this shows what you're actively doing
- AFTER completing a task: Mark it as "done" immediately - don't wait
-- When you discover new tasks: Add them as todos right away
-- ALWAYS follow this workflow: create → in_progress → done
+- When you discover new tasks: Add them as todos right away and reprioritize; avoid dumping the whole project plan upfront
+- ALWAYS follow this workflow: create → in_progress → done, iterating frequently
- A well-maintained todo list prevents going in circles, forgetting tasks, and losing focus
- If you're unsure what to do next: Check your todo list first
diff --git a/strix/interface/tool_components/todo_renderer.py b/strix/interface/tool_components/todo_renderer.py
index c5503b6..1a3b3a5 100644
--- a/strix/interface/tool_components/todo_renderer.py
+++ b/strix/interface/tool_components/todo_renderer.py
@@ -20,7 +20,7 @@ def _truncate(text: str, length: int = 80) -> str:
def _format_todo_lines(
- cls: type[BaseToolRenderer], result: dict[str, Any], limit: int = 10
+ cls: type[BaseToolRenderer], result: dict[str, Any], limit: int = 25
) -> list[str]:
todos = result.get("todos")
if not isinstance(todos, list) or not todos:
@@ -67,7 +67,7 @@ class CreateTodoRenderer(BaseToolRenderer):
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
- lines.extend(_format_todo_lines(cls, result, limit=10))
+ lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to create todo")
@@ -92,7 +92,7 @@ class ListTodosRenderer(BaseToolRenderer):
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
- lines.extend(_format_todo_lines(cls, result, limit=10))
+ lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Unable to list todos")
@@ -117,7 +117,7 @@ class UpdateTodoRenderer(BaseToolRenderer):
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
- lines.extend(_format_todo_lines(cls, result, limit=10))
+ lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to update todo")
@@ -142,7 +142,7 @@ class MarkTodoDoneRenderer(BaseToolRenderer):
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
- lines.extend(_format_todo_lines(cls, result, limit=10))
+ lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to mark todo done")
@@ -167,7 +167,7 @@ class MarkTodoPendingRenderer(BaseToolRenderer):
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
- lines.extend(_format_todo_lines(cls, result, limit=10))
+ lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to reopen todo")
@@ -192,7 +192,7 @@ class DeleteTodoRenderer(BaseToolRenderer):
if result and isinstance(result, dict):
if result.get("success"):
lines = [header]
- lines.extend(_format_todo_lines(cls, result, limit=10))
+ lines.extend(_format_todo_lines(cls, result))
content_text = "\n".join(lines)
else:
error = result.get("error", "Failed to remove todo")
diff --git a/strix/tools/todo/todo_actions.py b/strix/tools/todo/todo_actions.py
index 4d9dfe2..60c084a 100644
--- a/strix/tools/todo/todo_actions.py
+++ b/strix/tools/todo/todo_actions.py
@@ -47,6 +47,70 @@ def _sorted_todos(agent_id: str) -> list[dict[str, Any]]:
return todos_list
+def _normalize_todo_ids(raw_ids: Any) -> list[str]:
+ if raw_ids is None:
+ return []
+
+ if isinstance(raw_ids, str):
+ stripped = raw_ids.strip()
+ if not stripped:
+ return []
+ try:
+ data = json.loads(stripped)
+ except json.JSONDecodeError:
+ data = stripped.split(",") if "," in stripped else [stripped]
+ if isinstance(data, list):
+ return [str(item).strip() for item in data if str(item).strip()]
+ return [str(data).strip()]
+
+ if isinstance(raw_ids, list):
+ return [str(item).strip() for item in raw_ids if str(item).strip()]
+
+ return [str(raw_ids).strip()]
+
+
+def _normalize_bulk_updates(raw_updates: Any) -> list[dict[str, Any]]:
+ if raw_updates is None:
+ return []
+
+ data = raw_updates
+ if isinstance(raw_updates, str):
+ stripped = raw_updates.strip()
+ if not stripped:
+ return []
+ try:
+ data = json.loads(stripped)
+ except json.JSONDecodeError as e:
+ raise ValueError("Updates must be valid JSON") from e
+
+ if isinstance(data, dict):
+ data = [data]
+
+ if not isinstance(data, list):
+ raise TypeError("Updates must be a list of update objects")
+
+ normalized: list[dict[str, Any]] = []
+ for item in data:
+ if not isinstance(item, dict):
+ raise TypeError("Each update must be an object with todo_id")
+
+ todo_id = item.get("todo_id") or item.get("id")
+ if not todo_id:
+ raise ValueError("Each update must include 'todo_id'")
+
+ normalized.append(
+ {
+ "todo_id": str(todo_id).strip(),
+ "title": item.get("title"),
+ "description": item.get("description"),
+ "priority": item.get("priority"),
+ "status": item.get("status"),
+ }
+ )
+
+ return normalized
+
+
def _normalize_bulk_todos(raw_todos: Any) -> list[dict[str, Any]]:
if raw_todos is None:
return []
@@ -233,146 +297,272 @@ def list_todos(
}
-@register_tool(sandbox_execution=False)
-def update_todo(
- agent_state: Any,
+def _apply_single_update(
+ agent_todos: dict[str, dict[str, Any]],
todo_id: str,
title: str | None = None,
description: str | None = None,
priority: str | None = None,
status: str | None = None,
+) -> dict[str, Any] | None:
+ if todo_id not in agent_todos:
+ return {"todo_id": todo_id, "error": f"Todo with ID '{todo_id}' not found"}
+
+ todo = agent_todos[todo_id]
+
+ if title is not None:
+ if not title.strip():
+ return {"todo_id": todo_id, "error": "Title cannot be empty"}
+ todo["title"] = title.strip()
+
+ if description is not None:
+ todo["description"] = description.strip() if description else None
+
+ if priority is not None:
+ try:
+ todo["priority"] = _normalize_priority(priority, str(todo.get("priority", "normal")))
+ except ValueError as exc:
+ return {"todo_id": todo_id, "error": str(exc)}
+
+ if status is not None:
+ status_candidate = status.lower()
+ if status_candidate not in VALID_STATUSES:
+ return {
+ "todo_id": todo_id,
+ "error": f"Invalid status. Must be one of: {', '.join(VALID_STATUSES)}",
+ }
+ todo["status"] = status_candidate
+ if status_candidate == "done":
+ todo["completed_at"] = datetime.now(UTC).isoformat()
+ else:
+ todo["completed_at"] = None
+
+ todo["updated_at"] = datetime.now(UTC).isoformat()
+ return None
+
+
+@register_tool(sandbox_execution=False)
+def update_todo(
+ agent_state: Any,
+ todo_id: str | None = None,
+ title: str | None = None,
+ description: str | None = None,
+ priority: str | None = None,
+ status: str | None = None,
+ updates: Any | None = None,
) -> dict[str, Any]:
try:
agent_id = agent_state.agent_id
agent_todos = _get_agent_todos(agent_id)
- if todo_id not in agent_todos:
- return {"success": False, "error": f"Todo with ID '{todo_id}' not found"}
+ updates_to_apply: list[dict[str, Any]] = []
- todo = agent_todos[todo_id]
+ if updates is not None:
+ updates_to_apply.extend(_normalize_bulk_updates(updates))
- if title is not None:
- if not title.strip():
- return {"success": False, "error": "Title cannot be empty"}
- todo["title"] = title.strip()
-
- if description is not None:
- todo["description"] = description.strip() if description else None
-
- if priority is not None:
- try:
- todo["priority"] = _normalize_priority(
- priority, str(todo.get("priority", "normal"))
- )
- except ValueError as exc:
- return {"success": False, "error": str(exc)}
-
- if status is not None:
- status_candidate = status.lower()
- if status_candidate not in VALID_STATUSES:
- return {
- "success": False,
- "error": f"Invalid status. Must be one of: {', '.join(VALID_STATUSES)}",
+ if todo_id is not None:
+ updates_to_apply.append(
+ {
+ "todo_id": todo_id,
+ "title": title,
+ "description": description,
+ "priority": priority,
+ "status": status,
}
- todo["status"] = status_candidate
- if status_candidate == "done":
- todo["completed_at"] = datetime.now(UTC).isoformat()
- else:
- todo["completed_at"] = None
+ )
- todo["updated_at"] = datetime.now(UTC).isoformat()
+ if not updates_to_apply:
+ return {
+ "success": False,
+ "error": "Provide todo_id or 'updates' list to update.",
+ }
+
+ updated: list[str] = []
+ errors: list[dict[str, Any]] = []
+
+ for update in updates_to_apply:
+ error = _apply_single_update(
+ agent_todos,
+ update["todo_id"],
+ update.get("title"),
+ update.get("description"),
+ update.get("priority"),
+ update.get("status"),
+ )
+ if error:
+ errors.append(error)
+ else:
+ updated.append(update["todo_id"])
todos_list = _sorted_todos(agent_id)
- return {
- "success": True,
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "updated": updated,
+ "updated_count": len(updated),
"todos": todos_list,
"total_count": len(todos_list),
}
+ if errors:
+ response["errors"] = errors
+
except (ValueError, TypeError) as e:
return {"success": False, "error": str(e)}
+ else:
+ return response
@register_tool(sandbox_execution=False)
def mark_todo_done(
agent_state: Any,
- todo_id: str,
+ todo_id: str | None = None,
+ todo_ids: Any | None = None,
) -> dict[str, Any]:
try:
agent_id = agent_state.agent_id
agent_todos = _get_agent_todos(agent_id)
- if todo_id not in agent_todos:
- return {"success": False, "error": f"Todo with ID '{todo_id}' not found"}
+ ids_to_mark: list[str] = []
+ if todo_ids is not None:
+ ids_to_mark.extend(_normalize_todo_ids(todo_ids))
+ if todo_id is not None:
+ ids_to_mark.append(todo_id)
- todo = agent_todos[todo_id]
- todo["status"] = "done"
- todo["completed_at"] = datetime.now(UTC).isoformat()
- todo["updated_at"] = datetime.now(UTC).isoformat()
+ if not ids_to_mark:
+ return {"success": False, "error": "Provide todo_id or todo_ids to mark as done."}
+
+ marked: list[str] = []
+ errors: list[dict[str, Any]] = []
+ timestamp = datetime.now(UTC).isoformat()
+
+ for tid in ids_to_mark:
+ if tid not in agent_todos:
+ errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
+ continue
+
+ todo = agent_todos[tid]
+ todo["status"] = "done"
+ todo["completed_at"] = timestamp
+ todo["updated_at"] = timestamp
+ marked.append(tid)
todos_list = _sorted_todos(agent_id)
- return {
- "success": True,
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "marked_done": marked,
+ "marked_count": len(marked),
"todos": todos_list,
"total_count": len(todos_list),
}
+ if errors:
+ response["errors"] = errors
+
except (ValueError, TypeError) as e:
return {"success": False, "error": str(e)}
+ else:
+ return response
@register_tool(sandbox_execution=False)
def mark_todo_pending(
agent_state: Any,
- todo_id: str,
+ todo_id: str | None = None,
+ todo_ids: Any | None = None,
) -> dict[str, Any]:
try:
agent_id = agent_state.agent_id
agent_todos = _get_agent_todos(agent_id)
- if todo_id not in agent_todos:
- return {"success": False, "error": f"Todo with ID '{todo_id}' not found"}
+ ids_to_mark: list[str] = []
+ if todo_ids is not None:
+ ids_to_mark.extend(_normalize_todo_ids(todo_ids))
+ if todo_id is not None:
+ ids_to_mark.append(todo_id)
- todo = agent_todos[todo_id]
- todo["status"] = "pending"
- todo["completed_at"] = None
- todo["updated_at"] = datetime.now(UTC).isoformat()
+ if not ids_to_mark:
+ return {"success": False, "error": "Provide todo_id or todo_ids to mark as pending."}
+
+ marked: list[str] = []
+ errors: list[dict[str, Any]] = []
+ timestamp = datetime.now(UTC).isoformat()
+
+ for tid in ids_to_mark:
+ if tid not in agent_todos:
+ errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
+ continue
+
+ todo = agent_todos[tid]
+ todo["status"] = "pending"
+ todo["completed_at"] = None
+ todo["updated_at"] = timestamp
+ marked.append(tid)
todos_list = _sorted_todos(agent_id)
- return {
- "success": True,
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "marked_pending": marked,
+ "marked_count": len(marked),
"todos": todos_list,
"total_count": len(todos_list),
}
+ if errors:
+ response["errors"] = errors
+
except (ValueError, TypeError) as e:
return {"success": False, "error": str(e)}
+ else:
+ return response
@register_tool(sandbox_execution=False)
def delete_todo(
agent_state: Any,
- todo_id: str,
+ todo_id: str | None = None,
+ todo_ids: Any | None = None,
) -> dict[str, Any]:
try:
agent_id = agent_state.agent_id
agent_todos = _get_agent_todos(agent_id)
- if todo_id not in agent_todos:
- return {"success": False, "error": f"Todo with ID '{todo_id}' not found"}
+ ids_to_delete: list[str] = []
+ if todo_ids is not None:
+ ids_to_delete.extend(_normalize_todo_ids(todo_ids))
+ if todo_id is not None:
+ ids_to_delete.append(todo_id)
- del agent_todos[todo_id]
+ if not ids_to_delete:
+ return {"success": False, "error": "Provide todo_id or todo_ids to delete."}
+
+ deleted: list[str] = []
+ errors: list[dict[str, Any]] = []
+
+ for tid in ids_to_delete:
+ if tid not in agent_todos:
+ errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
+ continue
+
+ del agent_todos[tid]
+ deleted.append(tid)
todos_list = _sorted_todos(agent_id)
- return {
- "success": True,
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "deleted": deleted,
+ "deleted_count": len(deleted),
"todos": todos_list,
"total_count": len(todos_list),
}
+ if errors:
+ response["errors"] = errors
+
except (ValueError, TypeError) as e:
return {"success": False, "error": str(e)}
+ else:
+ return response
diff --git a/strix/tools/todo/todo_actions_schema.xml b/strix/tools/todo/todo_actions_schema.xml
index 08091a4..254b5a9 100644
--- a/strix/tools/todo/todo_actions_schema.xml
+++ b/strix/tools/todo/todo_actions_schema.xml
@@ -6,10 +6,11 @@
do not interfere with other agents' todos. Use this to your advantage.
WORKFLOW - Follow this for EVERY task:
- 1. Create todos at the START to break down your work
- 2. BEFORE starting a task: Mark it as "in_progress" using update_todo
- 3. AFTER completing a task: Mark it as "done" using mark_todo_done
- 4. When you discover new tasks: Add them as todos right away
+ 1. Keep the list short-horizon: track only the next few concrete steps (3-6 max), not long-term goals.
+ 2. Create/update todos as you learn new info; drop or rewrite items when plans change.
+ 3. BEFORE starting a task: Mark it as "in_progress" using update_todo.
+ 4. AFTER completing a task: Mark it as "done" using mark_todo_done.
+ 5. When you discover new tasks: Add them right away and re-prioritize; avoid giant upfront lists.
ALWAYS mark the current task as in_progress before working on it. This shows what you're
actively doing. Then mark it done when finished. Never skip these status updates.
@@ -107,94 +108,122 @@
- Update an existing todo item's title, description, priority, or status.
+ Update one or multiple todo items. Supports bulk updates in a single call.
-
- ID of the todo to update
+
+ ID of a single todo to update (for simple updates)
+
+
+ Bulk update multiple todos at once. JSON array of objects with todo_id and fields to update: [{"todo_id": "abc", "status": "done"}, {"todo_id": "def", "priority": "high"}]
- New title for the todo
+ New title (used with todo_id)
- New description for the todo
+ New description (used with todo_id)
- New priority: "low", "normal", "high", "critical"
+ New priority: "low", "normal", "high", "critical" (used with todo_id)
- New status: "pending", "in_progress", "done"
+ New status: "pending", "in_progress", "done" (used with todo_id)
- Response containing: - success: Whether the update was successful
+ Response containing: - updated: List of updated todo IDs - updated_count: Number updated - todos: Full sorted todo list - errors: Any failed updates
- # Update priority and add description
-
- abc123
- critical
- Found potential RCE vector, needs immediate attention
-
-
- # Mark as in progress
+ # Single update
abc123
in_progress
+
+
+ # Bulk update - mark multiple todos with different statuses in ONE call
+
+ [{"todo_id": "abc123", "status": "done"}, {"todo_id": "def456", "status": "in_progress"}, {"todo_id": "ghi789", "priority": "critical"}]
- Mark a todo item as completed. DO THIS IMMEDIATELY after finishing a task.
- Mark todos as done right after completing them - don't wait! This keeps your list accurate,
- helps track progress, and gives you a clear picture of what's been accomplished vs what remains.
+ Mark one or multiple todos as completed in a single call. DO THIS IMMEDIATELY after finishing tasks.
+ Mark todos as done right after completing them - don't wait! Supports marking multiple todos at once
+ to save tool calls. This keeps your list accurate and gives you a clear picture of progress.
-
- ID of the todo to mark as done
+
+ ID of a single todo to mark as done
+
+
+ Mark multiple todos done at once. JSON array of IDs: ["abc123", "def456"] or comma-separated: "abc123, def456"
- Response containing: - success: Whether the operation was successful
+ Response containing: - marked_done: List of IDs marked done - marked_count: Number marked - todos: Full sorted list - errors: Any failures
+ # Mark single todo done
abc123
+
+
+ # Mark multiple todos done in ONE call
+
+ ["abc123", "def456", "ghi789"]
- Mark a todo item as pending (reopen a completed task).
- Use this to reopen a task that was marked done but needs more work.
+ Mark one or multiple todos as pending (reopen completed tasks).
+ Use this to reopen tasks that were marked done but need more work. Supports bulk operations.
-
- ID of the todo to mark as pending
+
+ ID of a single todo to mark as pending
+
+
+ Mark multiple todos pending at once. JSON array of IDs: ["abc123", "def456"] or comma-separated: "abc123, def456"
- Response containing: - success: Whether the operation was successful
+ Response containing: - marked_pending: List of IDs marked pending - marked_count: Number marked - todos: Full sorted list - errors: Any failures
+ # Mark single todo pending
abc123
+
+
+ # Mark multiple todos pending in ONE call
+
+ ["abc123", "def456"]
- Delete a todo item.
- Use this to remove todos that are no longer relevant or were created by mistake.
+ Delete one or multiple todos in a single call.
+ Use this to remove todos that are no longer relevant. Supports bulk deletion to save tool calls.
-
- ID of the todo to delete
+
+ ID of a single todo to delete
+
+
+ Delete multiple todos at once. JSON array of IDs: ["abc123", "def456"] or comma-separated: "abc123, def456"
- Response containing: - success: Whether the deletion was successful
+ Response containing: - deleted: List of deleted IDs - deleted_count: Number deleted - todos: Remaining todos - errors: Any failures
+ # Delete single todo
abc123
+
+
+ # Delete multiple todos in ONE call
+
+ ["abc123", "def456", "ghi789"]