feat: server-side ui_meta on profiles.list/configure (#85440)
* feat: server-side ui_meta on profiles.list/configure Roster UIs built on profiles.* have per-profile presentation state (avatar, accent color, display title, pet) with nowhere server-side to live — client plugin storage paints a different roster on every machine. profiles.configure now accepts ui_meta (merged key-wise into profile.yaml's ui_meta block via the existing atomic_yaml_write path, null deletes a key, 64KB cap since it rides every roster paint) and profiles.list returns the block per row. Consumers namespace under their own key. No new files or config; profiles without the block are unchanged. * test: stop primary-runtime-restore tests probing live endpoints _make_agent left the compressor's lazy context-length resolution unmocked; for reachable base_urls (the nous portal test) the endpoint's 32K answer for the empty test model trips agent_init's 64K floor and fails the suite on network behavior. Pin get_model_context_length in the fixture.
This commit is contained in:
parent
8d4b1e4b0e
commit
2ffed55c32
|
|
@ -36,6 +36,15 @@ def _make_agent(fallback_model=None, provider="custom", base_url="https://my-llm
|
|||
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
|
||||
patch("run_agent.check_toolset_requirements", return_value={}),
|
||||
patch("run_agent.OpenAI"),
|
||||
# Unit tests must not probe live endpoints. The compressor resolves
|
||||
# context length lazily via a real network call against base_url; for
|
||||
# reachable hosts (the nous portal case) the endpoint's answer for the
|
||||
# empty test model (32K) trips agent_init's 64K floor and fails the
|
||||
# test on network behavior, not code under test.
|
||||
patch(
|
||||
"agent.context_compressor.get_model_context_length",
|
||||
return_value=200_000,
|
||||
),
|
||||
):
|
||||
agent = AIAgent(
|
||||
api_key="test-key-12345678",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,23 @@ def _(rid, params: dict) -> dict:
|
|||
}
|
||||
if include_sessions:
|
||||
row["last_session"] = _latest_profile_session_row(p.path)
|
||||
|
||||
# Client-agnostic UI metadata (avatars, accent colors, pinned
|
||||
# order, …) — stored server-side in profile.yaml so every
|
||||
# machine connecting to this gateway paints the same roster.
|
||||
try:
|
||||
import yaml as _yaml
|
||||
from pathlib import Path as _Path
|
||||
|
||||
meta_path = _Path(str(p.path)) / "profile.yaml"
|
||||
if meta_path.is_file():
|
||||
with open(meta_path, "r", encoding="utf-8") as f:
|
||||
raw_meta = _yaml.safe_load(f) or {}
|
||||
ui_meta = raw_meta.get("ui_meta")
|
||||
if isinstance(ui_meta, dict) and ui_meta:
|
||||
row["ui_meta"] = ui_meta
|
||||
except Exception:
|
||||
pass
|
||||
out.append(row)
|
||||
return _ok(rid, {"profiles": out})
|
||||
except Exception as e:
|
||||
|
|
@ -382,6 +399,50 @@ def _(rid, params: dict) -> dict:
|
|||
|
||||
applied = {}
|
||||
|
||||
if isinstance(params.get("ui_meta"), dict):
|
||||
# Client-agnostic UI metadata (avatar/pet/etc.), merged key-wise
|
||||
# into profile.yaml's ui_meta block. A key set to None deletes it.
|
||||
# Size-capped: this rides profiles.list on every roster paint, so
|
||||
# large blobs (e.g. raw base64 images) are rejected — persist big
|
||||
# assets elsewhere and store a reference.
|
||||
try:
|
||||
import json as _json
|
||||
|
||||
incoming = params["ui_meta"]
|
||||
if len(_json.dumps(incoming)) > 65536:
|
||||
applied["ui_meta"] = False
|
||||
else:
|
||||
import yaml as _yaml
|
||||
|
||||
meta_path = profile_dir / "profile.yaml"
|
||||
existing = {}
|
||||
if meta_path.is_file():
|
||||
try:
|
||||
with open(meta_path, "r", encoding="utf-8") as f:
|
||||
loaded = _yaml.safe_load(f) or {}
|
||||
if isinstance(loaded, dict):
|
||||
existing = loaded
|
||||
except Exception:
|
||||
existing = {}
|
||||
current = existing.get("ui_meta")
|
||||
if not isinstance(current, dict):
|
||||
current = {}
|
||||
for key, value in incoming.items():
|
||||
if value is None:
|
||||
current.pop(key, None)
|
||||
else:
|
||||
current[key] = value
|
||||
if current:
|
||||
existing["ui_meta"] = current
|
||||
else:
|
||||
existing.pop("ui_meta", None)
|
||||
from utils import atomic_yaml_write
|
||||
|
||||
atomic_yaml_write(meta_path, existing, sort_keys=False)
|
||||
applied["ui_meta"] = True
|
||||
except Exception:
|
||||
applied["ui_meta"] = False
|
||||
|
||||
if isinstance(params.get("soul"), str):
|
||||
try:
|
||||
(profile_dir / "SOUL.md").write_text(params["soul"], encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Reference in New Issue