Skip to content
Open
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
102 changes: 102 additions & 0 deletions tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8101,6 +8101,108 @@ def get_messages_as_conversation(self, key, include_ancestors=True, repair_alter
server._sessions.pop("sid", None)


def test_refine_uses_live_agent_and_persisted_history_without_spawning_worker(monkeypatch):
captured = {}

class _Agent:
valid_tool_names = {"memory", "skill_manage"}

def _spawn_background_review(self, **kwargs):
captured.update(kwargs)

class _DB:
def get_messages_as_conversation(self, key, include_ancestors=True, **_kwargs):
assert key == "session-key"
assert include_ancestors is True
return [
{"role": "user", "content": "persisted question"},
{"role": "assistant", "content": "persisted answer"},
]

class _ExplodingWorker:
def __init__(self, *_args, **_kwargs):
raise AssertionError("/refine must not run in the isolated slash worker")

server._sessions["sid"] = _session(
agent=_Agent(),
history=[{"role": "user", "content": "stale in-memory history"}],
slash_worker=None,
)
monkeypatch.setattr(server, "_SlashWorker", _ExplodingWorker)
monkeypatch.setattr(server, "_get_db", lambda: _DB())

try:
response = server.handle_request(
{
"id": "refine",
"method": "slash.exec",
"params": {
"command": "refine save the workflow",
"session_id": "sid",
},
}
)
finally:
server._sessions.pop("sid", None)

assert "Reviewing this conversation" in response["result"]["output"]
assert captured == {
"messages_snapshot": [
{"role": "user", "content": "persisted question"},
{"role": "assistant", "content": "persisted answer"},
],
"review_memory": True,
"review_skills": True,
"focus": "save the workflow",
}


def test_refine_forwards_to_compute_host_owner_without_spawning_worker(monkeypatch):
calls = []

class _ExplodingWorker:
def __init__(self, *_args, **_kwargs):
raise AssertionError("/refine must not run in the isolated slash worker")

session = _session(_compute_host_active=True)
session["agent"] = None
server._sessions["sid"] = session
monkeypatch.setattr(server, "_SlashWorker", _ExplodingWorker)
monkeypatch.setattr(server, "_session_uses_compute_host", lambda _session: True)

def send_control(*args, **kwargs):
calls.append((args, kwargs))
return {"type": "control.ack", "output": "host review started"}

monkeypatch.setattr(server, "_send_compute_host_control", send_control)

try:
response = server.handle_request(
{
"id": "refine-host",
"method": "slash.exec",
"params": {
"command": "refine save the workflow",
"session_id": "sid",
},
}
)
finally:
server._sessions.pop("sid", None)

assert response["result"]["output"] == "host review started"
assert calls == [
(
("sid",),
{
"route_name": "slash.refine",
"command": "/refine save the workflow",
"wait": True,
},
)
]


def test_prompt_submit_sets_approval_session_key(monkeypatch):
from tools.approval import get_current_session_key

Expand Down
49 changes: 49 additions & 0 deletions tests/tui_gateway/test_compute_host_phase1.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,61 @@ def test_mutator_route_table_matches_prd_inventory():
"slash.personality": "idle-gated",
"slash.prompt": "idle-gated",
"slash.compress": "idle-gated",
"slash.refine": "idle-gated",
"session.reset": "idle-gated",
"session.history.reload": "idle-gated",
"slash.retry": "idle-gated",
}


def test_compute_host_refine_control_uses_host_owned_agent(monkeypatch):
captured = {}

class _Agent:
valid_tool_names = {"memory", "skill_manage"}

def _spawn_background_review(self, **kwargs):
captured.update(kwargs)

