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:
vh
2026-07-19 08:53:45 -07:00
parent 0b23334c68
commit ca9a339050
13 changed files with 313 additions and 1110 deletions
+79 -1
View File
@@ -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