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": "首段安全可见输出。