Skip to content

LIA

v1.20.10 Breaking

This release includes breaking changes for platform teams planning a safe upgrade.

✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

ai ai-assistant apple llm claude conversational-ai
+13 more
docker emotional-ai google healthcare-ai human-in-the-loop langchain langgraph mcp multi-agent personal-assistant self-hosted skills vision-ai

Summary

AI summary

Fixed ReAct mode leaks of internal reasoning scaffolding and duplicate final answer output.

Full changelog

Fixed — ReAct mode: the agent's internal reasoning no longer leaks into the reply, and the answer is no longer shown twice

In ReAct execution mode two regressions surfaced on the final answer. (1) The reply reproduced the agent's step-by-step scaffolding — e.g. "PLANOBSERVATIONCROSS-CHECKRÉPONSE FINALE …" — or the model adopted the agent's role outright ("Je vais chercher l'adresse… il me faut ton point de départ… appel outil") instead of just answering. (2) The final text was streamed twice on screen before collapsing back to a single copy.

Root cause of (1) was twofold and converged on response_node. First, react_setup_node accumulates the full react_agent_prompt — including its <Workflow> PLAN/ACT/OBSERVE/CROSS-CHECK loop and the "you operate in ReAct mode, call tools" role — as SystemMessages in state["messages"], and filter_for_llm_context() kept all non-__ SystemMessages, so this internal scaffolding reached the response LLM and the (weaker, high-temperature) model either mimicked the reasoning structure or impersonated the agent. Second, the ReAct loop's clean final answer is handed to the response synthesizer via agent_results[…]["data"]["react_synthesis"], but that entry carries no status key, so _format_status_messages() fell through to "❓ react_agent: Statut inconnu (unknown)" and the answer text was silently dropped — leaving the LLM with no authoritative reply to reformulate, so it reconstructed one from the polluted context. A controlled A/B (deepseek-v4-flash, temp 1.0) confirmed the trigger: with the ReAct scaffolding present in history the leak/role-confusion appeared in ~20 % of runs; with it removed, 0 %.

Root cause of (2): LangGraph stream_mode="messages" emits both the LLM's streaming token deltas (AIMessageChunk) and the complete post-processed AIMessage the node returns to the messages channel. The streaming layer forwarded both, so the whole reply was sent a second time and a post-loop content_replacement chunk then collapsed it — the "double then single" flash. The previous guard (checking content_final_replacement in state) could never fire for the current turn, because that flag is set by the response node only after its own tokens have already streamed (the values snapshot arrives after the messages deltas). Confirmed in prod logs: streamed length was consistently ≈ len(raw_content) + len(cleaned_content) (≈ 2×).

The fix has three parts, all reusing existing structures. filter_for_llm_context() now allowlists only the compaction-summary SystemMessage (matched via a new COMPACTION_SUMMARY_MARKER constant centralized in core/constants.py and reused by compaction_node / compaction_service, replacing three duplicated literals) and drops every other SystemMessage as internal scaffolding — the compaction summary is preserved because it is the response LLM's sole carrier of compacted history (the compaction_summary state field is not re-injected into the prompt). _format_status_messages() now surfaces data["react_synthesis"] (keyed by a new shared FIELD_REACT_SYNTHESIS constant, eliminating the writer/reader literal drift that caused the silent drop) as the authoritative answer. StreamingService._process_messages_chunk() streams token deltas only (AIMessageChunk) and skips the complete returned AIMessage once deltas have been seen, with a non-streaming fallback that still emits the complete message when no delta occurred; this replaces the timing-broken content_final_replacement guard. Confirmed in dev logs: response_duplicate_message_skipped{message_type="AIMessage"} fires and the double is gone.

Teststest_agent_results_react_synthesis.py (new: react_synthesis surfaced verbatim, turn filtering, no "Statut inconnu", empty-synthesis defence); test_message_filters.py (compaction summary kept, ReAct scaffolding dropped, Human/AI turns preserved, list-content SystemMessage dropped without crashing); test_streaming_service.py (delta streamed + flag set, complete message skipped after deltas, complete message emitted when no prior delta, consecutive deltas). 270+ unit tests green; Ruff / Black / MyPy clean.

Files: apps/api/src/domains/agents/utils/message_filters.py, apps/api/src/domains/agents/formatters/agent_results.py, apps/api/src/domains/agents/services/streaming/service.py, apps/api/src/core/constants.py, apps/api/src/core/field_names.py, apps/api/src/domains/agents/nodes/response_node.py, apps/api/src/domains/agents/nodes/compaction_node.py, apps/api/src/domains/agents/services/compaction_service.py, and the three test files above.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track LIA

Get notified when new releases ship.

Sign up free

Beta — feedback welcome: [email protected]