merge: T61 per-turn event-lifecycle detection + completion promotion
This commit is contained in:
@@ -73,12 +73,15 @@ from sqlite3 import Connection
|
||||
|
||||
from chat.config import Settings
|
||||
from chat.eventlog.log import append_and_apply, append_event
|
||||
from chat.services.event_lifecycle import detect_event_transitions
|
||||
from chat.services.event_promotion import promote_completed_event
|
||||
from chat.services.interjection import detect_interjection
|
||||
from chat.services.memory_write import record_turn_memory_for_present
|
||||
from chat.services.multi_state_update import compute_state_updates_for_present
|
||||
from chat.services.prompt import assemble_narrative_prompt
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.events import list_active_events
|
||||
from chat.state.world import active_scene, get_chat
|
||||
from chat.web.pubsub import publish
|
||||
from chat.web.render import render_turn_html
|
||||
@@ -617,6 +620,67 @@ async def regenerate_assistant_turn(
|
||||
(new_assistant_event_id, original_interjection_event_id),
|
||||
)
|
||||
|
||||
# 10. Event-lifecycle detection (Phase 3, T61). Mirrors the post_turn
|
||||
# block: classify whether any active events transitioned in the
|
||||
# regenerated narrative and append the corresponding event_started /
|
||||
# event_completed / event_cancelled. ``promote_completed_event``
|
||||
# runs inline after a completion so promotion artifacts land in the
|
||||
# same regenerate path.
|
||||
#
|
||||
# Phase 3.5 follow-up: when a regenerate replaces a turn that had
|
||||
# already produced event transitions, those original transitions are
|
||||
# NOT undone here. The superseded ``assistant_turn`` group keeps its
|
||||
# prior ``event_started`` / ``event_completed`` events in the log
|
||||
# (they remain projected onto the events table). Phase 3.5 will add
|
||||
# an "undo lifecycle" step to roll back the prior transitions before
|
||||
# re-classifying the regenerated text. For v3 we accept that a
|
||||
# regenerate-after-completion will double-emit promotion artifacts
|
||||
# if the new text re-completes the same event — narratively rare,
|
||||
# and a true fix needs the lifecycle-undo pass.
|
||||
new_active_events = list_active_events(conn, chat_id)
|
||||
if new_active_events:
|
||||
lifecycle_decision = await detect_event_transitions(
|
||||
client,
|
||||
classifier_model=settings.classifier_model,
|
||||
narrative_text=new_text,
|
||||
active_events=new_active_events,
|
||||
timeout_s=settings.classifier_timeout_s,
|
||||
)
|
||||
for transition in lifecycle_decision.transitions:
|
||||
if transition.new_status == "active":
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="event_started",
|
||||
payload={
|
||||
"event_id": transition.event_id,
|
||||
"started_at": chat.get("time"),
|
||||
},
|
||||
)
|
||||
elif transition.new_status == "completed":
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="event_completed",
|
||||
payload={
|
||||
"event_id": transition.event_id,
|
||||
"completed_at": chat.get("time"),
|
||||
},
|
||||
)
|
||||
promote_completed_event(
|
||||
conn,
|
||||
event_id=transition.event_id,
|
||||
chat_id=chat_id,
|
||||
chat_clock_at=chat.get("time"),
|
||||
)
|
||||
elif transition.new_status == "cancelled":
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="event_cancelled",
|
||||
payload={
|
||||
"event_id": transition.event_id,
|
||||
"completed_at": chat.get("time"),
|
||||
},
|
||||
)
|
||||
|
||||
return new_text
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from chat.eventlog.log import append_and_apply, append_event
|
||||
from chat.services.addressee import detect_addressee
|
||||
from chat.services.background import SignificanceJob
|
||||
from chat.services.event_lifecycle import detect_event_transitions
|
||||
from chat.services.event_promotion import promote_completed_event
|
||||
from chat.services.interjection import detect_interjection
|
||||
from chat.services.memory_write import record_turn_memory_for_present
|
||||
from chat.services.multi_state_update import compute_state_updates_for_present
|
||||
@@ -67,6 +69,7 @@ from chat.services.scene_summarize import apply_scene_close_summary
|
||||
from chat.services.turn_parse import ParsedTurn, parse_turn
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.events import list_active_events
|
||||
from chat.state.world import active_scene, get_chat, get_container
|
||||
from chat.web.bots import get_conn
|
||||
from chat.web.kickoff import get_llm_client
|
||||
@@ -654,6 +657,76 @@ async def post_turn(
|
||||
)
|
||||
)
|
||||
|
||||
# 8a. Event-lifecycle detection (Phase 3, T61). Runs after the post-turn
|
||||
# classifier passes (memory write + state update + optional
|
||||
# interjection) and BEFORE scene-close detection. The classifier reads
|
||||
# ``primary_text`` against the chat's currently-active events and
|
||||
# returns a (usually empty) list of transitions. Each transition lands
|
||||
# an ``event_started`` / ``event_completed`` / ``event_cancelled``
|
||||
# event via ``append_and_apply`` so the events projection updates
|
||||
# synchronously. A completion is followed inline by
|
||||
# ``promote_completed_event`` so any structured artifacts the event
|
||||
# carries (knowledge_facts, relationship_change, acquired_objects)
|
||||
# land in state in the same turn — see chat/services/event_promotion.
|
||||
#
|
||||
# ``detect_event_transitions`` short-circuits when ``active_events``
|
||||
# is empty (per T52), so chats without active events don't pay a
|
||||
# classifier round-trip and existing fixtures need no extra canned
|
||||
# slots.
|
||||
active_events = list_active_events(conn, chat_id)
|
||||
if active_events:
|
||||
lifecycle_decision = await detect_event_transitions(
|
||||
client,
|
||||
classifier_model=settings.classifier_model,
|
||||
narrative_text=primary_text,
|
||||
active_events=active_events,
|
||||
timeout_s=settings.classifier_timeout_s,
|
||||
)
|
||||
for transition in lifecycle_decision.transitions:
|
||||
if transition.new_status == "active":
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="event_started",
|
||||
payload={
|
||||
"event_id": transition.event_id,
|
||||
"started_at": chat.get("time"),
|
||||
},
|
||||
)
|
||||
elif transition.new_status == "completed":
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="event_completed",
|
||||
payload={
|
||||
"event_id": transition.event_id,
|
||||
"completed_at": chat.get("time"),
|
||||
},
|
||||
)
|
||||
# Run promotion inline so the artifact-emitting events
|
||||
# (edge_update / manual_edit) land synchronously after
|
||||
# the completion. ``promote_completed_event`` is
|
||||
# synchronous (no await) and skips silently when the
|
||||
# event row's status isn't 'completed' — a safety net
|
||||
# for races, not expected to trigger in practice.
|
||||
promote_completed_event(
|
||||
conn,
|
||||
event_id=transition.event_id,
|
||||
chat_id=chat_id,
|
||||
chat_clock_at=chat.get("time"),
|
||||
)
|
||||
elif transition.new_status == "cancelled":
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="event_cancelled",
|
||||
payload={
|
||||
"event_id": transition.event_id,
|
||||
"completed_at": chat.get("time"),
|
||||
},
|
||||
)
|
||||
# Any other ``new_status`` value falls through silently —
|
||||
# the lifecycle service constrains the schema to the three
|
||||
# valid transitions, and a defensive no-op here keeps the
|
||||
# turn flow tolerant of unexpected outputs.
|
||||
|
||||
# 9. Scene-close detection (Plan §7.2, T26). Runs AFTER assistant_turn
|
||||
# and the optional interjection so the bots' responses are part of
|
||||
# the closing scene's final beat — closing before narrative would
|
||||
|
||||
Reference in New Issue
Block a user