diff --git a/persistent-memory.md b/persistent-memory.md index 1f95a9c..908b8f3 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -32,9 +32,9 @@ separate dev team rather than an in-tree Worldtree tool. ## Current state / in-flight -_As of 2026-05-25 (post-v0.7.1 thinking coalesce-by-newline):_ +_As of 2026-05-25 (post-v0.8.0 local tier-3 agent index in picker):_ -**Status: v0.7.1 shipped.** Ten core features complete (`sse_client` +**Status: v0.8.0 shipped.** Eleven core features complete (`sse_client` #1, `sessions` #2, `cli` #3, `tui` #4, `--end-user-id` #5, TUI startup error visibility #6, presenter contract semantics amendment #12, startup agent picker #8, §5 layout reshape + Tools pane #13) @@ -51,7 +51,8 @@ Static in the footer (static "Tools" v1; dynamic when more tabs land). CLI mode (--send) unaffected by design — INV-018. Last commits on `main`: -- v0.7.1 fix(tui): coalesce thinking deltas on `\n` — no more per-token newlines +- v0.8.0 feat(local_agents): JSON-backed local tier-3 index + picker merge +- `9918c10` fix(tui): coalesce thinking deltas on `\n` (v0.7.1) - `c086ae2` feat(tier3): ratatoskr.tier3 module + CLI (v0.7.0) - `d356990` refactor(tui): thinking streams into thinking-log (v0.6.5) - `82437bd` style(tui): picker highlighted item → Aurora blue (v0.6.4) diff --git a/pyproject.toml b/pyproject.toml index c2c4d46..6e986c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.7.1" +version = "0.8.0" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" diff --git a/src/ratatoskr/local_agents.py b/src/ratatoskr/local_agents.py new file mode 100644 index 0000000..c624268 --- /dev/null +++ b/src/ratatoskr/local_agents.py @@ -0,0 +1,134 @@ +"""Local index of tier-3 agents defined via `python -m ratatoskr.tier3`. + +Workaround for Worldtree's ``GET /agents`` not returning consumer-defined +agents (the public list excludes tier-3 per-spec; see issue #15 smoke +findings). Local file maintains a list of agent_ids + display metadata so +the picker can show them alongside foundational agents. + +Storage shape: JSON at ``$XDG_CONFIG_HOME/ratatoskr/local_agents.json`` +(default ``~/.config/ratatoskr/local_agents.json``). Override via +``$RATATOSKR_LOCAL_AGENTS`` env var for tests / per-machine isolation. + +If Worldtree later starts returning tier-3 agents in ``GET /agents``, this +module's role narrows to redundant local cache; can be removed cleanly +since the picker's dedup-by-agent-id keeps remote-wins behavior. + +Failure modes are lenient: missing file → empty index; corrupt JSON or +schema mismatch → empty index (no crash). The picker continues to show +foundational agents either way; the local-tier-3 surface degrades to +"operator passes --agent ratatoskr: explicitly" — the +pre-v0.8.0 workflow. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import asdict, dataclass +from pathlib import Path + +_SCHEMA_VERSION = 1 + + +@dataclass(frozen=True) +class LocalAgentEntry: + """One row in the local tier-3 agent index. + + Schema: + - ``agent_id``: full "user_id:agent_name" string (Worldtree-owned). + - ``agent_name``: slug from define (display name). + - ``model``: provider model ID at last define/patch. + - ``description``: synthetic display string (typically derived from + the system_prompt's first line + a "(tier 3)" prefix; the picker + uses this in its ``{id} · {name} — {description}`` rendering). + - ``defined_at``: ISO-8601 timestamp from the Tier3AgentInfo response. + """ + + agent_id: str + agent_name: str + model: str + description: str + defined_at: str + + +def _local_agents_path() -> Path: + """Resolve the local index file path with XDG + env-var override.""" + override = os.environ.get("RATATOSKR_LOCAL_AGENTS") + if override: + return Path(override) + xdg = os.environ.get("XDG_CONFIG_HOME") + base = Path(xdg) if xdg else (Path.home() / ".config") + return base / "ratatoskr" / "local_agents.json" + + +def load_local_agents() -> list[LocalAgentEntry]: + """Read the local index. Returns ``[]`` on missing file, corrupt JSON, + schema mismatch, or any read error — never raises. + """ + path = _local_agents_path() + if not path.exists(): + return [] + try: + raw = json.loads(path.read_text()) + except (json.JSONDecodeError, OSError): + return [] + if not isinstance(raw, dict) or raw.get("version") != _SCHEMA_VERSION: + return [] + agents = raw.get("agents", []) + if not isinstance(agents, list): + return [] + out: list[LocalAgentEntry] = [] + for item in agents: + if not isinstance(item, dict): + continue + try: + out.append(LocalAgentEntry(**item)) + except TypeError: + # Malformed row (missing/extra fields) — skip silently. + continue + return out + + +def _save_local_agents(agents: list[LocalAgentEntry]) -> None: + """Persist the index. Creates parent dir as needed.""" + path = _local_agents_path() + path.parent.mkdir(parents=True, exist_ok=True) + payload = {"version": _SCHEMA_VERSION, "agents": [asdict(a) for a in agents]} + path.write_text(json.dumps(payload, indent=2)) + + +def add_local_agent(entry: LocalAgentEntry) -> None: + """Add (or replace) an agent in the local index. agent_id is the key.""" + agents = [a for a in load_local_agents() if a.agent_id != entry.agent_id] + agents.append(entry) + _save_local_agents(agents) + + +def update_local_agent(entry: LocalAgentEntry) -> None: + """Update an existing entry. Identical semantics to ``add_local_agent`` + (agent_id is the dedup key), exposed separately so callers can + self-document intent. + """ + add_local_agent(entry) + + +def remove_local_agent(agent_id: str) -> None: + """Remove an entry by agent_id. No-op if absent (idempotent).""" + agents = [a for a in load_local_agents() if a.agent_id != agent_id] + _save_local_agents(agents) + + +def make_description(system_prompt: str) -> str: + """Synthesize a one-line description for the picker from a system prompt. + + Strategy: first non-empty line, stripped of leading markdown heading + markers and whitespace, prefixed with "(tier 3) ", truncated to 80 + chars. Falls back to "(tier 3) custom system prompt" if the prompt is + empty (defensive — define rejects empty prompts at PRE-002). + """ + for line in system_prompt.splitlines(): + stripped = line.lstrip("# ").strip() + if stripped: + label = f"(tier 3) {stripped}" + return label[:80] + ("…" if len(label) > 80 else "") + return "(tier 3) custom system prompt" diff --git a/src/ratatoskr/tier3.py b/src/ratatoskr/tier3.py index 16f27d6..457a0e9 100644 --- a/src/ratatoskr/tier3.py +++ b/src/ratatoskr/tier3.py @@ -309,6 +309,11 @@ def _resolve_auth(ns: argparse.Namespace) -> tuple[str, str]: async def _run_define(ns: argparse.Namespace) -> int: api_key, server_url = _resolve_auth(ns) from ratatoskr.cli import USER_AGENT + from ratatoskr.local_agents import ( + LocalAgentEntry, + add_local_agent, + make_description, + ) async with httpx.AsyncClient( base_url=server_url, @@ -324,6 +329,16 @@ async def _run_define(ns: argparse.Namespace) -> int: system_prompt=ns.system_prompt, model=ns.model, ) + # v0.8.0: persist to local index so the picker can show it. + add_local_agent( + LocalAgentEntry( + agent_id=info.agent_id, + agent_name=info.agent_name, + model=info.model, + description=make_description(info.system_prompt), + defined_at=info.created_at, + ) + ) print(f"defined {info.agent_id} ({info.model})") return 0 @@ -331,6 +346,11 @@ async def _run_define(ns: argparse.Namespace) -> int: async def _run_patch(ns: argparse.Namespace) -> int: api_key, server_url = _resolve_auth(ns) from ratatoskr.cli import USER_AGENT + from ratatoskr.local_agents import ( + LocalAgentEntry, + make_description, + update_local_agent, + ) if ns.system_prompt is None and ns.model is None: raise _Tier3UsageError( @@ -350,6 +370,16 @@ async def _run_patch(ns: argparse.Namespace) -> int: system_prompt=ns.system_prompt, model=ns.model, ) + # v0.8.0: refresh local index with the post-patch state. + update_local_agent( + LocalAgentEntry( + agent_id=info.agent_id, + agent_name=info.agent_name, + model=info.model, + description=make_description(info.system_prompt), + defined_at=info.updated_at, + ) + ) print(f"patched {info.agent_id}") return 0 @@ -357,6 +387,7 @@ async def _run_patch(ns: argparse.Namespace) -> int: async def _run_delete(ns: argparse.Namespace) -> int: api_key, server_url = _resolve_auth(ns) from ratatoskr.cli import USER_AGENT + from ratatoskr.local_agents import remove_local_agent async with httpx.AsyncClient( base_url=server_url, @@ -367,6 +398,8 @@ async def _run_delete(ns: argparse.Namespace) -> int: timeout=httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0), ) as client: await delete_agent(client, ns.agent_id) + # v0.8.0: drop from local index so the picker stops listing it. + remove_local_agent(ns.agent_id) print(f"deleted {ns.agent_id}") return 0 diff --git a/src/ratatoskr/tui.py b/src/ratatoskr/tui.py index 751db09..b50cde1 100644 --- a/src/ratatoskr/tui.py +++ b/src/ratatoskr/tui.py @@ -928,6 +928,14 @@ async def _resolve_then_run(args: ParsedArgs) -> int: # Issue #8: startup agent picker — fetch GET /agents and prompt when # --new is passed without --agent. list_agents errors land on real # stderr before any alt-screen opens (preserves issue #6 INV-001). + # + # v0.8.0: merge in local tier-3 agent index. Worldtree's GET /agents + # doesn't return consumer-defined agents (issue #15 smoke finding); + # ratatoskr keeps its own JSON-backed index of agents the operator + # defined via `python -m ratatoskr.tier3 define`. Merged here so the + # picker shows foundational + local-tier-3 in one list. Dedup by + # agent_id (remote wins on conflict, since a server-listed agent + # is the authoritative source). chosen_agent_id: str | None = args.agent_id if args.new and args.agent_id is None: try: @@ -940,6 +948,23 @@ async def _resolve_then_run(args: ParsedArgs) -> int: except (httpx.ConnectError, httpx.ReadTimeout, httpx.TransportError) as exc: sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n") return 21 + # v0.8.0: append local tier-3 entries not already in the remote list. + from ratatoskr.local_agents import load_local_agents + + remote_ids = {a.agent_id for a in agents} + for entry in load_local_agents(): + if entry.agent_id in remote_ids: + continue + agents.append(AgentInfo( + agent_id=entry.agent_id, + name=entry.agent_name, + description=entry.description, + version=None, + capabilities=[], + supported_models=[], + persona_traits={}, + ui_hints={}, + )) if not agents: sys.stderr.write("[no_agents] server returned empty agent list\n") return 13 diff --git a/tests/test_local_agents.py b/tests/test_local_agents.py new file mode 100644 index 0000000..97cd85b --- /dev/null +++ b/tests/test_local_agents.py @@ -0,0 +1,187 @@ +"""Tests for ratatoskr.local_agents. + +Use ``$RATATOSKR_LOCAL_AGENTS`` env-var override + pytest tmp_path to +isolate from the operator's real ``~/.config/ratatoskr/local_agents.json``. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from ratatoskr.local_agents import ( + LocalAgentEntry, + _local_agents_path, + add_local_agent, + load_local_agents, + make_description, + remove_local_agent, + update_local_agent, +) + + +@pytest.fixture +def local_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Point $RATATOSKR_LOCAL_AGENTS at a fresh tmp file for the test.""" + path = tmp_path / "local_agents.json" + monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", str(path)) + return path + + +def _entry( + agent_id: str = "ratatoskr:wizard", + agent_name: str = "wizard", + model: str = "qwen3.6-35-a3b", + description: str = "(tier 3) test agent", + defined_at: str = "2026-05-25T00:00:00+00:00", +) -> LocalAgentEntry: + return LocalAgentEntry( + agent_id=agent_id, + agent_name=agent_name, + model=model, + description=description, + defined_at=defined_at, + ) + + +class TestPathResolution: + def test_env_override(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", "/tmp/custom-agents.json") + assert _local_agents_path() == Path("/tmp/custom-agents.json") + + def test_xdg_config_home(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("RATATOSKR_LOCAL_AGENTS", raising=False) + monkeypatch.setenv("XDG_CONFIG_HOME", "/tmp/xdg-config") + assert ( + _local_agents_path() + == Path("/tmp/xdg-config/ratatoskr/local_agents.json") + ) + + def test_default_home(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("RATATOSKR_LOCAL_AGENTS", raising=False) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + path = _local_agents_path() + assert path == Path.home() / ".config" / "ratatoskr" / "local_agents.json" + + +class TestLoadEmpty: + def test_missing_file_returns_empty(self, local_path: Path) -> None: + assert not local_path.exists() + assert load_local_agents() == [] + + def test_corrupt_json_returns_empty(self, local_path: Path) -> None: + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_text("not json at all") + assert load_local_agents() == [] + + def test_wrong_schema_version_returns_empty(self, local_path: Path) -> None: + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_text(json.dumps({"version": 999, "agents": []})) + assert load_local_agents() == [] + + def test_missing_version_key_returns_empty(self, local_path: Path) -> None: + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_text(json.dumps({"agents": []})) + assert load_local_agents() == [] + + def test_malformed_row_skipped(self, local_path: Path) -> None: + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_text( + json.dumps( + { + "version": 1, + "agents": [ + {"agent_id": "incomplete"}, # missing required fields + { + "agent_id": "ratatoskr:good", + "agent_name": "good", + "model": "m", + "description": "d", + "defined_at": "t", + }, + ], + } + ) + ) + entries = load_local_agents() + assert len(entries) == 1 + assert entries[0].agent_id == "ratatoskr:good" + + +class TestAdd: + def test_add_one(self, local_path: Path) -> None: + add_local_agent(_entry()) + entries = load_local_agents() + assert len(entries) == 1 + assert entries[0].agent_id == "ratatoskr:wizard" + + def test_add_two_different(self, local_path: Path) -> None: + add_local_agent(_entry(agent_id="ratatoskr:a", agent_name="a")) + add_local_agent(_entry(agent_id="ratatoskr:b", agent_name="b")) + ids = {e.agent_id for e in load_local_agents()} + assert ids == {"ratatoskr:a", "ratatoskr:b"} + + def test_add_replaces_same_id(self, local_path: Path) -> None: + add_local_agent(_entry(model="old-model")) + add_local_agent(_entry(model="new-model")) + entries = load_local_agents() + assert len(entries) == 1 + assert entries[0].model == "new-model" + + def test_creates_parent_dirs( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + nested = tmp_path / "deep" / "nested" / "path" / "agents.json" + monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", str(nested)) + add_local_agent(_entry()) + assert nested.exists() + + +class TestUpdate: + def test_update_changes_existing(self, local_path: Path) -> None: + add_local_agent(_entry(model="v1")) + update_local_agent(_entry(model="v2")) + entries = load_local_agents() + assert len(entries) == 1 + assert entries[0].model == "v2" + + +class TestRemove: + def test_remove_existing(self, local_path: Path) -> None: + add_local_agent(_entry()) + remove_local_agent("ratatoskr:wizard") + assert load_local_agents() == [] + + def test_remove_missing_is_noop(self, local_path: Path) -> None: + add_local_agent(_entry()) + remove_local_agent("ratatoskr:doesnotexist") + assert len(load_local_agents()) == 1 + + +class TestMakeDescription: + def test_first_nonempty_line(self) -> None: + prompt = "\n\n# IDENTITY\nYou are a test agent..." + desc = make_description(prompt) + assert desc.startswith("(tier 3) IDENTITY") + + def test_strips_heading_markers(self) -> None: + prompt = "# A nice heading\nMore prompt..." + desc = make_description(prompt) + assert "(tier 3) A nice heading" == desc + + def test_truncates_long(self) -> None: + prompt = "x" * 200 + desc = make_description(prompt) + # 80 char cap including the prefix + assert len(desc) == 81 # 80 + ellipsis char + assert desc.endswith("…") + + def test_empty_prompt_fallback(self) -> None: + desc = make_description("") + assert desc == "(tier 3) custom system prompt" + + def test_whitespace_only_fallback(self) -> None: + desc = make_description(" \n\n ") + assert desc == "(tier 3) custom system prompt" diff --git a/tests/test_tier3.py b/tests/test_tier3.py index 52c2059..d17f60d 100644 --- a/tests/test_tier3.py +++ b/tests/test_tier3.py @@ -1,5 +1,7 @@ """Tests for ratatoskr.tier3 per docs/contracts/issues/15.contract.md.""" +from pathlib import Path + import httpx import pytest import respx @@ -315,12 +317,29 @@ class TestDeleteAgent: assert exc.value.status == 500 +@pytest.fixture +def _isolated_local_agents( + tmp_path: "Path", monkeypatch: pytest.MonkeyPatch +) -> "Path": + """Isolate the v0.8.0 local-tier-3 index from the operator's real file.""" + path = tmp_path / "local_agents.json" + monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", str(path)) + return path + + class TestCli: @respx.mock def test_cli_define_happy( - self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch + self, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + _isolated_local_agents: "Path", ) -> None: - """cli_define_happy [happy]: argv → 201 mock → stdout confirmation.""" + """cli_define_happy [happy]: argv → 201 mock → stdout confirmation; + local index updated with the new entry (v0.8.0 hook). + """ + from ratatoskr.local_agents import load_local_agents + monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example") monkeypatch.setenv("WORLDTREE_API_KEY", "k") respx.post("https://w.example/agents/define").mock( @@ -335,12 +354,24 @@ class TestCli: out = capsys.readouterr() assert rc == 0 assert out.out.strip() == "defined ratatoskr:wizard (qwen3.6-35-a3b)" + # v0.8.0: local index now has the new entry. + entries = load_local_agents() + assert len(entries) == 1 + assert entries[0].agent_id == "ratatoskr:wizard" + assert entries[0].model == "qwen3.6-35-a3b" @respx.mock def test_cli_patch_happy( - self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch + self, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + _isolated_local_agents: "Path", ) -> None: - """cli_patch_happy [happy]: argv → 200 mock → stdout confirmation.""" + """cli_patch_happy [happy]: argv → 200 mock → stdout confirmation; + local index refreshed with the post-patch state. + """ + from ratatoskr.local_agents import load_local_agents + monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example") monkeypatch.setenv("WORLDTREE_API_KEY", "k") respx.patch("https://w.example/agents/ratatoskr:wizard").mock( @@ -350,12 +381,34 @@ class TestCli: out = capsys.readouterr() assert rc == 0 assert out.out.strip() == "patched ratatoskr:wizard" + entries = load_local_agents() + assert len(entries) == 1 + assert entries[0].agent_id == "ratatoskr:wizard" @respx.mock def test_cli_delete_happy( - self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch + self, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + _isolated_local_agents: "Path", ) -> None: - """cli_delete_happy [happy]: argv → 204 mock → stdout confirmation.""" + """cli_delete_happy [happy]: argv → 204 mock → stdout confirmation; + local index entry removed (v0.8.0 hook). + """ + from ratatoskr.local_agents import ( + LocalAgentEntry, + add_local_agent, + load_local_agents, + ) + + # Pre-populate so we can verify removal. + add_local_agent(LocalAgentEntry( + agent_id="ratatoskr:wizard", + agent_name="wizard", + model="m", + description="d", + defined_at="t", + )) monkeypatch.setenv("WORLDTREE_API_URL", "https://w.example") monkeypatch.setenv("WORLDTREE_API_KEY", "k") respx.delete("https://w.example/agents/ratatoskr:wizard").mock( @@ -365,6 +418,7 @@ class TestCli: out = capsys.readouterr() assert rc == 0 assert out.out.strip() == "deleted ratatoskr:wizard" + assert load_local_agents() == [] def test_cli_missing_auth( self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch diff --git a/tests/test_tui.py b/tests/test_tui.py index 5513db8..78f5214 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -1,5 +1,6 @@ """Tests for ratatoskr.tui per docs/contracts/issues/4.contract.md.""" +from pathlib import Path from unittest.mock import MagicMock import httpx @@ -2121,6 +2122,72 @@ class TestResolveThenRunWithPicker: assert agents_route.call_count == 0 assert sessions_route.call_count == 1 + @respx.mock + def test_picker_merges_local_tier3_agents( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """picker_merges_local_tier3_agents [v0.8.0]: local index entries + appear in the picker's agent list alongside remote agents.""" + from ratatoskr.local_agents import LocalAgentEntry, add_local_agent + + # Isolate the local index in a tmp file. + monkeypatch.setenv( + "RATATOSKR_LOCAL_AGENTS", str(tmp_path / "local_agents.json") + ) + add_local_agent(LocalAgentEntry( + agent_id="ratatoskr:wizard", + agent_name="wizard", + model="qwen3.6-35-a3b", + description="(tier 3) test wizard", + defined_at="2026-05-25T00:00:00+00:00", + )) + respx.get("https://w.example/agents").mock( + return_value=httpx.Response(200, json=_AGENTS_RESP) + ) + respx.post("https://w.example/sessions").mock( + return_value=httpx.Response(201, json=_CREATE_OK_RESP) + ) + + from ratatoskr.tui import AgentPickerApp + + captured: list = [] + + async def capture_picker_init(self, *a, **kw): + captured.append(list(self.agents)) + return "mimir" # auto-pick something so the rest succeeds + + # Patch __init__ to capture the agent list passed to the picker. + orig_init = AgentPickerApp.__init__ + + def init_spy(self, agents): + captured.append(list(agents)) + orig_init(self, agents) + + monkeypatch.setattr(AgentPickerApp, "__init__", init_spy) + + async def picker_returns_mimir(self): + return "mimir" + + monkeypatch.setattr(AgentPickerApp, "run_async", picker_returns_mimir) + + async def fake_main(self, *a, **kw): + return 0 + + monkeypatch.setattr(RatatoskrApp, "run_async", fake_main) + from ratatoskr.tui import run_tui + + rc = run_tui(_args_new_no_agent()) + assert rc == 0 + # The local tier-3 agent should appear in the picker's agents list. + assert captured, "AgentPickerApp.__init__ was never called" + agent_ids = {a.agent_id for a in captured[0]} + assert "ratatoskr:wizard" in agent_ids + # Plus the remote agents. + assert "mimir" in agent_ids + assert "lofn" in agent_ids + @respx.mock def test_picker_skipped_when_session_mode(self, monkeypatch: pytest.MonkeyPatch) -> None: """picker_skipped_when_session_mode: --session s-1 → no list_agents, no create_session.""" diff --git a/uv.lock b/uv.lock index 065dac0..f469df0 100644 --- a/uv.lock +++ b/uv.lock @@ -968,7 +968,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.7.1" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "httpx" },