Skip to content

06 · Proactive Decision-Making & Gifts ​

Sixth installment of the architecture deep-dive: the Decider local small model handles "should I proactively act?", the APScheduler scheduling system, report/reminder/schedule/todo pipelines, and the gift system. Corresponds to ARCHITECTURE.md §7.


§7.1 Why Train a Local Small Model (Decider) ​

RequirementCloud LLMLocal Decider
Latency200-2000ms<50ms
PrivacyData leaves the machineFully local
FrequencyOne API call per signalUnlimited frequency
CostToken-based billingZero marginal cost

Proactive decision-making (whether to remind, whether to report, whether to proactively fetch information) must be low-latency, local, and private. Relational prompt-based judgments are too expensive to run through a cloud LLM → train a small classification model to act as a gating mechanism.

§7.2 Proactive Decision Architecture ​

Event/Signal → Assistant.evaluate_signal()
    → DeciderService.shared().evaluate(agent_id, trigger_type, ...)
    → DecisionOutcome(fire: bool, action_category: str, act_score: float)
    ├─fire=False → Ignore
    └─fire=True → Assistant.send_proactive_message(...)
        → dialogue.tree.attach_proactive_node(content, metadata)
        → reminder_dispatch._emit(event) → inbox.jsonl → SSE
        → Also send native OS notification

7.2.1 evaluate_signal Parameters ​

python
def evaluate_signal(
    self,
    *,
    trigger_type: str,           # "new_message" / "external_info" / ...
    trigger_summary: str,         # Trigger summary (≤300 characters)
    trigger_details: Optional[dict] = None,
    memory: Optional[list[str]] = None,
    user_state: Optional[dict] = None,
    recent_user_messages: Optional[list[str]] = None,
):

7.2.2 send_proactive_message ​

python
def send_proactive_message(
    self,
    *,
    content: str,
    signal_summary: str,
    action_category: str,
    act_score: Optional[float] = None,
    decision_id: Optional[str] = None,
    dialogue_id: int = 0,
    title: Optional[str] = None,
    fired_at: Optional[float] = None,
    extra: Optional[dict] = None,   # e.g. {"gift": {...}} triggers rich rendering on the frontend
) -> Optional[int]:
    """Returns the new node id, or None (if there is no dialogue).
    
    metadata contains decision_id / signal_summary / action_category / act_score →
    frontend scoring UI ("right/wrong") POSTs to /api/feedback/proactive.
    """

7.2.3 DeciderService Internal Logic ​

python
# src/decider/service.py
class DeciderService:
    """Threshold / cooldown / crisis-keyword / DND logic.
    
    evaluate() returns a DecisionOutcome and never raises an exception.
    Worst case: fire=False + source="predict_error".
    """
    
    @classmethod
    def shared(cls) -> "DeciderService":
        """Process-level singleton."""

§7.3 APScheduler Scheduling System ​

7.3.1 Scheduler Types ​

python
# src/utils/daily_tools.py
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
Schedule TypePurposeTrigger
cronScheduled reports/cleanupCronTrigger
scheduleSchedule remindersOne-shot / recurring
todoTodo remindersBefore deadline
reminderGeneral reminders60s interval scan

7.3.2 Recurring Schedules (v13 recurrence) ​

python
_FREQ_VALID = {"never", "daily", "weekly", "monthly", "yearly"}

def start_of_cycle(dt: datetime, frequency: str) -> datetime:
    """Returns the start of the cycle that dt belongs to.
    
    daily   → Midnight today
    weekly  → Midnight of this ISO Monday
    monthly → Midnight of the 1st of this month
    yearly  → Midnight of January 1st this year
    never   → dt truncated to midnight
    """

ScheduleList.create_schedule seeds last_cycle_start, and DailyTools._run_recurrence_rollover enumerates occurrences.

7.3.3 Schedule State Machine ​

pending → active → completed
                  → cancelled
       → expired (deadline passed)

