From 860e0d56bbe0b5878c5739f5a13c953384cd786e Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Fri, 17 Jul 2026 20:11:22 -0700 Subject: [PATCH] fix(tier3): adapt define/patch to b125 role schema (was model) Live Worldtree b125 changed POST /agents/define: the request field is now 'role' (a model-role like 'thoughtful-character'), replacing 'model'; the response still echoes it as 'model'. Update define_agent/patch_agent request bodies + CLI (--model -> --role); response parse + LocalAgentEntry unchanged. Verified end-to-end against live (delete->define round-trip); 26 tier3 tests green. Full b22->b125 spec-pin bump remains a follow-up. --- pyproject.toml | 2 +- src/ratatoskr/tier3.py | 32 +++++++++++++++++--------------- tests/test_tier3.py | 34 +++++++++++++++++----------------- uv.lock | 2 +- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9b5272f..5aea37d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.21.0" +version = "0.21.1" description = "Worldtree Conversation API debug console (web + headless CLI) — multi-pane observability" readme = "README.md" requires-python = ">=3.12" diff --git a/src/ratatoskr/tier3.py b/src/ratatoskr/tier3.py index 457a0e9..c375872 100644 --- a/src/ratatoskr/tier3.py +++ b/src/ratatoskr/tier3.py @@ -151,7 +151,7 @@ async def define_agent( *, agent_name: str, system_prompt: str, - model: str, + role: str, ) -> Tier3AgentInfo: """POST /agents/define — create a Tier 3 agent. @@ -165,12 +165,14 @@ async def define_agent( f"agent_name must match [a-z][a-z0-9-]{{2,63}}: {agent_name!r}" ) assert system_prompt, "system_prompt must be non-empty" - assert model, "model must be non-empty" + assert role, "role must be non-empty" + # b125 drift: /agents/define takes `role` (a model-role, e.g. "thoughtful-character") + # in the request; the response echoes it back as `model`. See #15 follow-up. body = { "agent_name": agent_name, "system_prompt": system_prompt, - "model": model, + "role": role, } resp = await client.post("/agents/define", json=body) @@ -198,7 +200,7 @@ async def patch_agent( agent_id: str, *, system_prompt: str | None = None, - model: str | None = None, + role: str | None = None, ) -> Tier3AgentInfo: """PATCH /agents/ — mutate system_prompt and/or model. @@ -207,15 +209,15 @@ async def patch_agent( """ assert client is not None assert ":" in agent_id, f"tier 3 agent_id must contain ':': {agent_id!r}" - assert system_prompt is not None or model is not None, ( - "patch requires at least one of system_prompt or model" + assert system_prompt is not None or role is not None, ( + "patch requires at least one of system_prompt or role" ) body: dict[str, str] = {} if system_prompt is not None: body["system_prompt"] = system_prompt - if model is not None: - body["model"] = model + if role is not None: + body["role"] = role resp = await client.patch(f"/agents/{agent_id}", json=body) if resp.status_code == 200: @@ -278,14 +280,14 @@ def _build_parser() -> argparse.ArgumentParser: help="System prompt the agent ships with.", ) p_define.add_argument( - "--model", required=True, - help="Provider model ID (NOT a profile alias; e.g., qwen3.6-35-a3b).", + "--role", required=True, + help="Model role, e.g. thoughtful-character (see GET /models/available-for-characters).", ) p_patch = sub.add_parser("patch", help="Mutate system_prompt and/or model.") p_patch.add_argument("agent_id", help='Full ":" form.') p_patch.add_argument("--system-prompt", dest="system_prompt", default=None) - p_patch.add_argument("--model", default=None) + p_patch.add_argument("--role", default=None) p_delete = sub.add_parser("delete", help="Hard-delete a Tier 3 agent.") p_delete.add_argument("agent_id", help='Full ":" form.') @@ -327,7 +329,7 @@ async def _run_define(ns: argparse.Namespace) -> int: client, agent_name=ns.name, system_prompt=ns.system_prompt, - model=ns.model, + role=ns.role, ) # v0.8.0: persist to local index so the picker can show it. add_local_agent( @@ -352,9 +354,9 @@ async def _run_patch(ns: argparse.Namespace) -> int: update_local_agent, ) - if ns.system_prompt is None and ns.model is None: + if ns.system_prompt is None and ns.role is None: raise _Tier3UsageError( - "patch requires at least one of --system-prompt or --model" + "patch requires at least one of --system-prompt or --role" ) async with httpx.AsyncClient( base_url=server_url, @@ -368,7 +370,7 @@ async def _run_patch(ns: argparse.Namespace) -> int: client, ns.agent_id, system_prompt=ns.system_prompt, - model=ns.model, + role=ns.role, ) # v0.8.0: refresh local index with the post-patch state. update_local_agent( diff --git a/tests/test_tier3.py b/tests/test_tier3.py index d17f60d..42da694 100644 --- a/tests/test_tier3.py +++ b/tests/test_tier3.py @@ -43,7 +43,7 @@ class TestDefineAgent: client, agent_name="wizard", system_prompt="You are a wizard.", - model="qwen3.6-35-a3b", + role="qwen3.6-35-a3b", ) assert isinstance(info, Tier3AgentInfo) assert info.agent_id == "ratatoskr:wizard" @@ -64,14 +64,14 @@ class TestDefineAgent: client, agent_name="wizard", system_prompt="You are a wizard.", - model="qwen3.6-35-a3b", + role="qwen3.6-35-a3b", ) body = _json.loads(route.calls[0].request.content) # INV-001: exactly these three keys — no layer fields, no metadata. assert body == { "agent_name": "wizard", "system_prompt": "You are a wizard.", - "model": "qwen3.6-35-a3b", + "role": "qwen3.6-35-a3b", } @respx.mock @@ -90,7 +90,7 @@ class TestDefineAgent: client, agent_name="overflow", system_prompt="x", - model="m", + role="m", ) assert exc.value.retry_after == 0 @@ -105,7 +105,7 @@ class TestDefineAgent: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(Tier3UserIdUnsupported): await define_agent( - client, agent_name="wizard", system_prompt="x", model="m" + client, agent_name="wizard", system_prompt="x", role="m" ) @respx.mock @@ -120,7 +120,7 @@ class TestDefineAgent: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(Tier3LayerDeferred) as exc: await define_agent( - client, agent_name="wizard", system_prompt="x", model="m" + client, agent_name="wizard", system_prompt="x", role="m" ) assert exc.value.field == "persona" @@ -133,7 +133,7 @@ class TestDefineAgent: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(AssertionError): await define_agent( - client, agent_name="Wizard", system_prompt="x", model="m" + client, agent_name="Wizard", system_prompt="x", role="m" ) assert route.call_count == 0 @@ -146,7 +146,7 @@ class TestDefineAgent: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(AssertionError): await define_agent( - client, agent_name="ab", system_prompt="x", model="m" + client, agent_name="ab", system_prompt="x", role="m" ) assert route.call_count == 0 @@ -159,7 +159,7 @@ class TestDefineAgent: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(AssertionError): await define_agent( - client, agent_name="wizard", system_prompt="", model="m" + client, agent_name="wizard", system_prompt="", role="m" ) assert route.call_count == 0 @@ -172,7 +172,7 @@ class TestDefineAgent: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(SessionApiFailed) as exc: await define_agent( - client, agent_name="wizard", system_prompt="x", model="m" + client, agent_name="wizard", system_prompt="x", role="m" ) assert exc.value.status == 503 @@ -196,16 +196,16 @@ class TestPatchAgent: client, "ratatoskr:wizard", system_prompt="new prompt", - model="different-model", + role="different-model", ) body = _json.loads(route.calls[0].request.content) - assert body == {"system_prompt": "new prompt", "model": "different-model"} + assert body == {"system_prompt": "new prompt", "role": "different-model"} assert info.system_prompt == "new prompt" assert info.model == "different-model" @respx.mock async def test_happy_patch_single_field(self) -> None: - """happy_patch_single_field: omit model → body has system_prompt only.""" + """happy_patch_single_field: omit role → body has system_prompt only.""" import json as _json updated = {**_FULL_AGENT_RESP, "system_prompt": "only this"} @@ -349,7 +349,7 @@ class TestCli: "define", "--name", "wizard", "--system-prompt", "You are a wizard.", - "--model", "qwen3.6-35-a3b", + "--role", "qwen3.6-35-a3b", ]) out = capsys.readouterr() assert rc == 0 @@ -429,7 +429,7 @@ class TestCli: "define", "--name", "wizard", "--system-prompt", "x", - "--model", "m", + "--role", "m", ]) err = capsys.readouterr().err assert rc == 11 @@ -449,7 +449,7 @@ class TestCli: "define", "--name", "wizard", "--system-prompt", "x", - "--model", "m", + "--role", "m", ]) err = capsys.readouterr().err assert rc == 20 @@ -473,7 +473,7 @@ class TestCli: "define", "--name", "wizard", "--system-prompt", "x", - "--model", "m", + "--role", "m", ]) err = capsys.readouterr().err assert rc == 20 diff --git a/uv.lock b/uv.lock index 11a79b7..ccb911f 100644 --- a/uv.lock +++ b/uv.lock @@ -472,7 +472,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.21.0" +version = "0.21.1" source = { editable = "." } dependencies = [ { name = "httpx" },