"""Built-in tool: manage scheduled recalls — cancel, skip, or list."""
from __future__ import annotations
from ._internal.base import Tool, ToolResult, current_session_id, current_user_id, current_user_role
class ManageRecallTool(Tool):
name = "manage_recall"
description = (
"Manage scheduled recalls for the current session. "
"Actions: cancel (remove pending), skip (defer recurring by one interval), list (show all). "
"Only one pending recall per session is allowed, so you often need cancel before creating a new one.\n\n"
"Actions explained:\n"
" cancel — deletes the pending recall. Use when the user no longer needs the callback, "
" or when you want to replace an existing recall with a new one.\n"
" skip — advances a recurring recall by interval_seconds. Use when the current scheduled check "
" is unnecessary (user already confirmed the build passed). Only works on recurring.\n"
" list — shows pending/fired/cancelled recalls for the session. Use before scheduling to verify "
" whether a recall already exists, or before telling the user they have no pending recalls.\n\n"
"Rules:\n"
" • Always call list before scheduling if you are unsure whether a recall already exists.\n"
" • The standard update pattern is: manage_recall(action=cancel) → schedule_recall(...).\n"
" • skip only works on recurring recalls; for one-time recalls use cancel to abort."
)
parameters = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["cancel", "skip", "list"],
"description": "What to do: cancel (remove pending), skip (defer recurring by one interval), list (show all).",
},
"session_id": {
"type": "string",
"description": "Optional target session ID. Defaults to current session if omitted.",
},
},
"required": ["action"],
}
def __init__(self, scheduler: "RecallScheduler" | None = None) -> None:
self._scheduler = scheduler
async def execute(self, params: dict) -> ToolResult:
from navi.core.scheduler import RecallScheduler
scheduler = self._scheduler
if scheduler is None:
return ToolResult(
success=False,
output="Scheduler not available.",
error="no scheduler",
)
action = (params.get("action") or "").strip().lower()
if action not in ("cancel", "skip", "list"):
return ToolResult(
success=False,
output=f"Unknown action: {action}. Use cancel, skip, or list.",
error="bad_action",
)
target_session = (params.get("session_id") or "").strip()
if not target_session:
target_session = current_session_id.get(None)
if not target_session:
return ToolResult(
success=False,
output="No session specified and no current session.",
error="missing session_id",
)
# cancel / skip need current-session context for ownership check
if action in ("cancel", "skip"):
current_sid = current_session_id.get(None)
if not current_sid:
return ToolResult(
success=False,
output="No current session context.",
error="missing session_id",
)
if action == "cancel":
ok = await scheduler.cancel_recall(current_sid)
if ok:
return ToolResult(success=True, output="Pending recall cancelled.")
return ToolResult(
success=False,
output="No pending recall found for this session.",
error="no_pending_recall",
)
else: # skip
ok = await scheduler.skip_next_recall(current_sid)
if ok:
return ToolResult(success=True, output="Next recurring occurrence skipped.")
return ToolResult(
success=False,
output="No recurring pending recall found for this session.",
error="no_recurring_recall",
)
# list
role = current_user_role.get()
is_admin = role == "admin"
user_id = current_user_id.get(None)
recalls = await scheduler.list_recalls(
session_id=target_session,
user_id=user_id if not is_admin else None,
is_admin=is_admin,
limit=50,
)
if not recalls:
return ToolResult(success=True, output="No recalls found.")
lines = [f"Recalls for session {target_session}:", ""]
for r in recalls:
lines.append(
f"- [{r.status.upper()}] {r.call_type} — trigger {r.trigger_at.isoformat()}"
)
if r.interval_seconds:
lines.append(f" interval: {r.interval_seconds}s")
if r.internal_comment:
lines.append(f" comment: {r.internal_comment}")
lines.append(f" context: {r.additional_context_message[:120]}...")
lines.append("")
return ToolResult(success=True, output="\n".join(lines))