Newer
Older
navi-1 / navi / tools / manage_recall.py
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 15 May 5 KB Add self-recall (scheduled callback) system
"""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))