docs(canonical): add client-spec-v2 pin + sync worldtree conversation-api canonicals
Operator decision A (client-spec tracking): add a first-class pin for docs/conversation-api-client-spec-v2.md (v2.1, sha 1ff1d73a — the client-facing spec our #371 parity pass shaped), vendored as a consumer copy, tolerate_drift=true (prose reference; OpenAPI+SSE stay the gates). canonical_sync is whole-manifest (no per-pin scope), so it also brought two independently-drifted pins current: - OpenAPI hard-gate 36148179 (2.3.0) -> 1c801547. Additive-only within the FROZEN v1 wire (Worldtree #326) — no code/SDK breakage; the SDK targets 2.3.0 and still works against the newer additive surface. - conversation-api-spec.md (full prose) -> f6e964ec (v1.3+). canonical_drift now exits 0. FOLLOW-UP (non-urgent): the OpenAPI moved, so a coverage-map re-audit against the new surface would surface any new in-scope I/O points worth consuming (ratatoskr v1 = full WT I/O coverage). Release-only cadence: no tag.
This commit is contained in:
@@ -104,8 +104,8 @@ id = "worldtree-conversation-api-openapi-v2"
|
||||
canonical_source = "Worldtree"
|
||||
canonical_path = "docs/conversation-api-openapi.json"
|
||||
consumer_path = "docs/conversation-api-openapi.json"
|
||||
pinned_sha256_16 = "36148179601453a0"
|
||||
pinned_at = "2026-07-06T16:09:05+00:00"
|
||||
pinned_sha256_16 = "1c801547bbdbccc6"
|
||||
pinned_at = "2026-08-01T23:56:50+00:00"
|
||||
|
||||
[[pins]]
|
||||
id = "worldtree-conversation-api-sse-events-v1"
|
||||
@@ -120,10 +120,19 @@ 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 = "70aabcb30af6c615"
|
||||
pinned_at = "2026-07-18T18:51:00+00:00"
|
||||
pinned_sha256_16 = "f6e964ecbf3deec0"
|
||||
pinned_at = "2026-08-01T23:56:50+00:00"
|
||||
tolerate_drift = true # prose reference; OpenAPI+SSE are the gates
|
||||
|
||||
[[pins]]
|
||||
id = "worldtree-conversation-api-client-spec-v2"
|
||||
canonical_source = "Worldtree"
|
||||
canonical_path = "docs/conversation-api-client-spec-v2.md"
|
||||
consumer_path = "docs/conversation-api-client-spec-v2.md"
|
||||
pinned_sha256_16 = "1ff1d73a24e72d43"
|
||||
pinned_at = "2026-08-01T23:56:50+00:00"
|
||||
tolerate_drift = true # client-facing prose reference (our #371 parity pass shaped it); OpenAPI+SSE are the gates
|
||||
|
||||
# Worldtree persona render canons (d2) — the deterministic affect->NL the agent is
|
||||
# context-injected. The web persona pane renders mood + relationship-directive BYTE-EXACT
|
||||
# from these (via the flat src/ratatoskr/web/static/persona_render_canon.json, regenerated
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -950,7 +950,7 @@
|
||||
"info": {
|
||||
"description": "Multi-turn conversation interface for Worldtree agents.",
|
||||
"title": "Worldtree Conversation API",
|
||||
"version": "2.3.0"
|
||||
"version": "2.4.0"
|
||||
},
|
||||
"openapi": "3.1.0",
|
||||
"paths": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Worldtree Conversation API — Client Interface Specification
|
||||
|
||||
**Version:** 1.1
|
||||
**Version:** 1.3
|
||||
**Date:** 2026-07-18
|
||||
**Status:** Stable
|
||||
**Contract:** `docs/contracts/conversation_api.contract.md`
|
||||
@@ -1209,7 +1209,19 @@ Send a message and receive the agent's response as a Server-Sent Events (SSE) st
|
||||
|
||||
The stream emits JSON-encoded events. Each SSE `data:` line contains a JSON object with a `type` field.
|
||||
|
||||
**Errors:** `404` if session not found (returned before stream starts).
|
||||
**Content size (normative, spec 2.4.0):** `content` is bounded on **UTF-8
|
||||
byte length** by `message_content_max_bytes` (server config; **default
|
||||
262144 = 256 KiB — the normative spec number**). Over-cap content is
|
||||
rejected with an eager `422` carrying `error_code: content_too_long`
|
||||
*before* any SSE bytes are emitted (plain JSON error envelope, the
|
||||
pre-stream error path). Content that is not valid UTF-8 (lone surrogates)
|
||||
is a deterministic `422 validation_failed`. This cap is the number
|
||||
client-side read bounds derive from: a max-page transcript body is
|
||||
structurally ≤ 200 items × (cap + envelope overhead), and no single
|
||||
message-bearing SSE frame can exceed cap + envelope overhead.
|
||||
|
||||
**Errors:** `404` if session not found (returned before stream starts);
|
||||
`422 content_too_long` for over-cap content (see above).
|
||||
|
||||
---
|
||||
|
||||
@@ -1634,7 +1646,7 @@ Creating an Echo session requires the `instantiate:echo` scope. This scope is bu
|
||||
|
||||
Public primitive for downstream consumers (Skaldsong, RPG/game engines, dialog tools) that need to spin up dozens of distinct OCEAN-driven personas per session. Worldtree owns no durable state — the consumer ships character JSON, gets a `character_id`, and runs sessions against it. Persists in-memory only; process restart drops everything.
|
||||
|
||||
The character displaces the **persona + model** layer of the bound session. The `agent_id` (typically `actor`) still resolves system prompt, tools, and the LLM provider unless the character carries a `model` override.
|
||||
The character displaces the **persona + model-role** layer of the bound session. The `agent_id` (typically `mask`, renamed from `actor` at v0.29.9/#211) still resolves system prompt, tools, and the LLM provider unless the character carries a `role` override (a configured model-role per ADR-0012 — the raw `model` selector left this surface at the role cutover).
|
||||
|
||||
### `POST /characters`
|
||||
|
||||
@@ -1655,7 +1667,7 @@ Create a transient character. Requires `character.write` scope.
|
||||
"description": "A melancholy prince.",
|
||||
"narrative": "He speaks to himself when alone, weighs every choice three ways…",
|
||||
"voice_profile_block": "Formal English; iambic pentameter under pressure; weights every word.",
|
||||
"model": "fast"
|
||||
"role": "fast"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
@@ -1691,7 +1703,7 @@ Remove the character. Requires `character.write` scope. Sessions bound to this c
|
||||
|
||||
### `GET /models/available-for-characters`
|
||||
|
||||
Return the model profiles the actor (and the `model` field on `CharacterSchema`) can reference. Requires `character.read` scope.
|
||||
Return the catalog bindings the `character` model-role resolves to (what the `role` field on `CharacterSchema` can reach). Requires `character.read` scope.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1725,7 +1737,7 @@ When supplied, the session binds the character. The character's persona (OCEAN,
|
||||
- `410 character_not_found` — character was deleted/expired (returned at the session-bind path on next turn)
|
||||
- `422 ttl_too_large` — `ttl_seconds` over the configured ceiling
|
||||
- `422 state_schema_outdated` — schema_version mismatch; detail includes `accepted_versions`
|
||||
- `422 model_not_available_for_characters` — `model` field references a profile not in the allowlist
|
||||
- `404 model_not_available` — `role` field names an unconfigured model-role (resolved through the `character` role gate; the pre-cutover `model_not_available_for_characters` code is retired)
|
||||
- `422 validation_failed` — OCEAN out of `[-1, 1]`, voice_profile_block too long, etc.
|
||||
- `429 quota_exceeded` — per-user character cap hit
|
||||
- `403 auth_scope_denied` — missing `character.write` or `character.read`
|
||||
@@ -2404,24 +2416,24 @@ The `POST /sessions/{session_id}/messages` endpoint accepts an additive `upload_
|
||||
2. Each upload existence/ownership/expiry check — first failure short-circuits with `410 upload_expired` or `404`
|
||||
3. Agent capability gate — if `upload_ids` non-empty and `agent.capabilities` does not include `accepts_uploads` → `422 agent_lacks_upload_support`
|
||||
|
||||
### Per-call model override (issue #157)
|
||||
### Per-call role override (issue #157, role-keyed since #261/#344)
|
||||
|
||||
The `POST /sessions/{session_id}/messages` endpoint also accepts an additive `model` field that overrides the character / agent default for this turn only:
|
||||
The `POST /sessions/{session_id}/messages` endpoint also accepts an additive `role` field that overrides the character / agent default model-role for this turn only:
|
||||
|
||||
```json
|
||||
{"content": "Reformat this YAML.", "model": "granite-structured"}
|
||||
{"content": "Reformat this YAML.", "role": "assistant"}
|
||||
```
|
||||
|
||||
**Semantics:**
|
||||
- Override is per-call only. Stored `CharacterSchema.model` is NOT mutated.
|
||||
- Validated against the same `available_for_characters` allowlist that gates `CharacterSchema.model` at create time (#153 INV-091).
|
||||
- Override displaces the character's bound model when both are set (per-call wins).
|
||||
- Override is per-call only. Stored `CharacterSchema.role` is NOT mutated.
|
||||
- Resolved through the same `character` model-role gate that validates `CharacterSchema.role` at create time (ADR-0012).
|
||||
- Override displaces the character's bound role when both are set (per-call wins).
|
||||
- Override is REJECTED on ephemeral (Echo) sessions — their config is frozen at session-create per INV-161-2.
|
||||
|
||||
**Validation:**
|
||||
1. Pydantic validates `model`: optional string, non-empty after stripping whitespace.
|
||||
2. If the session is ephemeral (`ephemeral_config != None`): `422 validation_failed` ("per-call 'model' override is not permitted on ephemeral sessions").
|
||||
3. If `model` is not in the `available_for_characters` allowlist: `422 model_not_available_for_characters`.
|
||||
1. Pydantic validates `role`: optional string, non-empty after stripping whitespace.
|
||||
2. If the session is ephemeral (`ephemeral_config != None`): `422 validation_failed` ("per-call 'role' override is not permitted on ephemeral sessions").
|
||||
3. If `role` names an unconfigured model-role: `404 model_not_available`.
|
||||
|
||||
**Audit:** the `turn.started` admin event carries the effective `model` plus a `model_override_applied: bool` flag so cross-system audit trails can see what model actually ran on a per-turn basis.
|
||||
|
||||
@@ -2714,7 +2726,7 @@ below document the active layers and their exact validated shapes.
|
||||
| -------- | --------------------------------- | ------------------------------------------ |
|
||||
| `POST` | `/agents/define` | Create a Tier 3 agent. |
|
||||
| `DELETE` | `/agents/<user_id>:<agent_name>` | Owner-initiated hard-delete. |
|
||||
| `PATCH` | `/agents/<user_id>:<agent_name>` | Mutate `system_prompt` and/or `model`. |
|
||||
| `PATCH` | `/agents/<user_id>:<agent_name>` | Mutate `system_prompt` and/or `role`. |
|
||||
| `POST` | `/sessions` | Tier 3 routing when `agent_id` has a `:`. |
|
||||
| `GET` | `/sessions/<session_id>/tools` | Owner-scoped session-tools introspection (#183, Phase 2.0.1). |
|
||||
|
||||
@@ -2724,7 +2736,7 @@ below document the active layers and their exact validated shapes.
|
||||
{
|
||||
"agent_name": "wizard",
|
||||
"system_prompt": "You are a guided-elicitation wizard...",
|
||||
"role": "gen-reasoning", // REQUIRED — a configured model-role (#344), not a raw model id
|
||||
"role": "thoughtful-assistant", // 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}
|
||||
},
|
||||
@@ -2742,12 +2754,19 @@ Response (`201 Created`):
|
||||
"user_id": "alice",
|
||||
"agent_name": "wizard",
|
||||
"system_prompt": "...",
|
||||
"model": "glm5-turbo",
|
||||
"role": "thoughtful-assistant",
|
||||
"created_at": "2026-05-19T12:00:00+00:00",
|
||||
"updated_at": "2026-05-19T12:00:00+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
The response `role` echoes the requested role — symmetric with the request
|
||||
(W-4 closed, spec 1.2; the example above would echo
|
||||
`"role": "thoughtful-assistant"`). The resolved engine catalog id is
|
||||
privileged and never surfaces (#344 INV-344-3). A legacy pre-cutover row
|
||||
defined with a raw model (null stored role) surfaces that stored model
|
||||
string in this field.
|
||||
|
||||
Caller must:
|
||||
|
||||
- hold `agents.define` scope (default for `user` tier);
|
||||
@@ -2762,6 +2781,19 @@ after definition.
|
||||
The 201 response includes an advisory `warnings` array (#219) — see
|
||||
"Model-assignment warnings" under `PATCH` below.
|
||||
|
||||
**Duplicate name (normative, spec 2.4.0):** re-defining an existing
|
||||
`agent_name` returns `409` with `error_code: agent_name_taken` AND a
|
||||
structured `agent_id` field naming the existing agent
|
||||
(`{"detail": {"error_code": "agent_name_taken", "message": "...",
|
||||
"agent_id": "alice:wizard"}}`). Define-then-catch-409 is therefore the
|
||||
sanctioned, race-safe **define-or-reuse** shape: one round trip, no
|
||||
client-side id composition, no lookup-then-define TOCTOU window. (The
|
||||
`user_id` field on `GET /me` is also normative — guaranteed present on
|
||||
every response shape including anonymous and Heimdall-degraded — so
|
||||
client-side `<user_id>:<agent_name>` composition remains sanctioned for
|
||||
addressing, per the id shape documented above; the 409 field simply makes
|
||||
it unnecessary for the reuse flow.)
|
||||
|
||||
> **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
|
||||
@@ -2969,16 +3001,18 @@ session bound to this agent and revokes the owner's per-resource
|
||||
|
||||
#### `PATCH /agents/<user_id>:<agent_name>`
|
||||
|
||||
**Mutable surface (Phase 2.3, #188): `system_prompt` and/or `model` only.**
|
||||
PATCH re-enforces the same validation as define — the `system_prompt`
|
||||
byte-cap and the `model` allowlist. Any other key returns a 422 BEFORE
|
||||
the DB lookup (so an immutable-field PATCH against a missing agent still
|
||||
422s, not 404s), with the error code chosen by *why* the field can't be
|
||||
set:
|
||||
**Mutable surface (Phase 2.3, #188; role-keyed since #344): `system_prompt`
|
||||
and/or `role` only.** PATCH re-enforces the same validation as define — the
|
||||
`system_prompt` byte-cap, and `role` must name a configured model-role
|
||||
(unknown role → 404 `model_not_available`). Any other key returns a 422
|
||||
BEFORE the DB lookup (so an immutable-field PATCH against a missing agent
|
||||
still 422s, not 404s), with the error code chosen by *why* the field can't
|
||||
be set:
|
||||
|
||||
| Field(s) | Code | Reason |
|
||||
|---|---|---|
|
||||
| `agent_name`, `user_id`, `agent_id` | `field_not_mutable` | Identity — fixed at creation. |
|
||||
| `model` | `field_not_mutable` | Engine catalog ids left the surface at the role cutover (#344) — select via `role`. |
|
||||
| `persona`, `motivational` | `field_not_mutable` | Shipped traits; an agent *is* its personality/goals. Change → define a new agent. |
|
||||
| `memory` | `field_not_mutable` | Rejected **wholesale** — see below. |
|
||||
| `valence` | `layer_deferred` | Not a shipped layer yet (matches define-time); not a frozen trait. |
|
||||
@@ -2997,18 +3031,23 @@ whole field with `field_not_mutable`. When a real long-term-memory tuning
|
||||
dial ships, its PATCH semantics will be specified at that time.
|
||||
|
||||
**Active sessions are unaffected.** A PATCH never mutates an in-flight
|
||||
session's cached `AgentContext`; new `system_prompt` / `model` values take
|
||||
session's cached `AgentContext`; new `system_prompt` / `role` values take
|
||||
effect only at the next session-create.
|
||||
|
||||
**Audit.** A successful PATCH emits one `agents.patch` event whose
|
||||
`changes` detail records before/after per mutated field: `model` as literal
|
||||
`{before, after}` values, and `system_prompt` as `{before_bytes,
|
||||
`changes` detail records before/after per mutated field: a `role` change is
|
||||
recorded as its **derived catalog** `model` `{before, after}` pair (the
|
||||
audit log is a privileged surface; #344 INV-344-6 updates role + derived
|
||||
catalog id in one write), and `system_prompt` as `{before_bytes,
|
||||
after_bytes}` only — the raw prompt text is never written to the audit log
|
||||
(potential PII).
|
||||
|
||||
**Model-assignment warnings (#219).** A `model` swap is **not blocked** for
|
||||
capability or context-window compatibility, but PATCH (and `define`) attach an
|
||||
advisory `warnings` array to the response — see the shared subsection below.
|
||||
**Model-assignment warnings (#219).** A role swap is **not blocked** for
|
||||
capability or context-window compatibility. PATCH (and `define`) attach an
|
||||
advisory `warnings` array to the response — but a **role-defined agent
|
||||
always receives `[]`** (#344 INV-344-3: the warning texts name catalog-level
|
||||
engine details the role consumer didn't pick and can't act on); only legacy
|
||||
null-role rows can surface them. See the shared subsection below.
|
||||
Correctness for over-budget prompts remains the runtime `context_overflow`
|
||||
guard; the warnings are an early, best-effort heads-up.
|
||||
|
||||
@@ -3028,9 +3067,12 @@ two mutation responses, not on `GET /agents/<id>`. Each entry is
|
||||
|
||||
Semantics:
|
||||
|
||||
- **`define`** has no prior model, so only `model_context_window_unknown` can
|
||||
fire there. **PATCH** computes warnings only when the payload changes `model`
|
||||
(a `system_prompt`-only PATCH returns `warnings: []`); the comparison is
|
||||
- **Role-defined agents never receive these warnings** — `warnings` is
|
||||
always `[]` for them (#344 INV-344-3); the codes below can only surface on
|
||||
legacy null-role rows. **`define`** has no prior model, so only
|
||||
`model_context_window_unknown` can fire there. **PATCH** computes warnings
|
||||
only when the payload actually changes the underlying model (a
|
||||
`system_prompt`-only PATCH returns `warnings: []`); the comparison is
|
||||
against the resulting model.
|
||||
- Capability warnings are **conditional by nature**: a Tier 3 agent row does
|
||||
not record whether it uses tools/vision/audio (tools arrive per-session via
|
||||
|
||||
Reference in New Issue
Block a user