class_name DmService extends RefCounted ## The client narrate loop (charter §2: the client owns the loop and consumes ## text). Posts the canon log to /dm/narrate through an injected transport, ## returns the prose to display and the [FACT:] tags to harvest. On ANY failure ## it returns an authored fallback line — never an error. It harvests facts ## only; the Narrator has no authority over disposition or moves, so those tags ## are dropped even if the model emits them (§6). narrate() does not mutate the ## log — the caller applies the returned facts, keeping the state write explicit. var _transport: DmTransport var _fallback: FallbackLibrary func _init(transport: DmTransport, fallback: FallbackLibrary) -> void: _transport = transport _fallback = fallback func narrate(canon_log: CanonLog) -> NarrateResult: var resp: DmResponse = await _transport.post_json( "/dm/narrate", {"canon_log": canon_log.to_dict()}) if not resp.transport_ok or resp.status != 200 \ or typeof(resp.body) != TYPE_DICTIONARY or not resp.body.has("prose"): return NarrateResult.fallback(_fallback.narrator_line()) var extracted := TagExtractor.extract(str(resp.body["prose"])) return NarrateResult.new(extracted["clean_text"], extracted["facts"], false)