Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/architecture/rfcs/manager-runtime-profile-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ These defaults do not change ordinary Goal Chat models or grant other Agents too
Argon tiers must be offered by the local account and match the requested reasoning effort.
A missing original cascade fails resume without silently starting a replacement. Stop waits
for the native cascade to become idle; timeout or partial output is not successful completion.
During execution, native `IDLE` alone is not completion: it can appear between input, tool
and planner steps. LoopX keeps observing the original invocation without resending input;
completion requires a final answer with the normal stop marker and all covered current
steps to be done, matching native result recovery's completion requirement.
Transient idle does not extend the existing no-progress or hard deadline. Permission gates,
native errors, unverified stop reasons and another input retain their existing refusal paths.
- `trusted_owner` applies to the local private owner-manager and a native private steward
binding freshly verified by the existing Core conversation owner. That owner checks the
App, owner principal, private source, current resource grant and frozen Session identity.
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture/rfcs/manager-runtime-profile-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的
这些契约。Argon 档位必须由本地账号提供,并与请求的思考深度一致。原始 cascade 丢失
时续接失败,不静默创建替代会话。停止等待原生 cascade 回到 idle;超时或部分输出
不代表成功完成。
执行时,原生 `IDLE` 本身不代表完成:输入、工具与 planner 步骤之间也可能出现此状态。
LoopX 继续观察原调用,不重发输入;最终答案须带正常停止标记,且本次覆盖的全部步骤均
已完成才能收尾,与原生结果恢复的完成要求一致。短暂 idle 不延长既有无进展或硬截止
时间;权限等待、原生错误、未核验的停止原因及另一个输入仍沿既有拒绝路径处理。
- `trusted_owner` 适用于本地私有 Owner 管家和由既有 Core conversation owner 重新核验的
原生本人管家私聊。Core 核验 App、Owner 身份、私聊来源、当前资源授权和冻结的 Session
身份;受众证明本身不授予工具,仍按实际机器策略执行,包括显式受限选择。其他外部
Expand Down
13 changes: 12 additions & 1 deletion loopx/capabilities/native_chat/antigravity_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,16 @@ def _run_turn(
if saw_running_or_new_step and status == "CASCADE_RUN_STATUS_IDLE" and observed_total == total_steps:
final = current_steps[-1][1] if current_steps else {}
planner = final.get("plannerResponse") or {}
# Native IDLE can occur between input, tool and planner steps.
# Keep observing this invocation; only a finished answer after
# all current work settles can close it. The existing idle and
# hard deadlines, permission gates and cancellation still apply.
if (planner.get("stopReason") in {None, "STOP_REASON_STOP_PATTERN"}
and (final.get("type") != "CORTEX_STEP_TYPE_PLANNER_RESPONSE" or planner.get("toolCalls")
or (planner.get("stopReason") is None and not self._read_only_review)
or any(step.get("status") in {"CORTEX_STEP_STATUS_GENERATING", "CORTEX_STEP_STATUS_RUNNING"}
for _, step in current_steps))):
continue
if self._read_only_review:
inputs = [step for _, step in current_steps if step.get("type") == "CORTEX_STEP_TYPE_USER_INPUT"]
execution = (inputs[0].get("metadata") or {}).get("executionId") if len(inputs) == 1 else None
Expand All @@ -807,8 +817,9 @@ def _run_turn(
gate=None, error_code="upstream_transport_failure")
if (final.get("type") != "CORTEX_STEP_TYPE_PLANNER_RESPONSE"
or final.get("status") != "CORTEX_STEP_STATUS_DONE" or planner.get("toolCalls")
or any(step.get("status") != "CORTEX_STEP_STATUS_DONE" for _, step in current_steps)
or not str(planner.get("modifiedResponse") or planner.get("response") or "").strip()
or planner.get("stopReason") not in {None, "STOP_REASON_STOP_PATTERN"}):
or planner.get("stopReason") != "STOP_REASON_STOP_PATTERN"):
raise CodexChatAgentError("Antigravity ended without a verified final response; execution may have occurred.",
gate=None, error_code="upstream_transport_failure")
break
Expand Down
92 changes: 90 additions & 2 deletions tests/test_antigravity_steward_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,89 @@ def test_default_core_keeps_an_original_progressing_invocation_past_five_minutes
controller.close_session(sid)


@pytest.mark.parametrize("pause", ["before-input", "input", "planner", "command", "command-done", "earlier-command", "unconfirmed-answer", "tool-call"])
def test_transient_idle_does_not_finish_or_cancel_the_original_turn(
tmp_path, language_server, monkeypatch, pause,
):
from loopx.chat_runtime import ChatRuntimeController
from loopx.chat_store import ChatSessionStore

