diff --git a/interfaces/admin/static/app.js b/interfaces/admin/static/app.js index 651d45c7..3246121a 100644 --- a/interfaces/admin/static/app.js +++ b/interfaces/admin/static/app.js @@ -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("—")); diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index 3a40de06..c04c6d42 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -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 ? `
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, diff --git a/interfaces/ws/server_methods_bridge.py b/interfaces/ws/server_methods_bridge.py index 72439af4..bbe91461 100644 --- a/interfaces/ws/server_methods_bridge.py +++ b/interfaces/ws/server_methods_bridge.py @@ -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 = { diff --git a/interfaces/ws/turn_runner.py b/interfaces/ws/turn_runner.py index 90dc59ad..459208fc 100644 --- a/interfaces/ws/turn_runner.py +++ b/interfaces/ws/turn_runner.py @@ -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"), diff --git a/runtime/gateway.py b/runtime/gateway.py index a0d0291d..4bf99707 100644 --- a/runtime/gateway.py +++ b/runtime/gateway.py @@ -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 收尾与落库兜底对é½? 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" - "要求:保持简洁、准确,不要暴露内部流程ã€? + "要求:保持简洁、准确,不要暴露内部流程。" ) 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"对于大表附件:当前上下文只提供前{preview_rows}行预览ã€? - f"单次读取上限为{max_rows_read}行ã€? - "如果需要更多行或更细节,请通过数据库工具(`query_tabular_attachment` / `run_tabular_sql`)结å�?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 ( - "对于长文本附件:上下文可能只包含摘要/预览ã€? - "如需细节证据,请使用 `text_ref` 提供çš?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 或图像细节,请使ç”?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` 提供çš?attachment_id 调用 `query_video_attachment` 获取元信æ�?转写ã€? + 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 "任务分配" - 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 = ( - "抱歉,我暂时无法给出可展示的结果,请稍后再试ã€? - 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": diff --git a/runtime/router.py b/runtime/router.py index 285e1a69..18b535e7 100644 --- a/runtime/router.py +++ b/runtime/router.py @@ -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" diff --git a/runtime/types.py b/runtime/types.py index 10101a0c..4e94b051 100644 --- a/runtime/types.py +++ b/runtime/types.py @@ -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: diff --git a/tests/test_channel_dispatch_defaults.py b/tests/test_channel_dispatch_defaults.py index d4980b02..d32f823b 100644 --- a/tests/test_channel_dispatch_defaults.py +++ b/tests/test_channel_dispatch_defaults.py @@ -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" diff --git a/tests/test_inbound_service_reply_suppress.py b/tests/test_inbound_service_reply_suppress.py index 33729b86..776ae5cd 100644 --- a/tests/test_inbound_service_reply_suppress.py +++ b/tests/test_inbound_service_reply_suppress.py @@ -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" diff --git a/tests/test_oclaw_gateway_trace.py b/tests/test_oclaw_gateway_trace.py index 1854e9e4..569f02ae 100644 --- a/tests/test_oclaw_gateway_trace.py +++ b/tests/test_oclaw_gateway_trace.py @@ -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: diff --git a/tests/test_oclaw_router.py b/tests/test_oclaw_router.py index 9e9ecc31..13f85023 100644 --- a/tests/test_oclaw_router.py +++ b/tests/test_oclaw_router.py @@ -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: