Remove Manager/comprehensive mode; route everything expert-only.

Keep WhatsApp/Weixin channels intact—only drop manager dispatch UI and force expert normalization end-to-end.
This commit is contained in:
oliver 2026-08-11 01:43:40 +08:00
parent cf31fd104d
commit ff9ef7279d
11 changed files with 99 additions and 636 deletions

View file

@ -1665,7 +1665,7 @@ async function renderStack() {
]);
const status = el("div", { class: "muted", text: `mode=${curMode} specialist=${curSpecialist} lang=${curLang}` });
const saveExpertBtn = el("button", {
class: "btn",
class: "btn btn--primary",
text: currentLang === "zh" ? "绑定专家" : "Bind specialist",
onclick: async () => {
const specialist = String(specialistSel.value || "generalist").trim() || "generalist";
@ -1678,20 +1678,6 @@ async function renderStack() {
status.textContent = `mode=${String(resp.interaction_mode || "expert")} specialist=${String(resp.specialist || specialist)} lang=${String(resp.lang || lang)}`;
},
});
const saveComprehensiveBtn = el("button", {
class: "btn btn--primary",
text: currentLang === "zh" ? "综合" : "Comprehensive",
onclick: async () => {
const specialist = String(specialistSel.value || "generalist").trim() || "generalist";
const lang = String(langSel.value || "auto").trim().toLowerCase() || "auto";
const resp = await apiPost(`/admin/api/chat/settings/channel-dispatch/${encodeURIComponent(channel)}`, {
interaction_mode: "comprehensive",
specialist,
lang,
});
status.textContent = `mode=${String(resp.interaction_mode || "comprehensive")} specialist=${String(resp.specialist || specialist)} lang=${String(resp.lang || lang)}`;
},
});
return el("div", { class: "card" }, [
el("div", { class: "card__title", text: title }),
el("div", { class: "row" }, [
@ -1700,10 +1686,9 @@ async function renderStack() {
el("label", { text: currentLang === "zh" ? "语言" : "Lang" }),
langSel,
saveExpertBtn,
saveComprehensiveBtn,
]),
status,
el("div", { class: "muted", text: currentLang === "zh" ? "默认绑定通用专家;综合模式下由全能者分派。" : "Defaults to generalist; comprehensive mode lets manager dispatch." }),
el("div", { class: "muted", text: currentLang === "zh" ? "通道统一走专家模式;选择默认专家即可。" : "Channels are expert-only; pick the default specialist." }),
]);
};
const weixinDispatchCard = createChannelDispatchCard("weixin", "Weixin dispatch", weixinDispatchResp || {});
@ -2320,8 +2305,8 @@ async function renderStack() {
class: "muted",
text:
currentLang === "zh"
? "任何 skill/tool/角色/提示词变更后,请立即预热;复杂变更可直接重启。综合模式下:全能者会产出 dispatch.instruction_text,并只把该指令传给专家执行。系统每10分钟自动异步预热一次。"
: "After any skill/tool/role/prompt change, run prewarm immediately; restart for complex changes. In comprehensive mode, manager produces dispatch.instruction_text and only this instruction is sent to specialists. System also auto-prewarms every 10 minutes.",
? "任何 skill/tool/角色/提示词变更后,请立即预热;复杂变更可直接重启。系统每10分钟自动异步预热一次。"
: "After any skill/tool/role/prompt change, run prewarm immediately; restart for complex changes. System also auto-prewarms every 10 minutes.",
},
),
el("div", { class: "row" }, [btnPrewarm]),
@ -2584,12 +2569,10 @@ async function renderUserManagement() {
} else {
accountActiveInput.checked = true;
}
const mode = String(((inst && inst.config) || {}).interaction_mode || "expert").trim().toLowerCase();
const specialist = String(((inst && inst.config) || {}).specialist || "generalist").trim().toLowerCase() || "generalist";
accountSpecialistInput.value = availableAccountSpecialists.includes(specialist) ? specialist : "generalist";
if (mode === "comprehensive") {
accountStatus.textContent = currentLang === "zh" ? "当前账号模式:综合" : "Current account mode: comprehensive";
}
accountStatus.textContent =
currentLang === "zh" ? `当前账号模式:专家(${specialist})` : `Current account mode: expert (${specialist})`;
botSecretInput.value = "";
clearBotChk.checked = false;
};
@ -2636,7 +2619,7 @@ async function renderUserManagement() {
const deleteCell = el("td", {});
if (inst) {
const btnBindExpert = el("button", {
class: "btn",
class: "btn btn--primary",
text: currentLang === "zh" ? "绑定专家" : "Bind specialist",
onclick: async (e) => {
e.stopPropagation();
@ -2653,24 +2636,6 @@ async function renderUserManagement() {
await loadAccounts();
},
});
const btnComprehensive = el("button", {
class: "btn btn--primary",
text: currentLang === "zh" ? "综合" : "Comprehensive",
onclick: async (e) => {
e.stopPropagation();
const specialist = String(accountSpecialistInput.value || "generalist").trim() || "generalist";
await apiPost("/admin/api/user-channel-accounts/upsert", {
tenant_id: tenantId,
user_id: selectedUserId,
channel: ch,
account_id: accountId,
name: displayName,
is_active: !!inst.is_active,
config: { interaction_mode: "comprehensive", specialist },
});
await loadAccounts();
},
});
const btnDeleteAccount = el("button", { class: "btn btn--danger", text: t("users.accountDelete"), onclick: async (e) => {
e.stopPropagation();
await apiPost("/admin/api/user-channel-accounts/delete", {
@ -2682,7 +2647,6 @@ async function renderUserManagement() {
await loadAccounts();
}});
deleteCell.appendChild(btnBindExpert);
deleteCell.appendChild(btnComprehensive);
deleteCell.appendChild(btnDeleteAccount);
} else {
deleteCell.appendChild(document.createTextNode("—"));

View file

@ -3361,8 +3361,8 @@ async function renderChatUi() {
disabled: "disabled",
});
const composerShell = el("div", { class: "chat-composer-shell" });
const MAIN_MODE_VALUE = "comprehensive";
const EXCLUDED_SPECIALISTS = new Set(["main", "memory", "manager_self", "pycache", "__pycache__"]);
const MAIN_MODE_VALUE = "generalist";
const EXCLUDED_SPECIALISTS = new Set(["main", "memory", "manager_self", "pycache", "__pycache__", "comprehensive"]);
let specialistCatalog = [];
/** Hidden: mode is global-only (⋯ menu). Kept for specialist option list in `publishUserMenuPrefsBridge`. */
const modeSelect = el("select", {
@ -3372,6 +3372,7 @@ async function renderChatUi() {
tabIndex: -1,
});
let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase();
if (globalMenuModeValue === "comprehensive" || globalMenuModeValue === "main") globalMenuModeValue = MAIN_MODE_VALUE;
const modelSelect = el("select", {
class: "input",
style: "min-width:150px;max-width:240px;padding:6px 8px;",
@ -3424,35 +3425,31 @@ async function renderChatUi() {
const execSelectWrap = el("span", { class: "chat-exec-mode-wrap", style: "display:inline-flex;align-items:center;" }, [
execSelect,
]);
const outboundPlanAgentVersion = () => {
const modeVal = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
if (modeVal === MAIN_MODE_VALUE) return PLAN_AGENT_V1;
return normalizePlanAgentVersion(currentPlanAgentVersion);
};
const outboundPlanAgentVersion = () => PLAN_AGENT_V1;
const refreshExecUi = () => {
// Plan mode removed; keep agent execution only.
execSelectWrap.style.display = "none";
};
const modeOptionLabel = (v) => {
const key = String(v || "").trim().toLowerCase();
if (key === MAIN_MODE_VALUE) return t("chat.modeComprehensive");
if (key === "generalist") return t("chat.specialistGeneralist");
const row = specialistCatalog.find((x) => String(x.id || "").toLowerCase() === key);
if (!row) return key || t("chat.modeComprehensive");
if (!row) return key || t("chat.specialistGeneralist");
const zh = String(row.display_name_zh || "").trim();
const en = String(row.display_name_en || "").trim();
return currentLang === "zh" ? (zh || en || key) : (en || zh || key);
};
const isSelectableSpecialist = (v) => {
const key = String(v || "").trim().toLowerCase();
if (!key || key === MAIN_MODE_VALUE) return false;
if (!key || key === "comprehensive" || key === "main") return false;
if (key === "generalist") return true;
return specialistCatalog.some((x) => String(x.id || "").toLowerCase() === key);
};
const persistModeSelection = () => {
const v = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
localStorage.setItem(CHAT_INTERACTION_MODE_KEY, v);
if (v !== MAIN_MODE_VALUE) localStorage.setItem(CHAT_SPECIALIST_PREF_KEY, v);
localStorage.setItem(CHAT_INTERACTION_MODE_KEY, "expert");
localStorage.setItem(CHAT_USER_MENU_MODE_KEY, v);
localStorage.setItem(CHAT_SPECIALIST_PREF_KEY, v);
};
const syncHiddenModeSelectFromGlobal = () => {
const g = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
@ -3460,9 +3457,9 @@ async function renderChatUi() {
else modeSelect.value = MAIN_MODE_VALUE;
};
const applyModeOptions = () => {
const prev = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
let prev = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
if (prev === "comprehensive" || prev === "main") prev = MAIN_MODE_VALUE;
modeSelect.innerHTML = "";
modeSelect.appendChild(el("option", { value: MAIN_MODE_VALUE, text: modeOptionLabel(MAIN_MODE_VALUE) }));
modeSelect.appendChild(el("option", { value: "generalist", text: modeOptionLabel("generalist") }));
specialistCatalog.forEach((x) => {
const sid = String(x.id || "").toLowerCase();
@ -3471,6 +3468,7 @@ async function renderChatUi() {
});
if (Array.from(modeSelect.options).some((o) => String(o.value || "") === prev)) {
modeSelect.value = prev;
globalMenuModeValue = prev;
} else {
modeSelect.value = MAIN_MODE_VALUE;
globalMenuModeValue = MAIN_MODE_VALUE;
@ -3584,11 +3582,9 @@ async function renderChatUi() {
if (gm) {
const gIm = String(gm.interaction_mode || "").toLowerCase();
const gSp = String(gm.specialist || "").toLowerCase();
if (gIm === "comprehensive") globalMenuModeValue = MAIN_MODE_VALUE;
else if (gIm === "expert") {
if (isSelectableSpecialist(gSp)) globalMenuModeValue = gSp;
else globalMenuModeValue = "generalist";
} else globalMenuModeValue = MAIN_MODE_VALUE;
if (isSelectableSpecialist(gSp)) globalMenuModeValue = gSp;
else if (isSelectableSpecialist(gIm)) globalMenuModeValue = gIm;
else globalMenuModeValue = MAIN_MODE_VALUE;
} else {
try {
const ur = await apiGet("/admin/api/chat/user-mode");
@ -3602,15 +3598,13 @@ async function renderChatUi() {
ur.plan_agent_version || localStorage.getItem(CHAT_PLAN_AGENT_VERSION_KEY),
);
localStorage.setItem(CHAT_PLAN_AGENT_VERSION_KEY, currentPlanAgentVersion);
if (gum === "comprehensive") globalMenuModeValue = MAIN_MODE_VALUE;
else if (gum === "expert") {
if (isSelectableSpecialist(gus)) globalMenuModeValue = gus;
else globalMenuModeValue = "generalist";
} else globalMenuModeValue = MAIN_MODE_VALUE;
if (isSelectableSpecialist(gus)) globalMenuModeValue = gus;
else if (isSelectableSpecialist(gum)) globalMenuModeValue = gum;
else globalMenuModeValue = MAIN_MODE_VALUE;
}
} catch (_) {
if (m === "comprehensive") globalMenuModeValue = MAIN_MODE_VALUE;
else if (m === "expert" && isSelectableSpecialist(s)) globalMenuModeValue = s;
if (m === "expert" && isSelectableSpecialist(s)) globalMenuModeValue = s;
else if (isSelectableSpecialist(s)) globalMenuModeValue = s;
else globalMenuModeValue = MAIN_MODE_VALUE;
}
}
@ -3633,17 +3627,18 @@ async function renderChatUi() {
const saveUserGlobalModePreference = async () => {
try {
const modeVal = String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase();
const isMain = modeVal === MAIN_MODE_VALUE;
const specialist = isSelectableSpecialist(modeVal) ? modeVal : MAIN_MODE_VALUE;
const resp = await apiPost("/admin/api/chat/user-mode", {
interaction_mode: isMain ? "comprehensive" : "expert",
specialist: isMain ? "generalist" : modeVal,
interaction_mode: "expert",
specialist,
confirm_strategy: String(currentConfirmStrategy || CONFIRM_STRATEGY_STRICT),
plan_agent_version: String(currentPlanAgentVersion || PLAN_AGENT_V1),
});
if (resp && typeof resp.plan_agent_v2_globally_enabled === "boolean") {
planAgentV2GloballyEnabled = !!resp.plan_agent_v2_globally_enabled;
}
localStorage.setItem(CHAT_USER_MENU_MODE_KEY, modeVal);
localStorage.setItem(CHAT_USER_MENU_MODE_KEY, specialist);
globalMenuModeValue = specialist;
if (!resp || resp.ok === false) {
const detail = String((resp && (resp.error || resp.detail)) || "unknown_error");
throw new Error(detail);
@ -3655,13 +3650,11 @@ async function renderChatUi() {
if (ur && ur.ok) {
const gum = String((ur.interaction_mode || "").toLowerCase());
const gus = String((ur.specialist || "").toLowerCase());
const expectedIm = isMain ? "comprehensive" : "expert";
const expectedSp = isMain ? "generalist" : modeVal;
if (gum !== expectedIm || gus !== expectedSp) {
if (gum !== "expert" || gus !== specialist) {
const msg =
currentLang === "zh"
? `模式未固化:后端返回 interaction_mode=${gum || "-"} specialist=${gus || "-"}(期望 ${expectedIm}/${expectedSp})`
: `Mode not persisted: server returned interaction_mode=${gum || "-"} specialist=${gus || "-"} (expected ${expectedIm}/${expectedSp})`;
? `模式未固化:后端返回 interaction_mode=${gum || "-"} specialist=${gus || "-"}(期望 expert/${specialist})`
: `Mode not persisted: server returned interaction_mode=${gum || "-"} specialist=${gus || "-"} (expected expert/${specialist})`;
showToast(msg, { kind: "error", ttlMs: 8000 });
}
}
@ -5247,15 +5240,11 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
sessionId: activeId,
text: userText,
attachments: attachmentPayload,
interactionMode:
String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase() === MAIN_MODE_VALUE ? "comprehensive" : "expert",
interactionMode: "expert",
idempotencyKey: String(turnId || ""),
specialist:
String(globalMenuModeValue || MAIN_MODE_VALUE).toLowerCase() === MAIN_MODE_VALUE
? "generalist"
: isSelectableSpecialist(String(globalMenuModeValue || "").toLowerCase())
? String(globalMenuModeValue || "generalist").toLowerCase()
: "generalist",
specialist: isSelectableSpecialist(String(globalMenuModeValue || "").toLowerCase())
? String(globalMenuModeValue || "generalist").toLowerCase()
: "generalist",
memoryMode: String(localStorage.getItem(CHAT_MEMORY_MODE_KEY) || "default"),
executionMode: String(currentExecutionMode || EXECUTION_MODE_AGENT),
signal: abortController.signal,

View file

@ -62,7 +62,7 @@ def build_gateway_context(
with abort_lock:
active_run_session[rid] = sid
p = dict(params or {})
interaction_mode = str(p.get("interaction_mode") or "comprehensive").strip().lower() or "comprehensive"
interaction_mode = str(p.get("interaction_mode") or "expert").strip().lower() or "expert"
specialist = str(p.get("specialist") or "generalist").strip().lower() or "generalist"
execution_mode = str(p.get("execution_mode") or "agent").strip().lower() or "agent"
if execution_mode not in {"agent", "plan"}:
@ -71,7 +71,7 @@ def build_gateway_context(
if plan_agent_version not in {"v1", "v2"}:
plan_agent_version = "v1"
if interaction_mode != "expert":
specialist = "generalist"
interaction_mode = "expert"
raw_env = dict(p.get("relay_share_envelope") or {}) if isinstance(p.get("relay_share_envelope"), dict) else {}
ok_env, _err_env, norm_env = validate_relay_share_envelope(raw_env) if raw_env else (False, "", {})
agent_params = {
@ -101,7 +101,7 @@ def build_gateway_context(
session_id = str(p.get("sessionId") or p.get("sessionKey") or "").strip()
run_id = str(p.get("idempotencyKey") or "").strip() or uuid.uuid4().hex
message = str(p.get("message") or "").strip()
interaction_mode = str(p.get("interaction_mode") or "comprehensive").strip().lower() or "comprehensive"
interaction_mode = str(p.get("interaction_mode") or "expert").strip().lower() or "expert"
specialist = str(p.get("specialist") or "generalist").strip().lower() or "generalist"
execution_mode = str(p.get("execution_mode") or "agent").strip().lower() or "agent"
if execution_mode not in {"agent", "plan"}:
@ -110,7 +110,7 @@ def build_gateway_context(
if plan_agent_version not in {"v1", "v2"}:
plan_agent_version = "v1"
if interaction_mode != "expert":
specialist = "generalist"
interaction_mode = "expert"
raw_env = dict(p.get("relay_share_envelope") or {}) if isinstance(p.get("relay_share_envelope"), dict) else {}
ok_env, _err_env, norm_env = validate_relay_share_envelope(raw_env) if raw_env else (False, "", {})
agent_params = {

View file

@ -289,7 +289,7 @@ async def run_agent_turn_via_bridge(
text=msg_text,
attachments=attachments,
metadata={
"interaction_mode": str(p.get("interaction_mode") or "comprehensive"),
"interaction_mode": str(p.get("interaction_mode") or "expert"),
"selected_specialist": str(p.get("specialist") or "generalist"),
"execution_mode": execution_mode,
"plan_agent_version": plan_agent_version,
@ -426,7 +426,7 @@ async def run_agent_turn_via_bridge(
"traceId": "",
"reply": stopped,
"selectedSpecialist": str(p.get("specialist") or "generalist"),
"interactionMode": str(p.get("interaction_mode") or "comprehensive"),
"interactionMode": str(p.get("interaction_mode") or "expert"),
"dispatchReason": "aborted",
"executionMode": execution_mode,
"managerSelectedSpecialist": str(p.get("specialist") or "generalist"),
@ -545,7 +545,7 @@ async def run_agent_turn_via_bridge(
"traceId": "",
"reply": user_facing_error,
"selectedSpecialist": str(p.get("specialist") or "generalist"),
"interactionMode": str(p.get("interaction_mode") or "comprehensive"),
"interactionMode": str(p.get("interaction_mode") or "expert"),
"dispatchReason": "execution_failed",
"executionMode": execution_mode,
"managerSelectedSpecialist": str(p.get("specialist") or "generalist"),
@ -657,7 +657,7 @@ async def run_agent_turn_via_bridge(
"traceId": str(getattr(result, "trace_id", "") or ""),
"reply": str(getattr(result, "reply_text", "") or ""),
"selectedSpecialist": str(getattr(result, "selected_specialist", "generalist") or "generalist"),
"interactionMode": str(getattr(result, "interaction_mode", "comprehensive") or "comprehensive"),
"interactionMode": str(getattr(result, "interaction_mode", "expert") or "expert"),
"dispatchReason": str(getattr(result, "dispatch_reason", "") or ""),
"executionMode": execution_mode,
"managerSelectedSpecialist": str(getattr(result, "manager_selected_specialist", "generalist") or "generalist"),

View file

@ -77,7 +77,7 @@ class OclawGatewayResult:
mode: str = "sync_direct"
task_id: str | None = None
selected_specialist: str = "generalist"
interaction_mode: str = "comprehensive"
interaction_mode: str = "expert"
dispatch_reason: str = ""
manager_selected_specialist: str = "generalist"
requested_specialist: str = "generalist"
@ -89,7 +89,8 @@ class OclawGatewayResult:
relay_ttl_turn_count: int = 0
relay_ttl_session_count: int = 0
relay_ttl_keep_count: int = 0
# agent-core 本轮 ``chat_message.turn_uuid``;供 WS 收尾与è<C5BD>½åº“兜底对é½? turn_uuid: str = ""
# agent-core 本轮 ``chat_message.turn_uuid``;供 WS 收尾与落库兜底对齐
turn_uuid: str = ""
@dataclass(frozen=True)
@ -207,7 +208,7 @@ class OclawGateway:
# - stage "3": renamed on third user message (final)
if stage_raw == "3":
return
if (cur_title not in ("新会�, "New Chat")) and (stage_raw != "1"):
if (cur_title not in ("新会话", "New Chat")) and (stage_raw != "1"):
return
try:
rows = self.store.get_messages(session_id=sid, limit=200)
@ -269,7 +270,7 @@ class OclawGateway:
if not sess:
return
cur_title = str(getattr(sess, "title", "") or "").strip()
if cur_title not in ("新会�, "New Chat"):
if cur_title not in ("新会话", "New Chat"):
return
try:
rows = self.store.get_messages(session_id=sid, limit=20)
@ -403,9 +404,9 @@ class OclawGateway:
user_text = (
"请基于以下信息输出最终答复。\n\n"
f"原始用户问题:\n{str(msg.text or '').strip()}\n\n"
f"已调用专� {str(specialist or '').strip()}\n\n"
f"已调用专家: {str(specialist or '').strip()}\n\n"
f"专家结果:\n{str(specialist_reply or '').strip()}\n\n"
"è¦<EFBFBD>求:ä¿<EFBFBD>æŒ<EFBFBD>简æ´<EFBFBD>ã€<EFBFBD>准确,ä¸<EFBFBD>è¦<EFBFBD>暴露内部æµ<EFBFBD>程ã€?
"要求:保持简洁、准确,不要暴露内部流程。"
)
messages = [{"role": "system", "content": manager_context}, {"role": "user", "content": user_text}]
ensure_no_tool_or_embedded_image_payload(messages=messages, path="gateway.manager_finalize")
@ -535,9 +536,9 @@ class OclawGateway:
"If you need more rows/details, use database tools (`query_tabular_attachment` / `run_tabular_sql`) with table_id."
)
return (
f"对于大表附件:当å‰<EFBFBD>上下文å<EFBFBD>ªæ<EFBFBD><EFBFBD>ä¾›å‰<EFBFBD>{preview_rows}行预览ã€?
f"å<EFBFBD>•次读å<EFBFBD>–上é™<EFBFBD>为{max_rows_read}行ã€?
"如果需è¦<EFBFBD>更多行或更细节,请通过数æ<EFBFBD>®åº“工具(`query_tabular_attachment` / `run_tabular_sql`)结å<E2809C>?table_id 查询ã€?
f"对于大表附件:当前上下文只提供前{preview_rows}行预览。"
f"单次读取上限为{max_rows_read}行。"
"如果需要更多行或更细节,请通过数据库工具(`query_tabular_attachment` / `run_tabular_sql`)结合 table_id 查询。"
)
@staticmethod
@ -548,8 +549,8 @@ class OclawGateway:
"For detailed evidence, use `query_text_attachment` with `text_id` from `text_ref` attachment."
)
return (
"对于长文本附件:上下文å<EFBFBD>¯èƒ½å<EFBFBD>ªåŒ…å<EFBFBD>«æ‘˜è¦<EFBFBD>/预览ã€?
"如需细节è¯<EFBFBD>æ<EFBFBD>®ï¼Œè¯·ä½¿ç”¨ `text_ref` æ<><C3A6>ä¾›çš?text_id 调用 `query_text_attachment`ã€?
"对于长文本附件:上下文可能只包含摘要/预览。"
"如需细节证据,请使用 `text_ref` 提供的 text_id 调用 `query_text_attachment`。"
)
@staticmethod
@ -560,7 +561,7 @@ class OclawGateway:
"for OCR/description when visual evidence is required."
)
return (
"对于图片附件:如需 OCR 或图åƒ<C3A5>细节,请使ç”?attachment_id 调用 `query_image_attachment`ã€?
"对于图片附件:如需 OCR 或图像细节,请使用 attachment_id 调用 `query_image_attachment`。"
)
@staticmethod
@ -570,7 +571,7 @@ class OclawGateway:
"For video attachments: use `query_video_attachment` with attachment_id from `video_ref` "
"to get metadata or transcript (if enabled)."
)
return "对于视频附件:请使用 `video_ref` æ<><C3A6>ä¾›çš?attachment_id 调用 `query_video_attachment` 获å<C2B7>–元信æ<C2A1>?转写ã€?
return "对于视频附件:请使用 `video_ref` 提供的 attachment_id 调用 `query_video_attachment` 获取元信息/转写。"
@staticmethod
def _tabular_limits_from_config() -> dict[str, int]:
@ -734,6 +735,9 @@ class OclawGateway:
trace_local: Callable[..., None],
started_at: float,
) -> GatewayDispatchPlan:
del msg, lang, memory_enabled, trace_local, started_at
# Expert-only product surface: never run Manager/comprehensive routing.
interaction_mode = "expert"
manager_specialist = requested_specialist
dispatch_reason = "expert_direct"
selected_executor = executor
@ -741,32 +745,12 @@ class OclawGateway:
specialist_input_msg: StandardMessage | None = None
manager_exec_msg: StandardMessage | None = None
manager_instruction_text = ""
if interaction_mode == "expert" and callable(specialist_executor_factory):
if callable(specialist_executor_factory):
try:
selected_executor = specialist_executor_factory(requested_specialist)
except Exception:
selected_executor = executor
dispatch_reason = "expert_factory_failed"
if interaction_mode == "comprehensive":
(
manager_specialist,
dispatch_reason,
dynamic_agent,
instruction_text,
) = self._manager_select_specialist(msg=msg, lang=lang, executor=executor, memory_enabled=memory_enabled)
manager_instruction_text = str(instruction_text or "").strip()
trace_local(
event_type="manager_decision",
payload={
"interaction_mode": interaction_mode,
"manager_selected_specialist": str(manager_specialist or ""),
"dispatch_reason": str(dispatch_reason or ""),
"instruction_chars": int(len(manager_instruction_text or "")),
"dynamic_agent_used": bool(dynamic_agent is not None),
"dynamic_agent_name": str((dynamic_agent or {}).get("name") or "") if isinstance(dynamic_agent, dict) else "",
},
started_at=started_at,
)
return GatewayDispatchPlan(
interaction_mode=interaction_mode,
requested_specialist=requested_specialist,
@ -915,6 +899,8 @@ class OclawGateway:
base_metadata = dict(msg.metadata or {})
memory_enabled = self._memory_enabled()
interaction_mode = normalize_interaction_mode(base_metadata.get("interaction_mode"))
# Defensive: product surface is expert-only regardless of inbound metadata.
interaction_mode = "expert"
requested_specialist = normalize_requested_specialist(base_metadata.get("selected_specialist"))
if requested_specialist == "memory" and not memory_enabled:
requested_specialist = "generalist"
@ -936,66 +922,6 @@ class OclawGateway:
specialist_input_msg = plan.specialist_input_msg
manager_exec_msg = plan.manager_exec_msg
manager_instruction_text = plan.manager_instruction_text
if interaction_mode == "comprehensive":
if str(manager_instruction_text or "").strip():
try:
assignment_title = "Task assignment" if str(lang or "").startswith("en") else "任务分é…<EFBFBD>"
assignment_text = (
f"{assignment_title}\n"
f"specialist={str(manager_specialist or '')}\n"
f"instruction:\n{str(manager_instruction_text or '').strip()}"
)
self.store.add_message(
session_id=msg.session_id,
role="assistant",
content=assignment_text,
tool_calls=None,
event_type="reasoning",
)
except Exception:
logger.exception(
"comprehensive_mode_assignment_message_persist_failed session_id=%s",
str(getattr(msg, "session_id", "") or ""),
)
# Build specialist/dynamic executor and dispatch only manager instruction to it.
specialist_input_msg = StandardMessage(
session_id=msg.session_id,
tenant_id=msg.tenant_id,
user_id=msg.user_id,
role=msg.role,
channel=msg.channel,
text=str(manager_instruction_text or "").strip(),
attachments=list(msg.attachments or []),
metadata=dict(base_metadata),
)
manager_exec_msg = StandardMessage(
session_id=msg.session_id,
tenant_id=msg.tenant_id,
user_id=msg.user_id,
role=msg.role,
channel=msg.channel,
text=msg.text,
attachments=list(msg.attachments or []),
metadata=dict(base_metadata),
)
if manager_specialist in {"ops", "generalist", "memory"}:
if callable(specialist_executor_factory):
try:
selected_executor = specialist_executor_factory(manager_specialist)
except Exception:
selected_executor = executor
dispatch_reason = "manager_factory_failed"
else:
# Dynamic ephemeral agents removed from product surface; fall back to generalist.
manager_specialist = "generalist"
dispatch_reason = "dynamic_agent_disabled_fallback"
if callable(specialist_executor_factory):
try:
selected_executor = specialist_executor_factory("generalist")
except Exception:
selected_executor = executor
dynamic_agent = None
system_prompt_override = ""
tools_override = None
@ -1032,7 +958,7 @@ class OclawGateway:
started_at=t0,
)
if on_progress:
on_progress("oclaw: running�)
on_progress("oclaw: running…")
if route_mode == "async_task":
worker_id = ensure_worker_started(store=self.store)
task = self.store.oclaw_task_create(
@ -1181,7 +1107,7 @@ class OclawGateway:
started_at=t0,
)
exec_msg = (
specialist_input_msg if (interaction_mode == "comprehensive" and specialist_input_msg is not None) else msg
msg
)
core_out = run_agent_core(
store=self.store,
@ -1211,32 +1137,12 @@ class OclawGateway:
on_tool_ui=on_tool_ui,
should_stop=should_stop,
skill_binding_role=str(manager_specialist or "generalist"),
wire_policy_role="manager" if interaction_mode == "comprehensive" else str(requested_specialist),
wire_policy_role=str(requested_specialist),
),
)
executed_turn_uuid = str(getattr(core_out.outcome, "turn_uuid", "") or "")
specialist_reply = str(core_out.outcome.final_text or "")
if interaction_mode == "comprehensive":
reply = self._manager_finalize_output(
msg=msg,
lang=lang,
executor=executor,
specialist=manager_specialist,
specialist_reply=specialist_reply,
memory_enabled=memory_enabled,
on_token=None,
)
if self._looks_like_manager_instruction(reply, manager_instruction_text):
if not self._looks_like_manager_instruction(specialist_reply, manager_instruction_text):
reply = str(specialist_reply or "").strip()
else:
reply = (
"抱歉,我暂时无法给出å<EFBFBD>¯å±•示的结果,请ç¨<EFBFBD>å<EFBFBD>Žå†<EFBFBD>试ã€?
if not str(lang or "").startswith("en")
else "Sorry, no user-safe result is available right now. Please try again later."
)
else:
reply = specialist_reply
reply = specialist_reply
if not str(reply or "").strip():
rs = getattr(core_out, "run_state", None)
if rs is not None and str(getattr(rs, "status", "") or "") == "failed":

View file

@ -16,7 +16,7 @@ class RouterDecision:
mode: str # sync_direct | async_task
reason: str
skill_signal: str = ""
interaction_mode: str = "comprehensive"
interaction_mode: str = "expert"
requested_specialist: str = "generalist"

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
InteractionMode = Literal["comprehensive", "expert"]
InteractionMode = Literal["expert"]
SpecialistId = str
ChannelId = Literal[
@ -34,10 +34,9 @@ class StandardMessage:
def normalize_interaction_mode(raw: Any) -> InteractionMode:
mode = str(raw or "").strip().lower()
if mode in {"expert", "specialist"}:
return "expert"
return "comprehensive"
"""Product surface is expert-only; Manager/comprehensive mode is removed."""
del raw
return "expert"
def normalize_requested_specialist(raw: Any) -> SpecialistId:

View file

@ -38,7 +38,7 @@ def test_channel_dispatch_uses_global_settings() -> None:
}
)
interaction_mode, specialist, lang = _resolve_channel_dispatch(store, channel="whatsapp", account=None)
assert interaction_mode == "comprehensive"
assert interaction_mode == "expert"
assert specialist == "ops"
assert lang == "en"

View file

@ -104,7 +104,7 @@ def test_channel_dispatch_wechat_reads_weixin_settings() -> None:
return str(data.get(key) or "")
mode, spec, lang = _resolve_channel_dispatch(_Store(), channel="wechat", account=None)
assert mode == "comprehensive"
assert mode == "expert"
assert spec == "ops"
assert lang == "zh"
assert _get_channel_dispatch_setting(_Store(), "channel.dispatch.specialist.", "wechat") == "ops"

View file

@ -266,7 +266,10 @@ def test_gateway_expert_plan_execution_mode_ignored_without_plan_agent(monkeypat
assert "Plan mode is active" not in prompt_text
def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch: pytest.MonkeyPatch) -> None:
def test_gateway_forces_expert_when_comprehensive_requested(monkeypatch: pytest.MonkeyPatch) -> None:
"""Manager/comprehensive mode is removed; inbound comprehensive metadata must not change routing."""
class Store:
def get_setting(self, _k: str) -> str:
return ""
@ -274,12 +277,8 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
def add_trace_event(self, **_kwargs: object) -> None:
return None
class _ManagerModel:
def chat(self, _messages, _tools, *, on_token=None):
return LLMResponse(
content='{"route":{"specialist":"generalist","reason":"needs image edits"},"dispatch":{"instruction_text":"Please edit the image background."}}',
tool_calls=[],
)
def add_message(self, **_kwargs: object) -> None:
raise AssertionError("comprehensive assignment message must not be written")
class _Exec:
def __init__(self, model=None):
@ -287,21 +286,13 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory", "image"),
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
},
)
captured: dict = {}
def _run_agent_core(**kwargs):
data = kwargs.get("data")
captured["exec_text"] = getattr(getattr(data, "msg", None), "text", None)
captured["persisted_user_text"] = getattr(data, "persisted_user_text", None)
return SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer"))
captured["wire_policy_role"] = getattr(data, "wire_policy_role", None)
return SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer", turn_uuid="tu-1"), run_state=None)
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
@ -322,402 +313,14 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
attachments=[],
metadata={"interaction_mode": "comprehensive", "selected_specialist": "ops"},
)
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=_ManagerModel()), specialist_executor_factory=_factory)
assert out.interaction_mode == "comprehensive"
assert out.selected_specialist == "generalist"
assert chosen.get("sid") == "generalist"
assert captured.get("exec_text") == "Please edit the image background."
assert captured.get("persisted_user_text") == "edit this image background"
def test_gateway_comprehensive_mode_writes_task_assignment_reasoning(monkeypatch: pytest.MonkeyPatch) -> None:
written: list[dict] = []
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
def add_message(self, **kwargs: object) -> None:
written.append(dict(kwargs))
class _ManagerModel:
def chat(self, _messages, _tools, *, on_token=None):
return LLMResponse(
content='{"route":{"specialist":"ops","reason":"ops task"},"dispatch":{"instruction_text":"请检查并修复网关启动失败。"}}',
tool_calls=[],
)
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory", "image"),
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
},
)
monkeypatch.setattr(
"runtime.gateway.run_agent_core",
lambda **kwargs: SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer")),
)
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-assign-1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="网关起不来,帮我修复",
attachments=[],
metadata={"interaction_mode": "comprehensive"},
)
_ = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
reasoning_rows = [x for x in written if str(x.get("event_type") or "") == "reasoning"]
assert reasoning_rows, "expected task-assignment reasoning row"
content = str(reasoning_rows[-1].get("content") or "")
assert "任务分配" in content
assert "specialist=ops" in content
assert "请检查并修复网关启动失败" in content
def test_gateway_comprehensive_ignores_wiki_inject_flags(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
class _ManagerModel:
def chat(self, _messages, _tools, *, on_token=None):
return LLMResponse(
content=(
'{"route":{"specialist":"generalist","reason":"general",'
'"need_wiki_inject":true,"wiki_query":"router issue"},'
'"dispatch":{"instruction_text":"请先分析问题并给出结论。",'
'"need_wiki_inject":true,"wiki_query":"router issue"}}'
),
tool_calls=[],
)
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory", "image"),
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
},
)
captured: dict[str, object] = {}
def _run_agent_core(**kwargs):
data = kwargs.get("data")
msg = getattr(data, "msg", None)
captured["metadata"] = dict(getattr(msg, "metadata", {}) or {})
return SimpleNamespace(outcome=SimpleNamespace(final_text="ok"))
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-wi-1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="分析一下",
attachments=[],
metadata={"interaction_mode": "comprehensive"},
)
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
assert out.interaction_mode == "comprehensive"
md = captured.get("metadata") or {}
assert isinstance(md, dict)
assert "need_wiki_inject" not in md
assert "wiki_query" not in md
def test_gateway_comprehensive_mode_has_manager_final_pass(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
class _ManagerModel:
def __init__(self) -> None:
self.calls = 0
def chat(self, _messages, _tools, *, on_token=None):
self.calls += 1
if self.calls == 1:
return LLMResponse(
content='{"route":{"specialist":"generalist","reason":"general"},"dispatch":{"instruction_text":"Analyze the finance report and provide key points."}}',
tool_calls=[],
)
return LLMResponse(content="final_from_manager", tool_calls=[])
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory", "image"),
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
},
)
monkeypatch.setattr(
"runtime.gateway.run_agent_core",
lambda **kwargs: SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer")),
)
gw = OclawGateway(store=Store())
model = _ManagerModel()
msg = StandardMessage(
session_id="sid-d1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="analyze finance report",
attachments=[],
metadata={"interaction_mode": "comprehensive", "selected_specialist": "ops"},
)
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=model))
assert out.interaction_mode == "comprehensive"
assert out.selected_specialist == "generalist"
assert out.reply_text == "final_from_manager"
def test_gateway_comprehensive_mode_dynamic_agent_falls_back_to_generalist(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
class _ManagerModel:
def chat(self, _messages, _tools, *, on_token=None):
return LLMResponse(
content=(
'{"route":{"specialist":"dyn:sql","reason":"needs ad-hoc expert"},'
'"dispatch":{"instruction_text":"Write a SQL query to compute daily active users."},'
'"dynamic_agent":{"name":"dyn:sql","system_prompt":"You are a SQL expert.","tool_policy":{"allow_tags":[],"allow_tools":[]},"reason":"dynamic"}}'
),
tool_calls=[],
)
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory"),
"allowed_fixed_quoted": '"generalist", "ops", "memory"',
},
)
captured: dict = {}
def _run_agent_core(**kwargs):
data = kwargs.get("data")
captured["exec_text"] = getattr(getattr(data, "msg", None), "text", None)
return SimpleNamespace(outcome=SimpleNamespace(final_text="generalist_answer", turn_uuid="t1"))
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
chosen: dict[str, str] = {}
def _factory(sid: str):
chosen["sid"] = sid
return _Exec(model=object())
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-dyn-1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="How do I compute DAU from events table?",
attachments=[],
metadata={"interaction_mode": "comprehensive"},
)
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=_ManagerModel()), specialist_executor_factory=_factory)
assert out.interaction_mode == "comprehensive"
assert out.selected_specialist == "generalist"
assert chosen.get("sid") == "generalist"
assert out.dispatch_reason == "dynamic_agent_disabled_fallback"
assert captured.get("exec_text") == "Write a SQL query to compute daily active users."
def test_gateway_comprehensive_mode_ignores_manager_self_and_dispatches_specialist(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
class _ManagerModel:
def __init__(self) -> None:
self.calls = 0
def chat(self, _messages, _tools, *, on_token=None):
self.calls += 1
if self.calls == 1:
return LLMResponse(
content='{"route":{"kind":"manager_self","specialist":"generalist","reason":"can answer directly"},"dispatch":{"instruction_text":"请直接给用户简明答案。"}}',
tool_calls=[],
)
return LLMResponse(content="manager_self_final", tool_calls=[])
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory", "image"),
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
},
)
captured: dict[str, str] = {}
def _run_agent_core(**kwargs):
data = kwargs["data"]
captured["exec_text"] = str(getattr(data.msg, "text", ""))
class _Outcome:
final_text = "specialist_result"
class _Out:
outcome = _Outcome()
return _Out()
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-self-1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="请直接回答这个简单问题",
attachments=[],
metadata={"interaction_mode": "comprehensive"},
)
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
assert out.interaction_mode == "comprehensive"
assert out.selected_specialist == "generalist"
assert captured.get("exec_text") == "请直接给用户简明答案。"
assert out.reply_text == "manager_self_final"
def test_gateway_comprehensive_mode_suppresses_instruction_echo(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
instruction = "用户说厉害;请友好回应,表达感谢并询问是否有具体问题。"
class _ManagerModel:
def __init__(self) -> None:
self.calls = 0
def chat(self, _messages, _tools, *, on_token=None):
self.calls += 1
if self.calls == 1:
return LLMResponse(
content=(
'{"route":{"specialist":"generalist","reason":"smalltalk"},'
f'"dispatch":{{"instruction_text":"{instruction}"}}'
"}"
),
tool_calls=[],
)
# Simulate finalize path echoing instruction.
return LLMResponse(content=instruction, tool_calls=[])
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "memory", "image"),
"allowed_fixed_quoted": '"generalist", "ops", "memory", "image"',
},
)
# Specialist output also echoes instruction -> should still be suppressed.
def _run_agent_core(**kwargs):
class _Outcome:
final_text = instruction
class _Out:
outcome = _Outcome()
return _Out()
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core)
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-echo-1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="你真厉害",
attachments=[],
metadata={"interaction_mode": "comprehensive"},
)
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
assert out.interaction_mode == "comprehensive"
assert "用户说厉害" not in out.reply_text
assert "请友好回应" not in out.reply_text
assert out.reply_text == "抱歉,我暂时无法给出可展示的结果,请稍后再试。"
out = gw.handle_turn(msg=msg, lang="en", executor=_Exec(model=object()), specialist_executor_factory=_factory)
assert out.interaction_mode == "expert"
assert out.dispatch_reason == "expert_direct"
assert out.selected_specialist == "ops"
assert chosen.get("sid") == "ops"
assert captured.get("exec_text") == "edit this image background"
assert captured.get("wire_policy_role") == "ops"
assert out.reply_text == "specialist_answer"
def test_gateway_command_hook_uses_parsed_command_and_context(monkeypatch: pytest.MonkeyPatch) -> None:

View file

@ -97,11 +97,13 @@ def test_router_skill_signal_from_metadata() -> None:
assert d.skill_signal == "skills=4"
def test_interaction_mode_normalization_supports_legacy_values() -> None:
assert normalize_interaction_mode("comprehensive") == "comprehensive"
def test_interaction_mode_normalization_is_expert_only() -> None:
assert normalize_interaction_mode("comprehensive") == "expert"
assert normalize_interaction_mode("expert") == "expert"
assert normalize_interaction_mode("specialist") == "expert"
assert normalize_interaction_mode("composite") == "comprehensive"
assert normalize_interaction_mode("composite") == "expert"
assert normalize_interaction_mode("") == "expert"
assert normalize_interaction_mode(None) == "expert"
def test_requested_specialist_normalization_defaults_to_generalist() -> None: