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.
10 KiB
contract_version, module, purpose, touches, scope, depends_on, used_by, language, complexity, estimated_loc, confidence, assumptions
| contract_version | module | purpose | touches | scope | depends_on | used_by | language | complexity | estimated_loc | confidence | assumptions | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2.1 | ratatoskr.first_message | Per-agent authored first-message presets — seed an agent's opening as a #347 authored turn-0 onto new sessions (CLI + web), the durable replacement for a system-prompt startup instruction. |
|
Per-agent authored first-message presets (Worldtree #347 consumer feature). When a new session is created for an agent that has a preset opening, seed it as a #347 authored first-message (POST /sessions/{id}/history, author=assistant, seq-0) so the session opens in-character before the user speaks — the durable replacement for a system-prompt "startup" instruction. Two entry points: `preset_for` (lookup) and `seed_preset_first_message` (best-effort seed). Consumed by ratatoskr.cli (the `--new` session path) and ratatoskr.web.server (the POST /api/sessions endpoint). Depends on ratatoskr.wt (`wt.write_authored_history` + AuthoredHistoryUnavailable) over a WorldtreeClient (worldtree-sdk cutover slice-3, #20); no core.* / worldtree.* SOURCE imports. |
|
|
python | low | 60 | 0.9 |
|
First-message presets — authored openings on session-create (#347)
Context
ratatoskr.first_message holds per-agent authored-opening presets and seeds them
onto new sessions via the #347 authored-history-write primitive. It is the
durable form of "give an agent a first message": instead of a system-prompt
Startup: instruction (a workaround for the pre-#347 world where the assistant
could not author turn-0), the opening lives as a real seeded assistant turn-0.
Consumed at both session-create sites — ratatoskr.cli._amain (the --new path)
and ratatoskr.web.server._create_session_endpoint (POST /api/sessions) — so
every new session for a preset agent opens in-character regardless of surface.
Data flow
In: a WorldtreeClient (the wt-adapter client, built over ratatoskr's
caller-owned transport), a fresh session_id, and the bound agent_id.
Out: on a preset agent, one POST /sessions/{session_id}/history (author=assistant,
the preset text, per-content idempotency key). Returns the seeded content on
success, else None.
Side effects: at most one outbound authored-history write; never raises to the caller (best-effort).
Invariants
- INV-001 [hard]:
seed_preset_first_messageNEVER raises (the sole exception isasyncio.CancelledError, which propagates — cancellation is not a seed failure) and NEVER blocks session creation. It soft-guards its inputs (a bad arg returnsNone, notAssertionError), bounds the write withasyncio.wait_for(_SEED_TIMEOUT_S)so a stalled/historycan't hang the create path, and swallows EVERY other exception (the hide-404,SessionApiFailed,httpx.HTTPError,TimeoutError, and any unexpected error) →None. Thebroad-exceptis deliberate: this helper is wired INTO three session-create paths, so any escape would abort a create that already succeeded. - INV-002 [hard]: a no-preset agent issues ZERO HTTP (early return before any request).
- INV-003 [hard]: the seed body is the preset text verbatim, author="assistant",
with a per-content idempotency key (
"ratatoskr-preset-" + sha256(text)[:12]), so a repeat seed of the same session+preset is an idempotent 200 replay, never a duplicate turn. - INV-004 [hard]: no
core.*/worldtree.*imports (reference-consumer boundary; verified bytests/test_no_worldtree_imports.py, which rglobs every.pyundersrc/ratatoskr/— this module included, so no per-module import test is needed here).
Out of scope
- Multi-turn / scripted openers. v1 seeds exactly one assistant turn-0. A multi-message opening scene is a future concern.
- Runtime/remote preset config. The registry is an in-module dict; no file/DB/env loading. Add that only when a second consumer needs operator-editable presets.
- Non-assistant authors. v1 is author=assistant only (matches #347 v1); a user/system opener is deferred with the #347 engine surface.
- TUI-only surfaces. Both real session-create paths (CLI + web) are wired; the bare-TUI picker resumes existing sessions (no create), so it needs no seed.
FN preset_for(agent_id: str) -> str | None
BRIEF: Return the authored first-message preset for agent_id, or None when the agent has no preset. Pure dict lookup over FIRST_MESSAGE_PRESETS.
PRE: [PRE-001 hard] agent_id is a non-empty str -- assert agent_id and isinstance(agent_id, str)
POST: [POST-001 return_value] returns FIRST_MESSAGE_PRESETS.get(agent_id) (str for a preset agent, None otherwise)
STEPS:
1. [setup, prescriptive] assert PRE-001
2. [sequential, prescriptive] RETURN FIRST_MESSAGE_PRESETS.get(agent_id)
TESTS:
preset_hit [happy]: preset_for("ratatoskr:sindra") is a non-empty str
preset_miss [happy]: preset_for("mimir") is None
empty_agent_id [adversarial]: preset_for("") → AssertionError
FN seed_preset_first_message(client: WorldtreeClient, session_id: str, agent_id: str) -> str | None
BRIEF: Best-effort seed of an agent's preset opening as a #347 authored first-message on session_id. If agent_id has a preset, write it via wt.write_authored_history (author=assistant, per-content idempotency key, the await bounded by asyncio.wait_for(_SEED_TIMEOUT_S)) and return the seeded content; on no-preset, a malformed input, OR ANY exception except asyncio.CancelledError, return None WITHOUT raising. Never raises (except CancelledError, which propagates) and never blocks session creation — it is wired into the CLI + web create paths.
PRE: [PRE-001 hard] client is not None -- soft-guarded: return None (NOT assert) if violated, so a wiring bug can't crash the create path (INV-001)
PRE: [PRE-002 hard] session_id is a non-empty str -- soft-guarded: return None if violated
PRE: [PRE-003 hard] agent_id is a non-empty str -- soft-guarded: return None if violated (also guards FIRST_MESSAGE_PRESETS.get against a non-hashable/non-str id)
POST: [POST-001 return_value] preset agent + successful write → returns the preset text; no-preset, malformed input, OR any swallowed failure → None
POST: [POST-002 side_effect] a no-preset / malformed-input call issues ZERO writes; a preset agent issues exactly one authored-history write (POST /sessions/{session_id}/history via the SDK) with entry author="assistant", content=preset, idempotency_key="ratatoskr-preset-"+sha256(preset)[:12], the await bounded by _SEED_TIMEOUT_S so a stalled response cannot block
ERROR_ROUTING:
asyncio.CancelledError:
local_handling: RE-RAISE (cancellation is not a seed failure; never swallow it — and it is a BaseException, so `except Exception` would miss it anyway)
flow_control: propagate
state_recovery: n/a
any other Exception (hide-404 AuthoredHistoryUnavailable, wt.SessionApiFailed 409/422/etc., SDK ConnectFailed, TimeoutError from wait_for, any unexpected error):
local_handling: swallow; return None
flow_control: continue (never blocks session create)
state_recovery: session opens with no seeded greeting
STEPS:
1. [setup, prescriptive] Soft-guard: IF agent_id is not a non-empty str: RETURN None (before any dict lookup — guards a non-hashable id)
2. [sequential, prescriptive] content = FIRST_MESSAGE_PRESETS.get(agent_id); IF content is None: RETURN None (INV-002 — zero HTTP)
3. [sequential, prescriptive] Soft-guard: IF client is None OR session_id is not a non-empty str: RETURN None
4. [sequential, prescriptive] key = "ratatoskr-preset-" + sha256(content utf-8)[:12]
5. [sequential, prescriptive] TRY: await asyncio.wait_for(wt.write_authored_history(client, session_id, content=content, idempotency_key=key), timeout=_SEED_TIMEOUT_S)
tool: { destructive: false, idempotent: true, read_only: false, open_world: false }
6. [branch, prescriptive] EXCEPT asyncio.CancelledError: RAISE; EXCEPT Exception: RETURN None
7. [cleanup, prescriptive] RETURN content
TESTS: (driven through a fake WorldtreeClient whose sessions.write_history returns/raises — the wire is the SDK's to prove via its parity corpus)
seeds_preset [happy,tracer]: preset agent, fake write_history returns an ack → returns the preset text; exactly one write_history call; entry author="assistant" + content=preset + idempotency_key="ratatoskr-preset-"+sha256(preset)[:12]
no_preset_zero_write [happy]: agent "mimir" → returns None; ZERO write_history call
feature_absent_swallowed [error]: preset agent, fake raises ApiError(404) → adapter maps to AuthoredHistoryUnavailable → returns None, no raise
session_api_failed_swallowed [error]: preset agent, fake raises ApiError(409) → wt.SessionApiFailed → returns None, no raise
transport_error_swallowed [error]: preset agent, fake raises SDK ConnectFailed → returns None, no raise
unexpected_exception_swallowed [error]: preset agent, write raises ValueError → returns None, no raise (INV-001 broad never-raise)
cancellation_propagates [error]: preset agent, write raises asyncio.CancelledError → RE-RAISED (never swallowed)
malformed_agent_id_no_write [adversarial]: agent_id=123 (non-str) OR "" → None; ZERO write; no raise
empty_session_id [adversarial]: session_id="" (preset agent) → None (soft guard); ZERO write; no raise