From e643d38f582f96b3190f0bede9499c2171c48d6c Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 6 Jul 2026 09:56:46 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20persona=5Fstate=20SET=20body=20=E2=86=92?= =?UTF-8?q?=20canonical=20{pad:{pleasure,arousal,dominance}}=20+=20re-vend?= =?UTF-8?q?or=20Tier-3=20prose=20(v0.19.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit worldtree-dev landed the Tier-3 persona/memory/persona_state prose docs (c9e59ec) — shapes that serialize as freeform Any in the OpenAPI, so the prose markdown is their source of truth. Re-vendored docs/conversation-api-spec.md (tolerate_drift markdown pin; worldtree-spec-rev 879cefe→c9e59ec). Consumer alignment: --set-persona-pad / _set_persona_probe was building {pad:[list]}, but the canonical POST /sessions/{id}/persona_state body (#317) is {pad:{pleasure,arousal,dominance}} (named dict). Aligned the probe to the named dict + a len!=3 guard; updated contract #2's note, the set_persona_state docstring, and the tests. The set_persona_state wrapper was already correct (freeform pass-through) — only the CLI probe's body construction drifted. Suite 602 green. (Also this session: heid-code-review on the #347 slice returned unanimous zero drift across all three panel arms.) --- .corviduo-canonicals.toml | 4 +- docs/SPEC-PIN.md | 9 +- docs/contracts/issues/2.contract.md | 6 +- docs/conversation-api-spec.md | 167 ++++++++++++++++++++++++++-- persistent-memory.md | 2 + pyproject.toml | 4 +- src/ratatoskr/cli.py | 11 +- src/ratatoskr/sessions.py | 10 +- tests/test_cli.py | 13 ++- tests/test_sessions.py | 8 +- uv.lock | 2 +- 11 files changed, 208 insertions(+), 28 deletions(-) diff --git a/.corviduo-canonicals.toml b/.corviduo-canonicals.toml index c468a94..dc6ba67 100644 --- a/.corviduo-canonicals.toml +++ b/.corviduo-canonicals.toml @@ -120,8 +120,8 @@ id = "worldtree-conversation-api-spec-v1" canonical_source = "Worldtree" canonical_path = "docs/conversation-api-spec.md" consumer_path = "docs/conversation-api-spec.md" -pinned_sha256_16 = "2d111a3b8322b7d1" -pinned_at = "2026-06-30T22:25:56+00:00" +pinned_sha256_16 = "c656a789caceef14" +pinned_at = "2026-07-06T16:51:09+00:00" tolerate_drift = true # prose reference; OpenAPI+SSE are the gates # Worldtree persona render canons (d2) — the deterministic affect->NL the agent is diff --git a/docs/SPEC-PIN.md b/docs/SPEC-PIN.md index 0cc7e2a..96958fd 100644 --- a/docs/SPEC-PIN.md +++ b/docs/SPEC-PIN.md @@ -7,16 +7,17 @@ documents the pin, the vendored artifacts, and the bump procedure. | Field | Value | |---|---| -| Worldtree git SHA | `879cefef02bbf442c2d01ff4214299fac7c89809` | -| Worldtree HEAD message | `fix(#347): freeze the authored-history route into the OpenAPI schema (spec 2.3.0)` | +| Worldtree git SHA | `c9e59ec` | +| Worldtree HEAD message | `docs: document Tier-3 persona/memory schemas + persona_state SET body (OpenAPI 2.3.0)` | | Pinned on | 2026-07-06 | -| Pinned by | ratatoskr-dev (re-vendor OpenAPI 2.2.0→2.3.0 — #347 authored-history-write route shipped) | +| Pinned by | ratatoskr-dev (re-vendor prose markdown — Tier-3 persona/memory/persona_state consumer shapes) | | Worldtree version at pin | `v1.0.0b22` | ## Pin history | Date | SHA | Version | Notable deltas consumed | |---|---|---|---| +| 2026-07-06 | `c9e59ec` | v1.0.0b22 | **Re-vendor the prose markdown — Tier-3 consumer shapes documented.** `c9e59ec` (docs-only, OpenAPI byte-unchanged vs `879cefe`) adds `docs/conversation-api-spec.md` § "Tier 3 — Consumer-defined agents": the persona / memory / persona_state SET-body shapes that serialize as freeform `Any` in the OpenAPI (so prose is their source of truth). Drove a consumer fix: `--set-persona-pad` now sends the canonical `{pad:{pleasure,arousal,dominance}}` named dict (was `{pad:[list]}`) — #317, `v0.19.7`. Foot-guns encoded: persona.ocean single-letter `{O,C,E,A,N}` on `/agents/define` (spelled-out → 422, the #348 mismatch) vs spelled-out on `POST /characters`; memory `{embedder_version, tier3_dreaming}`, stm_* deprecated, allows_world_scope removed→422; only `valence` still 422s. `pin:`-only for the markdown; the `v0.19.7` bump rode the persona_state code fix. | | 2026-07-06 | `879cefe` | v1.0.0b22 | **Re-vendor OpenAPI 2.2.0→2.3.0 — Worldtree shipped #347 authored-history-write.** One new REST path-group: `POST /sessions/{session_id}/history` (the authored-history-write primitive) + the `AuthoredTurnResponse` schema (openapi path count 40→41). #347 is **OpenAPI-only** — the prose `conversation-api-spec.md` + server `conversation_api.contract.md` are byte-unchanged since the 5810a26 pin (empty `git log` delta), so those `tolerate_drift` pins stay clean; the SSE schema is unchanged (#347 is event-silent by design). **Consumer side NOT yet built** — `POST /sessions/{id}/history` is a fresh in-scope ⬜ gap in `docs/coverage-map.md` (re-opens the v1 coverage-audit with exactly one gap; Heimdall-gated hide-existence → consumer treats 404 as feature-absent). `pin:`-only, no version bump. | | 2026-06-30 | `5810a26` | v1.0.0b2 | **Re-pin to Worldtree's FROZEN v1 surface (#326), as part of the v1 coverage-audit.** Vendored the machine-readable artifacts — `conversation-api-openapi.json` (OpenAPI **2.2.0**, 40 path-groups) + `conversation-api-sse-events.schema.json` (11 events) — now the **authoritative drift gates** (pinned in `.corviduo-canonicals.toml`, CI-checked by `canonical_drift.py`). The prose `conversation-api-spec.md` is **byte-identical** to the v0.35.16 pin (last WT markdown edit 2026-05-31), kept as the human reference (`tolerate_drift`). b2 deltas already consumed in code: 409/503 eager turn-launch statuses (#331, v0.18.3/.4) + the unified error envelope (#328). 7 endpoints documented only in the OpenAPI, not the prose, all classified in `docs/coverage-map.md`: `admin/keys/bulk`, `admin/persona/{archive,erase}`, `admin/usage`, `embed`, `judgments`, `me/usage`. No client-breaking change — `pin:`-only, no version bump. | | 2026-06-17 | `f1b59f8` | v0.35.16 | **#297 + #298/#299 — Worldtree adopts the bifrost v0.6 scope wire (emits `scope_any`/`scope_all`) + client-side per-scope-value union recall. With our v0.17.6 provider this closes cold cross-session recall end-to-end.** Catch-up bump (v0.29.0→v0.35.16). Intervening client-facing deltas reviewed, none break our consumer: #211 agent rename (`saga`→`echo`, `actor`→`mask` — slugs only); #245 `end_user_id` persistence + memory-scope resolver; #187/#188/#219 Tier-3 define/PATCH policy (additive); `bifrost` binding field + `ephemeral_does_not_accept_bifrost` 422 now documented (the #17 surface). Error codes stable; no ratatoskr code change required. | @@ -32,7 +33,7 @@ documents the pin, the vendored artifacts, and the bump procedure. **Reference (prose; allowed to lag — `tolerate_drift`):** -- `docs/conversation-api-spec.md` — copy of `Worldtree/docs/conversation-api-spec.md` at the pinned SHA. The **client-facing prose narrative**. Byte-frozen at v0.35.16-era content (last WT edit 2026-05-31); the OpenAPI/SSE JSON above are the source of truth where they diverge. Pinned `worldtree-conversation-api-spec-v1` (tolerate_drift). +- `docs/conversation-api-spec.md` — copy of `Worldtree/docs/conversation-api-spec.md` at the pinned SHA. The **client-facing prose narrative**. Re-vendored at `c9e59ec` (2026-07-06) to carry the § "Tier 3 — Consumer-defined agents" subsections (persona/memory/persona_state SET body) that serialize as freeform `Any` in the OpenAPI JSON — so the **prose is the source of truth for those consumer shapes** (e.g. persona.ocean single-letter `{O,C,E,A,N}` on `/agents/define`; `POST /sessions/{id}/persona_state` body `{pad:{pleasure,arousal,dominance}}`). Elsewhere the OpenAPI/SSE JSON above remain authoritative. Pinned `worldtree-conversation-api-spec-v1` (tolerate_drift). - `docs/conversation_api.contract.md` — copy of `Worldtree/docs/contracts/conversation_api.contract.md` at the pinned SHA (byte-identical at b2 — server contract unchanged since the v0.35.16 pin). The **server-side contract** including INV-001..INV-052 and amendments. Useful for understanding load-bearing server invariants (e.g., INV-014 turn-id-public, INV-046 admin-events-envelope-stable, INV-049 admin-events-pii-discipline) when designing client behavior against them. Not in the canonical manifest (reference-only). Both files are vendored — they reflect Worldtree at the pinned SHA, not diff --git a/docs/contracts/issues/2.contract.md b/docs/contracts/issues/2.contract.md index 0031f17..1a86550 100644 --- a/docs/contracts/issues/2.contract.md +++ b/docs/contracts/issues/2.contract.md @@ -322,7 +322,9 @@ via a `--characters` one-shot lifecycle probe; persona-state write surfaced via wrappers: parsed dict verbatim (or None on 204), any off-status → SessionApiFailed. **Note:** `set_persona_state`'s request body is FREEFORM — the frozen OpenAPI 2.2.0 declares no request schema and the prose spec documents only the GET counterpart, -so the caller supplies the snapshot shape (`--set-persona-pad` sends `{pad:[…]}`). +so the caller supplies the snapshot shape. **Canonical (worldtree-dev prose #317, +`c9e59ec`): `{pad:{pleasure,arousal,dominance}}` — a named-key dict, NOT a list; +`--set-persona-pad` builds + sends the named dict (each float in [-1,1]).** ```contract FN list_character_models(client) -> dict[str, Any] @@ -369,7 +371,7 @@ POST: [POST-001 return_value] on 204 returns None; [POST-002 side_effect] outbou STEPS: 1. [sequential, prescriptive] resp = await client.post(f"/sessions/{session_id}/persona_state", json=snapshot); IF 204 RETURN None; ELSE RAISE SessionApiFailed TESTS: - happy [happy]: 204 → None; body == {"pad":[...]} verbatim + happy [happy]: 204 → None; body == {"pad":{"pleasure","arousal","dominance"}} verbatim (canonical named-key dict, #317) non_204 [error]: 422 → SessionApiFailed(422) ``` diff --git a/docs/conversation-api-spec.md b/docs/conversation-api-spec.md index fd41ccf..a646ab4 100644 --- a/docs/conversation-api-spec.md +++ b/docs/conversation-api-spec.md @@ -2699,10 +2699,13 @@ The `turn.started` event always carries `bifrost_override_applied: bool` (True/F Tier 3 agents are consumer-owned, Worldtree-hosted agents whose identity lives at `:`. They share the persistent -session infrastructure with Tier 1 / Tier 2 but layer-specific -machinery (persona, motivational, memory, valence) is reserved for -later phases — Phase 2.0 ships baseline addressing + ownership + -lifecycle only. +session infrastructure with Tier 1 / Tier 2. The layer-specific +machinery is now largely active: **`persona` (Phase 2.1, #186), +`memory` (Phase 2.1, #197), and `motivational` (Phase 2.2, #187) are +shipped and consumer-settable at define-time.** Only **`valence` remains +deferred** (non-null → 422 `layer_deferred`). Phase 2.0 shipped the +baseline addressing + ownership + lifecycle substrate; the subsections +below document the active layers and their exact validated shapes. ### Endpoints @@ -2720,11 +2723,13 @@ lifecycle only. { "agent_name": "wizard", "system_prompt": "You are a guided-elicitation wizard...", - "model": "glm5-turbo", - "persona": null, // schema-reserved; non-null → 422 layer_deferred - "motivational": null, - "valence": null, - "memory": null + "role": "gen-reasoning", // REQUIRED — a configured model-role (#344), not a raw model id + "persona": { // active (Phase 2.1) — single-letter OCEAN keys; see "Persona layer" + "ocean": {"O": 0.4, "C": 0.6, "E": -0.3, "A": 0.2, "N": 0.5} + }, + "motivational": null, // active (Phase 2.2) — see "Motivational layer" + "memory": null, // active (Phase 2.1) — see "Memory layer" + "valence": null // still deferred — non-null → 422 layer_deferred } ``` @@ -2756,6 +2761,150 @@ after definition. The 201 response includes an advisory `warnings` array (#219) — see "Model-assignment warnings" under `PATCH` below. +> **Vendoring note (OpenAPI 2.3.0).** In the frozen OpenAPI 2.3.0 document +> the `persona` / `motivational` / `memory` / `valence` request fields +> serialize as **untyped/freeform** — the `POST /agents/define` request +> model types them as `Any` so the layers can activate without a +> schema-breaking change. The shapes documented in the subsections below +> are the **authoritative, validator-enforced** schemas; generate client +> types from this section, not from the freeform OpenAPI fields. + +##### Persona layer (Phase 2.1, #186) + +`persona` is **active** as of Phase 2.1. It carries the agent's OCEAN +personality vector — the durable trait profile from which Worldtree +derives the mood setpoint (`baseline_pad`) and the mood dynamics +(gain + relaxation time-constants). Shape: + +```json +"persona": { + "ocean": { // REQUIRED — exactly these 5 keys, no more, no fewer + "O": 0.4, // Openness — float in [-1.0, 1.0] + "C": 0.6, // Conscientiousness + "E": -0.3, // Extraversion + "A": 0.2, // Agreeableness + "N": 0.5 // Neuroticism + }, + "behavioral_notes": "...", // optional, ≤ 4096 chars + "temperament_notes": "..." // optional, ≤ 4096 chars +} +``` + +**⚠ OCEAN key format — single-letter, uppercase.** The `/agents/define` +persona validator requires the `ocean` map to contain **exactly** the five +uppercase single-letter keys `O, C, E, A, N`. This is a deliberate, +load-bearing contrast with the transient-character primitive +(`POST /characters`), whose `ocean` block uses the **spelled-out** +lowercase keys (`openness`, `conscientiousness`, …). Sending spelled-out +keys to `/agents/define` returns 422 `persona_ocean_required` ("must +contain exactly the 5 keys O, C, E, A, N"). + +> **Fixed in v1.0.0b21 (#348).** Before that build a correctly +> single-letter-keyed persona was accepted and stored, but resolved to a +> **neutral** mood, because Worldtree's internal mood-derivation read the +> spelled-out key form. On v1.0.0b21+ an API-declared persona correctly +> drives the derived mood setpoint. If you observe neutral mood on a +> persona-defined agent, confirm the deployment is ≥ v1.0.0b21. + +**Range.** Each value is a float in `[-1.0, 1.0]` **signed** — `0.0` is the +population mean, NOT `[0.0, 1.0]`. Booleans are rejected. Out-of-range → 422 +`persona_ocean_out_of_range`. See [`docs/ocean-traits.md`](ocean-traits.md) +for the SOTA-grounded 5-band behavioural mapping. + +Semantics: + +- **Per-agent identity trait** — identical for every end-user and session; + immutable post-define (`PATCH {"persona": …}` → 422 `field_not_mutable`). + To change the OCEAN profile, delete and re-define the agent. +- **`extensions` is reserved** — the field exists but must be empty at v0.1; + a non-empty `extensions` returns 422 `layer_deferred`. +- **Sets the mood SETPOINT, not the current mood.** The OCEAN vector fixes + `baseline_pad` (the PAD point the mood relaxes toward over time); the + *current* per-session mood point is seeded separately via + `POST /sessions/{id}/persona_state` (below). + +Validation 422 codes: `persona_ocean_required` (missing `ocean`, or keys +≠ {O,C,E,A,N}), `persona_ocean_out_of_range` (a value outside [-1.0, 1.0], or +a boolean), `persona_notes_too_large` (a note > 4096 chars), `layer_deferred` +(non-empty `extensions`), `validation_failed` (unknown top-level field). + +##### `POST /sessions/{session_id}/persona_state` — seed the session mood point (Phase 2.1, #186/#189) + +Session-scoped mood seed. Sets the *current* PAD mood point for one +session's bound agent — the starting emotional state, distinct from the +OCEAN-derived setpoint the mood relaxes toward. Works on any +persona-enabled session (Tier 1 or Tier 3); most useful for a Tier 3 +durable-agent session that wants to start a conversation from a specific +mood. + +Request: + +```json +{ + "pad": { + "pleasure": 0.42, // float in [-1.0, 1.0] + "arousal": 0.25, + "dominance": 0.33 + } +} +``` + +Response: **`204 No Content`** — no body, no audit event (a session-scoped +runtime overlay, not a security-relevant event). + +Semantics: + +- **PAD-only** (#317 Option A). The body accepts exactly one key, `pad`, + which must carry all three of `pleasure` / `arousal` / `dominance`, each a + float in `[-1.0, 1.0]`. Any other top-level key → 422 `validation_failed`; + a missing or malformed `pad` → 422 `persona_seed_invalid`. +- **Seeds the current mood POINT, not the setpoint.** The OCEAN persona + (above) fixes the setpoint the mood relaxes toward; this endpoint sets + where the mood *starts*. It does not alter the persona. +- **Cross-owner sessions return 404** (existence-hiding — a session that + isn't yours is indistinguishable from one that doesn't exist). +- **Pull-over-push precedence (#289).** Once a session's baseline has been + rehydrated from an `affect.fetch` (the authoritative cross-session + source), a later SET seed is silently ignored — the fetched baseline wins. + +There is **no** `POST /agents/{id}/persona_state` — mood is per-session, not +a durable agent property. `GET /agents/{agent_id}/persona_state` +short-circuits to 404 for Tier-3 colon-ids: Tier-3 mood is observable only +over the Bifrost `affect.emit` egress (ADR-0009), never read back through +the HTTP API. + +##### Memory layer (Phase 2.1, #197) + +`memory` is **active** as of Phase 2.1 but exposes a deliberately minimal +surface — the short-term-memory (STM) tier was removed (#197), so the +historically-present `stm_*` knobs are accept-and-ignore no-ops. Shape: + +```json +"memory": { + "embedder_version": "", // optional; MUST equal the library-pinned version + "tier3_dreaming": false // optional bool, default false +} +``` + +Semantics: + +- **`embedder_version`** — optional. If supplied it MUST equal the library's + currently-pinned embedder version; a mismatch → 422 + `embedder_version_mismatch` (with `expected` / `received` in the detail). + Omit it to accept the pin. Fixed at define-time and library-pinned + thereafter. +- **`tier3_dreaming`** — optional bool (default `false`); opt-in flag for the + Tier-3 dreaming / consolidation path. +- **`stm_capacity` / `stm_token_budget`** — **deprecated no-ops.** Accepted at + define (201) with a `DeprecationWarning`; they carry no runtime effect since + the STM tier was removed, and are slated for rejection at the next schema + break. Do not send them in new integrations. +- **`allows_world_scope` — removed.** Sending it → 422 `validation_failed` + ("world-shared knowledge belongs in the KB/Mimir plane"). +- **Wholesale-immutable post-define.** `PATCH {"memory": …}` → 422 + `field_not_mutable` (even for the deprecated `stm_*` fields) — see the + PATCH table above. + ##### Motivational layer (Phase 2.2, #187) `motivational` is **active** as of Phase 2.2 (persona + memory activated in diff --git a/persistent-memory.md b/persistent-memory.md index 8906436..1470fbf 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -159,6 +159,8 @@ decision. Captures rationale that won't be obvious from code alone. - `[2026-07-06]` **OpenAPI re-vendored 2.2.0->2.3.0 (`75da676`, pin-only no bump).** worldtree-dev shipped #347 as spec 2.3.0 (`879cefe`, = the deployed personal b22 image); the SessionStart drift-check flagged our openapi pin STALE. `canonical_sync` pulled 2.3.0; updated the 4 pin-tracking files (`.corviduo-canonicals.toml`, vendored openapi.json, SPEC-PIN.md, pyproject `worldtree-spec-rev`->879cefe). #347 is OpenAPI-only (prose + server contract byte-unchanged, SSE unchanged=event-silent). The re-vendor re-opened the coverage-audit with one new in-scope path-group (the #347 route). - `[2026-07-06]` **#347 authored-history-write CONSUMER SIDE SHIPPED (`v0.19.6`) — direct in-session TDD.** `write_authored_history(client, session_id, *, content, idempotency_key, author="assistant", effects=None, claimed_original_at=None) -> dict` (POST /sessions/{id}/history; body server-pinned `AuthoredWriteRequest` extra="forbid" so omit null effects/claimed_original_at; 200-replay/201-fresh both -> ack dict; **404 -> `AuthoredHistoryUnavailable`** NOT SessionApiFailed = the hide-existence "feature-absent, never probe" contract; 409/422->SessionApiFailed) + `get_session_messages` (un-deferred GET /sessions/{id}/messages, the seed read-back proving model-invisible provenance) + a `--seed-first-message "" --agent ` one-shot probe (create session -> seed -> read-back; 404->benign feature-absent exit 0). Contract #2 amended (2 FNs, validated OK) + 19 tests (12 wrapper + 7 cli). Suite **601 green** (clean env; the 2 "fails" under `source env.sh` are the RATATOSKR_ADMIN_API_KEY env-leak into TestParseArgs, not a regression). Coverage: **REST 19/41** (`docs/coverage-map.md` re-converged). Patch bump (coverage tail; consistent w/ the Tier-2 v0.19.1 cadence). **Live-proof pending** the `session.history.write` grant (infra-ops `01KWW3KQEY`). heid-code-review NOT run (offered). +- `[2026-07-06]` **Tail-2 SHIPPED (`v0.19.7`) — Tier-3 prose docs re-vendored + persona_state body-shape aligned.** worldtree-dev landed the Tier-3 persona/memory/persona_state PROSE docs (`c9e59ec`, on origin) — they serialize as freeform `Any` in the OpenAPI JSON, so the **prose is their source of truth** (my earlier "2.3.0 = #347-only, tail-2 collapsed" was half-wrong: the JSON was #347-only but the prose is separate). Re-vendored `docs/conversation-api-spec.md` (markdown pin, tolerate_drift; `worldtree-spec-rev` 879cefe->c9e59ec, SPEC-PIN history row added). **Consumer fix:** `--set-persona-pad`/`_set_persona_probe` was sending `{pad:[list]}` but the canonical SET body (#317) is `{pad:{pleasure,arousal,dominance}}` (named dict) — aligned it + added a len!=3 guard; updated contract #2 note + set_persona_state docstring + tests. The `set_persona_state` WRAPPER was already correct (freeform pass-through); only the CLI probe drifted. TDD (probe test asserts the dict; +1 wrong-count test). Suite **602 green**, ruff clean. **heid-code-review on #347 (dispatched + returned this session): UNANIMOUS ZERO DRIFT** (Gróa/Hulda/Regin all confirmed the hide-existence 404->`AuthoredHistoryUnavailable` routing holds at wrapper/probe/test layers + the extra="forbid" body-omission + the deliberate write-vs-read 404 asymmetry — confirmation-not-discovery for a well-TDD'd slice against a prescriptive contract). worldtree-dev foot-guns banked in SPEC-PIN + [[reference_worldtree_affect_surface_map]]: ocean single-letter `{O,C,E,A,N}` on /agents/define (#348) vs spelled-out on /characters; memory `{embedder_version, tier3_dreaming}`, stm_* deprecated, allows_world_scope removed->422; only `valence` still 422s. + _41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._ _For per-issue TDD implementation notes, Volva findings, and contract amendments, see the git log — every per-issue commit carries a structured message capturing the trail._ diff --git a/pyproject.toml b/pyproject.toml index 88adc58..1758e73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.19.6" +version = "0.19.7" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" @@ -60,7 +60,7 @@ Repository = "https://gitea.phasefinal.com/vh/ratatoskr" # Ratatoskr is built against Worldtree at this commit; the vendored # spec snapshot in docs/ reflects that SHA. [tool.ratatoskr.spec-pin] -worldtree-spec-rev = "879cefef02bbf442c2d01ff4214299fac7c89809" +worldtree-spec-rev = "c9e59ec" worldtree-version = "v1.0.0b22" pinned-on = "2026-07-06" diff --git a/src/ratatoskr/cli.py b/src/ratatoskr/cli.py index 233c2d2..b5e40cf 100644 --- a/src/ratatoskr/cli.py +++ b/src/ratatoskr/cli.py @@ -740,9 +740,18 @@ async def _set_persona_probe(args: ParsedArgs) -> int: "(e.g. '0.4,0.1,-0.2')\n" ) return 10 + if len(pad) != 3: + sys.stderr.write( + "[usage_error] --set-persona-pad needs exactly 3 floats " + "(pleasure,arousal,dominance), e.g. '0.4,0.1,-0.2'\n" + ) + return 10 + # Canonical POST /sessions/{id}/persona_state body (#317): a named-key dict, + # NOT a bare list — {"pad": {"pleasure", "arousal", "dominance"}}. + snapshot = {"pad": {"pleasure": pad[0], "arousal": pad[1], "dominance": pad[2]}} async with _probe_client(args) as client: try: - await set_persona_state(client, args.session_id, {"pad": pad}) + await set_persona_state(client, args.session_id, snapshot) except SessionApiFailed as exc: sys.stderr.write(f"[session_api_failed] status={exc.status} body={exc.body!r}\n") return 20 diff --git a/src/ratatoskr/sessions.py b/src/ratatoskr/sessions.py index b9e3c82..80731fe 100644 --- a/src/ratatoskr/sessions.py +++ b/src/ratatoskr/sessions.py @@ -508,10 +508,12 @@ async def set_persona_state( ) -> None: """POST /sessions/{session_id}/persona_state — set a session's persona state (affect injection). - The request body is FREEFORM: the frozen OpenAPI 2.2.0 declares no request - schema and the prose spec documents only the GET counterpart — so the caller - supplies the snapshot shape (e.g. `{pad: [p, a, d]}`, mirroring the GET - `snapshot`). 204 No Content → None; any other status → SessionApiFailed. + The request body is FREEFORM on the wire (the OpenAPI declares no request + schema), but worldtree-dev's prose now pins the canonical shape (#317): + `{"pad": {"pleasure": p, "arousal": a, "dominance": d}}` — a named-key dict + (each in [-1, 1]), NOT a bare list; PAD-only, session-scoped, pull-over-push + (#289). The caller supplies the snapshot. 204 No Content → None; any other + status → SessionApiFailed. """ assert client is not None assert session_id and isinstance(session_id, str) diff --git a/tests/test_cli.py b/tests/test_cli.py index 2204c10..a66e445 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1692,7 +1692,18 @@ class TestTier2Probes: ) assert rc == 0 assert "persona_state set" in capsys.readouterr().out - assert _json.loads(route.calls[0].request.content) == {"pad": [0.4, 0.1, -0.2]} + # canonical POST /sessions/{id}/persona_state body: named-key dict, NOT a list + assert _json.loads(route.calls[0].request.content) == { + "pad": {"pleasure": 0.4, "arousal": 0.1, "dominance": -0.2} + } + + def test_set_persona_wrong_count(self) -> None: + """set_persona_wrong_count [adversarial]: not exactly 3 floats → exit 10, no HTTP.""" + rc = main( + ["--set-persona-pad", "0.4,0.1", "--session", "s1", + "--api-key", "k", "--server", "https://w.example"] + ) + assert rc == 10 class TestSeedFirstMessageProbe: diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 4352c9a..6c8d903 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -1182,9 +1182,13 @@ class TestSetPersonaState: return_value=httpx.Response(204) ) async with httpx.AsyncClient(base_url="https://w.example") as client: - result = await set_persona_state(client, "s1", {"pad": [0.4, 0.1, -0.2]}) + result = await set_persona_state( + client, "s1", {"pad": {"pleasure": 0.4, "arousal": 0.1, "dominance": -0.2}} + ) assert result is None - assert _json.loads(route.calls[0].request.content) == {"pad": [0.4, 0.1, -0.2]} + assert _json.loads(route.calls[0].request.content) == { + "pad": {"pleasure": 0.4, "arousal": 0.1, "dominance": -0.2} + } @respx.mock async def test_non_204_raises(self) -> None: diff --git a/uv.lock b/uv.lock index 624ef6a..f0d105f 100644 --- a/uv.lock +++ b/uv.lock @@ -1052,7 +1052,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.19.6" +version = "0.19.7" source = { editable = "." } dependencies = [ { name = "httpx" },