mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
Converge manager out of product roles and drop dead inherit prefs.
Specialists-only binding/prewarm/MCP allowlists fold legacy manager aliases into generalist; stop persisting confirm/plan prefs and strip inherit from prompt signatures. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bc20b41ee2
commit
454c841af7
20 changed files with 131 additions and 290 deletions
|
|
@ -24,8 +24,8 @@ def _read_text(path: Path) -> str:
|
|||
|
||||
def _workspace_for_role(role: str) -> str:
|
||||
r = str(role or "").strip().lower()
|
||||
if r == "manager":
|
||||
return "main"
|
||||
if r in {"manager", "manager_self", "main"}:
|
||||
return "generalist"
|
||||
return r or "generalist"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from runtime.agents.specialists import (
|
|||
SpecialistProfile,
|
||||
normalize_specialist_id,
|
||||
agent_role_ids,
|
||||
MANAGER_AGENT_ID,
|
||||
specialist_ids,
|
||||
default_system_prefix_for_specialist,
|
||||
default_tool_tags_for_specialist,
|
||||
|
|
@ -196,7 +195,7 @@ def _build_executor_components(
|
|||
return active_model, active_mode
|
||||
return _build_chat_model_for_profile(bound_pid, allow_runtime_overrides=False)
|
||||
|
||||
manager_model, manager_mode = _pick_model_for_role(MANAGER_AGENT_ID)
|
||||
manager_model, manager_mode = _pick_model_for_role("generalist")
|
||||
specialist_models: dict[str, object] = {}
|
||||
specialist_modes: dict[str, str] = {}
|
||||
for sid in specialist_ids():
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ def specialist_ids() -> tuple[SpecialistId, ...]:
|
|||
|
||||
|
||||
def agent_role_ids() -> tuple[AgentRoleId, ...]:
|
||||
return (MANAGER_AGENT_ID, *specialist_ids())
|
||||
return specialist_ids()
|
||||
|
||||
|
||||
def expert_name_for_specialist(specialist_id: SpecialistId) -> str:
|
||||
|
|
@ -101,6 +101,8 @@ def model_role_for_specialist(specialist_id: SpecialistId) -> AgentRoleId:
|
|||
|
||||
def normalize_specialist_id(specialist_id: SpecialistId | None) -> SpecialistId:
|
||||
sid = (specialist_id or "").strip().lower()
|
||||
if sid in {"manager", "manager_self", "main", "comprehensive"}:
|
||||
return "generalist"
|
||||
if sid in _REMOVED_SPECIALIST_IDS:
|
||||
return "generalist"
|
||||
if sid in SPECIALISTS:
|
||||
|
|
@ -125,6 +127,11 @@ def parse_agent_profile_bindings(raw: str | None) -> dict[AgentRoleId, str]:
|
|||
return out
|
||||
if not isinstance(obj, dict):
|
||||
return out
|
||||
# Fold legacy manager profile into generalist when generalist empty.
|
||||
mgr = str(obj.get("manager") or "").strip()
|
||||
if mgr and not str(obj.get("generalist") or "").strip():
|
||||
obj = dict(obj)
|
||||
obj["generalist"] = mgr
|
||||
for rid in agent_role_ids():
|
||||
v = obj.get(rid)
|
||||
if v is None:
|
||||
|
|
|
|||
|
|
@ -171,7 +171,6 @@ def _tool_wire_settings_signature(store: Any) -> tuple[bool, str]:
|
|||
f"skill_rt={str(store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}",
|
||||
f"skill_disabled={str(store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}",
|
||||
f"bind_en={str(store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}",
|
||||
f"bind_inherit={str(store.get_setting('AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT') or '')}",
|
||||
]
|
||||
)
|
||||
return runtime_enabled, sig
|
||||
|
|
|
|||
|
|
@ -519,8 +519,7 @@ class OclawGateway:
|
|||
cache_key = (
|
||||
f"base={base_url}|skill_rt={str(self.store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}|"
|
||||
f"skill_disabled={str(self.store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}|"
|
||||
f"bind_en={str(self.store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}|"
|
||||
f"bind_inherit={str(self.store.get_setting('AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT') or '')}"
|
||||
f"bind_en={str(self.store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}"
|
||||
)
|
||||
now = time.time()
|
||||
with _SKILL_MANIFEST_CACHE_LOCK:
|
||||
|
|
|
|||
|
|
@ -8,14 +8,9 @@ from svc.persistence.assistant_store import get_assistant_store
|
|||
from runtime.agent_context import build_role_system_context
|
||||
from runtime.agents.specialists import discover_specialist_ids
|
||||
from runtime.direct_loop import tool_wire_freeze_status, warm_tool_wire_cache
|
||||
from runtime.skill_role_binding import SKILL_ROLE_BINDING_KEY
|
||||
from runtime.skills import workspace_skills_layout_signature
|
||||
from runtime.system_prompt import get_executor_prompt_static, warm_executor_prompt_cache
|
||||
from runtime.tools.catalog import default_registry
|
||||
from runtime.workspaces.experts import expert_workspace_signature_token, list_experts
|
||||
|
||||
_MANAGER_PREBUILD_CACHE_LOCK = threading.Lock()
|
||||
_MANAGER_PREBUILD_CACHE: dict[tuple[Any, ...], dict[str, Any]] = {}
|
||||
_RUNTIME_PREWARM_LOCK = threading.Lock()
|
||||
_RUNTIME_PREWARM_RUNNING = False
|
||||
_RUNTIME_PREWARM_LAST: dict[str, Any] = {
|
||||
|
|
@ -31,47 +26,6 @@ _RUNTIME_PREWARM_HISTORY: list[dict[str, Any]] = []
|
|||
_RUNTIME_PREWARM_HISTORY_LIMIT = 40
|
||||
|
||||
|
||||
def _manager_settings_signature(store: Any) -> tuple[str, ...]:
|
||||
keys = (
|
||||
"AIA_SKILL_RUNTIME_ENABLED",
|
||||
"AIA_SKILL_DISABLED_NAMES",
|
||||
"AIA_SKILL_ROLE_BINDING_ENABLED",
|
||||
"AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT",
|
||||
SKILL_ROLE_BINDING_KEY,
|
||||
"AIA_CHAT_SPECIALIST_FLAGS_JSON",
|
||||
)
|
||||
parts: list[str] = []
|
||||
for key in keys:
|
||||
try:
|
||||
val = str(store.get_setting(key) or "")
|
||||
except Exception:
|
||||
val = ""
|
||||
parts.append(f"{key}={val}")
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def _compact_line(text: str, *, limit: int = 80) -> str:
|
||||
s = " ".join(str(text or "").strip().split())
|
||||
if len(s) <= limit:
|
||||
return s
|
||||
return s[: max(0, limit - 1)] + "…"
|
||||
|
||||
|
||||
def _build_expert_desc_map() -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for row in list_experts():
|
||||
eid = str(row.get("id") or "").strip().lower()
|
||||
if not eid:
|
||||
continue
|
||||
files = row.get("files") if isinstance(row, dict) else {}
|
||||
if not isinstance(files, dict):
|
||||
continue
|
||||
desc = _compact_line(str(files.get("ROLE_SYSTEM.md") or ""), limit=80)
|
||||
if desc:
|
||||
out[eid] = desc
|
||||
return out
|
||||
|
||||
|
||||
def get_manager_prompt_prebuild(
|
||||
*,
|
||||
store: Any,
|
||||
|
|
@ -79,18 +33,8 @@ def get_manager_prompt_prebuild(
|
|||
base_url: str,
|
||||
memory_enabled: bool,
|
||||
) -> dict[str, Any]:
|
||||
cache_key = (
|
||||
str(base_url or "").strip(),
|
||||
bool(memory_enabled),
|
||||
expert_workspace_signature_token(),
|
||||
_manager_settings_signature(store),
|
||||
workspace_skills_layout_signature(),
|
||||
)
|
||||
with _MANAGER_PREBUILD_CACHE_LOCK:
|
||||
cached = _MANAGER_PREBUILD_CACHE.get(cache_key)
|
||||
if isinstance(cached, dict):
|
||||
return dict(cached)
|
||||
|
||||
"""Legacy stub: Manager prompt packing removed; return specialist catalog only."""
|
||||
del store, registry, base_url
|
||||
allowed_fixed = [str(x).strip().lower() for x in discover_specialist_ids() if str(x).strip()]
|
||||
if not allowed_fixed:
|
||||
allowed_fixed = ["generalist"]
|
||||
|
|
@ -99,38 +43,17 @@ def get_manager_prompt_prebuild(
|
|||
if "generalist" not in allowed_fixed:
|
||||
allowed_fixed.insert(0, "generalist")
|
||||
allowed_fixed_quoted = ", ".join([f'"{x}"' for x in allowed_fixed])
|
||||
|
||||
desc_by_id = _build_expert_desc_map()
|
||||
candidate_lines = [f"- {sid}: {desc_by_id.get(sid) or 'no description'}" for sid in allowed_fixed]
|
||||
dynamic_hint = (
|
||||
f"\n{chr(10).join(candidate_lines)}\n"
|
||||
"不要假设固定专家集合。"
|
||||
)
|
||||
manager_context = build_role_system_context(
|
||||
"manager",
|
||||
template_vars={"MANAGER_DYNAMIC_EXPERTS_HINT": dynamic_hint},
|
||||
)
|
||||
out = {
|
||||
"manager_context": manager_context,
|
||||
return {
|
||||
"manager_context": "",
|
||||
"allowed_fixed": tuple(allowed_fixed),
|
||||
"allowed_fixed_quoted": allowed_fixed_quoted,
|
||||
}
|
||||
with _MANAGER_PREBUILD_CACHE_LOCK:
|
||||
_MANAGER_PREBUILD_CACHE[cache_key] = dict(out)
|
||||
if len(_MANAGER_PREBUILD_CACHE) > 64:
|
||||
_MANAGER_PREBUILD_CACHE.clear()
|
||||
return out
|
||||
|
||||
|
||||
def warm_startup_prompt_prebuild(*, store: Any, registry: Any, base_url: str, memory_enabled: bool) -> dict[str, Any]:
|
||||
del memory_enabled
|
||||
t0 = time.perf_counter()
|
||||
manager_pack = get_manager_prompt_prebuild(
|
||||
store=store,
|
||||
registry=registry,
|
||||
base_url=base_url,
|
||||
memory_enabled=memory_enabled,
|
||||
)
|
||||
role_systems: dict[str, str] = {"manager": str(manager_pack.get("manager_context") or "")}
|
||||
role_systems: dict[str, str] = {}
|
||||
for sid in discover_specialist_ids():
|
||||
role_systems[str(sid)] = build_role_system_context(str(sid))
|
||||
role_warm = warm_executor_prompt_cache(
|
||||
|
|
@ -144,7 +67,7 @@ def warm_startup_prompt_prebuild(*, store: Any, registry: Any, base_url: str, me
|
|||
return {
|
||||
"ok": True,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"manager_candidates": int(len(manager_pack.get("allowed_fixed") or [])),
|
||||
"manager_candidates": 0,
|
||||
"roles_warmed": int(role_warm.get("roles_warmed") or 0),
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +110,7 @@ def run_runtime_prewarm(
|
|||
base_url=base_url,
|
||||
memory_enabled=bool(memory_enabled),
|
||||
)
|
||||
roles = ["manager", *list(discover_specialist_ids())]
|
||||
roles = list(discover_specialist_ids())
|
||||
tool_stats = warm_tool_wire_cache(
|
||||
store=own_store,
|
||||
tools=registry,
|
||||
|
|
@ -245,24 +168,16 @@ def runtime_prewarm_prompts_snapshot(
|
|||
own_store = store if store is not None else get_assistant_store()
|
||||
registry = default_registry(store=own_store)
|
||||
target = str(role or "").strip().lower()
|
||||
allowed_roles = ["manager", *list(discover_specialist_ids())]
|
||||
if target in {"manager", "manager_self", "main"}:
|
||||
target = "generalist"
|
||||
allowed_roles = list(discover_specialist_ids())
|
||||
if target and target not in allowed_roles:
|
||||
return {"ok": False, "error": "invalid_role", "allowed_roles": allowed_roles}
|
||||
selected_roles = [target] if target else allowed_roles
|
||||
|
||||
manager_pack = get_manager_prompt_prebuild(
|
||||
store=own_store,
|
||||
registry=registry,
|
||||
base_url=base_url,
|
||||
memory_enabled=memory_enabled,
|
||||
)
|
||||
prompts: dict[str, dict[str, Any]] = {}
|
||||
for rid in selected_roles:
|
||||
base_system = (
|
||||
str(manager_pack.get("manager_context") or "")
|
||||
if rid == "manager"
|
||||
else build_role_system_context(str(rid))
|
||||
)
|
||||
base_system = build_role_system_context(str(rid))
|
||||
executor_system = get_executor_prompt_static(
|
||||
store=own_store,
|
||||
tools=registry,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from runtime.agents.specialists import discover_specialist_ids
|
|||
|
||||
SKILL_ROLE_BINDING_KEY = "skill_role_binding"
|
||||
SKILL_ROLE_BINDING_ENABLED_SETTING = "AIA_SKILL_ROLE_BINDING_ENABLED"
|
||||
# Deprecated: Manager inherit removed; kept so old settings keys do not crash readers.
|
||||
SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING = "AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT"
|
||||
|
||||
|
||||
|
|
@ -22,7 +23,8 @@ def ordered_specialist_ids() -> list[str]:
|
|||
|
||||
|
||||
def ordered_binding_roles() -> list[str]:
|
||||
return ["manager", *ordered_specialist_ids()]
|
||||
"""Roles that appear in skill-binding UI/maps (specialists only; no manager)."""
|
||||
return ordered_specialist_ids()
|
||||
|
||||
|
||||
def skill_role_binding_enabled_env_present() -> bool:
|
||||
|
|
@ -80,24 +82,33 @@ def normalize_skill_role_binding(
|
|||
roles = available_roles if available_roles is not None else ordered_binding_roles()
|
||||
role_set = set(roles)
|
||||
out: dict[str, list[str]] = {r: [] for r in roles}
|
||||
for k, v in (mapping_raw or {}).items():
|
||||
rk = str(k or "").strip().lower()
|
||||
|
||||
def _append(rk: str, items: Any) -> None:
|
||||
if rk not in role_set:
|
||||
continue
|
||||
items = v if isinstance(v, list) else []
|
||||
seen: set[str] = set()
|
||||
for x in items:
|
||||
return
|
||||
rows = items if isinstance(items, list) else []
|
||||
seen = set(out[rk])
|
||||
for x in rows:
|
||||
nm = str(x or "").strip()
|
||||
if not nm or nm not in valid_skill_names or nm in seen:
|
||||
continue
|
||||
seen.add(nm)
|
||||
out[rk].append(nm)
|
||||
|
||||
for k, v in (mapping_raw or {}).items():
|
||||
rk = str(k or "").strip().lower()
|
||||
# Legacy Manager bindings fold into generalist.
|
||||
if rk == "manager":
|
||||
rk = "generalist"
|
||||
_append(rk, v)
|
||||
return out
|
||||
|
||||
|
||||
def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]:
|
||||
"""Union of manager-bound skills and skills bound to the given specialist role."""
|
||||
"""Skills bound to the given specialist role, plus public workspace skills."""
|
||||
r = str(role or "").strip().lower()
|
||||
if r == "manager":
|
||||
r = "generalist"
|
||||
if not r:
|
||||
return set()
|
||||
from runtime.skills import discover_public_workspace_skill_names
|
||||
|
|
@ -107,18 +118,8 @@ def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]
|
|||
mapping_raw=load_skill_role_binding_dict(store),
|
||||
valid_skill_names=_all_installed_skill_names(store),
|
||||
)
|
||||
try:
|
||||
raw_env = str(os.getenv(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip()
|
||||
if raw_env:
|
||||
inherit_mgr = _truthy(raw_env)
|
||||
else:
|
||||
raw = str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip()
|
||||
inherit_mgr = _truthy(raw) if raw else True
|
||||
except Exception:
|
||||
inherit_mgr = True
|
||||
mgr = {str(x).strip() for x in (mapping.get("manager") or []) if str(x).strip()} if inherit_mgr else set()
|
||||
sp = {str(x).strip() for x in (mapping.get(r) or []) if str(x).strip()}
|
||||
return mgr | sp | public
|
||||
return sp | public
|
||||
|
||||
|
||||
def _all_installed_skill_names(store: Any) -> set[str]:
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ def _executor_prompt_settings_signature(store: Any) -> tuple[str, ...]:
|
|||
"AIA_SKILLS_PROMPT_IN_SYSTEM",
|
||||
"AIA_SKILL_DISABLED_NAMES",
|
||||
"AIA_SKILL_ROLE_BINDING_ENABLED",
|
||||
"AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT",
|
||||
SKILL_ROLE_BINDING_KEY,
|
||||
"AIA_PROJECT_CONTEXT_MAX_FILE_CHARS",
|
||||
"AIA_PROJECT_CONTEXT_MAX_TOTAL_CHARS",
|
||||
|
|
|
|||
|
|
@ -230,10 +230,8 @@ def materialize_mcp_tools_for_specialist(
|
|||
return "mcp-remote" in joined and "/api/v1/mcps/webparser/sse" in joined
|
||||
|
||||
sp = str(specialist or "").strip().lower()
|
||||
if sp == "manager":
|
||||
# Manager is a first-class binding role in admin UI/config.
|
||||
# We keep it separate from generalist instead of aliasing.
|
||||
sp = "manager"
|
||||
if sp in {"manager", "manager_self", "main"}:
|
||||
sp = "generalist"
|
||||
# Preferred mapping: specialist -> server_ids
|
||||
binding_server_ids: set[str] | None = None
|
||||
try:
|
||||
|
|
@ -243,6 +241,9 @@ def materialize_mcp_tools_for_specialist(
|
|||
obj = json.loads(raw_binding)
|
||||
if isinstance(obj, dict):
|
||||
rows = obj.get(sp)
|
||||
# Legacy: if this specialist has no key, try manager bindings for generalist.
|
||||
if rows is None and sp == "generalist" and "manager" in obj:
|
||||
rows = obj.get("manager")
|
||||
# 缺键或 null:视为未配置该专家的绑定 → 走下方「仅 coarse allowlist」逻辑(可见全部已启用 MCP)。
|
||||
# 仅当键存在且为 JSON 数组时,才按白名单过滤(含空数组 = 刻意不给该专家任何 MCP)。
|
||||
if rows is None:
|
||||
|
|
@ -262,8 +263,12 @@ def materialize_mcp_tools_for_specialist(
|
|||
except Exception:
|
||||
raw_allowed = ""
|
||||
if not raw_allowed:
|
||||
raw_allowed = str(os.getenv("AIA_MCP_SPECIALISTS") or "generalist,manager,ops").strip()
|
||||
raw_allowed = str(os.getenv("AIA_MCP_SPECIALISTS") or "generalist,ops").strip()
|
||||
allowed = {x.strip().lower() for x in raw_allowed.split(",") if x.strip()}
|
||||
allowed.discard("manager")
|
||||
allowed.discard("manager_self")
|
||||
if "generalist" not in allowed and "ops" in allowed:
|
||||
pass
|
||||
if binding_server_ids is None and sp and sp not in allowed:
|
||||
return []
|
||||
out: list[ToolSpec] = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue