mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 07:00:48 +08:00
初始化:独立 oclaw 仓库首提交
- 在 oclaw/ 下重新初始化 Git 仓库 - 补齐子仓库 .gitignore,避免提交本地运行态数据(_local、node_modules、logs 等) - 提交当前工程代码与配置 Made-with: Cursor
This commit is contained in:
commit
ba3836f00f
579 changed files with 83112 additions and 0 deletions
125
tests/test_tool_loop_guard.py
Normal file
125
tests/test_tool_loop_guard.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from oclaw.chat.tool_runtime import ToolExecutionContext, ToolExecutor
|
||||
from oclaw.platform.llm.chat_models import LLMToolCall
|
||||
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
||||
from oclaw.tools.base import ToolRegistry, ToolSpec
|
||||
|
||||
|
||||
def test_tool_loop_guard_blocks_repeated_signature(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "g.sqlite"))
|
||||
sess = store.create_session("t")
|
||||
calls = {"n": 0}
|
||||
|
||||
def _handler(args):
|
||||
calls["n"] += 1
|
||||
return {"ok": True, "echo": args}
|
||||
|
||||
reg = ToolRegistry(
|
||||
[
|
||||
ToolSpec(
|
||||
name="echo",
|
||||
description="echo",
|
||||
parameters={"type": "object", "properties": {"x": {"type": "integer"}}},
|
||||
handler=_handler,
|
||||
read_only=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
tool_uses = [
|
||||
LLMToolCall(id="c1", name="echo", arguments={"x": 1}),
|
||||
LLMToolCall(id="c2", name="echo", arguments={"x": 1}),
|
||||
LLMToolCall(id="c3", name="echo", arguments={"x": 1}),
|
||||
]
|
||||
_, results = ToolExecutor().execute_tool_uses(
|
||||
ctx=ToolExecutionContext(store=store, tools=reg, session_id=sess.id),
|
||||
assistant_msg_id=1,
|
||||
tool_uses=tool_uses,
|
||||
signature_budget=2,
|
||||
)
|
||||
assert calls["n"] == 2
|
||||
blocked, _ = results["c3"]
|
||||
assert blocked.get("error_code") == "tool_loop_guard"
|
||||
|
||||
|
||||
def test_repeated_tool_results_are_compacted_in_history(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "g2.sqlite"))
|
||||
sess = store.create_session("t")
|
||||
|
||||
def _handler(_args):
|
||||
# Emulate large-ish tabular payload that should not be replayed verbatim repeatedly.
|
||||
return {"ok": True, "rows": [{"i": i, "v": f"row-{i}"} for i in range(0, 200)], "rows_returned": 200}
|
||||
|
||||
reg = ToolRegistry(
|
||||
[
|
||||
ToolSpec(
|
||||
name="run_tabular_sql",
|
||||
description="fetch rows",
|
||||
parameters={"type": "object", "properties": {"x": {"type": "integer"}}},
|
||||
handler=_handler,
|
||||
read_only=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
tool_uses = [
|
||||
LLMToolCall(id="c1", name="run_tabular_sql", arguments={"x": 1}),
|
||||
LLMToolCall(id="c2", name="run_tabular_sql", arguments={"x": 2}),
|
||||
LLMToolCall(id="c3", name="run_tabular_sql", arguments={"x": 3}),
|
||||
]
|
||||
ToolExecutor().execute_tool_uses(
|
||||
ctx=ToolExecutionContext(store=store, tools=reg, session_id=sess.id, turn_uuid="turn-1"),
|
||||
assistant_msg_id=1,
|
||||
tool_uses=tool_uses,
|
||||
)
|
||||
rows = store.get_messages(session_id=sess.id, limit=50)
|
||||
tool_rows = [m for m in rows if str(getattr(m, "role", "") or "") == "tool"]
|
||||
assert len(tool_rows) == 3
|
||||
payloads = [json.loads(str(getattr(m, "content", "") or "{}")) for m in tool_rows]
|
||||
assert bool(payloads[0].get("_history_compacted"))
|
||||
assert bool(payloads[1].get("_history_compacted"))
|
||||
assert bool(payloads[2].get("_history_compacted"))
|
||||
assert str(payloads[2].get("_history_compact_reason") or "") == "repeated_tool_calls_in_turn"
|
||||
assert int(payloads[0].get("_tool_observed_rows_cumulative_in_turn") or 0) == 200
|
||||
assert int(payloads[1].get("_tool_observed_rows_cumulative_in_turn") or 0) == 400
|
||||
assert int(payloads[2].get("_tool_observed_rows_this_call") or 0) == 200
|
||||
assert int(payloads[2].get("_tool_observed_rows_cumulative_in_turn") or 0) == 600
|
||||
assert "audit_note" in payloads[2]
|
||||
|
||||
|
||||
def test_repeated_non_sql_tools_are_not_compacted(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "g3.sqlite"))
|
||||
sess = store.create_session("t")
|
||||
|
||||
def _handler(_args):
|
||||
return {"ok": True, "rows": [{"i": i} for i in range(0, 50)], "rows_returned": 50}
|
||||
|
||||
reg = ToolRegistry(
|
||||
[
|
||||
ToolSpec(
|
||||
name="generic_fetch",
|
||||
description="generic",
|
||||
parameters={"type": "object", "properties": {"x": {"type": "integer"}}},
|
||||
handler=_handler,
|
||||
read_only=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
tool_uses = [
|
||||
LLMToolCall(id="c1", name="generic_fetch", arguments={"x": 1}),
|
||||
LLMToolCall(id="c2", name="generic_fetch", arguments={"x": 2}),
|
||||
LLMToolCall(id="c3", name="generic_fetch", arguments={"x": 3}),
|
||||
]
|
||||
ToolExecutor().execute_tool_uses(
|
||||
ctx=ToolExecutionContext(store=store, tools=reg, session_id=sess.id, turn_uuid="turn-2"),
|
||||
assistant_msg_id=1,
|
||||
tool_uses=tool_uses,
|
||||
)
|
||||
rows = store.get_messages(session_id=sess.id, limit=50)
|
||||
tool_rows = [m for m in rows if str(getattr(m, "role", "") or "") == "tool"]
|
||||
payloads = [json.loads(str(getattr(m, "content", "") or "{}")) for m in tool_rows]
|
||||
assert payloads
|
||||
assert not any(bool(p.get("_history_compacted")) for p in payloads)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue