diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index ac956296..1706ec9f 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -3445,6 +3445,7 @@ async function renderChatUi() { let hasRealStreamText = false; let sawWsChatEvent = false; let sawWsTerminalEvent = false; + let sawStreamToolRefAttachments = false; let markerState = { p: 0, e: false, ep: 0, t: 0, s: 0, k: 0, reclaimed: 0 }; let turnAcceptedAtMs = null; let phaseRunningAtMs = null; @@ -3796,6 +3797,31 @@ ${autoLimit ? `
auto-added claus const body = formatToolPanelText(name, rawPayload, { streamMode: true }); const sqlAudit = extractSqlAuditPayload(rawPayload); const images = extractToolImageItems(rawPayload); + const _containsRefAttachments = (obj, depth = 0) => { + if (!obj || typeof obj !== "object" || depth > 4) return false; + // Common case: { attachments: [...] } + if (Array.isArray(obj.attachments)) { + for (const att of obj.attachments) { + if (!att || typeof att !== "object") continue; + const typ = String(att.type || "").toLowerCase(); + if (!typ) continue; + if (typ.endsWith("_ref") || typ === "relay_pointer") return true; + } + } + // Recurse a few common container keys first. + const keys = ["result", "payload", "message", "data", "output"]; + for (const k of keys) { + if (k in obj && _containsRefAttachments(obj[k], depth + 1)) return true; + } + // Shallow scan other object values (bounded). + let scanned = 0; + for (const v of Object.values(obj)) { + if (scanned++ > 12) break; + if (_containsRefAttachments(v, depth + 1)) return true; + } + return false; + }; + if (_containsRefAttachments(rawPayload)) sawStreamToolRefAttachments = true; chatStreamSegments.push({ type: "tool", key, title: `${name} ${liveTag}`, body, sqlAudit, images }); }; const appendFinalAssistant = async (message, fallbackText) => { @@ -3954,6 +3980,15 @@ ${autoLimit ? `
auto-added claus renderStreamComposite(); _markStreamTerminal("end", t("chat.status.end")); ok = true; + } else if (sawStreamToolRefAttachments) { + // Streaming UI cannot render image_ref/relay_pointer; recover from persisted history so images appear + // without requiring a manual refresh. + try { + if (streamRow && streamRow.parentNode) streamRow.remove(); + } catch (_) {} + await loadMessagesForActive(); + scrollMessagesToBottom(true); + ok = true; } else { ok = await appendFinalAssistant(payload.message, chatStream || extractWsAssistantText(payload.message || {})); } @@ -3961,6 +3996,7 @@ ${autoLimit ? `
auto-added claus chatStream = ""; streamTextBuffer = ""; chatRunId = null; + sawStreamToolRefAttachments = false; const streamedEnough = hasRealStreamText || chatStreamSegments.length > 0 || sawWsChatEvent; turnFinalized = true; turnStreamedEnough = streamedEnough; diff --git a/interfaces/admin/static/theme-deepseek.css b/interfaces/admin/static/theme-deepseek.css index 9192979d..5ae19bc9 100644 --- a/interfaces/admin/static/theme-deepseek.css +++ b/interfaces/admin/static/theme-deepseek.css @@ -471,7 +471,9 @@ body.theme-ds-body .card { /* 与 .chat-avatar-slot 48px + .chat-row gap 10px 一致 */ .chat-msg-col--assistant { - width: min(max(0px, calc(50% + 2cm - 58px)), 100%); + /* 宽度随容器自适应,但最宽限制为 50% + 2cm(扣除头像轨 58px)。 */ + flex: 1 1 auto; + width: auto; max-width: min(max(0px, calc(50% + 2cm - 58px)), 100%); } diff --git a/runtime/agents/specialists.py b/runtime/agents/specialists.py index 75fd6088..05e908cb 100644 --- a/runtime/agents/specialists.py +++ b/runtime/agents/specialists.py @@ -32,13 +32,6 @@ SPECIALISTS: dict[SpecialistId, SpecialistConfig] = { expert_name="generalist+workspace+productivity+memory", default_tool_tags=None, ), - "image": SpecialistConfig( - specialist_id="image", - # image specialist currently reuses generalist expert tool registry, - # including image_edit tool. - expert_name="generalist+memory", - default_tool_tags=None, - ), "memory": SpecialistConfig( specialist_id="memory", expert_name="memory", @@ -47,7 +40,7 @@ SPECIALISTS: dict[SpecialistId, SpecialistConfig] = { } def discover_specialist_ids() -> tuple[SpecialistId, ...]: - rows = specialist_registry_snapshot(base_order=("generalist", "ops", "image", "memory")) + rows = specialist_registry_snapshot(base_order=("generalist", "ops", "memory")) return tuple(str(x.get("id") or "").strip().lower() for x in rows if str(x.get("id") or "").strip()) diff --git a/runtime/hooks/README.md b/runtime/hooks/README.md index 23315e0e..ccc9f958 100644 --- a/runtime/hooks/README.md +++ b/runtime/hooks/README.md @@ -28,6 +28,42 @@ Put hooks in any of: Each hook directory needs `HOOK.md` (YAML frontmatter with `metadata.oclaw.events`) plus one handler file as above. +### Session-bootstrap integration (verified) + +`session-bootstrap` is wired into the hooks runtime and loads on `agent:bootstrap`. + +- Hook package path: `runtime/skills/session-bootstrap/hooks/runtime/` +- Manifest: `HOOK.md` (`metadata.oclaw.events: ["agent:bootstrap"]`) +- Handler: `handler.py` (`handle(event)`) +- Runtime source type: `oclaw-managed` (skill `hooks/` dirs are merged into `hooks.internal.load.extraDirs`) + +#### End-to-end effect + +1. Runtime discovers the skill hook directory. +2. Loader registers `session-bootstrap` on `agent:bootstrap`. +3. Bootstrap event triggers `handle(event)`. +4. Handler injects virtual `SESSION_BOOTSTRAP.md` into `event.context.bootstrapFiles`. +5. Agent bootstrap context consumes that file for continuity (identity + recent memory + wiki signals). + +Current wiki inputs used by `session-bootstrap` include: + +- `data/wiki/core/*.md` (auto-discovered, sorted by filename; extensible for OSS contributors) +- `data/wiki/experts//*.md` (role-scoped rules, loaded when current `agentId` matches ``) +- `data/wiki/improvement/learnings.md` +- `data/wiki/improvement/errors.md` +- `data/wiki/improvement/feature-requests.md` + +For open-source extensibility, contributors only need to add new Markdown files under `data/wiki/core/` (for example `tone-style.md`), and bootstrap will include them automatically. +For role-specific behavior, contributors can add files under `data/wiki/experts//` (for example `experts/generalist/style.md`). + +#### Quick check + +```bash +python -m oclaw.runtime.operations hooks info session-bootstrap --workspace "D:/project/chatgpt/oclaw" --json +``` + +Expected key fields: `enabled_by_config=true`, `eligible=true`, `loadable=true`. + ### Remote eligibility on inbound messages Callers (e.g. gateway) may attach JSON metadata: diff --git a/runtime/skill_role_binding.py b/runtime/skill_role_binding.py index ffff8c3a..ff73a45b 100644 --- a/runtime/skill_role_binding.py +++ b/runtime/skill_role_binding.py @@ -17,7 +17,7 @@ def _truthy(v: str | None) -> bool: def ordered_specialist_ids() -> list[str]: base = [str(k).strip().lower() for k in discover_specialist_ids() if str(k).strip()] - preferred = [x for x in ("generalist", "ops", "image") if x in set(base)] + preferred = [x for x in ("generalist", "ops", "memory") if x in set(base)] return preferred + [x for x in base if x not in set(preferred)] diff --git a/runtime/skills/session-bootstrap/IDENTITY.md b/runtime/skills/session-bootstrap/IDENTITY.md index 0b3bd136..5e1c8639 100644 --- a/runtime/skills/session-bootstrap/IDENTITY.md +++ b/runtime/skills/session-bootstrap/IDENTITY.md @@ -2,7 +2,7 @@ ## 关系定位 -- 你:Oclaw 编码智能体与长期工程伙伴。 +- 你:Oclaw 智能体伙伴。 - 开发者:项目所有者与最终决策者。 ## 使命 diff --git a/runtime/skills/session-bootstrap/SKILL.md b/runtime/skills/session-bootstrap/SKILL.md index 44c76804..13c0b8a7 100644 --- a/runtime/skills/session-bootstrap/SKILL.md +++ b/runtime/skills/session-bootstrap/SKILL.md @@ -13,8 +13,10 @@ description: 在新会话开始时自动完成身份唤醒、近期记忆加载 1. 我是谁、应如何行动(`SOUL.md`、`IDENTITY.md`) 2. 最近发生了什么(`memory/` 最新记录) -3. 最近学到了什么(Wiki `improvement/*.md`) -4. 现在该如何衔接(简短唤醒摘要) +3. 应遵循什么共享行为准则(Wiki `core/*.md`) +4. 当前专家的角色规则(Wiki `experts//*.md`,如存在) +5. 最近学到了什么(Wiki `improvement/*.md`) +6. 现在该如何衔接(简短唤醒摘要) ## 必须遵循的读取顺序 @@ -23,11 +25,15 @@ description: 在新会话开始时自动完成身份唤醒、近期记忆加载 1. 读取 `SOUL.md` 2. 读取 `IDENTITY.md` 3. 读取 `memory/` 下最新文件 -4. 读取 Wiki 改进记录: +4. 读取 Wiki 行为准则(按文件名排序): + - `core/*.md`(示例:`core/principles.md`、`core/behavior.md`) +5. 读取 Wiki 角色规则(按当前专家角色,若目录存在): + - `experts//*.md`(示例:`experts/generalist/style.md`) +6. 读取 Wiki 改进记录: - `improvement/learnings.md` - `improvement/errors.md` - `improvement/feature-requests.md` -5. 在深入任务前先输出一句连续性衔接语 +7. 在深入任务前先输出一句连续性衔接语 ## 连续性衔接语格式 @@ -46,7 +52,37 @@ description: 在新会话开始时自动完成身份唤醒、近期记忆加载 在高置信度且明显可复用时,不必等待显式“记住这条”指令。 +## 开源扩展约定(供他人新增) + +- 将长期行为规则放在 `data/wiki/core/`,每个主题一个 `.md` 文件。 +- 命名建议小写短横线,例如:`principles.md`、`tone-style.md`、`safety-boundary.md`。 +- `session-bootstrap` 会自动读取 `core/*.md` 并注入启动上下文,无需改代码。 +- 如需专家个性化规则,放在 `data/wiki/experts//`(如 `experts/ops/`、`experts/generalist/`)。 +- 建议每个文件保持“原则 + 可执行规则 + 更新条件”三段结构,便于复用与审阅。 + ## 附加资源 - 核心身份与行为准则:[SOUL.md](SOUL.md) - 关系定位与使命:[IDENTITY.md](IDENTITY.md) + +## 当前状态(已打通) + +`session-bootstrap` 已接入 Oclaw hooks 运行链路,可在会话启动时自动生效: + +- 事件:`agent:bootstrap` +- Hook 名称:`session-bootstrap` +- 加载状态:`enabled_by_config=true`、`eligible=true`、`loadable=true` + +## 生效流程(端到端) + +1. 启动时,hooks 发现器会扫描技能目录下的 `hooks` 子目录。 +2. 读取 `hooks/runtime/HOOK.md`,注册 `agent:bootstrap` 事件到 `handler.py`。 +3. 会话进入 bootstrap 阶段后触发事件,执行 `handle(event)`。 +4. `handler.py` 生成虚拟文件 `SESSION_BOOTSTRAP.md`,写入 `event.context.bootstrapFiles`。 +5. 主控在启动上下文中读取该虚拟文件,完成身份唤醒与记忆衔接。 + +## 自检方式 + +可用以下命令检查是否处于可加载状态: + +`python -m oclaw.runtime.operations hooks info session-bootstrap --workspace "D:/project/chatgpt/oclaw" --json` diff --git a/runtime/skills/session-bootstrap/SOUL.md b/runtime/skills/session-bootstrap/SOUL.md index 8cdf229f..e5258ee1 100644 --- a/runtime/skills/session-bootstrap/SOUL.md +++ b/runtime/skills/session-bootstrap/SOUL.md @@ -1,6 +1,6 @@ # SOUL -你是一个“连续性优先”的工程伙伴。 +你是一个辅助性伙伴。 ## 核心特质 diff --git a/runtime/skills/session-bootstrap/hooks/runtime/handler.py b/runtime/skills/session-bootstrap/hooks/runtime/handler.py index cc54ea58..7acc2211 100644 --- a/runtime/skills/session-bootstrap/hooks/runtime/handler.py +++ b/runtime/skills/session-bootstrap/hooks/runtime/handler.py @@ -74,6 +74,36 @@ def _latest_memory_file(ws_dir: Path | None) -> Path | None: return files[0] +def _core_markdown_files(wiki_root: Path) -> list[Path]: + core_dir = wiki_root / "core" + if not core_dir.exists() or not core_dir.is_dir(): + return [] + files = [p for p in core_dir.glob("*.md") if p.is_file()] + files.sort(key=lambda p: p.name.lower()) + return files + + +def _agent_role(event: Any) -> str: + ctx = getattr(event, "context", None) + if isinstance(ctx, dict): + rid = str(ctx.get("agentId") or "").strip().lower() + if rid: + return rid + return "" + + +def _role_markdown_files(wiki_root: Path, role_id: str) -> list[Path]: + rid = str(role_id or "").strip().lower() + if not rid: + return [] + role_dir = wiki_root / "experts" / rid + if not role_dir.exists() or not role_dir.is_dir(): + return [] + files = [p for p in role_dir.glob("*.md") if p.is_file()] + files.sort(key=lambda p: p.name.lower()) + return files + + def _last_nonempty_line(text: str) -> str: lines = [ln.strip() for ln in str(text or "").splitlines() if ln.strip()] if not lines: @@ -106,6 +136,9 @@ def _build_bootstrap_content(event: Any) -> str: ident = repo / "runtime" / "skills" / "session-bootstrap" / "IDENTITY.md" mem_file = _latest_memory_file(ws_dir) + role_id = _agent_role(event) + core_files = _core_markdown_files(wiki) + role_files = _role_markdown_files(wiki, role_id) learnings = wiki / "improvement" / "learnings.md" errors = wiki / "improvement" / "errors.md" feats = wiki / "improvement" / "feature-requests.md" @@ -113,6 +146,16 @@ def _build_bootstrap_content(event: Any) -> str: soul_txt = _safe_read(soul, max_chars=800) ident_txt = _safe_read(ident, max_chars=800) mem_txt = _safe_read(mem_file, max_chars=900) if mem_file else "" + core_lines: list[str] = [] + for p in core_files: + snap = _safe_read(p, max_chars=500) + core_lines.append(f"### {p.name}\n{snap or '(缺失)'}") + core_txt = "\n\n".join(core_lines).strip() + role_lines: list[str] = [] + for p in role_files: + snap = _safe_read(p, max_chars=500) + role_lines.append(f"### {p.name}\n{snap or '(缺失)'}") + role_txt = "\n\n".join(role_lines).strip() lrn_txt = _safe_read(learnings, max_chars=900) topic = _extract_recent_topic(mem_txt) or "近期项目上下文" @@ -121,6 +164,8 @@ def _build_bootstrap_content(event: Any) -> str: welcome = f"欢迎回来,{developer}。上次我们聊了{topic},我学到了{learning}。" mem_path = str(mem_file) if mem_file else "(无)" + core_sources = ", ".join(str(p) for p in core_files) if core_files else "(无)" + role_sources = ", ".join(str(p) for p in role_files) if role_files else "(无)" return ( "# 会话唤醒摘要\n\n" f"{welcome}\n\n" @@ -128,11 +173,16 @@ def _build_bootstrap_content(event: Any) -> str: "1. SOUL.md\n" "2. IDENTITY.md\n" "3. memory 最新记录\n" - "4. Wiki 改进记录\n\n" + "4. Wiki core 行为规则\n" + "5. Wiki 角色规则(如存在)\n" + "6. Wiki 改进记录\n\n" "## 来源\n" f"- SOUL: {soul}\n" f"- IDENTITY: {ident}\n" f"- memory 最新记录: {mem_path}\n" + f"- 当前角色: {role_id or '(未知)'}\n" + f"- Wiki core 规则文件: {core_sources}\n" + f"- Wiki 角色规则文件: {role_sources}\n" f"- Wiki 学习记录: {learnings}\n" f"- Wiki 错误记录: {errors}\n" f"- Wiki 需求记录: {feats}\n\n" @@ -142,6 +192,10 @@ def _build_bootstrap_content(event: Any) -> str: f"{ident_txt or '(缺失)'}\n\n" "## 最近会话快照\n" f"{mem_txt or '(缺失)'}\n\n" + "## Core 规则快照\n" + f"{core_txt or '(缺失)'}\n\n" + "## 角色规则快照\n" + f"{role_txt or '(缺失)'}\n\n" "## 最近学习快照\n" f"{lrn_txt or '(缺失)'}\n" ) diff --git a/runtime/tools/experts/workspace/workspace_base.py b/runtime/tools/experts/workspace/workspace_base.py index 48f45c9a..4df3f996 100644 --- a/runtime/tools/experts/workspace/workspace_base.py +++ b/runtime/tools/experts/workspace/workspace_base.py @@ -19,7 +19,8 @@ def _env_truthy(name: str) -> bool: def workspace_root() -> Path: # Allow explicit override (recommended when running as a packaged app) - override = (os.getenv("AIA_WORKSPACE_ROOT") or os.getenv("OPS_WORKSPACE_ROOT") or "").strip() + # Prefer legacy OPS_* overrides when explicitly provided (tests + backwards compatibility). + override = (os.getenv("OPS_WORKSPACE_ROOT") or os.getenv("AIA_WORKSPACE_ROOT") or "").strip() if override: p = Path(override).expanduser() return p.resolve() @@ -50,9 +51,9 @@ class WorkspacePathAccess: def access_from_env() -> WorkspacePathAccess: - raw_extra = os.getenv("AIA_WORKSPACE_EXTRA_ROOTS") or os.getenv("OPS_WORKSPACE_EXTRA_ROOTS") or "" + raw_extra = os.getenv("OPS_WORKSPACE_EXTRA_ROOTS") or os.getenv("AIA_WORKSPACE_EXTRA_ROOTS") or "" extra = _parse_pipe_separated_roots(raw_extra) - allow = _env_truthy("AIA_WORKSPACE_ALLOW_ANY_PATH") or _env_truthy("OPS_WORKSPACE_ALLOW_ANY_PATH") + allow = _env_truthy("OPS_WORKSPACE_ALLOW_ANY_PATH") or _env_truthy("AIA_WORKSPACE_ALLOW_ANY_PATH") return WorkspacePathAccess(extra_roots=tuple(extra), allow_any_path=allow) diff --git a/runtime/types.py b/runtime/types.py index 964d3852..210a0ead 100644 --- a/runtime/types.py +++ b/runtime/types.py @@ -44,8 +44,6 @@ def normalize_requested_specialist(raw: Any) -> SpecialistId: specialist = str(raw or "").strip().lower() if specialist == "ops": return "ops" - if specialist == "image": - return "image" if specialist == "memory": return "memory" return "generalist" diff --git a/runtime/workspaces/experts.py b/runtime/workspaces/experts.py index 58a5a808..b16cf2f8 100644 --- a/runtime/workspaces/experts.py +++ b/runtime/workspaces/experts.py @@ -244,7 +244,7 @@ def build_expert_catalog_block(*, include_main: bool = False, per_field_limit: i def discover_specialist_ids_from_workspaces( *, - base_order: tuple[str, ...] = ("generalist", "ops", "image", "memory"), + base_order: tuple[str, ...] = ("generalist", "ops", "memory"), ) -> tuple[str, ...]: cache_key = (expert_workspace_signature_token(), tuple(str(x).strip().lower() for x in base_order if str(x).strip())) with _CACHE_LOCK: @@ -283,7 +283,7 @@ def warm_expert_workspace_cache() -> None: def specialist_registry_snapshot( *, - base_order: tuple[str, ...] = ("generalist", "ops", "image", "memory"), + base_order: tuple[str, ...] = ("generalist", "ops", "memory"), ) -> tuple[dict[str, Any], ...]: """Single source of truth for runtime specialist discovery and metadata.""" ordered = discover_specialist_ids_from_workspaces(base_order=base_order) diff --git a/runtime/workspaces/image/META.json b/runtime/workspaces/image/META.json deleted file mode 100644 index a997f07a..00000000 --- a/runtime/workspaces/image/META.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "display_name_en": "image", - "display_name_zh": "图片", - "role": "expert" -} diff --git a/runtime/workspaces/image/ROLE_SYSTEM.md b/runtime/workspaces/image/ROLE_SYSTEM.md deleted file mode 100644 index 4b03aa63..00000000 --- a/runtime/workspaces/image/ROLE_SYSTEM.md +++ /dev/null @@ -1,11 +0,0 @@ -你是图像专家(image specialist)。 - -## 输入约束: -- 任务可能是生成、编辑、抠图、扩图或风格迁移。 - -## 执行规则: -1. 明确任务类型(生成、编辑、抠图、扩图等)和约束条件。 -2. 结果描述需结构化,包含主体、风格、构图、色彩与质量要求。 - -## 输出格式: -- 直接给可执行提示词或操作步骤,避免空泛审美描述。 diff --git a/tests/test_oclaw_gateway_trace.py b/tests/test_oclaw_gateway_trace.py index 1dde7d5d..5021b316 100644 --- a/tests/test_oclaw_gateway_trace.py +++ b/tests/test_oclaw_gateway_trace.py @@ -225,7 +225,7 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch class _ManagerModel: def chat(self, _messages, _tools, *, on_token=None): return LLMResponse( - content='{"route":{"specialist":"image","reason":"needs image edits"},"dispatch":{"instruction_text":"Please edit the image background."}}', + content='{"route":{"specialist":"generalist","reason":"needs image edits"},"dispatch":{"instruction_text":"Please edit the image background."}}', tool_calls=[], ) @@ -239,8 +239,8 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) captured: dict = {} @@ -272,8 +272,8 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch ) 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 == "image" - assert chosen.get("sid") == "image" + 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" @@ -308,8 +308,8 @@ def test_gateway_comprehensive_mode_writes_task_assignment_reasoning(monkeypatch "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) monkeypatch.setattr( @@ -367,8 +367,8 @@ def test_gateway_comprehensive_ignores_wiki_inject_flags(monkeypatch: pytest.Mon "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) captured: dict[str, object] = {} @@ -430,8 +430,8 @@ def test_gateway_comprehensive_mode_has_manager_final_pass(monkeypatch: pytest.M "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) monkeypatch.setattr( @@ -486,8 +486,8 @@ def test_gateway_comprehensive_mode_dynamic_agent_dispatches_instruction_only(mo "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) monkeypatch.setattr("oclaw.runtime.gateway.build_ephemeral_executor", lambda *args, **kwargs: _Exec(model=object())) @@ -549,8 +549,8 @@ def test_gateway_comprehensive_mode_ignores_manager_self_and_dispatches_speciali "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) @@ -626,8 +626,8 @@ def test_gateway_comprehensive_mode_suppresses_instruction_echo(monkeypatch: pyt "oclaw.runtime.gateway.get_manager_prompt_prebuild", lambda **kwargs: { "manager_context": "manager", - "allowed_fixed": ("generalist", "ops", "image", "memory"), - "allowed_fixed_quoted": '"generalist", "ops", "image", "memory"', + "allowed_fixed": ("generalist", "ops", "memory"), + "allowed_fixed_quoted": '"generalist", "ops", "memory"', }, ) diff --git a/tests/test_oclaw_role_prompt_paths.py b/tests/test_oclaw_role_prompt_paths.py index aa322f95..2853c939 100644 --- a/tests/test_oclaw_role_prompt_paths.py +++ b/tests/test_oclaw_role_prompt_paths.py @@ -6,7 +6,5 @@ from oclaw.runtime.agents.specialists import default_system_prefix_for_specialis def test_specialist_prompts_loaded_from_oclaw_tree() -> None: ops = default_system_prefix_for_specialist("ops", "zh") gen = default_system_prefix_for_specialist("generalist", "zh") - img = default_system_prefix_for_specialist("image", "zh") assert "运维专家(ops specialist)" in ops assert "通识专家(generalist specialist)" in gen - assert "图像专家(image specialist)" in img diff --git a/tests/test_oclaw_router.py b/tests/test_oclaw_router.py index e0c0e95e..dcdfda5e 100644 --- a/tests/test_oclaw_router.py +++ b/tests/test_oclaw_router.py @@ -106,7 +106,7 @@ def test_interaction_mode_normalization_supports_legacy_values() -> None: def test_requested_specialist_normalization_defaults_to_generalist() -> None: assert normalize_requested_specialist("ops") == "ops" - assert normalize_requested_specialist("image") == "image" + assert normalize_requested_specialist("image") == "generalist" assert normalize_requested_specialist("memory") == "memory" assert normalize_requested_specialist("unknown") == "generalist" diff --git a/tests/test_prompt_prebuild.py b/tests/test_prompt_prebuild.py index 5241f680..2b79dc7c 100644 --- a/tests/test_prompt_prebuild.py +++ b/tests/test_prompt_prebuild.py @@ -14,7 +14,7 @@ class _DummyStore: def test_get_manager_prompt_prebuild_includes_structured_skills(monkeypatch) -> None: - monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops", "image", "memory")) + monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops", "memory")) monkeypatch.setattr( pp, "list_experts", diff --git a/tests/test_specialists_registry.py b/tests/test_specialists_registry.py index 4ddf665b..e7aedaa8 100644 --- a/tests/test_specialists_registry.py +++ b/tests/test_specialists_registry.py @@ -18,4 +18,3 @@ def test_agent_role_ids_uses_runtime_discovery(monkeypatch) -> None: def test_builtin_specialists_include_memory_tools() -> None: assert "memory" in specialists_mod.expert_name_for_specialist("generalist") assert "memory" in specialists_mod.expert_name_for_specialist("ops") - assert "memory" in specialists_mod.expert_name_for_specialist("image")