feat(agents): per-agent inference — momentum, overdue-task, recent-patterns, focus-area (ADR-0014 step 7)

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>
This commit is contained in:
2026-05-05 11:21:10 +00:00
parent ad6747c242
commit afb0e9b0cb
8 changed files with 383 additions and 26 deletions

View File

@@ -1,12 +1,32 @@
from __future__ import annotations
from typing import ClassVar
from .base import BaseAgent, AgentInput, AgentOutput
from .manifest import AgentManifest
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.0.0",
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",
@@ -25,6 +45,15 @@ MANIFEST = AgentManifest(
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,
),
],
)
@@ -35,7 +64,11 @@ class OverdueTaskAgent(BaseAgent):
version: ClassVar[str] = MANIFEST.version
def compute(self, inp: AgentInput) -> AgentOutput:
overdue = [t for t in inp.tasks if t.get("is_overdue")]
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:
@@ -59,6 +92,7 @@ class OverdueTaskAgent(BaseAgent):
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