diff --git a/loopx/capabilities/native_chat/antigravity_adapter.py b/loopx/capabilities/native_chat/antigravity_adapter.py
index 6bf7428478..b509ef9c18 100644
--- a/loopx/capabilities/native_chat/antigravity_adapter.py
+++ b/loopx/capabilities/native_chat/antigravity_adapter.py
@@ -756,13 +756,43 @@ def _run_turn(
delta_chunk = text[len(prev_text) :]
else:
delta_chunk = text
- emitted_text_by_step[step_idx] = text
latest_response_text = text
if delta_chunk and event_sink is not None:
+ if not prev_text:
+ combined_marker = stream_filter.marker_pending + delta_chunk.lstrip()
+ continues_control_tag = bool(
+ combined_marker
+ and (
+ combined_marker.startswith(CHAT_REVIEW_OPEN_TAG)
+ or CHAT_REVIEW_OPEN_TAG.startswith(combined_marker)
+ )
+ )
+ if (
+ continues_control_tag
+ and not combined_marker.startswith(CHAT_REVIEW_OPEN_TAG)
+ and step_status != "CORTEX_STEP_STATUS_DONE"
+ ):
+ continue
+ prior_texts = [
+ val
+ for idx, val in sorted(emitted_text_by_step.items())
+ if idx != step_idx and val
+ ]
+ if (
+ prior_texts
+ and not delta_chunk.startswith("\n")
+ and not continues_control_tag
+ ):
+ last_prior = prior_texts[-1]
+ if not last_prior.endswith("\n"):
+ delta_chunk = "\n\n" + delta_chunk
+ elif not last_prior.endswith("\n\n"):
+ delta_chunk = "\n" + delta_chunk
visible_delta = stream_filter.feed(delta_chunk)
if visible_delta:
emitted_visible_delta = True
event_sink("answer.delta", {"text": visible_delta})
+ emitted_text_by_step[step_idx] = text
elif step_type == "CORTEX_STEP_TYPE_ERROR_MESSAGE":
err_obj = step.get("errorMessage") or {}
err_msg = str(
diff --git a/tests/test_antigravity_steward_adapter.py b/tests/test_antigravity_steward_adapter.py
index 4438c5a13b..4eb6cfc095 100644
--- a/tests/test_antigravity_steward_adapter.py
+++ b/tests/test_antigravity_steward_adapter.py
@@ -728,3 +728,176 @@ def test_another_native_input_is_neither_adopted_nor_cancelled(tmp_path, languag
assert error.value.error_code == "upstream_transport_failure"
assert not language_server["cancelled"].is_set()
assert len([1 for method, _ in language_server["calls"] if method == "SendUserCascadeMessage"]) == 1
+
+
+def test_multistep_planner_responses_insert_paragraph_breaks_between_steps(tmp_path, language_server, monkeypatch):
+ frames = [
+ {
+ "at": 1.0,
+ "status": "CASCADE_RUN_STATUS_RUNNING",
+ "steps": [
+ {
+ "type": "CORTEX_STEP_TYPE_USER_INPUT",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 0}},
+ },
+ {
+ "type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 1}},
+ "plannerResponse": {
+ "response": "正在读取当前会话的最新消息内容。",
+ "toolCalls": [{"name": "read_file"}],
+ },
+ },
+ {
+ "type": "CORTEX_STEP_TYPE_VIEW_FILE",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 2}},
+ },
+ ],
+ },
+ {
+ "at": 2.0,
+ "status": "CASCADE_RUN_STATUS_IDLE",
+ "steps": [
+ {
+ "type": "CORTEX_STEP_TYPE_USER_INPUT",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 0}},
+ },
+ {
+ "type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 1}},
+ "plannerResponse": {
+ "response": "正在读取当前会话的最新消息内容。",
+ "toolCalls": [{"name": "read_file"}],
+ },
+ },
+ {
+ "type": "CORTEX_STEP_TYPE_VIEW_FILE",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 2}},
+ },
+ {
+ "type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 3}},
+ "plannerResponse": {
+ "response": "最终结论:首读展开边界合理。\n\n"
+ + json.dumps({"message": "", "proposals": [], "context_handoff": [], "gate": None})
+ + "",
+ "stopReason": "STOP_REASON_STOP_PATTERN",
+ },
+ },
+ ],
+ },
+ ]
+ _native_clock(monkeypatch, language_server, frames)
+ events: list[tuple[str, dict[str, Any]]] = []
+ result = adapter_for(tmp_path, runtime_profile="trusted_owner").start_turn(
+ "检查首读体积", lambda kind, data: events.append((kind, data))
+ )
+ streamed = "".join(data.get("text", "") for kind, data in events if kind == "answer.delta")
+ assert streamed == "正在读取当前会话的最新消息内容。\n\n最终结论:首读展开边界合理。\n\n"
+ assert result["message"] == "最终结论:首读展开边界合理。"
+
+
+def test_multistep_planner_responses_preserve_split_control_marker_across_steps(
+ tmp_path, language_server, monkeypatch
+):
+ from loopx.extensions.lark.private_progress import project_progress
+
+ envelope_tail = (
+ "json>"
+ + json.dumps(
+ {
+ "message": "",
+ "proposals": [],
+ "context_handoff": [],
+ "gate": {"kind": "PROTOCOL_ONLY_SENTINEL", "summary": "PROTOCOL_ONLY_SENTINEL"},
+ }
+ )
+ + ""
+ )
+ frames = [
+ {
+ "at": 1.0,
+ "status": "CASCADE_RUN_STATUS_RUNNING",
+ "steps": [
+ {
+ "type": "CORTEX_STEP_TYPE_USER_INPUT",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 0}},
+ },
+ {
+ "type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ "status": "CORTEX_STEP_STATUS_DONE",
+ "metadata": {"sourceTrajectoryStepInfo": {"stepIndex": 1}},
+ "plannerResponse": {
+ "response": "首段安全可见输出。