Skip to content
Merged
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
1 change: 1 addition & 0 deletions SEAMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ surface: `graph/plugins/registry.py` in the protoAgent repo.
| `register_surface` | `nudge.py` | Lifecycle-managed due-card checker; inert at `nudge_interval_hours: 0` — the pull path (`review_next`) always works. |
| `register_skill_dir` | `skills/learning-tutor/` | The tutor *policy* lives in a skill, not a pasted prompt — skill-level placement survives the long chats where mega-prompts decay. |
| `register_subagent` ×2 | `subagents.py` | `review-coach` (runs one session, grades honestly) and `wiki-lint` (**read-only by tool allowlist** — curation can't edit). Trust boundaries expressed as tool lists. |
| `graph.sdk.plugin_store` | `__init__._data_dir` | Resolves the instance-scoped durable store. A missing or failed host seam raises instead of silently crossing into a global home-directory fallback. |
| `graph.sdk.schedule_recurring` | `__init__._arm_crons`, `/learn` | Plugin-owned crons (`plugin:learning_wiki:*`, swept on disable — #1642). Scheduled review + weekly lint = the dream/distill pattern (ADR 0054): a cron fires a normal agent turn; no new scheduling machinery. |
| `register_goal_verifier` ×2 | `goals.py` | Learning goals become ground-truthed conditions: `learning_wiki:strength` and `learning_wiki:reviews_clear` read the ledger, never the conversation. Usable from `/goal` and from watches. |
| `register_watch_hook` | `goals.py` | When a `/learn` watch trips (target strength reached), the hook retires the loop — `stop_goal_loop` for sugar-armed loops, a hand `cancel_scheduled` for composed ones — and emits `goal_achieved`. Both watch-id schemes recognized. |
Expand Down
8 changes: 7 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ def _data_dir(cfg: dict) -> Path:
# manifest's v0.148 floor guarantees this seam in a real host.
from graph import sdk

p = sdk.plugin_store(plugin_id="learning_wiki")
try:
p = sdk.plugin_store(plugin_id="learning_wiki")
except Exception as exc:
raise RuntimeError("learning_wiki requires an available instance-scoped plugin store") from exc
if p is None:
raise RuntimeError("learning_wiki received no instance-scoped plugin store")
p = Path(p)
p.mkdir(parents=True, exist_ok=True)
return p

Expand Down
17 changes: 15 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,17 @@ def registry(tmp_path, monkeypatch):


@pytest.fixture
def host_stub(monkeypatch):
def host_stub(monkeypatch, tmp_path):
"""Fake graph.* modules; returns a call-capture dict."""
calls = {"scheduled": [], "cancelled": [], "watches": [], "metrics": [], "loops": [], "stopped_loops": []}
calls = {
"scheduled": [],
"cancelled": [],
"watches": [],
"metrics": [],
"loops": [],
"stopped_loops": [],
"plugin_stores": [],
}

g = types.ModuleType("graph")
goals_pkg = types.ModuleType("graph.goals")
Expand Down Expand Up @@ -157,6 +165,10 @@ def record_metric(name, value, *, ts=None, plugin_id):
calls["metrics"].append((name, value, plugin_id))
return {}

def plugin_store(*, plugin_id):
calls["plugin_stores"].append(plugin_id)
return tmp_path / "plugin-store" / plugin_id

class Knobs:
def __init__(self, **kw):
self.values_map: dict = {}
Expand Down Expand Up @@ -197,6 +209,7 @@ def stop_goal_loop(*, plugin_id, loop_id):
sdk.cancel_scheduled = cancel_scheduled
sdk.create_watch = create_watch
sdk.record_metric = record_metric
sdk.plugin_store = plugin_store
sdk.Knobs = Knobs
sdk.make_knob_tools = make_knob_tools
sdk.start_goal_loop = start_goal_loop
Expand Down
22 changes: 22 additions & 0 deletions tests/test_store_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import sys
import types

import pytest

import learning_wiki


Expand All @@ -26,6 +28,26 @@ def plugin_store(*, plugin_id):
assert calls == ["learning_wiki"]


@pytest.mark.parametrize("result", [None, RuntimeError("store unavailable")])
def test_default_store_refuses_an_unscoped_fallback(monkeypatch, result):
monkeypatch.delenv("LEARNING_WIKI_DIR", raising=False)
sdk = types.ModuleType("graph.sdk")

def plugin_store(*, plugin_id):
if isinstance(result, Exception):
raise result
return result

sdk.plugin_store = plugin_store
graph = types.ModuleType("graph")
graph.sdk = sdk
monkeypatch.setitem(sys.modules, "graph", graph)
monkeypatch.setitem(sys.modules, "graph.sdk", sdk)

with pytest.raises(RuntimeError, match="instance-scoped plugin store"):
learning_wiki._data_dir({})


def test_config_and_env_overrides_stay_literal_and_skip_the_sdk(monkeypatch, tmp_path):
environment = tmp_path / "environment"
configured = tmp_path / "configured"
Expand Down
Loading