Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lingtai/kernel/ANATOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ The kernel root holds the coordinator (`base_agent/`) plus a flat collection of

- `base_agent/` — `BaseAgent`, the kernel coordinator (package of 7 modules). `base_agent/__init__.py` defines `BaseAgent` (~2170 lines: constructor, properties, state machine, hooks, cross-cutting stubs including the `.notification/` sync trio, pass-throughs to submodules). Submodules: `lifecycle.py` (start/stop/heartbeat/signals/refresh — heartbeat tick now also applies the hidden fixed 24h IDLE→ASLEEP timeout before `_sync_notifications`; terminal refresh relaunch failure writes a bounded `logs/refresh_failed_permanent.json` artifact and a high-priority `.notification/system.json` event), `turn.py` (main loop/message dispatch/AED/response processing), `worker_recovery.py` (WorkerStillRunning poison guard, unfinished-turn artifacts, refresh recovery), `tools.py` (tool schemas/dispatch/registry), `identity.py` (naming/manifest/status), `prompt.py` (system prompt building/flushing), `messaging.py` (mail/notification producers/outbound). Soul-flow domain logic lives in `lingtai/tools/soul/flow.py`. See `base_agent/ANATOMY.md`.
- `maintenance/` — explicit, operator-run maintenance reporters. `retention.py` implements the dry-run-only retention report for stale terminal daemon run dirs, old sent-mail copies, opt-in archive mail, rebuildable `logs/log.sqlite`, and read-only high-footprint observations for portal replay, agent logs, and history; it never deletes or rewrites files. See `maintenance/ANATOMY.md`.
- `nudge/` — **per-agent periodic checks** that publish notification reminders when something needs the agent's attention. `__init__.py:run_checks(agent)` is called once per heartbeat tick from `base_agent/lifecycle.py:_heartbeat_loop` (~line 320, wrapped in try/except). Each check is a self-contained module under `nudge/` exposing `check(agent) -> None`. `kernel_version.py` uses the shared `.notification/nudge.json` multi-entry payload for packaged runtime refresh/package-update reminders; `source_drift.py` compares startup and on-disk runtime fingerprints for non-dev runtimes and skips editable/source/dev checkouts; `goal.py` is an IDLE-only check that reads protected `.notification/goal.json`; if the file exists, is active, and the idle delay has elapsed, it publishes one short `goal.reminder` event into `.notification/system.json` pointing back to `goal.json` and the goal manual. See `nudge/ANATOMY.md` for details and the "add a new nudge" recipe.
- `nudge/` — **per-agent periodic checks** that publish notification reminders when something needs the agent's attention. `__init__.py:run_checks(agent)` is called once per heartbeat tick from `base_agent/lifecycle.py:_heartbeat_loop` (~line 320, wrapped in try/except). Each check is a self-contained module under `nudge/` exposing `check(agent) -> None`. `kernel_version.py` uses the shared `.notification/nudge.json` multi-entry payload for packaged runtime refresh/package-update reminders; `source_drift.py` compares startup and on-disk runtime fingerprints for non-dev runtimes and skips editable/source/dev checkouts; `goal.py` is an IDLE-only check that reads protected `.notification/goal.json` on an in-memory 10-second cadence; if the file exists, is active, and the idle delay has elapsed, it publishes one short `goal.reminder` event into `.notification/system.json` pointing back to `goal.json` and the goal manual. See `nudge/ANATOMY.md` for details and the "add a new nudge" recipe.
- `event_journal/` — Core-owned outbound Port for append-only structured events plus authoritative `JournalPosition` provenance. `BaseAgent` receives this Port; POSIX JSONL/SQLite composition lives outside Core. See `event_journal/ANATOMY.md` (`event_journal/__init__.py:9-26`).
- `mail_transport/` — Core-owned outbound Port for fire-and-forget agent messaging (`MailTransportPort`: `send`/`listen`/`stop`/`address`). `BaseAgent` receives this Port as `mail_service`; the concrete POSIX filesystem mailbox (`PosixFilesystemMailAdapter`) lives outside Core and is wired by the CLI. See `mail_transport/ANATOMY.md` and `mail_transport/CONTRACT.md` (`mail_transport/__init__.py:16-62`).
- `workdir_lease/` — Core-owned outbound Port for the exclusive working-directory lease (`WorkdirLeasePort`: `acquire`/`release`). `BaseAgent` receives this Port as a **required** `workdir_lease` (no unlocked/no-op fallback), acquires it once with a 10s grace at construction, and releases it at teardown; the SQLite rebuild receives one too. The concrete POSIX `flock` adapter (`PosixWorkdirLeaseAdapter`) and the fail-loud platform selector live outside Core and are wired by the wrapper/CLI. Read-only lock observers (maintenance/doctor) inspect lock state without acquiring this lease. See `workdir_lease/ANATOMY.md` and `workdir_lease/CONTRACT.md` (`workdir_lease/__init__.py:15-46`).
Expand Down
10 changes: 5 additions & 5 deletions src/lingtai/kernel/nudge/ANATOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ bad check never breaks the loop). It dispatches to each check's
development checkouts are skipped so agents are not nudged into arbitrary
in-flight source changes.
- `goal.py` — IDLE-only goal reminder check. It reads the allowlisted protected
`.notification/goal.json`; if and only if that file exists, is active, and the
idle delay has elapsed, it publishes one short `goal.reminder` event into
`.notification/system.json` saying to read `goal.json` and the goal manual.
It dedupes an existing reminder with the same `ref_id` and waits another delay
after that reminder is dismissed.
`.notification/goal.json` on an in-memory 10-second cadence; if and only if
that file exists, is active, and the idle delay has elapsed, it publishes one
short `goal.reminder` event into `.notification/system.json` saying to read
`goal.json` and the goal manual. It dedupes an existing reminder with the same
`ref_id` and waits another delay after that reminder is dismissed.
- `ANATOMY.md` — this file.

