fix(stapp): own stream bubble inside fragment for cross-version rerun safety
This commit is contained in:
1 parent
b0d270394d
commit
30b24ad31d
1 file changed
+13
-17
+13
-17
@@ -434,20 +434,20 @@ if prompt:
|
||||
with st.chat_message("user"): st.markdown(prompt)
|
||||
_start_main_task(prompt)
|
||||
|
||||
# Stream hosts only when this session owns the queue.
|
||||
# Poll quickly while active; reduce idle renderer churn.
|
||||
_stream_fh = None
|
||||
if st.session_state.get('display_queue') is not None:
|
||||
with st.chat_message("assistant"):
|
||||
_stream_fh = st.container()
|
||||
elif agent.is_running:
|
||||
# Stream bubble is owned by the fragment below: a fragment atomically replaces
|
||||
# its *own* subtree on every rerun in all Streamlit versions, whereas writing
|
||||
# into a container created outside the fragment is version-dependent (pre-1.62
|
||||
# appends forever → duplicates; ≥1.62 resets/GCs → disappears). So never hoist
|
||||
# the host out of the fragment; paint the full desired state each tick.
|
||||
_owns_stream = st.session_state.get('display_queue') is not None
|
||||
if not _owns_stream and agent.is_running:
|
||||
st.chat_message("assistant").markdown(T("detached_running"))
|
||||
|
||||
@st.fragment(run_every=timedelta(seconds=1 if (_stream_fh is not None or agent.is_running or st.session_state.get('loop_enabled')) else 5))
|
||||
@st.fragment(run_every=timedelta(seconds=1 if (_owns_stream or agent.is_running or st.session_state.get('loop_enabled')) else 5))
|
||||
def _tick():
|
||||
"""Poll every second while active and every five seconds while idle."""
|
||||
# 1) Own stream: drain done, paint all_outputs
|
||||
if _stream_fh is not None:
|
||||
if _owns_stream:
|
||||
done = _poll_main_task()
|
||||
if done is not None:
|
||||
if done:
|
||||
@@ -466,16 +466,12 @@ def _tick():
|
||||
_dq = st.session_state.get("display_queue")
|
||||
steps = (list(((agent.all_outputs or [{}])[-1].get("outputs")) or [])
|
||||
if _dq is getattr(agent, "_current_queue", None) else [])
|
||||
# Streamlit ≥1.62: a fragment's outside-container wrapper resets to
|
||||
# index 0 on every fragment rerun and children not re-emitted are GC'd
|
||||
# (see runtime/fragment.py:_reset_outside_wrappers). Incremental appends
|
||||
# don't survive across ticks, so repaint *everything* every tick — frozen
|
||||
# expanders plus the live tail — into the single host container. Always
|
||||
# writing (even when steps is empty) also claims the slot on the initial
|
||||
# run, avoiding "could not reserve a stable position".
|
||||
live = re.sub(r'\**LLM Running \(Turn \d+\) \.\.\.\**\s*$', '',
|
||||
(steps[-1] if steps else '') or '').rstrip()
|
||||
with _stream_fh:
|
||||
# Idempotent repaint inside the fragment's own subtree: the whole
|
||||
# bubble (expanders + live tail) is re-emitted from state every tick,
|
||||
# so a rerun can neither drop nor duplicate elements.
|
||||
with st.chat_message("assistant"):
|
||||
for i in range(max(0, len(steps) - 1)):
|
||||
body = steps[i] or ''
|
||||
with st.expander(_step_title(body, i), expanded=False): st.markdown(body)
|
||||
|
||||
Reference in new issue
Block a user