v8__schedules_status.sql adds the status column.

§7.4 Reminder Dispatch (src/utils/reminder_dispatch.py) ​

7.4.1 Architecture ​

python
def _inbox_path(workspace_route: str) -> Path:
    return Path(workspace_route) / "workspace" / "reminders" / "inbox.jsonl"

def _emit(workspace_route: str, event: dict) -> None:
    """Append a reminder event to the inbox JSONL. The bridge tails this file."""
    event = {**event, "ts": time.time()}
    inbox = _inbox_path(workspace_route)
    with inbox.open("a", encoding="utf-8") as f:
        f.write(json.dumps(event, ensure_ascii=False) + "\n")
    _maybe_native(event)

7.4.2 Reminders ≠ Chat Messages ​

DimensionReminderChat Message
Dialogue treeNot inserted (no Q/A node)Inserted
LLMNot consumedConsumed
HistoryNot added to ConversationSession.historyAdded
Frontend renderingReminderToast overlayChat bubble
SSE stream/api/reminders/stream/api/chat/stream

7.4.3 Native Notifications ​

python
def _maybe_native(event: dict) -> None:
    """Send a native OS notification for due reminders. Always fires.
    
    Reminders must reach the user regardless of whether the app window is visible.
    The inbox always carries the payload; the native window channel fires in a
    worker thread, so a slow PowerShell/osascript never blocks the DB scan.
    
    Double-trigger protection: skip the daemon toast when the renderer is
    already attached → prevents two identical OS notifications.
    """

7.4.4 Notification Deduplication ​

Uses client_presence's _active counter to determine whether the renderer is online:

  • Renderer online → renderer path handles it → daemon skips native toast
  • Renderer offline → daemon sends native toast

§7.5 DailyTools ​

python
# src/utils/daily_tools.py
class DailyTools:
    """Collection of schedule / todo / reminder / report tools.
    
    Integrates with APScheduler, providing:
    - create_schedule / update_schedule / list_schedules
    - create_todo / update_todo / list_todos
    - create_reminder / list_reminders
    - _run_recurrence_rollover()  # 60s interval scan for recurring schedules
    """

7.5.1 Report Generation (Deprecated) ​

python
# src/models/assistant.py
def write_report(self, type: str = "day", timestamp=None) -> str:
    """Deprecated: Per-agent daily/weekly/monthly/annual reports
    have been replaced by the manager_review skill.
    
    This stub is kept so that any residual import fails loudly
    rather than silently.
    """
    raise RuntimeError(
        "Assistant.write_report is deprecated — use the manager_review "
        "skill (triggered per-agent by dialogue-turn threshold)."
    )

7.5.2 manager_review Skill ​

workspace/manager/skills/manager_review/SKILL.md

Triggered by per-agent dialogue-turn thresholds, replacing the old cron-based reports.

§7.6 Gift Pipeline ​

7.6.1 Architecture ​

User sends a gift → /api/gift/... → gift.py route handles it
    → Redemption code verification
    → Token/coin transfer
    → Guild settlement

7.6.2 Gift-Triggered Proactive Messages ​

python
# The extra parameter of Assistant.send_proactive_message
extra = {"gift": {...}}  # The frontend keys off this field to switch to a rich-rendering bubble

The extra dict from send_proactive_message is merged into metadata; the frontend renderer checks for the presence of metadata.gift → switches to a custom chat bubble.

§7.7 Decider VRAM Management ​

The Decider model's position in the VRAM gate:

ConditionPriorityCategory
mode0 (no GPU)N/AUses cloud LLM 2-call hack
tier4 (~4GB)3 — NICEqueue, triggered when GPU is idle
tier6 (~6GB)3 — NICEqueue, bf16
tier8 (≥8GB)2 — IMPORTANTsync, always resident

Next: 07-Digital Human →

Familiars · 多 Agent AI 桌面应用