feat(#20): persona + authored-history + first-message onto the wt adapter (slice-3)
Slice-3 of the worldtree-sdk cutover: migrate the session persona-state write, the #347 authored-history write, and get_session_messages onto ratatoskr.wt, and route the first-message preset seed through the adapter. Retire the last hand-rolled sessions.py paths the --seed-first-message probe kept alive (create_session + SessionInfo, set_persona_state, write_authored_history, get_session_messages, _bifrost_error_from). - wt.set_persona_state (SDK PadState) — the CLI passes three finite PAD axes; the SDK owns the {"pad": {...}} wire (#317). No route-specific error row → the SessionApiFailed default. - wt.write_authored_history (SDK write_history) — v1 author=assistant; 404 → AuthoredHistoryUnavailable (hide-existence; the route is the discriminator, never the body); every other ApiError → the default. Drops the unused author/effects/claimed_original_at params (no caller uses them). - first_message.seed_preset_first_message now takes a WorldtreeClient and routes through wt.write_authored_history; the best-effort invariants (INV-001..004, never-raise/never-block/one-write/zero-worldtree-source-import) are unchanged. Tests drive a fake WorldtreeClient — the wire is the SDK's to prove. - CLI --set-persona-pad / --seed-first-message + the _amain and web create-path first-message seeds rewired onto the adapter. --set-persona-pad pre-validates PAD finiteness (clean usage_error, never a crash on the SDK ConfigurationError). LIVE-SMOKE on personal :8081 (b128, INV-CUT-5): --seed-first-message → 201 (seq=0, phase=seeded) → read-back verbatim; --set-persona-pad → 204; the --new create-path preset seed observed routing through the adapter. All slice-3 route families proven end-to-end through the ratatoskr surface. docs/coverage-map.md + first_message.contract.md re-anchored onto the adapter; the slice-2 create/stream/cancel rows re-anchored too (they still named the deleted sse_client/sessions symbols). Suite 466 green; mypy no new errors (baseline 22 → 20 in the touched modules); ruff clean. INV-CUT-1..5 held. Bifrost provider planes untouched.
This commit is contained in:
+79
-1
@@ -17,10 +17,11 @@ from typing import Any, cast
|
||||
import httpx
|
||||
import pytest
|
||||
import worldtree_sdk as wtsdk
|
||||
from worldtree_sdk import ApiError, CancelResult, WorldtreeClient
|
||||
from worldtree_sdk import ApiError, CancelResult, PadState, WorldtreeClient
|
||||
|
||||
from ratatoskr.sessions import (
|
||||
AgentNotFound,
|
||||
AuthoredHistoryUnavailable,
|
||||
BifrostBinding,
|
||||
BifrostConsumerKeyMissing,
|
||||
BifrostHandshakeFailed,
|
||||
@@ -46,8 +47,10 @@ from ratatoskr.wt import (
|
||||
get_session_messages,
|
||||
get_session_tools,
|
||||
list_sessions,
|
||||
set_persona_state,
|
||||
stream_turn,
|
||||
translate_error,
|
||||
write_authored_history,
|
||||
)
|
||||
|
||||
|
||||
@@ -101,6 +104,12 @@ class _FakeSessions:
|
||||
async def cancel_turn(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return await self._dispatch("cancel_turn", *args, **kwargs)
|
||||
|
||||
async def set_persona_state(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return await self._dispatch("set_persona_state", *args, **kwargs)
|
||||
|
||||
async def write_history(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return await self._dispatch("write_history", *args, **kwargs)
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self, sessions: _FakeSessions) -> None:
|
||||
@@ -440,3 +449,72 @@ class TestCancelTurn:
|
||||
with pytest.raises(SessionApiFailed) as ei:
|
||||
await cancel_turn(_wt(fake), "s", 42)
|
||||
assert ei.value.status == 500
|
||||
|
||||
|
||||
class TestSetPersonaState:
|
||||
"""slice-3: set_persona_state → SDK sessions.set_persona_state(PadState). The
|
||||
adapter builds the canonical PadState (the SDK owns the {"pad": {...}} wire
|
||||
shape); no error row beyond the § Error map default (SessionApiFailed)."""
|
||||
|
||||
async def test_happy_builds_padstate_and_returns_none(self) -> None:
|
||||
fake = _FakeSessions(result=None) # SDK resolves the 204 to None
|
||||
out = await set_persona_state(
|
||||
_wt(fake), "s-1", pleasure=0.4, arousal=0.1, dominance=-0.2
|
||||
)
|
||||
assert out is None
|
||||
name, args, _kwargs = fake.calls[-1]
|
||||
assert name == "set_persona_state"
|
||||
assert args[0] == "s-1"
|
||||
pad = args[1]
|
||||
assert isinstance(pad, PadState)
|
||||
assert (pad.pleasure, pad.arousal, pad.dominance) == (0.4, 0.1, -0.2)
|
||||
|
||||
async def test_falsy_zero_pad_preserved(self) -> None:
|
||||
# A 0.0 axis must survive verbatim (not be dropped as falsy).
|
||||
fake = _FakeSessions(result=None)
|
||||
await set_persona_state(_wt(fake), "s", pleasure=0.0, arousal=0.0, dominance=0.0)
|
||||
pad = fake.calls[-1][1][1]
|
||||
assert (pad.pleasure, pad.arousal, pad.dominance) == (0.0, 0.0, 0.0)
|
||||
|
||||
async def test_error_maps_to_session_api_failed(self) -> None:
|
||||
fake = _FakeSessions(error=ApiError("upstream", "boom", status=500, body="x"))
|
||||
with pytest.raises(SessionApiFailed) as ei:
|
||||
await set_persona_state(_wt(fake), "s", pleasure=0.0, arousal=0.0, dominance=0.0)
|
||||
assert ei.value.status == 500
|
||||
|
||||
|
||||
class TestWriteAuthoredHistory:
|
||||
"""slice-3: write_authored_history → SDK sessions.write_history. Builds the
|
||||
v1 authored-write entry (author="assistant", the only accepted author);
|
||||
404 → AuthoredHistoryUnavailable (hide-existence); else the default."""
|
||||
|
||||
async def test_happy_builds_entry_and_returns_dict(self) -> None:
|
||||
ack = {"seq": 0, "phase": "seeded", "turn_id": "t1", "content_chars": 3}
|
||||
fake = _FakeSessions(result=ack)
|
||||
out = await write_authored_history(
|
||||
_wt(fake), "s-1", content="hi!", idempotency_key="k1"
|
||||
)
|
||||
assert out is ack # open-world passthrough
|
||||
name, args, _kwargs = fake.calls[-1]
|
||||
assert name == "write_history"
|
||||
assert args[0] == "s-1"
|
||||
assert args[1] == {
|
||||
"author": "assistant",
|
||||
"content": "hi!",
|
||||
"idempotency_key": "k1",
|
||||
}
|
||||
|
||||
async def test_404_maps_to_authored_history_unavailable(self) -> None:
|
||||
# Hide-existence: the ROUTE is the discriminator (never the body) — any 404
|
||||
# on write_history → AuthoredHistoryUnavailable, no capability-probe.
|
||||
fake = _FakeSessions(error=ApiError("session_not_found", "no", status=404))
|
||||
with pytest.raises(AuthoredHistoryUnavailable) as ei:
|
||||
await write_authored_history(_wt(fake), "s-1", content="hi", idempotency_key="k")
|
||||
assert ei.value.session_id == "s-1"
|
||||
|
||||
async def test_other_error_maps_to_session_api_failed(self) -> None:
|
||||
# 409 generation_active (retryable) is NOT a hide-existence 404 → default.
|
||||
fake = _FakeSessions(error=ApiError("generation_active", "busy", status=409))
|
||||
with pytest.raises(SessionApiFailed) as ei:
|
||||
await write_authored_history(_wt(fake), "s", content="hi", idempotency_key="k")
|
||||
assert ei.value.status == 409
|
||||
|
||||
Reference in New Issue
Block a user