diff --git a/priv/python/albedo_plugins/commands.py b/priv/python/albedo_plugins/commands.py index b55b15b..da068b9 100644 --- a/priv/python/albedo_plugins/commands.py +++ b/priv/python/albedo_plugins/commands.py @@ -1,10 +1,11 @@ """This session's slash commands as typed bindings, minted from the catalog. -The daemon sends one immutable command catalog at kernel boot; each -model-callable command becomes a typed async method whose docstring and -signature come from its declared arguments, so help(commands.) is the -command's help text. A command whose method name is reserved, invalid, or -taken stays callable through commands.invoke(). +Each model-callable command in the catalog at kernel boot becomes a typed +async method whose docstring and signature come from its declared arguments, +so help(commands.) is the command's help text. catalog() and invoke() +read the live catalog, so a session /reload reaches them without a kernel +restart; a command whose method name is reserved, invalid, taken, or added +after boot stays callable through commands.invoke(). """ from __future__ import annotations @@ -104,17 +105,16 @@ def _method(host: Host, summary: CommandSummary) -> object: class Commands: - """This session's slash commands. Every model-callable command is a typed - async method minted from the catalog at kernel boot; see the catalog for - method names and help(commands.) for one command's help.""" + """This session's slash commands. Every model-callable command at kernel + boot is a typed async method; see the catalog for method names and + help(commands.) for one command's help.""" - def __init__(self, host: Host, catalog: list[CommandSummary]) -> None: + def __init__(self, host: Host) -> None: self._host = host - self._catalog = catalog async def catalog(self) -> list[CommandSummary]: - """This session's immutable command catalog (minted at kernel boot).""" - return list(self._catalog) + """This session's current command catalog, including reloaded ones.""" + return cast(list[CommandSummary], await self._host("commands.list", {})) async def invoke(self, name: str, arguments: str | dict[str, object] = "") -> object: """Run any model-callable command by slash name or method name. @@ -124,7 +124,7 @@ class Commands: submits a turn. """ target = name if name.startswith("/") else "/" + name - for summary in self._catalog: + for summary in await self.catalog(): if summary["name"] == target or summary["method"] == name: break else: @@ -152,4 +152,4 @@ async def setup(api: PythonApi) -> dict[str, object]: continue bindings[method] = staticmethod(_method(api.host, summary)) session_commands = type("SessionCommands", (Commands,), bindings) - return {"commands": session_commands(api.host, catalog), "CommandsError": api.HostError} + return {"commands": session_commands(api.host), "CommandsError": api.HostError} diff --git a/test/daemon/extensions_integration.py b/test/daemon/extensions_integration.py index 92cea28..4b02e71 100644 --- a/test/daemon/extensions_integration.py +++ b/test/daemon/extensions_integration.py @@ -144,7 +144,12 @@ def run(endpoint): assert error.code == 409, error.read() def snapshot(session): - with api(f"/sessions/{session}/stream") as response: + # `api` would json.load an endless event stream; read one frame instead. + # after_seq=0 replays the live log: a fresh stream gets a transcript + # snapshot, which does not carry live-only events like `compacted`. + request = urllib.request.Request(f"http://127.0.0.1:{connection['port']}/sessions/{session}/stream?after_seq=0", + headers={"Authorization": "Bearer " + connection["token"]}) + with urllib.request.urlopen(request, timeout=25) as response: while True: line = response.readline() if line.startswith(b"data: "): @@ -249,7 +254,7 @@ def run(endpoint): api(f"/sessions/{session}/events", {"content": "hot probe before reload"}) ready(session) assert len(Provider.requests) == before + 2 - tool_output = next(item["output"] for item in Provider.requests[-1]["input"] if item.get("type") == "function_call_output") + tool_output = next(item["output"] for item in reversed(Provider.requests[-1]["input"]) if item.get("type") == "function_call_output") assert "BEFORE_RELOAD_OK" in tool_output, tool_output reloaded = api(f"/sessions/{session}/commands", {"name": "/reload", "args": {"target": "session"}}) assert reloaded["result"]["reloaded"] == "session", reloaded @@ -260,21 +265,21 @@ def run(endpoint): api(f"/sessions/{session}/events", {"content": "hot probe after reload"}) ready(session) assert len(Provider.requests) == before + 2 - tool_output = next(item["output"] for item in Provider.requests[-1]["input"] if item.get("type") == "function_call_output") + tool_output = next(item["output"] for item in reversed(Provider.requests[-1]["input"]) if item.get("type") == "function_call_output") assert "AFTER_RELOAD_OK" in tool_output, tool_output python_session = json.loads(cli("new", str(workspace)))["session"] before = len(Provider.requests) api(f"/sessions/{python_session}/events", {"content": "activate demo via python"}) ready(python_session) assert len(Provider.requests) == before + 2, "python activation must not submit another user turn" - tool_output = next(item["output"] for item in Provider.requests[-1]["input"] if item.get("type") == "function_call_output") + tool_output = next(item["output"] for item in reversed(Provider.requests[-1]["input"]) if item.get("type") == "function_call_output") assert "SKILL_PYTHON_ACTIVATION_OK" in tool_output, tool_output model_session = json.loads(cli("new", str(workspace)))["session"] before = len(Provider.requests) api(f"/sessions/{model_session}/events", {"content": "model probe via python"}) ready(model_session) assert len(Provider.requests) == before + 2 - tool_output = next(item["output"] for item in Provider.requests[-1]["input"] if item.get("type") == "function_call_output") + tool_output = next(item["output"] for item in reversed(Provider.requests[-1]["input"]) if item.get("type") == "function_call_output") assert "MODEL_COMMAND_OK" in tool_output, tool_output switched = api(f"/sessions/{model_session}/commands", {"name": "/model", "args": {"model": "switched-model"}}) assert switched["result"]["model"] == "switched-model", switched