user = {"type": "CORTEX_STEP_TYPE_USER_INPUT", "status": "CORTEX_STEP_STATUS_DONE"}
planner = {"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE", "status": "CORTEX_STEP_STATUS_GENERATING",
"plannerResponse": {"thinking": "private native reasoning"}}
command = {"type": "CORTEX_STEP_TYPE_RUN_COMMAND", "status": "CORTEX_STEP_STATUS_GENERATING",
"runCommand": {"commandLine": "synthetic-work", "combinedOutput": ""}}
final = {"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE", "status": "CORTEX_STEP_STATUS_DONE",
"plannerResponse": {"response": "original task finished", "stopReason": "STOP_REASON_STOP_PATTERN"}}
paused = {
"before-input": [], "input": [user], "planner": [user, planner], "command": [user, command],
"command-done": [user, {**command, "status": "CORTEX_STEP_STATUS_DONE"}],
"earlier-command": [user, command, final],
"unconfirmed-answer": [user, {"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE", "status": "CORTEX_STEP_STATUS_DONE",
"plannerResponse": {"response": "intermediate answer"}}],
"tool-call": [user, {"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE", "status": "CORTEX_STEP_STATUS_DONE",
"plannerResponse": {"response": "working", "toolCalls": [{"name": "run_command"}]}}],
}[pause]
running = deepcopy(paused) or [user]
if not any(step["type"] == "CORTEX_STEP_TYPE_RUN_COMMAND" for step in running):
running.append(command)
completed = [{**step, "status": "CORTEX_STEP_STATUS_DONE"} for step in running]
if completed[-1] != final:
completed.append(final)
_native_clock(monkeypatch, language_server, [
{"at": 1, "steps": [], "status": "CASCADE_RUN_STATUS_RUNNING"},
{"at": 2, "steps": paused, "status": "CASCADE_RUN_STATUS_IDLE"},
{"at": 3, "steps": running, "status": "CASCADE_RUN_STATUS_RUNNING"},
{"at": 4, "steps": completed, "status": "CASCADE_RUN_STATUS_IDLE"},
])
if pause == "tool-call":
# Ordinary Chat is restricted. Exercise native tool continuation only
# through the existing trusted-owner provider profile.
result = adapter_for(tmp_path, runtime_profile="trusted_owner").start_turn("finish the task")
assert result["message"] == "original task finished"
assert language_server["now"] == 4
assert not language_server["cancelled"].is_set()
assert sum(method == "SendUserCascadeMessage" for method, _ in language_server["calls"]) == 1
return
store = ChatSessionStore(tmp_path / "runtime" / "chat")
controller = ChatRuntimeController(store=store, codex_bin="unused-codex")
session, _ = controller.open_session(goal_id="synthetic-goal", agent_id="antigravity", work_dir=tmp_path,
objective="Synthetic conversation", mode="new", channel_id="synthetic-argon-chat")
sid = session["session_id"]
try:
turn, _ = controller.submit_turn(session_id=sid, client_turn_id="one-original-input", message="finish the task",
work_dir=tmp_path, objective="Synthetic conversation")
result = controller.wait_for_turn(session_id=sid, turn_id=turn["turn_id"], timeout_sec=8)
assert result["status"] == "completed"
assert result["response"]["message"] == "original task finished"
assert language_server["now"] == 4
assert not language_server["cancelled"].is_set()
assert [m["text"] for m in store.messages(sid) if m["role"] == "agent"] == ["original task finished"]
assert sum(method == "SendUserCascadeMessage" for method, _ in language_server["calls"]) == 1
finally:
controller.close_session(sid)


@pytest.mark.parametrize("pending", ["command", "unconfirmed-answer"])
def test_transient_idle_cannot_reset_the_no_progress_deadline(tmp_path, language_server, monkeypatch, pending):
from loopx.chat_agent import CodexChatAgentError

user = {"type": "CORTEX_STEP_TYPE_USER_INPUT", "status": "CORTEX_STEP_STATUS_DONE"}
command = {"type": "CORTEX_STEP_TYPE_RUN_COMMAND", "status": "CORTEX_STEP_STATUS_GENERATING"}
if pending == "unconfirmed-answer":
command = {"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE", "status": "CORTEX_STEP_STATUS_DONE",
"plannerResponse": {"response": "intermediate answer"}}
_native_clock(monkeypatch, language_server, [
{"at": 1, "steps": [user, command], "status": "CASCADE_RUN_STATUS_IDLE"},
{"at": 302, "steps": [user, command], "status": "CASCADE_RUN_STATUS_IDLE"},
])
with pytest.raises(CodexChatAgentError) as error:
adapter_for(tmp_path, runtime_profile="trusted_owner").start_turn("finish the task")
assert error.value.error_code == "response_timeout"
assert language_server["cancelled"].is_set()
assert sum(method == "SendUserCascadeMessage" for method, _ in language_server["calls"]) == 1


@pytest.mark.parametrize("hard_limit,stagnant", [(300.0, False), (None, True)])
def test_explicit_deadline_and_default_no_progress_timeout_still_stop(tmp_path, language_server, monkeypatch, hard_limit, stagnant):
from loopx.chat_agent import CodexChatAgentError
Expand All @@ -603,14 +686,19 @@ def test_invalid_hard_timeout_creates_no_native_invocation(tmp_path, language_se


@pytest.mark.parametrize("reason", ["STOP_REASON_CLIENT_STREAM_ERROR", "unrecognized-native-stop"])
def test_idle_done_with_an_unverified_native_stop_never_becomes_success(tmp_path, language_server, monkeypatch, reason):
@pytest.mark.parametrize("pending", ["none", "generating", "tool-call"])
def test_idle_done_with_an_unverified_native_stop_never_becomes_success(tmp_path, language_server, monkeypatch, reason, pending):
from loopx.chat_agent import CodexChatAgentError
frames = _native_frames(stopped=reason)
frames[-1]["at"] = 1.0
if pending == "generating":
frames[-1]["steps"][-1]["status"] = "CORTEX_STEP_STATUS_GENERATING"
elif pending == "tool-call":
frames[-1]["steps"][-1]["plannerResponse"]["toolCalls"] = [{"name": "run_command"}]
_native_clock(monkeypatch, language_server, [frames[-1]])
events = []
with pytest.raises(CodexChatAgentError) as error:
adapter_for(tmp_path).start_turn("task", lambda kind, data: events.append((kind, data)))
adapter_for(tmp_path, runtime_profile="trusted_owner").start_turn("task", lambda kind, data: events.append((kind, data)))
assert error.value.error_code == "upstream_transport_failure"
assert not any(kind == "answer.final" for kind, _ in events)
assert len([1 for method, _ in language_server["calls"] if method == "SendUserCascadeMessage"]) == 1
Expand Down
Loading