session = {
"agent": _Agent(),
"session_key": "host-session",
"history": [
{"role": "user", "content": "host question"},
{"role": "assistant", "content": "host answer"},
],
"history_lock": threading.Lock(),
"history_version": 2,
"running": False,
}
monkeypatch.setattr(server, "_sessions", {"sid": session})
monkeypatch.setattr(server, "_get_db", lambda: None)
stdout = io.StringIO()
host = ComputeHost(stdout=stdout, heartbeat_secs=0)
try:
host._handle_control(
{
"type": "control",
"sid": "sid",
"request_id": "refine",
"route_name": "slash.refine",
"command": "/refine save the workflow",
}
)
finally:
host.close()

ack = _json_lines(stdout)[-1]
assert ack["type"] == "control.ack"
assert "Reviewing this conversation" in ack["output"]
assert captured == {
"messages_snapshot": session["history"],
"review_memory": True,
"review_skills": True,
"focus": "save the workflow",
}


def test_append_log_record_single_write_lines(tmp_path):
path = tmp_path / "agent.log"

Expand Down
8 changes: 7 additions & 1 deletion tui_gateway/compute_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,13 @@ def _handle_control(self, frame: dict[str, Any]) -> None:
return
command = str(frame.get("command") or "")
output = ""
if command:
if route_name == "slash.refine":
parts = command.lstrip("/").split(maxsplit=1)
focus = parts[1] if len(parts) > 1 else ""
output = server._live_slash_command_output(
sid, session, "refine", focus
) or ""
elif command:
output = server._mirror_slash_side_effects(sid, session, command)
with session["history_lock"]:
messages = server._history_to_messages(list(session.get("history") or []))
Expand Down
1 change: 1 addition & 0 deletions tui_gateway/host_supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"slash.personality": "idle-gated",
"slash.prompt": "idle-gated",
"slash.compress": "idle-gated",
"slash.refine": "idle-gated",
"session.reset": "idle-gated",
"session.history.reload": "idle-gated",
"slash.retry": "idle-gated",
Expand Down
57 changes: 57 additions & 0 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12658,6 +12658,7 @@ def _model_picker_context(agent):
"history",
"models",
"prompt",
"refine",
"rename",
"status",
"usage",
Expand Down Expand Up @@ -12875,6 +12876,62 @@ def _live_slash_command_output(sid: str, session: Optional[dict], name: str, arg
if session is None:
return "No active agent -- send a message first."
return _format_live_prompt_output(session)
if name == "refine":
if session is None:
return "Refine unavailable (no session)."
if session.get("running"):
return "Agent is running — wait for the turn to finish, then /refine."
if _session_uses_compute_host(session):
command = "/refine" + (f" {arg}" if arg.strip() else "")
try:
ack = _send_compute_host_control(
sid,
route_name="slash.refine",
command=command,
wait=True,
)
except Exception as exc:
return f"compute-host slash.refine failed: {exc}"
if ack.get("type") in {"control.error", "error"}:
return str(ack.get("message") or "compute-host slash.refine failed")
_apply_compute_host_metadata_mirror(session, ack)
return str(ack.get("output") or "")
agent = session.get("agent")
if agent is None:
return "Nothing to refine yet — send a message first."

snapshot = []
session_key = str(session.get("session_key") or "")
if session_key:
try:
with _session_db(session) as db:
if db is not None:
snapshot = db.get_messages_as_conversation(
session_key, include_ancestors=True
)
except Exception:
logger.debug("failed to load persisted /refine transcript", exc_info=True)
if not snapshot:
with session["history_lock"]:
snapshot = list(session.get("history", []))
if not snapshot:
return "Nothing to refine yet — the conversation is empty."

review_skills = "skill_manage" in getattr(agent, "valid_tool_names", set())
try:
agent._spawn_background_review(
messages_snapshot=list(snapshot),
review_memory=True,
review_skills=review_skills,
focus=arg.strip() or None,
)
except Exception as exc:
return f"/refine failed to start: {exc}"
tail = f" (focus: {arg.strip()})" if arg.strip() else ""
return (
f"⚗ Reviewing this conversation in the background{tail} — "
"any memory/skill updates will be reported when done."
)
if name == "status":
response = _methods["session.status"]("status", {"session_id": sid})
if response.get("error"):
Expand Down
Loading