mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
Collapse todo tools to a single list-based form
create_todo / update_todo / mark_todo_done / mark_todo_pending / delete_todo used to accept either a single-item form (title, todo_id, …) or a bulk form (todos, updates, todo_ids), reject the call if the agent set both, and explain the rule in the docstring. The agent kept tripping the validator. Drop the single-item form everywhere — each tool now takes one list arg. Single calls just pass a one-item list. While the API was being reshaped, line the result schemas up: created_count replaces the lone "count", _mark returns a single "marked" key plus new_status instead of marked_done / marked_pending, and list_todos splits the overloaded total_count into filtered_count (matches) and total_count (grand total) so a filtered call no longer hides the real size. Docstrings now spell out each item's fields with required/optional and the legal status / priority values, plus a worked example. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d3ab3f836b
commit
763df86f17
+71
-184
@@ -288,15 +288,11 @@ def _apply_single_update(
|
|||||||
|
|
||||||
|
|
||||||
@function_tool(timeout=30)
|
@function_tool(timeout=30)
|
||||||
async def create_todo(
|
async def create_todo(ctx: RunContextWrapper, todos: str) -> str:
|
||||||
ctx: RunContextWrapper,
|
|
||||||
title: str | None = None,
|
|
||||||
description: str | None = None,
|
|
||||||
priority: str = "normal",
|
|
||||||
todos: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Create one or many todos for the current agent.
|
"""Create one or many todos for the current agent.
|
||||||
|
|
||||||
|
Always pass a list, even for a single todo (wrap it in a one-item array).
|
||||||
|
|
||||||
Each agent (including subagents) has its **own private todo list** —
|
Each agent (including subagents) has its **own private todo list** —
|
||||||
your todos don't leak to other agents and vice versa.
|
your todos don't leak to other agents and vice versa.
|
||||||
|
|
||||||
@@ -311,53 +307,27 @@ async def create_todo(
|
|||||||
- Simple linear workflows where progress is obvious.
|
- Simple linear workflows where progress is obvious.
|
||||||
- Single quick task — just do it.
|
- Single quick task — just do it.
|
||||||
|
|
||||||
Batch related todos in one call via the ``todos`` bulk parameter
|
|
||||||
rather than firing many ``create_todo`` calls.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title: Short, actionable title (e.g., "Test /api/admin for IDOR").
|
todos: JSON array of todo objects. For one todo, pass a one-item
|
||||||
description: Optional details / context for the single todo.
|
list. Each object's fields:
|
||||||
priority: ``"low"`` / ``"normal"`` / ``"high"`` / ``"critical"``.
|
|
||||||
todos: Bulk create — either JSON array of
|
- ``title`` (str, **required**): short actionable title,
|
||||||
``{"title": "...", "description": "...", "priority": "..."}``
|
e.g. ``"Test /api/admin for IDOR"``.
|
||||||
objects, or a newline-separated bullet list (``- item\\n- item``).
|
- ``description`` (str, optional): extra context or
|
||||||
|
acceptance criteria.
|
||||||
|
- ``priority`` (str, optional): one of ``"low"`` /
|
||||||
|
``"normal"`` / ``"high"`` / ``"critical"``. Defaults to
|
||||||
|
``"normal"``.
|
||||||
|
|
||||||
|
Example: ``[{"title": "Probe /admin", "priority": "high"},
|
||||||
|
{"title": "Check JWT alg=none"}]``.
|
||||||
"""
|
"""
|
||||||
agent_id = _agent_id_from(ctx)
|
agent_id = _agent_id_from(ctx)
|
||||||
try:
|
try:
|
||||||
default_priority = _normalize_priority(priority)
|
tasks = _normalize_bulk_todos(todos)
|
||||||
single_mode = bool(title and title.strip())
|
|
||||||
bulk_mode = todos is not None and str(todos).strip() != ""
|
|
||||||
if single_mode and bulk_mode:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"error": (
|
|
||||||
"Pass either `title` (single create) or `todos` (bulk create), "
|
|
||||||
"not both. To batch related todos, use `todos` only."
|
|
||||||
),
|
|
||||||
"todo_id": None,
|
|
||||||
},
|
|
||||||
ensure_ascii=False,
|
|
||||||
default=str,
|
|
||||||
)
|
|
||||||
tasks: list[dict[str, Any]] = []
|
|
||||||
if bulk_mode:
|
|
||||||
tasks.extend(_normalize_bulk_todos(todos))
|
|
||||||
elif title is not None and title.strip():
|
|
||||||
tasks.append(
|
|
||||||
{
|
|
||||||
"title": title.strip(),
|
|
||||||
"description": description.strip() if description else None,
|
|
||||||
"priority": default_priority,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not tasks:
|
if not tasks:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": False, "error": "Provide a non-empty 'todos' list to create."},
|
||||||
"success": False,
|
|
||||||
"error": "Provide a title or 'todos' list to create.",
|
|
||||||
"todo_id": None,
|
|
||||||
},
|
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
default=str,
|
default=str,
|
||||||
)
|
)
|
||||||
@@ -365,7 +335,7 @@ async def create_todo(
|
|||||||
agent_todos = _get_agent_todos(agent_id)
|
agent_todos = _get_agent_todos(agent_id)
|
||||||
created: list[dict[str, Any]] = []
|
created: list[dict[str, Any]] = []
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
task_priority = _normalize_priority(task.get("priority"), default_priority)
|
task_priority = _normalize_priority(task.get("priority"))
|
||||||
todo_id = str(uuid.uuid4())[:6]
|
todo_id = str(uuid.uuid4())[:6]
|
||||||
timestamp = datetime.now(UTC).isoformat()
|
timestamp = datetime.now(UTC).isoformat()
|
||||||
agent_todos[todo_id] = {
|
agent_todos[todo_id] = {
|
||||||
@@ -380,7 +350,7 @@ async def create_todo(
|
|||||||
created.append({"todo_id": todo_id, "title": task["title"], "priority": task_priority})
|
created.append({"todo_id": todo_id, "title": task["title"], "priority": task_priority})
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{"success": False, "error": f"Failed to create todo: {e}", "todo_id": None},
|
{"success": False, "error": f"Failed to create todo: {e}"},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
default=str,
|
default=str,
|
||||||
)
|
)
|
||||||
@@ -390,7 +360,7 @@ async def create_todo(
|
|||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"created": created,
|
"created": created,
|
||||||
"count": len(created),
|
"created_count": len(created),
|
||||||
"todos": _sorted_todos(agent_id),
|
"todos": _sorted_todos(agent_id),
|
||||||
"total_count": len(_get_agent_todos(agent_id)),
|
"total_count": len(_get_agent_todos(agent_id)),
|
||||||
},
|
},
|
||||||
@@ -443,6 +413,7 @@ async def list_todos(
|
|||||||
"success": False,
|
"success": False,
|
||||||
"error": f"Failed to list todos: {e}",
|
"error": f"Failed to list todos: {e}",
|
||||||
"todos": [],
|
"todos": [],
|
||||||
|
"filtered_count": 0,
|
||||||
"total_count": 0,
|
"total_count": 0,
|
||||||
"summary": {"pending": 0, "in_progress": 0, "done": 0},
|
"summary": {"pending": 0, "in_progress": 0, "done": 0},
|
||||||
},
|
},
|
||||||
@@ -454,7 +425,8 @@ async def list_todos(
|
|||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"todos": todos_list,
|
"todos": todos_list,
|
||||||
"total_count": len(todos_list),
|
"filtered_count": len(todos_list),
|
||||||
|
"total_count": len(agent_todos),
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -463,61 +435,41 @@ async def list_todos(
|
|||||||
|
|
||||||
|
|
||||||
@function_tool(timeout=30)
|
@function_tool(timeout=30)
|
||||||
async def update_todo(
|
async def update_todo(ctx: RunContextWrapper, updates: str) -> str:
|
||||||
ctx: RunContextWrapper,
|
"""Update one or many todos.
|
||||||
todo_id: str | None = None,
|
|
||||||
title: str | None = None,
|
|
||||||
description: str | None = None,
|
|
||||||
priority: str | None = None,
|
|
||||||
status: str | None = None,
|
|
||||||
updates: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Update one or many todos. Prefer the bulk form for multiple updates.
|
|
||||||
|
|
||||||
For toggling status only, use the dedicated ``mark_todo_done`` /
|
Always pass a list, even for a single update (wrap it in a one-item
|
||||||
``mark_todo_pending`` tools — they're simpler and accept bulk
|
array).
|
||||||
``todo_ids``.
|
|
||||||
|
For toggling status only, prefer the dedicated ``mark_todo_done`` /
|
||||||
|
``mark_todo_pending`` tools — they're simpler and accept the same
|
||||||
|
list-of-ids form.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
todo_id: Single-todo target.
|
updates: JSON array of update objects. For one update, pass a
|
||||||
title / description / priority / status: New values for the
|
one-item list. Each object's fields:
|
||||||
single todo. Omit to leave unchanged.
|
|
||||||
updates: Bulk form — JSON array like
|
- ``todo_id`` (str, **required**): ID returned by
|
||||||
``[{"todo_id": "abc", "status": "done"}, ...]``.
|
``create_todo``.
|
||||||
|
- ``title`` (str, optional): new title.
|
||||||
|
- ``description`` (str, optional): new description (empty
|
||||||
|
string clears it).
|
||||||
|
- ``priority`` (str, optional): one of ``"low"`` /
|
||||||
|
``"normal"`` / ``"high"`` / ``"critical"``.
|
||||||
|
- ``status`` (str, optional): one of ``"pending"`` /
|
||||||
|
``"in_progress"`` / ``"done"``.
|
||||||
|
|
||||||
|
Omitted fields stay unchanged. Example:
|
||||||
|
``[{"todo_id": "abc", "status": "in_progress",
|
||||||
|
"priority": "high"}]``.
|
||||||
"""
|
"""
|
||||||
agent_id = _agent_id_from(ctx)
|
agent_id = _agent_id_from(ctx)
|
||||||
try:
|
try:
|
||||||
agent_todos = _get_agent_todos(agent_id)
|
agent_todos = _get_agent_todos(agent_id)
|
||||||
single_mode = todo_id is not None and str(todo_id).strip() != ""
|
updates_to_apply = _normalize_bulk_updates(updates)
|
||||||
bulk_mode = updates is not None and str(updates).strip() != ""
|
|
||||||
if single_mode and bulk_mode:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"error": (
|
|
||||||
"Pass either `todo_id` (single update) or `updates` (bulk update), "
|
|
||||||
"not both. To batch updates, use `updates` only."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
ensure_ascii=False,
|
|
||||||
default=str,
|
|
||||||
)
|
|
||||||
updates_to_apply: list[dict[str, Any]] = []
|
|
||||||
if bulk_mode:
|
|
||||||
updates_to_apply.extend(_normalize_bulk_updates(updates))
|
|
||||||
elif single_mode:
|
|
||||||
updates_to_apply.append(
|
|
||||||
{
|
|
||||||
"todo_id": todo_id,
|
|
||||||
"title": title,
|
|
||||||
"description": description,
|
|
||||||
"priority": priority,
|
|
||||||
"status": status,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not updates_to_apply:
|
if not updates_to_apply:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{"success": False, "error": "Provide todo_id or 'updates' list to update."},
|
{"success": False, "error": "Provide a non-empty 'updates' list."},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
default=str,
|
default=str,
|
||||||
)
|
)
|
||||||
@@ -554,36 +506,12 @@ async def update_todo(
|
|||||||
return json.dumps(response, ensure_ascii=False, default=str)
|
return json.dumps(response, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
def _mark(
|
def _mark(*, agent_id: str, todo_ids: str, new_status: str) -> str:
|
||||||
*,
|
|
||||||
agent_id: str,
|
|
||||||
todo_id: str | None,
|
|
||||||
todo_ids: str | None,
|
|
||||||
new_status: str,
|
|
||||||
) -> str:
|
|
||||||
try:
|
try:
|
||||||
agent_todos = _get_agent_todos(agent_id)
|
agent_todos = _get_agent_todos(agent_id)
|
||||||
single_mode = todo_id is not None and str(todo_id).strip() != ""
|
ids = _normalize_todo_ids(todo_ids)
|
||||||
bulk_mode = todo_ids is not None and str(todo_ids).strip() != ""
|
|
||||||
if single_mode and bulk_mode:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"error": (
|
|
||||||
"Pass either `todo_id` (single) or `todo_ids` (bulk), not both. "
|
|
||||||
"To batch, use `todo_ids` only."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
ensure_ascii=False,
|
|
||||||
default=str,
|
|
||||||
)
|
|
||||||
ids: list[str] = []
|
|
||||||
if bulk_mode:
|
|
||||||
ids.extend(_normalize_todo_ids(todo_ids))
|
|
||||||
elif todo_id is not None and str(todo_id).strip():
|
|
||||||
ids.append(todo_id)
|
|
||||||
if not ids:
|
if not ids:
|
||||||
msg = f"Provide todo_id or todo_ids to mark as {new_status}."
|
msg = f"Provide a non-empty 'todo_ids' list to mark as {new_status}."
|
||||||
return json.dumps({"success": False, "error": msg}, ensure_ascii=False, default=str)
|
return json.dumps({"success": False, "error": msg}, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
marked: list[str] = []
|
marked: list[str] = []
|
||||||
@@ -603,11 +531,11 @@ def _mark(
|
|||||||
|
|
||||||
if marked:
|
if marked:
|
||||||
_persist()
|
_persist()
|
||||||
key = "marked_done" if new_status == "done" else "marked_pending"
|
|
||||||
response: dict[str, Any] = {
|
response: dict[str, Any] = {
|
||||||
"success": len(errors) == 0,
|
"success": len(errors) == 0,
|
||||||
key: marked,
|
"marked": marked,
|
||||||
"marked_count": len(marked),
|
"marked_count": len(marked),
|
||||||
|
"new_status": new_status,
|
||||||
"todos": _sorted_todos(agent_id),
|
"todos": _sorted_todos(agent_id),
|
||||||
"total_count": len(agent_todos),
|
"total_count": len(agent_todos),
|
||||||
}
|
}
|
||||||
@@ -617,89 +545,48 @@ def _mark(
|
|||||||
|
|
||||||
|
|
||||||
@function_tool(timeout=30)
|
@function_tool(timeout=30)
|
||||||
async def mark_todo_done(
|
async def mark_todo_done(ctx: RunContextWrapper, todo_ids: str) -> str:
|
||||||
ctx: RunContextWrapper,
|
|
||||||
todo_id: str | None = None,
|
|
||||||
todo_ids: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Mark one or many todos as done.
|
"""Mark one or many todos as done.
|
||||||
|
|
||||||
Pass either ``todo_id`` (single) or ``todo_ids`` (bulk), not both.
|
Always pass a list, even for a single ID (wrap it in a one-item array).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
todo_id: Single todo's ID.
|
todo_ids: JSON array of todo IDs to mark done. For one todo,
|
||||||
todo_ids: Bulk form — JSON array, comma-separated string, or
|
pass a one-item list.
|
||||||
single ID.
|
|
||||||
"""
|
"""
|
||||||
return _mark(
|
return _mark(agent_id=_agent_id_from(ctx), todo_ids=todo_ids, new_status="done")
|
||||||
agent_id=_agent_id_from(ctx),
|
|
||||||
todo_id=todo_id,
|
|
||||||
todo_ids=todo_ids,
|
|
||||||
new_status="done",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@function_tool(timeout=30)
|
@function_tool(timeout=30)
|
||||||
async def mark_todo_pending(
|
async def mark_todo_pending(ctx: RunContextWrapper, todo_ids: str) -> str:
|
||||||
ctx: RunContextWrapper,
|
|
||||||
todo_id: str | None = None,
|
|
||||||
todo_ids: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Reset one or many todos to pending (e.g., to retry a failed task).
|
"""Reset one or many todos to pending (e.g., to retry a failed task).
|
||||||
|
|
||||||
Pass either ``todo_id`` (single) or ``todo_ids`` (bulk), not both.
|
Always pass a list, even for a single ID (wrap it in a one-item array).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
todo_id: Single todo's ID.
|
todo_ids: JSON array of todo IDs to reset to pending. For one
|
||||||
todo_ids: Bulk form — JSON array, comma-separated, or single ID.
|
todo, pass a one-item list.
|
||||||
"""
|
"""
|
||||||
return _mark(
|
return _mark(agent_id=_agent_id_from(ctx), todo_ids=todo_ids, new_status="pending")
|
||||||
agent_id=_agent_id_from(ctx),
|
|
||||||
todo_id=todo_id,
|
|
||||||
todo_ids=todo_ids,
|
|
||||||
new_status="pending",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@function_tool(timeout=30)
|
@function_tool(timeout=30)
|
||||||
async def delete_todo(
|
async def delete_todo(ctx: RunContextWrapper, todo_ids: str) -> str:
|
||||||
ctx: RunContextWrapper,
|
|
||||||
todo_id: str | None = None,
|
|
||||||
todo_ids: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Delete one or many todos. Removes them entirely (no soft-delete).
|
"""Delete one or many todos. Removes them entirely (no soft-delete).
|
||||||
|
|
||||||
Pass either ``todo_id`` (single) or ``todo_ids`` (bulk), not both.
|
Always pass a list, even for a single ID (wrap it in a one-item array).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
todo_id: Single todo's ID.
|
todo_ids: JSON array of todo IDs to delete. For one todo, pass
|
||||||
todo_ids: Bulk form — JSON array, comma-separated, or single ID.
|
a one-item list.
|
||||||
"""
|
"""
|
||||||
agent_id = _agent_id_from(ctx)
|
agent_id = _agent_id_from(ctx)
|
||||||
try:
|
try:
|
||||||
agent_todos = _get_agent_todos(agent_id)
|
agent_todos = _get_agent_todos(agent_id)
|
||||||
single_mode = todo_id is not None and str(todo_id).strip() != ""
|
ids = _normalize_todo_ids(todo_ids)
|
||||||
bulk_mode = todo_ids is not None and str(todo_ids).strip() != ""
|
|
||||||
if single_mode and bulk_mode:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"error": (
|
|
||||||
"Pass either `todo_id` (single) or `todo_ids` (bulk), not both. "
|
|
||||||
"To batch, use `todo_ids` only."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
ensure_ascii=False,
|
|
||||||
default=str,
|
|
||||||
)
|
|
||||||
ids: list[str] = []
|
|
||||||
if bulk_mode:
|
|
||||||
ids.extend(_normalize_todo_ids(todo_ids))
|
|
||||||
elif todo_id is not None and str(todo_id).strip():
|
|
||||||
ids.append(todo_id)
|
|
||||||
if not ids:
|
if not ids:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{"success": False, "error": "Provide todo_id or todo_ids to delete."},
|
{"success": False, "error": "Provide a non-empty 'todo_ids' list to delete."},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
default=str,
|
default=str,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user