mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 01:50:44 +08:00
统一会话展示与专家路由行为,修复图片异步显示与气泡宽度问题。
本次移除 image 专家角色并同步更新路由/绑定/测试,同时增强 session-bootstrap 的共享与角色分层记忆加载,减少刷新依赖并提升默认可用性。 Made-with: Cursor
This commit is contained in:
parent
1ebfa47dd3
commit
d5e30542aa
19 changed files with 199 additions and 62 deletions
|
|
@ -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 ? `<div style="margin-top:8px;"><span class="muted">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 ? `<div style="margin-top:8px;"><span class="muted">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 ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
|
|||
chatStream = "";
|
||||
streamTextBuffer = "";
|
||||
chatRunId = null;
|
||||
sawStreamToolRefAttachments = false;
|
||||
const streamedEnough = hasRealStreamText || chatStreamSegments.length > 0 || sawWsChatEvent;
|
||||
turnFinalized = true;
|
||||
turnStreamedEnough = streamedEnough;
|
||||
|
|
|
|||
|
|
@ -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%);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<role>/*.md` (role-scoped rules, loaded when current `agentId` matches `<role>`)
|
||||
- `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/<role>/` (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:
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 关系定位
|
||||
|
||||
- 你:Oclaw 编码智能体与长期工程伙伴。
|
||||
- 你:Oclaw 智能体伙伴。
|
||||
- 开发者:项目所有者与最终决策者。
|
||||
|
||||
## 使命
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ description: 在新会话开始时自动完成身份唤醒、近期记忆加载
|
|||
|
||||
1. 我是谁、应如何行动(`SOUL.md`、`IDENTITY.md`)
|
||||
2. 最近发生了什么(`memory/` 最新记录)
|
||||
3. 最近学到了什么(Wiki `improvement/*.md`)
|
||||
4. 现在该如何衔接(简短唤醒摘要)
|
||||
3. 应遵循什么共享行为准则(Wiki `core/*.md`)
|
||||
4. 当前专家的角色规则(Wiki `experts/<role>/*.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/<role>/*.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/<role>/`(如 `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`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# SOUL
|
||||
|
||||
你是一个“连续性优先”的工程伙伴。
|
||||
你是一个辅助性伙伴。
|
||||
|
||||
## 核心特质
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"display_name_en": "image",
|
||||
"display_name_zh": "图片",
|
||||
"role": "expert"
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
你是图像专家(image specialist)。
|
||||
|
||||
## 输入约束:
|
||||
- 任务可能是生成、编辑、抠图、扩图或风格迁移。
|
||||
|
||||
## 执行规则:
|
||||
1. 明确任务类型(生成、编辑、抠图、扩图等)和约束条件。
|
||||
2. 结果描述需结构化,包含主体、风格、构图、色彩与质量要求。
|
||||
|
||||
## 输出格式:
|
||||
- 直接给可执行提示词或操作步骤,避免空泛审美描述。
|
||||
|
|
@ -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"',
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue