File size: 1,718 Bytes
039dd77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"""ayllu.daemon — always-on-but-cheap listener, learned from the tribe soul-daemon.

The tribe's insight: a persona can be "always on & listening" without burning tokens,
because an idle tick does only a free file/inbox check and a model is invoked ONLY when
a real message arrives. a11oy keeps that insight and adds a hard bound: at most
`max_wakes` model-invoking wakes per run. The daemon itself never calls a model — it
tells the caller WHEN to (via ayllu.loop.run_turn), keeping cost and control explicit.
"""
from __future__ import annotations

from typing import Any


class Daemon:
    def __init__(self, persona, *, max_wakes: int = 8) -> None:
        self.persona = persona
        self.max_wakes = int(max_wakes)
        self.wakes = 0
        self.idle_ticks = 0

    def tick(self, inbox: list) -> dict[str, Any]:
        """One cheap tick. `inbox` is the already-read list of pending messages.

        Returns whether the persona should wake (and thus incur a model call). An
        empty inbox is FREE — no model, no cost.
        """
        if not inbox:
            self.idle_ticks += 1
            return {"woke": False, "cost": "free", "idle_ticks": self.idle_ticks}
        if self.wakes >= self.max_wakes:
            return {"woke": False, "reason": "max_wakes reached (bounded)",
                    "pending": len(inbox)}
        self.wakes += 1
        return {"woke": True, "message": inbox[0], "wakes": self.wakes,
                "pending_after": len(inbox) - 1}

    def stats(self) -> dict[str, Any]:
        return {"persona": getattr(self.persona, "name", None),
                "wakes": self.wakes, "idle_ticks": self.idle_ticks,
                "max_wakes": self.max_wakes}