All four agents bumped to v1.1.0. momentum (#114): infers engagement_trend ('up'|'stable'|'down') by comparing done-rate in the last 7 days vs the prior 7 days. Agent surfaces the trend in its snippet ("trending up — build on the momentum"). overdue-task (#115): infers lateness_tolerance_days (0/1/2) from snooze rate. Agent now filters tasks against the tolerance so low-urgency users aren't nagged about tasks that are only hours overdue. recent-patterns (#116): infers window_days (7/14/30) from feedback event density — sparse users get a wider window so the snippet isn't always empty. focus-area (#113): no inferred params (project-level feedback linkage needed, tracked under #78). preferred_areas pref was declared but ignored; agent now honours it as a tiebreaker and mentions it in the snippet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
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)
|