## The shared channel
Expand Down
7 changes: 7 additions & 0 deletions src/lingtai/kernel/nudge/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@


_DEFAULT_DELAY_SECONDS = 120.0
_CHECK_INTERVAL_SECONDS = 10.0
_REMINDER_BODY = "Goal reminder: read .notification/goal.json and follow its instructions; see the goal manual under system-manual."


Expand All @@ -26,6 +27,12 @@ def check(agent) -> None:
if getattr(agent, "_state", None) != AgentState.IDLE:
return

check_now = time.monotonic()
last_check = float(getattr(agent, "_goal_reminder_last_check_at", 0.0) or 0.0)
if last_check and check_now - last_check < _CHECK_INTERVAL_SECONDS:
return
agent._goal_reminder_last_check_at = check_now

store = agent._notification_store
allow = _get_allow_predicate()
notifications = store.snapshot(allow)
Expand Down
64 changes: 61 additions & 3 deletions tests/test_goal_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ def _enqueue_system_notification(
)


@dataclass
class _Clock:
now: float = 100.0

def advance(self, seconds: float) -> None:
self.now += seconds


def test_goal_reminder_publishes_short_system_event_after_idle_delay(tmp_path: Path) -> None:
agent = _GoalAgent(tmp_path)
publish_test_payload(
Expand Down Expand Up @@ -87,6 +95,35 @@ def test_goal_reminder_does_not_duplicate_existing_event(tmp_path: Path) -> None
assert len(snapshot_notifications(tmp_path)["system"]["data"]["events"]) == 1


def test_goal_reminder_scans_disk_at_most_once_per_cadence(tmp_path: Path, monkeypatch) -> None:
from lingtai.kernel.nudge import goal as goal_mod

agent = _GoalAgent(tmp_path)
clock = _Clock()
snapshots = 0
real_snapshot = agent._notification_store.snapshot

def counted_snapshot(allow):
nonlocal snapshots
snapshots += 1
return real_snapshot(allow)

monkeypatch.setattr(goal_mod.time, "monotonic", lambda: clock.now)
monkeypatch.setattr(agent._notification_store, "snapshot", counted_snapshot)

check_goal(agent)
assert snapshots == 1

for _ in range(9):
clock.advance(1)
check_goal(agent)
assert snapshots == 1

clock.advance(1)
check_goal(agent)
assert snapshots == 2


def test_goal_reminder_skips_completed_goal(tmp_path: Path) -> None:
agent = _GoalAgent(tmp_path)
publish_test_payload(tmp_path, "goal", {"data": {"id": "demo", "status": "done", "reminder_delay_seconds": 1}})
Expand All @@ -103,8 +140,12 @@ def test_goal_reminder_requires_goal_json(tmp_path: Path) -> None:
assert not (tmp_path / ".notification" / "system.json").exists()


def test_goal_reminder_republishes_after_whole_system_dismiss_and_fresh_delay(tmp_path: Path) -> None:
def test_goal_reminder_republishes_after_whole_system_dismiss_and_fresh_delay(tmp_path: Path, monkeypatch) -> None:
from lingtai.kernel.nudge import goal as goal_mod

agent = _GoalAgent(tmp_path)
clock = _Clock()
monkeypatch.setattr(goal_mod.time, "monotonic", lambda: clock.now)
publish_test_payload(tmp_path, "goal", {"data": {"id": "demo", "status": "active", "reminder_delay_seconds": 1}})
check_goal(agent)
assert "system" in snapshot_notifications(tmp_path)
Expand All @@ -119,29 +160,46 @@ def test_goal_reminder_republishes_after_whole_system_dismiss_and_fresh_delay(tm
assert "system" not in snapshot_notifications(tmp_path)

agent._goal_reminder_last_dismissed_at = time.time() - 2
clock.advance(10)
check_goal(agent)
assert snapshot_notifications(tmp_path)["system"]["data"]["events"][0]["ref_id"] == "goal:demo"


def test_goal_reminder_clears_when_goal_becomes_done(tmp_path: Path) -> None:
def test_goal_reminder_clears_when_goal_becomes_done(tmp_path: Path, monkeypatch) -> None:
from lingtai.kernel.nudge import goal as goal_mod

agent = _GoalAgent(tmp_path)
clock = _Clock()
monkeypatch.setattr(goal_mod.time, "monotonic", lambda: clock.now)
publish_test_payload(tmp_path, "goal", {"data": {"id": "demo", "status": "active", "reminder_delay_seconds": 1}})
check_goal(agent)
assert "system" in snapshot_notifications(tmp_path)

publish_test_payload(tmp_path, "goal", {"data": {"id": "demo", "status": "done", "reminder_delay_seconds": 1}})
clock.advance(10)
check_goal(agent)

assert "system" not in snapshot_notifications(tmp_path)


def test_goal_reminder_clears_when_goal_json_is_deleted(tmp_path: Path) -> None:
def test_goal_reminder_clears_when_goal_json_is_deleted(tmp_path: Path, monkeypatch) -> None:
from lingtai.kernel.nudge import goal as goal_mod

agent = _GoalAgent(tmp_path)
clock = _Clock()
monkeypatch.setattr(goal_mod.time, "monotonic", lambda: clock.now)
publish_test_payload(tmp_path, "goal", {"data": {"id": "demo", "status": "active", "reminder_delay_seconds": 1}})
check_goal(agent)
assert "system" in snapshot_notifications(tmp_path)

assert agent._notification_store.clear("goal") is True
clock.advance(10)
check_goal(agent)

assert "system" not in snapshot_notifications(tmp_path)

publish_test_payload(tmp_path, "goal", {"data": {"id": "replacement", "status": "active", "reminder_delay_seconds": 1}})
clock.advance(10)
check_goal(agent)

assert snapshot_notifications(tmp_path)["system"]["data"]["events"][0]["ref_id"] == "goal:replacement"