mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-08 23:33:16 +08:00
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:
parent
628a9dffd2
commit
efa72df362
40 changed files with 169 additions and 3033 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}"]
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 [])}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue