fix(conductor): restore standalone model selection

This commit is contained in:
Liang Jiaqing committed 2026-08-24 10:09:15 +08:00
1 parent fb91ec22fa
commit ffe97347df
3 files changed
+102 -4

No files matched your search

+6 -1
View File
@@ -540,7 +540,12 @@
messagesEl.innerHTML=''; (data.chat||[]).forEach(addMsg);
renderLog(data.log||[]);
llmEl.innerHTML=(data.llms||[]).map(x=>`<option value="${x[0]}">${esc(x[1])}</option>`).join(''); llmEl.value=data.llm; llmEl.disabled=!llmEl.options.length;
} else if(data.type==='subagents'){ renderCards(data.items||[]); }
} else if(data.type==='model_selected'){ llmEl.value=String(data.llm); }
else if(data.type==='model_error'){
if(data.llm!==undefined) llmEl.value=String(data.llm);
showLimitToast(data.error||'Failed to select model');
}
else if(data.type==='subagents'){ renderCards(data.items||[]); }
else if(data.type==='chat'){ addMsg(data.item); }
else if(data.type==='chat_read'){ markAllRead(); }
else if(data.type==='log'){ appendLog(data.item); }
+52 -3
View File
@@ -1,5 +1,6 @@
import os, sys, re, time, json, uuid, queue, asyncio, threading, argparse, base64, secrets
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Any, Optional, List
from contextlib import asynccontextmanager, suppress
@@ -8,6 +9,10 @@ from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
FRONTENDS_DIR = os.path.dirname(os.path.abspath(__file__))
if FRONTENDS_DIR not in sys.path: sys.path.insert(0, FRONTENDS_DIR)
from desktop_settings import DesktopSettingsError, update_settings
def _resolve_ga_root() -> str:
"""Use the external core selected by the package-owned desktop bridge when valid."""
value = (os.environ.get("GA_ROOT", "") or "").strip()
@@ -67,15 +72,28 @@ HOST = args.host
PORT = args.port
SETTINGS_PATH = Path.home() / ".ga_desktop_settings.json"
def _settings_doc() -> dict:
try:
from pathlib import Path
doc = json.loads((Path.home() / ".ga_desktop_settings.json").read_text(encoding="utf-8"))
doc = json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
return doc if isinstance(doc, dict) else {}
except Exception:
return {}
def _persist_conductor_llm_no(llm_no: int) -> None:
def mutate(document: dict) -> None:
section = document.get("conductor")
if not isinstance(section, dict):
section = {}
document["conductor"] = section
section["llmNo"] = llm_no
update_settings(SETTINGS_PATH, mutate)
def _conductor_llm_no() -> Optional[int]:
"""Read the model index bound to the conductor session.
Falls back to the legacy desktop default ui.llmNo for existing installs."""
@@ -180,6 +198,24 @@ def _apply_desktop_model(agent: "GenericAgent") -> dict:
"fallbackReason": "no_models", "current": None}
def _selected_conductor_llm_no(agent: "GenericAgent") -> int:
configured = _conductor_llm_no()
return configured if _usable_model(agent, configured) else agent.llm_no
def _set_conductor_llm_no(agent: "GenericAgent", value: Any) -> int:
"""Persist the standalone UI binding without mutating an in-flight client.
The Conductor loop applies this binding at its existing idle task boundary, so
a selection made immediately before sending a message controls that next turn.
"""
no = _parse_model_no(value)
if no is None or not _usable_model(agent, no):
raise ValueError(f"llm index out of range or unavailable: {value}")
_persist_conductor_llm_no(no)
return no
def _select_llm(agent: "GenericAgent", llm: Any) -> bool:
if llm is None or str(llm).strip() == "": return False
q = str(llm).strip()
@@ -800,9 +836,22 @@ async def websocket(ws: WebSocket):
await ws.send_json({"type": "hello", "subagents": pool.snapshot(), "chat": chat_messages,
"log": conductor.log, "running": running,
"model": conductor.model_snapshot(),
"llms": conductor.agent.list_llms(), "llm": conductor.agent.llm_no})
"llms": conductor.agent.list_llms(),
"llm": _selected_conductor_llm_no(conductor.agent)})
while True:
data = await ws.receive_json()
if "llm" in data:
try:
llm_no = _set_conductor_llm_no(conductor.agent, data["llm"])
except (TypeError, ValueError, OSError, DesktopSettingsError) as error:
await ws.send_json({
"type": "model_error",
"error": str(error),
"llm": _selected_conductor_llm_no(conductor.agent),
})
else:
await broadcast({"type": "model_selected", "llm": llm_no})
continue
msg = (data.get("msg") or "").strip()
if not msg: continue
add_chat(msg, role="user", files=data.get("files") or [], images=data.get("images") or [])
@@ -164,6 +164,50 @@ def test_explicit_numeric_worker_model_rejects_out_of_range():
assert agent.next_llm_calls == []
def test_standalone_model_selection_persists_without_mutating_live_client(tmp_path, monkeypatch):
settings_path = tmp_path / "settings.json"
settings_path.write_text('{"theme":"dark","ui":{"llmNo":0}}', encoding="utf-8")
monkeypatch.setattr(conductor, "SETTINGS_PATH", settings_path)
agent = FakeAgent(["zero", "one", "two"])
selected = conductor._set_conductor_llm_no(agent, 2)
assert selected == 2
assert agent.llm_no == 0
assert agent.next_llm_calls == []
assert conductor._settings_doc() == {
"theme": "dark",
"ui": {"llmNo": 0},
"conductor": {"llmNo": 2},
}
def test_standalone_model_selection_rejects_unavailable_model(tmp_path, monkeypatch):
settings_path = tmp_path / "settings.json"
settings_path.write_text('{"theme":"dark"}', encoding="utf-8")
monkeypatch.setattr(conductor, "SETTINGS_PATH", settings_path)
agent = FakeAgent(["zero", None])
with pytest.raises(ValueError, match="unavailable"):
conductor._set_conductor_llm_no(agent, 1)
assert conductor._settings_doc() == {"theme": "dark"}
def test_standalone_model_selection_applies_at_next_task_boundary(tmp_path, monkeypatch):
settings_path = tmp_path / "settings.json"
monkeypatch.setattr(conductor, "SETTINGS_PATH", settings_path)
agent = FakeAgent(["zero", "one", "two"])
conductor._set_conductor_llm_no(agent, 2)
state = conductor._apply_desktop_model(agent)
assert agent.next_llm_calls == [2]
assert state["configured"] == 2
assert state["effective"] == 2
assert state["fallbackReason"] is None
def test_runtime_model_snapshot_is_broadcast_with_running_state(monkeypatch):
instance = conductor.Conductor()
payloads: list[dict] = []