Files
ratatoskr/docs/proposals/authored-message-injection.md
T
vh 7156b25957 docs(proposals): note assistant-first provider constraint (#347 rev 1.1 validation)
Worldtree accepted the v1 wire validation green; contract rev 1.1 folds all three
consumer flags. Flag #1 surfaced a real provider constraint: first-message makes
the assistant seq 0 -> vLLM/openai_compat tolerate it (sindra unaffected), but
Anthropic-family providers 400 on an assistant-first array. Consumer must gate
first-message on provider compat; provider-agnostic normalization deferred.
2026-07-05 17:10:44 -07:00

9.4 KiB

Proposal: Authored History Write (non-generating ledger seed)

From: ratatoskr-dev (reference Tier-3 consumer) To: worldtree-dev (Conversation API / engine owner) Status: Draft for scoping — pre-contract (heid-panel-reviewed 2026-07-05) Date: 2026-07-05

Motivation

Consumer apps need to write a turn into a session's history as the agent (or another author) without triggering a model generation — e.g. an authored opening/greeting, imported history, scripted narration. Ratatoskr's immediate driver is a SillyTavern-style first-message: a fixed authored opening that replaces the model-generated greeting and sets tone/tense/style by example.

This cannot be done client-side. Worldtree assembles context server-side, and the current API exposes no author-role write path: POST /sessions/{id}/messages's role is a model-role override (role: "assistant"404 "Unknown model role"), and assistant as an author-role exists only as a read-side /search filter. So a model-visible authored turn needs engine support.

The primitive (recentered)

The fundamental operation is write a turn into the session ledger WITHOUT generation. "Author" (who wrote it) is an attribute of that write, not the defining axis — so we name the operation, not the attribute:

Authored history write — persist a model-visible turn into a session's ledger: no generation, no lived-turn side-effects by default, provenance always set.

The design space is two independent axes; this primitive is one cell:

side-effects ON side-effects OFF
generation ON POST /messages (today)
generation OFF (future: affect replay) authored history write

First-message = one caller: author=assistant, at session-create, effects=none.

v1 use cases (narrowed)

  1. First-message / greeting (the driver).
  2. Append-only narrator / scripted / scene turns.
  3. Debug / test state injection (ratatoskr instrumentation).

Explicitly OUT of v1 — separate future primitives (share infra, not shape)

  • History import (batch) — atomic multi-turn seed with memory/trust policy + idempotency. A batch API, not a single POST.
  • Edit / regenerate — history mutation (replace / supersede / tombstone / audit), not injection.
  • Few-shot priming — likely context-assembly config (exemplar block), not fake ledger history.
  • Arbitrary mid-history insertion — a "rewrite-history" capability with explicit invalidation semantics.
  • Prefill / assistant-continuation (author + generate) and authored tool-result turns — noted; outside the seed-only contract.

Design decisions

1. Side-effects — DEFAULT OFF; bounded opt-in [operator-locked default; opt-in surface tightened by review]

Authored writes are inert by default: no affect appraisal (no PAD update), no memory write, no Bifrost/tool emission. Opt-in is a bounded enum, not loose booleans:

effects: "none" (default) | "memory_import"

Synthetic affect and Bifrost emission are deliberately not opt-in-able here — replaying affect for authored content is a separate primitive (the generation-OFF / side-effects-ON cell). Rationale: keep this one write-API from becoming a cross-subsystem mutation backdoor. Load-bearing for affect/memory consumers — ratatoskr instruments exactly these signals.

2. Author-role — distinct field, restricted set [rec]

  • New field author, distinct from the model-role role (the collision that 404s).
  • v1 roles: assistant (agent) + system (OOC / narrator). user is NOT injectable on this endpoint — model-visible spoofed user input is a consent / audit / abuse surface; deferred to the future import API under owner/service scope.
  • Nuance for the engine owner: author risks doing double duty — provenance ("who wrote it") vs rendering-role ("how it appears in assembled context"; an assistant turn renders as model output, a system turn as instruction). These likely want to be separable (a rendering/turn-class vs an authored_by provenance). Final shape is engine-owned (context assembly is yours) — but the concern is ours to raise, not punt.

3. Generation contract — seed-only, DISTINCT SUB-RESOURCE [position taken]

Authored writes never trigger generation. We take a position (not defer): a distinct sub-resource, e.g. POST /sessions/{id}/history, not a generate:false flag on POST /messages. Reasons: explicit-over-implicit (don't make "did generation happen?" a parameter — the same implicit-mode coupling that bit us with role); different response contract (no generation id, no SSE stream, no token usage); different error surface. Exact path is yours.

4. Provenance — structured, always present [rec, expanded]

Not a boolean. Every authored turn carries: the write actor (which consumer/caller injected it), the claimed author, injected-at vs claimed-original timestamps, trust/origin, and visibility flags (model-visible? user-visible? memory-eligible?). Available to admin/audit APIs even when not rendered to the model.

5. Positioning — append-only + create-time (v1) [revised: was arbitrary insertion]

v1 supports create-time seed and append-to-tail only. Arbitrary mid-history insertion is deferred: it breaks turn-numbering, stales existing embeddings, desyncs the affect timeline, and races in-flight generation — a separate future "rewrite-history" capability with explicit invalidation semantics.

Event / lifecycle contract — positions we take (consumer contracts we validate)

  • Default-off authored seed emits NO turn.started / done and NO Bifrost appraisal wire. Stated explicitly so instrumented consumers (us) don't read silence as failure.
  • Authored turns get a distinct lifecycle phase — propose seeded (or authored), NOT completed (which implies generation ran). Consumers filter/display by phase.
  • Idempotency keys required on authored writes (retries must not duplicate turns).
  • In-progress generation — authored writes are rejected or serialized while a session has an active generation (ordering safety).

Inherent property (documented, not a bug)

Indirect affect contamination. Even with effects:none, the next generated turn is appraised in the context of the authored turn — so an emotionally charged authored beat perturbs affect regardless of any flag. No flag prevents it; it is inherent. Consumers (ratatoskr especially, as the affect instrument) must not misattribute the resulting drift.

Genuinely engine-owned open questions

  • Exact endpoint path + field / enum names.
  • Model-visible provenance in assembled context — an engine-consistency call and a security one: an authored system / user turn indistinguishable from real input is a spoofing vector. Framed as security, not just rendering.
  • memory_import semantics when the future import API opts in (embedding, origin/trust tagging, retrieval ranking vs lived memory).
  • Auth/scope: we assume owner-only for v1; per-author-role restrictions (esp. system) TBD — confirm or correct.

Ratatoskr as reference consumer

First consumer: first-message (author=assistant, create-time, effects:none) in the web surface + debug seed in the CLI. We commit to validating the primitive — including the event-silence contract and the seeded phase — end-to-end against the reference planes.

Consumer integration constraint (engine-imposed — Worldtree #347)

The primitive is Heimdall-gated with hide-existence (a per-tenant policy decision — some tenants are never granted it, not a rollout stage). Ratatoskr's consumer side MUST tolerate per-tenant absence:

  • A granted tenant gets the sub-resource; an ungranted tenant sees 404 (not 403) — as if the feature never existed.
  • Treat 404 on the authored-history-write sub-resource as "feature absent for this tenant" → fall back gracefully (no authored first-message; the model-generated greeting), never surface it as an error or "denied."
  • Do NOT capability-probe or advertise-detect — the feature is deliberately undiscoverable in /capabilities for ungranted tenants (same hide-existence posture as the R27-V1A cross-owner pattern).

Provider constraint (first-message specifically). A create-time first-message makes the assistant turn seq 0. Assistant-first-tolerant providers (vLLM / openai_compat — what our Tier-3 characters, incl. sindra, run) accept it out of the box. Anthropic-family providers reject an assistant-first array ("first message must use the user role") → the next generation 400s. So the consumer must gate first-message on provider compatibility (or treat it as vLLM/openai_compat-only for v1). Sindra = openai_compat → unaffected; provider-agnostic normalization is a deferred engine follow-up.


This brief was cold-read-pressure-tested by a cross-frontier panel (Grok / Codex / GLM) before handoff; the v1 narrowing (append-only, bounded effects enum, edit/regenerate + import split out) and the positions-taken (sub-resource, event-silence, seeded phase, structured provenance, user-author restriction) are the triaged result.