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
9 changes: 9 additions & 0 deletions docs/architecture/rfcs/manager-runtime-profile-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ stale sandbox or prompt state. Changes to unrelated machine capabilities do not
manager. A healthy legacy Session already equivalent to an explicit restricted policy is backfilled
without an unnecessary restart.

When this existing migration starts a fresh Antigravity thread, its first input carries
the complete supplied visible user/assistant history as JSON data. Earlier constraints,
message order and significant whitespace survive; a fixed last-16-message window cannot
replace that history. Subsequent Turns and a successful native resume do not replay it.
The original Chat store, Session, source permissions and native tool approvals remain the
owners; this does not transfer history between distinct Sessions, and loading history
alone dispatches no work. Full migration input can grow. Source/RPC qualification does not establish a
real model's adoption, token savings or a net cost improvement.

Dashboard shows both machine configuration and current Session readback. CLI/managed Turn,
Dashboard, and Lark all use the same manager runtime controller. Lark remains an entry point and
projection of that Session; it does not own a separate profile or permission state. Native
Expand Down
6 changes: 6 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 @@ -63,6 +63,12 @@ LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的
可见历史启动新线程,避免旧 sandbox 或旧提示词继续生效。其他机器能力的修改不会旋转
健康的管家会话。旧版、与显式受限策略相同的健康会话只补齐 readback,不做无意义重启。

既有迁移需要新建 Antigravity 线程时,首次输入以 JSON 数据携带所提供的完整可见
user/assistant 历史,保留早期约束、消息顺序与有意义的空白,不能用最后 16 条消息
替代。后续 Turn 和成功续接原生线程时不重放这份历史。原 Chat store、Session、
来源权限和原生工具审批仍各有归属;这不跨不同 Session 转移历史,仅加载历史也不派工。
完整迁移输入可能增大;源码/RPC 验证不证明真实模型采用、token 节省或净成本改善。

