Remove plan mode, skill toolcall path, and inline netx tools.

Wave C subtraction: keep skills as prompt-only, route netx via MCP with a shared netx_http client for xlsx/context inject, and delete plan_agent_v2 from the gateway.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-11 01:17:39 +08:00
parent 628a9dffd2
commit efa72df362
40 changed files with 169 additions and 3033 deletions

View file

@ -172,8 +172,8 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
payload = json.loads(str(task.payload or "{}"))
self.assertEqual(str(payload.get("selected_specialist") or ""), "generalist")
def test_user_mode_plan_version_sets_v2_feature_flag_in_store(self) -> None:
"""POST /user-mode mirrors plan_agent_version to AIA_EXPERT_PLAN_AGENT_V2_ENABLED (v2→1, v1→0)."""
def test_user_mode_always_clears_plan_agent_v2_flag(self) -> None:
"""Plan mode removed: POST /user-mode always keeps AIA_EXPERT_PLAN_AGENT_V2_ENABLED=0."""
token = self._login()
headers = {
"authorization": f"Bearer {token}",
@ -193,8 +193,8 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
self.assertEqual(r2.status_code, 200)
body2 = r2.json()
self.assertTrue(body2.get("ok"), body2)
self.assertTrue(body2.get("plan_agent_v2_globally_enabled"), body2)
self.assertEqual(str(self.store.get_setting("AIA_EXPERT_PLAN_AGENT_V2_ENABLED") or "").strip(), "1")
self.assertFalse(body2.get("plan_agent_v2_globally_enabled"), body2)
self.assertEqual(str(self.store.get_setting("AIA_EXPERT_PLAN_AGENT_V2_ENABLED") or "").strip(), "0")
r1 = self.client.post(
"/admin/api/chat/user-mode",

View file

@ -1,9 +1,7 @@
"""Expert registry: netx builtin tools gated by OCLAW_NETX_BUILTIN_TOOLS."""
"""Expert registry: inline netx_* tools removed; MCP only."""
from __future__ import annotations
import os
import pytest
from runtime.tools import expert_registry
@ -18,18 +16,10 @@ def _clear_expert_cache():
expert_registry._CACHED_SPECS_BY_EXPERT = None
def test_netx_tools_skipped_when_builtin_disabled(monkeypatch):
monkeypatch.delenv("OCLAW_NETX_BUILTIN_TOOLS", raising=False)
factories = expert_registry.discover_expert_tool_factories()
network_ops = factories.get("network_ops") or []
names = {f().name for f in network_ops}
assert not any(n.startswith("netx_") for n in names)
def test_netx_tools_registered_when_builtin_enabled(monkeypatch):
def test_netx_inline_tools_never_registered(monkeypatch):
monkeypatch.setenv("OCLAW_NETX_BUILTIN_TOOLS", "1")
factories = expert_registry.discover_expert_tool_factories()
network_ops = factories.get("network_ops") or []
names = {f().name for f in network_ops}
assert "netx_query_ume_alarms" in names
assert "netx_exec_managed_ne" in names
assert not any(n.startswith("netx_") for n in names)
assert "ume_alarm_xlsx_report" in names

View file

@ -1,49 +0,0 @@
from __future__ import annotations
from typing import Any
import pytest
def test_netx_list_managed_ne_forwards_params(monkeypatch: pytest.MonkeyPatch) -> None:
import runtime.tools.experts.network_ops.netx_tools as nt
calls: list[tuple[str, str, dict[str, Any] | None]] = []
def fake(method: str, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
calls.append((method, path, params))
return {"ok": True, "data": {"total": 0, "items": []}}
monkeypatch.setattr(nt, "_http_json", fake)
spec = nt.netx_list_managed_ne_tool()
out = spec.handler({"keyword": "192.168", "connect_status": "pass", "page": 1, "page_size": 20})
assert out.get("ok") is True
assert calls[0] == ("GET", "/v1/managed-ne", {"page": 1, "page_size": 20, "keyword": "192.168", "connect_status": "pass"})
def test_netx_exec_managed_ne_posts_body(monkeypatch: pytest.MonkeyPatch) -> None:
import runtime.tools.experts.network_ops.netx_tools as nt
bodies: list[dict[str, Any]] = []
def fake_post(path: str, body: dict[str, Any], *, timeout: float = 180.0) -> dict[str, Any]:
bodies.append(body)
return {"ok": True, "data": {"ok": True, "output": "R2#show version\n..."}}
monkeypatch.setattr(nt, "_http_post_json", fake_post)
spec = nt.netx_exec_managed_ne_tool()
out = spec.handler({"ne_id": "abc", "commands": ["show version"], "read_timeout_sec": 90})
assert out.get("ok") is True
assert bodies[0]["ne_id"] == "abc"
assert bodies[0]["commands"] == ["show version"]
assert bodies[0]["read_timeout_sec"] == 90
def test_netx_exec_requires_commands(monkeypatch: pytest.MonkeyPatch) -> None:
import runtime.tools.experts.network_ops.netx_tools as nt
monkeypatch.setattr(nt, "_http_post_json", lambda *a, **k: {"ok": True, "data": {}})
spec = nt.netx_exec_managed_ne_tool()
out = spec.handler({"ne_id": "abc"})
assert out.get("ok") is False
assert out.get("error_code") == "commands_required"

View file

@ -1,6 +1,6 @@
import unittest
from runtime.tools.experts.network_ops import netx_tools as nt
from runtime.tools.experts.network_ops import netx_http as nt
class NetxProtocolLocalizeTests(unittest.TestCase):

View file

@ -1,50 +0,0 @@
from __future__ import annotations
from typing import Any
import pytest
def test_netx_query_ume_ne_inventory_forwards_params(monkeypatch: pytest.MonkeyPatch) -> None:
import runtime.tools.experts.network_ops.netx_tools as nt
calls: list[tuple[str, str, dict[str, Any] | None]] = []
def fake(method: str, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
calls.append((method, path, params))
return {"ok": True, "data": {"total": 0, "page": 1, "page_size": 50, "items": []}}
monkeypatch.setattr(nt, "_http_json", fake)
spec = nt.netx_query_ume_ne_inventory_tool()
out = spec.handler({"keyword": "10.0.0", "page": 2, "page_size": 100})
assert out.get("ok") is True
assert len(calls) == 1
assert calls[0][0] == "GET"
assert calls[0][1] == "/v1/ume/inventory/ne"
assert calls[0][2] == {"page": 2, "page_size": 100, "keyword": "10.0.0"}
def test_netx_get_ume_ne_requires_id(monkeypatch: pytest.MonkeyPatch) -> None:
import runtime.tools.experts.network_ops.netx_tools as nt
monkeypatch.setattr(nt, "_http_json", lambda *a, **k: {"ok": True, "data": {}})
spec = nt.netx_get_ume_ne_tool()
out = spec.handler({})
assert out.get("ok") is False
assert out.get("error_code") == "ne_id_required"
def test_netx_get_ume_ne_quotes_path(monkeypatch: pytest.MonkeyPatch) -> None:
import runtime.tools.experts.network_ops.netx_tools as nt
paths: list[str] = []
def fake(method: str, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
paths.append(path)
return {"ok": True, "data": {"ne_id": "x"}}
monkeypatch.setattr(nt, "_http_json", fake)
spec = nt.netx_get_ume_ne_tool()
nid = "550e8400-e29b-41d4-a716-446655440000"
spec.handler({"ne_id": nid})
assert paths == [f"/v1/ume/inventory/ne/{nid}"]

View file

@ -218,11 +218,9 @@ def test_gateway_expert_mode_uses_requested_specialist() -> None:
assert chosen.get("sid") == "ops"
def test_gateway_expert_plan_execution_mode_runs_v2_with_plan_prompt(monkeypatch: pytest.MonkeyPatch) -> None:
def test_gateway_expert_plan_execution_mode_ignored_without_plan_agent(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, k: str) -> str:
if str(k or "") == "AIA_EXPERT_PLAN_AGENT_V2_ENABLED":
return "1"
def get_setting(self, _k: str) -> str:
return ""
def set_setting(self, _k: str, _v: str) -> None:
@ -234,9 +232,6 @@ def test_gateway_expert_plan_execution_mode_runs_v2_with_plan_prompt(monkeypatch
def add_trace_events_batch(self, _rows: list[dict[str, object]]) -> None:
return None
def set_setting(self, _k: str, _v: str) -> None:
return None
class _Exec:
model = object()
tools = object()
@ -247,7 +242,7 @@ def test_gateway_expert_plan_execution_mode_runs_v2_with_plan_prompt(monkeypatch
def _run_agent_core_ok(**kwargs: object) -> object:
data = kwargs.get("data")
captured["system_prompt"] = str(getattr(data, "system_prompt", "") or "")
return SimpleNamespace(outcome=SimpleNamespace(final_text="plan_llm_reply", turn_uuid="turn-1"))
return SimpleNamespace(outcome=SimpleNamespace(final_text="agent_reply", turn_uuid="turn-1"))
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core_ok)
@ -265,130 +260,10 @@ def test_gateway_expert_plan_execution_mode_runs_v2_with_plan_prompt(monkeypatch
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec())
assert out.interaction_mode == "expert"
assert out.dispatch_reason == "expert_direct"
assert str(out.reply_text or "") == "plan_llm_reply"
assert str(out.reply_text or "") == "agent_reply"
prompt_text = str(captured.get("system_prompt") or "")
assert ("plan 模式" in prompt_text) or ("Plan mode is active" in prompt_text)
assert ("计划工作流" in prompt_text) or ("Plan Workflow" in prompt_text)
def test_gateway_expert_plan_mode_filters_non_readonly_tools(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, k: str) -> str:
if str(k or "") == "AIA_EXPERT_PLAN_AGENT_V2_ENABLED":
return "1"
return ""
def set_setting(self, _k: str, _v: str) -> None:
return None
def add_trace_event(self, **_kwargs: object) -> None:
return None
def add_trace_events_batch(self, _rows: list[dict[str, object]]) -> None:
return None
def _mk_tool(name: str, read_only: bool) -> ToolSpec:
return ToolSpec(
name=name,
description=name,
parameters={"type": "object", "properties": {}, "additionalProperties": True},
handler=lambda args: {"ok": True, "args": args},
read_only=read_only,
)
class _Exec:
model = object()
system_prompt = "base-system"
tools = ToolRegistry([_mk_tool("read_file", True), _mk_tool("edit_file", False)])
captured: dict[str, object] = {}
def _run_agent_core_ok(**kwargs: object) -> object:
data = kwargs.get("data")
tools = getattr(data, "tools", None)
captured["tool_names"] = [t.name for t in tools.list()] if hasattr(tools, "list") else []
return SimpleNamespace(outcome=SimpleNamespace(final_text="ok", turn_uuid="turn-1"))
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core_ok)
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-plan-tools",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="先给我一个执行计划",
attachments=[],
metadata={"interaction_mode": "expert", "selected_specialist": "generalist", "execution_mode": "plan"},
)
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec())
assert str(out.reply_text or "") == "ok"
names = list(captured.get("tool_names") or [])
assert "read_file" in names
assert "edit_file" not in names
def test_gateway_expert_agent_mode_injects_plan_control_tools(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def __init__(self) -> None:
self.kv: dict[str, str] = {}
def get_setting(self, k: str) -> str:
return str(self.kv.get(k) or "")
def set_setting(self, k: str, v: str) -> None:
self.kv[k] = str(v or "")
def add_trace_event(self, **_kwargs: object) -> None:
return None
def add_trace_events_batch(self, _rows: list[dict[str, object]]) -> None:
return None
def _mk_tool(name: str, read_only: bool) -> ToolSpec:
return ToolSpec(
name=name,
description=name,
parameters={"type": "object", "properties": {}, "additionalProperties": True},
handler=lambda args: {"ok": True, "args": args},
read_only=read_only,
)
class _Exec:
model = object()
system_prompt = "base-system"
tools = ToolRegistry([_mk_tool("read_file", True)])
captured: dict[str, object] = {}
def _run_agent_core_ok(**kwargs: object) -> object:
data = kwargs.get("data")
tools = getattr(data, "tools", None)
captured["tool_names"] = [t.name for t in tools.list()] if hasattr(tools, "list") else []
return SimpleNamespace(outcome=SimpleNamespace(final_text="ok", turn_uuid="turn-1"))
monkeypatch.setattr("runtime.gateway.run_agent_core", _run_agent_core_ok)
store = Store()
store.kv["AIA_EXPERT_PLAN_AGENT_V2_ENABLED"] = "1"
gw = OclawGateway(store=store)
msg = StandardMessage(
session_id="sid-agent-tools",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="直接执行",
attachments=[],
metadata={"interaction_mode": "expert", "selected_specialist": "generalist", "execution_mode": "agent"},
)
out = gw.handle_turn(msg=msg, lang="zh", executor=_Exec())
assert str(out.reply_text or "") == "ok"
names = list(captured.get("tool_names") or [])
assert "enter_plan_mode_v2" in names
assert "exit_plan_mode_v2" in names
assert store.get_setting("AIA_PLAN_AGENT_V2_DEFAULT_SESSION_ID") == "sid-agent-tools"
assert "plan 模式" not in prompt_text
assert "Plan mode is active" not in prompt_text
def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch: pytest.MonkeyPatch) -> None:

View file

@ -1,142 +0,0 @@
from __future__ import annotations
from pathlib import Path
from svc.persistence.sqlite_store import SqliteStore
from runtime.gateway import OclawGatewayResult
from runtime.plan_agent_v2 import (
build_shadow_gateway_result,
evaluate_gateway_expert_turn_shadow,
legacy_gateway_result_keys,
)
from runtime.types import StandardMessage
def _msg(text: str) -> StandardMessage:
return StandardMessage(
session_id="sess-dryrun",
tenant_id="tenant-1",
user_id="user-1",
role="user",
channel="chat",
text=text,
attachments=[],
metadata={},
)
def test_gateway_shadow_stays_off_without_force_or_flag(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
out = evaluate_gateway_expert_turn_shadow(
store=store,
msg=_msg("实现一个功能"),
lang="zh",
interaction_mode="expert",
requested_specialist="generalist",
base_system_prompt="base-system",
force_flag=False,
)
assert out.used_v2 is False
assert out.decision is None
def test_gateway_shadow_force_path_matches_legacy_shape(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
out = evaluate_gateway_expert_turn_shadow(
store=store,
msg=_msg("请先给计划"),
lang="zh",
interaction_mode="expert",
requested_specialist="generalist",
base_system_prompt="base-system",
force_flag=True,
)
assert out.used_v2 is True
assert out.decision is not None
shadow_row = build_shadow_gateway_result(
decision=out.decision,
run_id="run-1",
trace_id="trace-1",
elapsed_ms=9,
requested_specialist="generalist",
)
assert set(shadow_row.keys()) == legacy_gateway_result_keys()
baseline = OclawGatewayResult(run_id="run-1", reply_text="", trace_id="trace-1", elapsed_ms=9)
assert shadow_row["mode"] == baseline.mode
assert shadow_row["task_id"] == baseline.task_id
assert shadow_row["dynamic_agent_used"] == baseline.dynamic_agent_used
assert shadow_row["relay_pointer_count"] == baseline.relay_pointer_count
def test_gateway_shadow_confirm_path_builds_compatible_result(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
# Enter plan
first = evaluate_gateway_expert_turn_shadow(
store=store,
msg=_msg("我要改造一下"),
lang="zh",
interaction_mode="expert",
requested_specialist="generalist",
base_system_prompt="base-system",
force_flag=True,
)
assert first.used_v2 is True
assert first.decision is not None
assert first.decision.action == "run_agent"
assert "base-system" in str(first.decision.system_prompt_override or "")
# Confirm plan
second = evaluate_gateway_expert_turn_shadow(
store=store,
msg=_msg("确认"),
lang="zh",
interaction_mode="expert",
requested_specialist="generalist",
base_system_prompt="base-system",
force_flag=True,
)
assert second.used_v2 is True
assert second.decision is not None
assert second.decision.action == "stay_plan"
assert "切换到 agent 模式" in str(second.decision.reply_text or "")
row = build_shadow_gateway_result(
decision=second.decision,
run_id="run-2",
trace_id="trace-2",
elapsed_ms=12,
requested_specialist="generalist",
)
assert row["interaction_mode"] == "expert"
assert str(row["dispatch_reason"]).startswith("plan_agent_v2:")
def test_gateway_shadow_skips_v2_when_metadata_plan_agent_version_v1(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
out = evaluate_gateway_expert_turn_shadow(
store=store,
msg=StandardMessage(
session_id="sess-dryrun",
tenant_id="tenant-1",
user_id="user-1",
role="user",
channel="chat",
text="请先给计划",
attachments=[],
metadata={"plan_agent_version": "v1"},
),
lang="zh",
interaction_mode="expert",
requested_specialist="generalist",
base_system_prompt="base-system",
force_flag=True,
)
assert out.used_v2 is False
assert out.decision is None

View file

@ -1,450 +0,0 @@
from __future__ import annotations
from pathlib import Path
from svc.persistence.sqlite_store import SqliteStore
from runtime.plan_agent_v2.adapter import evaluate_for_expert_mode
from runtime.plan_agent_v2.compat import build_shadow_gateway_result, legacy_gateway_result_keys
from runtime.plan_agent_v2.gateway_adapter import evaluate_gateway_expert_turn_shadow
from runtime.plan_agent_v2.manager import PlanModeManagerV2
from runtime.plan_agent_v2.models import PLAN_MODE_PLAN, PlanAgentStateV2
from runtime.plan_agent_v2.prompt_injector import build_plan_mode_prefix
from runtime.plan_agent_v2.state_store import PlanAgentStateStoreV2
from runtime.plan_agent_v2.switch import should_route_to_v2, v2_feature_enabled
from runtime.plan_agent_v2.tool_specs import materialize_plan_mode_v2_tools
from runtime.plan_agent_v2.tool_policy import filter_tools_for_mode
from runtime.plan_agent_v2.trace import emit_plan_agent_v2_trace
from runtime.plan_agent_v2 import should_route_to_v2 as should_route_to_v2_pkg
from runtime.gateway import OclawGatewayResult
from runtime.tools.base import ToolRegistry, ToolSpec
from runtime.types import StandardMessage
def _dummy_tool(name: str, read_only: bool) -> ToolSpec:
def _handler(args):
return {"ok": True, "echo": args}
return ToolSpec(
name=name,
description=name,
parameters={"type": "object", "properties": {}, "additionalProperties": True},
handler=_handler,
read_only=read_only,
)
def test_state_store_roundtrip(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
ss = PlanAgentStateStoreV2(store)
st = ss.load(session_id="s1")
assert st.mode == "normal"
saved = ss.save(session_id="s1", state=st)
loaded = ss.load(session_id="s1")
assert loaded.mode == saved.mode
def test_manager_enter_and_confirm(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
plan_root = tmp_path / "plans"
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(plan_root))
mgr = PlanModeManagerV2(store=store)
st1 = mgr.enter(session_id="sess-1", owner_specialist="generalist")
assert st1.mode == PLAN_MODE_PLAN
assert st1.plan_path
assert Path(st1.plan_path).exists()
st2 = mgr.confirm(session_id="sess-1")
assert st2.mode == "normal"
assert st2.plan_confirmed is True
assert "## Goal" in str(st2.plan_content or "")
def test_tool_policy_filters_non_readonly_in_plan_mode() -> None:
reg = ToolRegistry([_dummy_tool("read_a", True), _dummy_tool("write_a", False)])
out = filter_tools_for_mode(registry=reg, mode="plan")
names = {t.name for t in out}
assert "read_a" in names
assert "write_a" not in names
def test_tool_policy_keeps_plan_mode_control_tools() -> None:
reg = ToolRegistry([_dummy_tool("exit_plan_mode_v2", False), _dummy_tool("write_a", False)])
out = filter_tools_for_mode(registry=reg, mode="plan")
names = {t.name for t in out}
assert "exit_plan_mode_v2" in names
assert "write_a" not in names
def test_adapter_agent_mode_repeated_user_injects_stall_guard(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
sid = store.create_session("stall-test").id
duplicate_line = "please handle this request"
store.add_message(session_id=sid, role="user", content=duplicate_line, event_type="user_text")
store.add_message(session_id=sid, role="assistant", content="I will analyze first…", event_type="assistant_text")
dec = evaluate_for_expert_mode(
store=store,
session_id=sid,
lang="en",
requested_specialist="generalist",
user_text=duplicate_line,
execution_mode="agent",
base_system_prompt="base",
)
assert dec.action == "run_agent"
assert "Conversation stall guard" in str(dec.system_prompt_override or "")
assert "base" in str(dec.system_prompt_override or "")
def test_adapter_plan_flow(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
d1 = evaluate_for_expert_mode(
store=store,
session_id="s1",
lang="zh",
requested_specialist="generalist",
user_text="帮我做一个功能",
execution_mode="plan",
base_system_prompt="base",
)
assert d1.action == "run_agent"
assert isinstance(d1.plan_state, dict)
assert str(d1.plan_state.get("mode") or "") == "plan"
assert "base" in str(d1.system_prompt_override or "")
d2 = evaluate_for_expert_mode(
store=store,
session_id="s1",
lang="zh",
requested_specialist="generalist",
user_text="确认",
execution_mode="agent",
base_system_prompt="base",
)
assert d2.action == "run_agent"
assert "base" in str(d2.system_prompt_override or "")
def test_adapter_confirm_blocked_until_agent_mode(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
_ = evaluate_for_expert_mode(
store=store,
session_id="s2",
lang="zh",
requested_specialist="generalist",
user_text="先给计划",
execution_mode="plan",
base_system_prompt="base",
)
d2 = evaluate_for_expert_mode(
store=store,
session_id="s2",
lang="zh",
requested_specialist="generalist",
user_text="确认",
execution_mode="plan",
base_system_prompt="base",
)
assert d2.action == "stay_plan"
assert "切换到 agent 模式" in str(d2.reply_text or "")
def test_adapter_confirm_strategy_auto_allows_confirm_in_plan_mode(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
store.set_setting("AIA_EXPERT_PLAN_CONFIRM_STRATEGY", "auto")
_ = evaluate_for_expert_mode(
store=store,
session_id="s-auto",
lang="zh",
requested_specialist="generalist",
user_text="先给计划",
execution_mode="plan",
base_system_prompt="base",
)
d2 = evaluate_for_expert_mode(
store=store,
session_id="s-auto",
lang="zh",
requested_specialist="generalist",
user_text="确认",
execution_mode="plan",
base_system_prompt="base",
)
assert d2.action == "run_agent"
assert "已确认计划" in str(d2.reply_text or "")
def test_adapter_confirm_strategy_off_allows_confirm_in_plan_mode(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
store.set_setting("AIA_EXPERT_PLAN_CONFIRM_STRATEGY", "off")
_ = evaluate_for_expert_mode(
store=store,
session_id="s-off",
lang="zh",
requested_specialist="generalist",
user_text="先给计划",
execution_mode="plan",
base_system_prompt="base",
)
d2 = evaluate_for_expert_mode(
store=store,
session_id="s-off",
lang="zh",
requested_specialist="generalist",
user_text="确认",
execution_mode="plan",
base_system_prompt="base",
)
assert d2.action == "run_agent"
assert "已确认计划" in str(d2.reply_text or "")
def test_adapter_plan_loop_guard_blocks_repeated_input(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
common = dict(
store=store,
session_id="s-loop",
lang="zh",
requested_specialist="generalist",
execution_mode="plan",
base_system_prompt="base",
)
_ = evaluate_for_expert_mode(user_text="继续", **common)
d2 = evaluate_for_expert_mode(user_text="继续", **common)
assert d2.action == "stay_plan"
assert "低信息续写" in str(d2.reply_text or "")
def test_adapter_plan_low_signal_continue_short_circuit(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
_ = evaluate_for_expert_mode(
store=store,
session_id="s-low",
lang="zh",
requested_specialist="generalist",
user_text="先给我一版计划",
execution_mode="plan",
base_system_prompt="base",
)
d2 = evaluate_for_expert_mode(
store=store,
session_id="s-low",
lang="zh",
requested_specialist="generalist",
user_text="继续",
execution_mode="plan",
base_system_prompt="base",
)
assert d2.action == "stay_plan"
assert "低信息续写" in str(d2.reply_text or "")
def test_prompt_prefix_uses_ccmini_like_phases(tmp_path: Path) -> None:
plan_file = tmp_path / "plan.md"
plan_file.write_text("# Plan\n", encoding="utf-8")
st = PlanAgentStateV2(mode="plan", plan_path=str(plan_file))
zh = build_plan_mode_prefix(state=st, lang="zh")
en = build_plan_mode_prefix(state=st, lang="en")
assert "阶段1:理解问题" in zh
assert "计划工作流" in zh
assert "计划模式工具" in zh
assert "执行纪律" in zh
assert "enter_plan_mode_v2" in zh
assert "Phase 1: Initial Understanding" in en
assert "Plan Workflow" in en
assert "Plan mode tools" in en
assert "Execution discipline" in en
assert "enter_plan_mode_v2" in en
def test_shadow_tool_specs_work(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
tools = materialize_plan_mode_v2_tools(store=store)
assert len(tools) == 2
enter = next(t for t in tools if t.name == "enter_plan_mode_v2")
exit_tool = next(t for t in tools if t.name == "exit_plan_mode_v2")
out1 = enter.handler({"session_id": "s-1", "owner_specialist": "generalist"})
assert out1.get("ok") is True
out2 = exit_tool.handler({"session_id": "s-1", "confirm": True})
assert out2.get("ok") is True
assert bool((out2.get("state") or {}).get("plan_confirmed")) is True
def test_shadow_plan_tools_emit_trace_when_trace_id(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
tools = materialize_plan_mode_v2_tools(store=store)
enter = next(t for t in tools if t.name == "enter_plan_mode_v2")
exit_tool = next(t for t in tools if t.name == "exit_plan_mode_v2")
enter.handler(
{
"session_id": "s-tr",
"owner_specialist": "generalist",
"trace_id": "tid-1",
"parent_span_id": "ps-9",
}
)
exit_tool.handler({"session_id": "s-tr", "confirm": False, "trace_id": "tid-1"})
rows = store.list_trace_events_for_trace(session_id="s-tr", trace_id="tid-1")
types = [r.get("event_type") for r in rows]
assert "plan_mode_tool_enter" in types
assert "plan_mode_tool_exit" in types
enter_ev = next(r for r in rows if r.get("event_type") == "plan_mode_tool_enter")
exit_ev = next(r for r in rows if r.get("event_type") == "plan_mode_tool_exit")
assert (enter_ev.get("payload") or {}).get("tool") == "enter_plan_mode_v2"
assert (exit_ev.get("payload") or {}).get("tool") == "exit_plan_mode_v2"
assert (exit_ev.get("payload") or {}).get("confirmed") is False
def test_shadow_tool_specs_can_use_default_session_key(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
store.set_setting("AIA_PLAN_AGENT_V2_DEFAULT_SESSION_ID", "s-default")
tools = materialize_plan_mode_v2_tools(store=store)
enter = next(t for t in tools if t.name == "enter_plan_mode_v2")
out = enter.handler({})
assert out.get("ok") is True
st = out.get("state") or {}
assert str(st.get("mode") or "") == "plan"
def test_switch_default_off_and_opt_in(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
assert v2_feature_enabled(store=store) is False
assert should_route_to_v2(store=store, interaction_mode="expert") is False
assert should_route_to_v2(store=store, interaction_mode="expert", force_flag=True) is True
store.set_setting("AIA_EXPERT_PLAN_AGENT_V2_ENABLED", "1")
assert v2_feature_enabled(store=store) is True
assert should_route_to_v2(store=store, interaction_mode="expert") is True
assert should_route_to_v2(store=store, interaction_mode="comprehensive") is False
def test_gateway_adapter_shadow_force_flag(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
msg = StandardMessage(
session_id="s1",
tenant_id="t1",
user_id="u1",
role="user",
channel="chat",
text="帮我实现一个功能",
attachments=[],
metadata={},
)
out = evaluate_gateway_expert_turn_shadow(
store=store,
msg=msg,
lang="zh",
interaction_mode="expert",
requested_specialist="generalist",
base_system_prompt="base",
force_flag=True,
)
assert out.used_v2 is True
assert out.decision is not None
assert out.decision.action == "run_agent"
def test_shadow_compat_result_shape(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
dec = evaluate_for_expert_mode(
store=store,
session_id="s1",
lang="zh",
requested_specialist="generalist",
user_text="我要改造",
base_system_prompt="base",
)
row = build_shadow_gateway_result(
decision=dec,
run_id="r1",
trace_id="t1",
elapsed_ms=12,
requested_specialist="generalist",
)
assert set(row.keys()) == legacy_gateway_result_keys()
def test_trace_helper_no_crash() -> None:
events = []
class _S:
def add_trace_event(self, **kwargs):
events.append(kwargs)
emit_plan_agent_v2_trace(
store=_S(),
session_id="s1",
trace_id="t1",
parent_span_id=None,
event_type="plan_mode_entered",
payload={"x": 1},
)
assert len(events) == 1
assert events[0].get("event_type") == "plan_mode_entered"
def test_shadow_gateway_result_defaults_align_legacy_baseline(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
store.set_setting("AIA_EXPERT_PLAN_FILE_DIR", str(tmp_path / "plans"))
dec = evaluate_for_expert_mode(
store=store,
session_id="s1",
lang="zh",
requested_specialist="generalist",
user_text="继续",
base_system_prompt="base",
)
row = build_shadow_gateway_result(
decision=dec,
run_id="r1",
trace_id="t1",
elapsed_ms=1,
requested_specialist="generalist",
)
# Baseline invariant fields expected by legacy result dataclass.
baseline = OclawGatewayResult(run_id="r1", reply_text="", trace_id="t1", elapsed_ms=1)
assert row["mode"] == baseline.mode
assert row["task_id"] == baseline.task_id
assert row["dynamic_agent_used"] == baseline.dynamic_agent_used
assert row["dynamic_agent_name"] == baseline.dynamic_agent_name
assert row["relay_pointer_count"] == baseline.relay_pointer_count
assert row["relay_envelope_present"] == baseline.relay_envelope_present
assert row["relay_envelope_pointer_count"] == baseline.relay_envelope_pointer_count
assert row["relay_ttl_turn_count"] == baseline.relay_ttl_turn_count
assert row["relay_ttl_session_count"] == baseline.relay_ttl_session_count
assert row["relay_ttl_keep_count"] == baseline.relay_ttl_keep_count
# Package export should be wired and callable.
assert callable(should_route_to_v2_pkg)
def test_package_exports_stable_symbols() -> None:
import runtime.plan_agent_v2 as p
required = [
"PlanAgentStateV2",
"PlanAgentStateStoreV2",
"PlanModeManagerV2",
"PlanAgentV2Decision",
"GatewayPlanV2AdapterOutput",
"evaluate_for_expert_mode",
"evaluate_gateway_expert_turn_shadow",
"should_route_to_v2",
"v2_feature_enabled",
"emit_plan_agent_v2_trace",
"build_shadow_gateway_result",
"legacy_gateway_result_keys",
]
for name in required:
assert hasattr(p, name), name

View file

@ -6,7 +6,6 @@ import unittest
from pathlib import Path
from runtime.skills import discover_workspace_skill_manifests
from runtime.tools.skills_runtime.materialize_skill_tools import materialize_executable_skill_tools
class SkillRuntimeMetadataAndToolsTests(unittest.TestCase):
@ -52,28 +51,6 @@ class SkillRuntimeMetadataAndToolsTests(unittest.TestCase):
assert m.runtime.get("entry") == "scripts/run.py"
assert isinstance(m.runtime.get("schema"), dict)
def test_materialize_creates_tool_for_runtime_skill(self) -> None:
d = self.skills_root / "demo_runtime_tool"
d.mkdir(parents=True, exist_ok=True)
(d / "scripts").mkdir(parents=True, exist_ok=True)
(d / "scripts" / "run.py").write_text("print('{\"ok\": true}')\n", encoding="utf-8")
(d / "SKILL.md").write_text(
"---\n"
"name: demo_runtime_tool\n"
"description: demo\n"
"metadata:\n"
" oclaw:\n"
" runtime:\n"
" type: python\n"
" entry: scripts/run.py\n"
"---\n",
encoding="utf-8",
)
tools = materialize_executable_skill_tools(store=None)
names = {t.name for t in tools}
assert "demo_runtime_tool" in names
if __name__ == "__main__":
unittest.main()

View file

@ -126,10 +126,9 @@ def test_filter_preset_and_row_helpers() -> None:
assert meta["total"] == 2
def test_ume_alarm_xlsx_report_registered_when_builtin_disabled(monkeypatch) -> None:
def test_ume_alarm_xlsx_report_registered_without_inline_netx(monkeypatch) -> None:
from runtime.tools import expert_registry
monkeypatch.delenv("OCLAW_NETX_BUILTIN_TOOLS", raising=False)
expert_registry.clear_expert_tool_cache()
factories = expert_registry.discover_expert_tool_factories()
names = {f().name for f in (factories.get("network_ops") or [])}