Tighten todo + think tool contracts
Reject ambiguous calls in todo tools that previously combined the single-target params and the bulk-array param (e.g. create_todo with both `title` and `todos` would silently create N+1 items). Each tool now errors with a mode-specific hint pointing the model at the appropriate form. Also drop the meaningless char-count from `think`'s success message — the model already knows what it wrote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,10 +34,4 @@ async def think(thought: str) -> str:
|
||||
"""
|
||||
if not thought or not thought.strip():
|
||||
return json.dumps({"success": False, "message": "Thought cannot be empty"})
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": (f"Thought recorded successfully with {len(thought.strip())} characters"),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return json.dumps({"success": True, "message": "Thought recorded"})
|
||||
|
||||
@@ -325,10 +325,25 @@ async def create_todo(
|
||||
agent_id = _agent_id_from(ctx)
|
||||
try:
|
||||
default_priority = _normalize_priority(priority)
|
||||
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 todos is not None:
|
||||
if bulk_mode:
|
||||
tasks.extend(_normalize_bulk_todos(todos))
|
||||
if title and title.strip():
|
||||
elif title is not None and title.strip():
|
||||
tasks.append(
|
||||
{
|
||||
"title": title.strip(),
|
||||
@@ -473,10 +488,24 @@ async def update_todo(
|
||||
agent_id = _agent_id_from(ctx)
|
||||
try:
|
||||
agent_todos = _get_agent_todos(agent_id)
|
||||
single_mode = todo_id is not None and str(todo_id).strip() != ""
|
||||
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 updates is not None:
|
||||
if bulk_mode:
|
||||
updates_to_apply.extend(_normalize_bulk_updates(updates))
|
||||
if todo_id is not None:
|
||||
elif single_mode:
|
||||
updates_to_apply.append(
|
||||
{
|
||||
"todo_id": todo_id,
|
||||
@@ -534,10 +563,24 @@ def _mark(
|
||||
) -> str:
|
||||
try:
|
||||
agent_todos = _get_agent_todos(agent_id)
|
||||
single_mode = todo_id is not None and str(todo_id).strip() != ""
|
||||
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 todo_ids is not None:
|
||||
if bulk_mode:
|
||||
ids.extend(_normalize_todo_ids(todo_ids))
|
||||
if todo_id is not None:
|
||||
elif todo_id is not None and str(todo_id).strip():
|
||||
ids.append(todo_id)
|
||||
if not ids:
|
||||
msg = f"Provide todo_id or todo_ids to mark as {new_status}."
|
||||
@@ -581,10 +624,12 @@ async def mark_todo_done(
|
||||
) -> str:
|
||||
"""Mark one or many todos as done.
|
||||
|
||||
Pass either ``todo_id`` (single) or ``todo_ids`` (bulk), not both.
|
||||
|
||||
Args:
|
||||
todo_id: Single todo's ID.
|
||||
todo_ids: Bulk form — JSON array, comma-separated string, or
|
||||
single ID. Combinable with ``todo_id`` for one-off plus bulk.
|
||||
single ID.
|
||||
"""
|
||||
return _mark(
|
||||
agent_id=_agent_id_from(ctx),
|
||||
@@ -602,6 +647,8 @@ async def mark_todo_pending(
|
||||
) -> str:
|
||||
"""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.
|
||||
|
||||
Args:
|
||||
todo_id: Single todo's ID.
|
||||
todo_ids: Bulk form — JSON array, comma-separated, or single ID.
|
||||
@@ -622,6 +669,8 @@ async def delete_todo(
|
||||
) -> str:
|
||||
"""Delete one or many todos. Removes them entirely (no soft-delete).
|
||||
|
||||
Pass either ``todo_id`` (single) or ``todo_ids`` (bulk), not both.
|
||||
|
||||
Args:
|
||||
todo_id: Single todo's ID.
|
||||
todo_ids: Bulk form — JSON array, comma-separated, or single ID.
|
||||
@@ -629,10 +678,24 @@ async def delete_todo(
|
||||
agent_id = _agent_id_from(ctx)
|
||||
try:
|
||||
agent_todos = _get_agent_todos(agent_id)
|
||||
single_mode = todo_id is not None and str(todo_id).strip() != ""
|
||||
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 todo_ids is not None:
|
||||
if bulk_mode:
|
||||
ids.extend(_normalize_todo_ids(todo_ids))
|
||||
if todo_id is not None:
|
||||
elif todo_id is not None and str(todo_id).strip():
|
||||
ids.append(todo_id)
|
||||
if not ids:
|
||||
return json.dumps(
|
||||
|
||||
Reference in New Issue
Block a user