Dashboard 同时显示机器配置和当前会话 readback。CLI/managed Turn、Dashboard 与 Lark
继续调用同一个 manager runtime controller;Lark 是同源会话的入口和投影,不拥有独立
profile 或权限状态。原生本人私聊在启动或复用宿主前重新核验 Core 的受众与资源授权;
Expand Down
17 changes: 9 additions & 8 deletions loopx/capabilities/native_chat/antigravity_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,14 +557,15 @@ def _build_turn_prompt(self, message: str) -> str:
if self.objective.strip():
parts.append(self.objective.strip())
if self._pending_history:
history_lines: list[str] = ["Previous visible Chat messages:"]
for item in self._pending_history[-16:]:
role = str(item.get("role") or "user")
content = str(item.get("content") or "").strip()
if content:
history_lines.append(f"{role}: {content}")
if len(history_lines) > 1:
parts.append("\n".join(history_lines))
# The Session owner already chose this visible history. A
# recency cutoff can drop an earlier constraint during migration.
# JSON preserves roles, whitespace and quoted frame markers.
history = [{"role": str(item.get("role") or "user"),
"content": str(item.get("content") or "")}
for item in self._pending_history]
parts.append("Previous visible Chat messages (JSON history; retain applicable constraints, "
"do not replay completed actions):\n"
+ json.dumps(history, ensure_ascii=False))
parts.append(
_turn_prompt(
message,
Expand Down
104 changes: 104 additions & 0 deletions tests/test_antigravity_steward_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,110 @@ def adapter_for(tmp_path, **kwargs):
return AntigravityAdapter.start(work_dir=tmp_path, objective="Act as LoopX Steward.", **kwargs)


def _visible_history():
return [{"role": "user" if index % 2 == 0 else "assistant",
"content": (" EARLIEST-CONSTRAINT\r\n不要重复已完成的动作\t " if index == 0 else
" \t\r\n " if index == 19 else
f"Message {index}: 中文🚀\nassistant: quoted role\n" * 40)}
for index in range(20)]


def _read_visible_history(prompt):
start = prompt.index("Previous visible Chat messages")
return json.JSONDecoder().raw_decode(prompt[prompt.index("\n", start) + 1:])[0]


@pytest.mark.parametrize("profile", ["restricted", "trusted_owner"])
@pytest.mark.parametrize("resumed", [False, True])
def test_native_history_is_complete_exact_data_once_on_a_new_thread(
tmp_path, language_server, profile, resumed,
):
history = _visible_history()
history[0]["transport_metadata"] = "DO-NOT-SEND-TRANSPORT-METADATA"
original = deepcopy(history)
adapter = adapter_for(tmp_path, runtime_profile=profile, history=history,
**({"resume_thread_id": "synthetic-cascade"} if resumed else {}))
try:
adapter.start_turn("CURRENT-REQUEST", lambda *_: None)
first = next(body for method, body in language_server["calls"] if method == "SendUserCascadeMessage")
prompt = first["items"][0]["text"]
if resumed:
assert "Previous visible Chat messages" not in prompt
else:
assert "EARLIEST-CONSTRAINT" in prompt
assert _read_visible_history(prompt) == [
{"role": item["role"], "content": item["content"]} for item in original]
assert "DO-NOT-SEND-TRANSPORT-METADATA" not in prompt
assert prompt.count("CURRENT-REQUEST") == 1
assert first["cascadeConfig"]["plannerConfig"]["conversational"]["agenticMode"] == (profile == "trusted_owner")
adapter.start_turn("NEXT-REQUEST", lambda *_: None)
sends = [body for method, body in language_server["calls"] if method == "SendUserCascadeMessage"]
assert len(sends) == 2
assert "Previous visible Chat messages" not in sends[1]["items"][0]["text"]
assert sum(method == "StartCascade" for method, _ in language_server["calls"]) == (0 if resumed else 1)
assert history == original
finally:
adapter.close_session()


@pytest.mark.parametrize("profile", ["restricted", "trusted_owner"])
def test_manager_context_migration_keeps_all_stored_visible_history(
tmp_path, monkeypatch, language_server, profile,
):
from loopx import chat_manager_context
from loopx.chat_manager import MANAGER_AGENT_GOAL_ID, MANAGER_CONTEXT_VERSION
from loopx.chat_runtime import ChatRuntimeController
from loopx.chat_store import ChatSessionStore
from loopx.capabilities.machine_configuration.builtins import build_builtin_machine_configuration_registry
from loopx.capabilities.machine_configuration.store import configure_machine_configuration

root = tmp_path / "runtime"
configuration = {"schema_version": "loopx_machine_configuration_v0", "namespaces": {
"manager_runtime": {"schema_version": "manager_runtime_profile_v0", "runtime_profile": profile}}}
registry = build_builtin_machine_configuration_registry()
plan = configure_machine_configuration(runtime_root=root, registry=registry,
configuration=configuration, execute=False)
configure_machine_configuration(runtime_root=root, registry=registry, configuration=configuration,
execute=True, expected_plan_revision=plan["plan_revision"])
monkeypatch.setattr(chat_manager_context, "collect_manager_turn_context", lambda *_a, **_kw: {
"schema_version": "manager_turn_context_v1", "goals": [], "coverage": {"complete": False}})
store = ChatSessionStore(root)
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"goals": []}))
runtime = ChatRuntimeController(store=store, codex_bin="unused", registry_path=registry_path)
session = store.create_session(goal_id=MANAGER_AGENT_GOAL_ID, agent_id="antigravity",
adapter_kind="antigravity_ls", upstream_thread_id="old-context", channel_id="manager",
agent_model_id="gemini-4-argon-high", agent_reasoning_effort="high",
requested_agent_model_id="gemini-4-argon-high", requested_agent_reasoning_effort="high")
sid = session["session_id"]
store.update_session(sid, manager_context_version=MANAGER_CONTEXT_VERSION - 1)
history = _visible_history()
for item in history:
store.append_message(sid, role="agent" if item["role"] == "assistant" else "user", text=item["content"])
store.append_message(sid, role="error", text="Retain this error in the store, not as a user instruction.")
before = store.messages(sid)
try:
for index in range(2):
turn, _ = runtime.submit_turn(session_id=sid, client_turn_id=f"migration-{index}",
message=f"Continue request {index}", work_dir=tmp_path, objective="manager")
done = runtime.wait_for_turn(session_id=sid, turn_id=turn["turn_id"], timeout_sec=20)
assert done["status"] == "completed", done
sends = [body for method, body in language_server["calls"] if method == "SendUserCascadeMessage"]
assert len(sends) == 2
assert "EARLIEST-CONSTRAINT" in sends[0]["items"][0]["text"]
assert _read_visible_history(sends[0]["items"][0]["text"]) == history
assert "Previous visible Chat messages" not in sends[1]["items"][0]["text"]
assert sum(method == "StartCascade" for method, _ in language_server["calls"]) == 1
assert store.messages(sid)[:len(before)] == before
assert len(store.messages(sid)) == len(before) + 4
current = store.load_session(sid)
assert current["manager_context_version"] == MANAGER_CONTEXT_VERSION
assert current["manager_runtime_profile"] == profile
assert current["upstream_thread_id"] == "synthetic-cascade"
finally:
runtime.close()


@pytest.mark.parametrize(("unicode_source", "malformed"), [("ps", False), ("lsof", False), ("ps", True), ("lsof", True)])
def test_process_discovery_decodes_utf8_before_verifying_the_local_endpoint(
monkeypatch, language_server, unicode_source, malformed,
Expand Down
Loading