from __future__ import annotations from typing import ClassVar from .base import BaseAgent, AgentInput, AgentOutput from .inference.history import UserHistory from .manifest import AgentManifest, InferredParam def _infer_lateness_tolerance(history: UserHistory) -> int: """Estimate how many days past due a task needs to be before the user acts. High snooze rate → user doesn't act immediately → raise tolerance so the agent doesn't nag them about tasks they'll handle in their own time. """ total = len(history.events) if total == 0: return 0 snooze_rate = sum(1 for e in history.events if e.action == "snooze") / total if snooze_rate > 0.40: return 2 if snooze_rate > 0.20: return 1 return 0 MANIFEST = AgentManifest( id="overdue-task", version="1.1.0", # bumped: lateness_tolerance_days InferredParam added (#115) description="Reports the user's overdue tasks by count and age.", pref_schema={ "type": "object", "additionalProperties": False, "properties": { "lateness_tolerance_days": { "type": "integer", "minimum": 0, "default": 0, "description": "Days past due before a task is considered overdue. 0 = the moment it's late.", }, }, }, context_schema=["todoist.tasks"], required_consents=["data:core", "data:todoist", "agent:overdue-task"], output_contract={"type": "snippet", "format": "free_text"}, ttl_sec=3600, silenced_in_contexts=["vacation"], inferred_params=[ InferredParam( key="lateness_tolerance_days", ttl_sec=86_400, # recompute daily — snooze pattern shifts slowly cold_start_default=0, min_history=10, infer=_infer_lateness_tolerance, ), ], ) class OverdueTaskAgent(BaseAgent): """Reports the user's overdue tasks by count and age.""" agent_id: ClassVar[str] = MANIFEST.id ttl_seconds: ClassVar[int] = MANIFEST.ttl_sec version: ClassVar[str] = MANIFEST.version def compute(self, inp: AgentInput) -> AgentOutput: tolerance = max(0, int(inp.agent_prefs.get("lateness_tolerance_days", 0))) overdue = [ t for t in inp.tasks if t.get("is_overdue") and t.get("task_age_days", 0) >= tolerance ] top = sorted(overdue, key=lambda t: -t.get("task_age_days", 0))[:3] if not overdue: prompt = "The user has no overdue tasks at this time." elif len(overdue) == 1: t = top[0] age = round(t.get("task_age_days", 0)) prompt = ( f'The user has 1 overdue task: "{t["content"]}" ' f"({age} day{'s' if age != 1 else ''} overdue)." ) else: items = ", ".join( f'"{t["content"]}" ({round(t.get("task_age_days", 0))}d)' for t in top ) prompt = ( f"The user has {len(overdue)} overdue tasks. " f"Top {len(top)}: {items}." ) snapshot = { "overdue_count": len(overdue), "lateness_tolerance_days": tolerance, "top_overdue": [ {"content": t["content"], "task_age_days": t.get("task_age_days", 0)} for t in top ], } return self._make_output(inp, prompt, snapshot)