Skip to content

proactive_agent

proactive_agent

Proactive Agent — runs on a cron (default 5am local) to autonomously handle routine tasks based on learned user behavior.

Lifecycle per run
  1. Load USER.md + MEMORY.md for behavioral context.
  2. Collect overnight data from connected sources via digest_collect.
  3. Use the LLM to classify each item and propose actions with a tier + permission key.
  4. For each proposed action:
  5. TRIVIAL tier → queue + immediately approve
  6. Known always_approve → queue + immediately approve
  7. Known always_deny → skip silently
  8. Everything else → queue as pending, notify user
  9. Execute all approved actions via execute_pending_actions.
  10. Send the user a concise summary: what was done + numbered list of what needs approval.

Approval reply format (user replies to the notification message): {action_id} yes approve one action {action_id} no deny one action always yes {action_id} approve + remember for this pattern always no {action_id} deny + remember for this pattern yes all / no all bulk decision

Wire up parse_approval_response from proactive_tools in your channel message handler to process replies without running the full agent.

Scheduling

The agent self-registers a 5am daily cron task when register_cron is called from your app startup:

from openjarvis.agents.proactive_agent import register_cron
register_cron(scheduler, notification_channel_id="your-channel-id")

Classes

ProactiveAgent

ProactiveAgent(*args: Any, **kwargs: Any)

Bases: ToolUsingAgent

Autonomous agent that handles routine tasks based on learned user behavior.

Source code in src/openjarvis/agents/proactive_agent.py
def __init__(self, *args: Any, **kwargs: Any) -> None:
    self._notification_channel_id: str = kwargs.pop("notification_channel_id", "")
    self._hours_back: int = kwargs.pop("hours_back", 24)
    self._approval_store: Optional[ApprovalStore] = kwargs.pop(
        "approval_store", None
    )
    self._timezone: str = kwargs.pop("timezone", "America/Los_Angeles")

    # Read config defaults before super().__init__ so we can inject tools
    try:
        cfg = load_config()
        p = cfg.proactive
        if not self._notification_channel_id:
            self._notification_channel_id = p.notification_channel
            self._hours_back = p.hours_back
            self._timezone = p.timezone
    except Exception:
        pass

    # Build the required tools and inject them into the executor.
    # This must happen before super().__init__ is called because
    # ToolUsingAgent builds the ToolExecutor from kwargs["tools"].
    store = self._approval_store or get_store()
    self._approval_store = store

    notification_channel = _build_notification_channel(
        self._notification_channel_id
    )
    self._notification_channel = notification_channel

    from openjarvis.tools.channel_tools import ChannelSendTool
    from openjarvis.tools.digest_collect import DigestCollectTool
    from openjarvis.tools.proactive_tools import (
        CheckPermissionTool,
        ExecutePendingActionsTool,
        GetPendingActionsTool,
        QueueActionTool,
        RecordDecisionTool,
    )

    proactive_tools = [
        DigestCollectTool(),
        ExecutePendingActionsTool(store=store),
        ChannelSendTool(channel=notification_channel),
        CheckPermissionTool(store=store),
        QueueActionTool(store=store),
        GetPendingActionsTool(store=store),
        RecordDecisionTool(store=store),
    ]

    # Merge with any tools passed by the caller
    caller_tools: List[Any] = kwargs.pop("tools", None) or []
    kwargs["tools"] = proactive_tools + caller_tools

    # The agent emits a JSON array of proposals — one entry per actionable
    # item — and a typical morning digest produces dozens. The default
    # max_tokens (often ~1024) truncates the array mid-element, which the
    # parser then rejects.  Give it real room unless the caller overrode.
    kwargs.setdefault("max_tokens", 8192)
    # Deterministic-ish output makes the JSON shape more reliable.
    kwargs.setdefault("temperature", 0.2)

    super().__init__(*args, **kwargs)

Functions

register_cron

register_cron(scheduler: Any, *, notification_channel_id: str = '', cron_expr: str = '', hours_back: int = 0, timezone: str = '') -> Any

Register the proactive agent as a daily cron task.

All defaults are read from config.toml [proactive] when not explicitly passed. Call this once from app startup after the scheduler is started.

PARAMETER DESCRIPTION
scheduler

A TaskScheduler instance.

TYPE: Any

notification_channel_id

Override the channel ID from config. If empty, uses notification_channel from [proactive] in config.toml.

TYPE: str DEFAULT: ''

cron_expr

Override the cron schedule. Defaults to config value ("0 5 * * *").

TYPE: str DEFAULT: ''

hours_back

Override hours of data to scan. Defaults to config value (24).

TYPE: int DEFAULT: 0

timezone

Override timezone string. Defaults to config value.

TYPE: str DEFAULT: ''

Source code in src/openjarvis/agents/proactive_agent.py
def register_cron(
    scheduler: Any,
    *,
    notification_channel_id: str = "",
    cron_expr: str = "",
    hours_back: int = 0,
    timezone: str = "",
) -> Any:
    """Register the proactive agent as a daily cron task.

    All defaults are read from ``config.toml [proactive]`` when not explicitly
    passed.  Call this once from app startup after the scheduler is started.

    Parameters
    ----------
    scheduler:
        A ``TaskScheduler`` instance.
    notification_channel_id:
        Override the channel ID from config.  If empty, uses ``notification_channel``
        from ``[proactive]`` in config.toml.
    cron_expr:
        Override the cron schedule.  Defaults to config value (``"0 5 * * *"``).
    hours_back:
        Override hours of data to scan.  Defaults to config value (24).
    timezone:
        Override timezone string.  Defaults to config value.
    """
    try:
        cfg = load_config()
        p = cfg.proactive
        notification_channel_id = notification_channel_id or p.notification_channel
        cron_expr = cron_expr or p.schedule
        hours_back = hours_back or p.hours_back
        timezone = timezone or p.timezone
    except Exception:
        cron_expr = cron_expr or "0 5 * * *"
        hours_back = hours_back or 24
        timezone = timezone or "America/Los_Angeles"

    return scheduler.create_task(
        prompt="Run the proactive agent: collect overnight data, execute approved actions, notify pending approvals.",
        schedule_type="cron",
        schedule_value=cron_expr,
        agent="proactive",
        context_mode="isolated",
        metadata={
            "notification_channel_id": notification_channel_id,
            "hours_back": hours_back,
            "timezone": timezone,
        },
    )