Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/self-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Historical verification snapshot: `51be33361422e55e1f2f00c33a0e0f8c56132a91`
(the post-#54 `main` revision, captured before this #55 documentation-only
update). Snapshot date: 2026-09-04.

Current repository test count at this snapshot: **183 unittest cases**.
Current repository test count at this snapshot: **185 unittest cases**.

## Verified surface

Expand Down
80 changes: 75 additions & 5 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3150,6 +3150,8 @@ def _collect_unwrapped_tool_calls(text: str) -> tuple[list[dict], bool]:
malformed = False
value = str(text or "")
for match in _ACTION_MARKER_RE.finditer(value):
if not _action_marker_is_protocol(value, match, final=True):
continue
line_end = value.find("\n", match.end())
line_end = len(value) if line_end < 0 else line_end
prefix = value[value.rfind("\n", 0, match.start()) + 1:match.start()].rstrip()
Expand Down Expand Up @@ -3851,6 +3853,56 @@ def _is_question(text: str) -> bool:
_ACTION_MARKER_RE = re.compile(
r"(?i)(?<![\w])(?P<name>(?:" + "|".join(map(re.escape, _ACTION_MARKER_NAMES)) + r"))\s*:"
)
_ACTION_SENTENCE_ENDS = (".", "!", "?", "…", "。", "!", "?", ")", "]", "}", "`", '"', "'")
_ACTION_PROSE_START_RE = re.compile(
r"(?i)^(?:a|an|and|are|accepts|can|does|for|from|is|means|must|not|or|returns|the|that|this|to|used|use|will|which|with)\b"
)


def _marker_line_prefix(value: str, start: int) -> str:
return value[value.rfind("\n", 0, start) + 1:start].rstrip()


def _action_marker_is_protocol(value: str, match: re.Match[str], *, final: bool) -> bool:
"""Accept only bounded alias lines, not prose that mentions a tool name."""
prefix = _marker_line_prefix(value, match.start())
if prefix and not prefix.endswith(_ACTION_SENTENCE_ENDS):
return False
cursor = match.end()
while cursor < len(value) and value[cursor] in " \t":
cursor += 1
if value[cursor:cursor + 2] == "\r\n":
cursor += 2
elif value[cursor:cursor + 1] == "\n":
cursor += 1
while cursor < len(value) and value[cursor] in " \t":
cursor += 1
if value[cursor:cursor + 3] == "```":
return True
line_end = value.find("\n", cursor)
line_end = len(value) if line_end < 0 else line_end
args = value[cursor:line_end].strip().strip("`\"'")
if not args:
return final
return not _ACTION_PROSE_START_RE.match(args)


def _control_marker_is_protocol(value: str, match: re.Match[str]) -> bool:
prefix = _marker_line_prefix(value, match.start())
if not prefix or prefix.endswith(_ACTION_SENTENCE_ENDS):
return True
return bool(re.search(
r"(?i)(?:Action|Plan|TaskList|TaskDone|DefineTool)\s*:", prefix[-400:],
))


def _remove_protocol_headings(value: str) -> str:
"""Remove control headings only when their context is protocol-like."""
matches = list(DeepSeekDSMLFilter._CONTROL_RE.finditer(value))
for match in reversed(matches):
if _control_marker_is_protocol(value, match):
value = value[:match.start()] + value[match.end():]
return value


class DeepSeekDSMLFilter:
Expand Down Expand Up @@ -3919,6 +3971,20 @@ def _control_partial_suffix_length(cls, value: str) -> int:
return length
return 0

@classmethod
def _find_control_marker(cls, value: str, *, final: bool) -> re.Match[str] | None:
for match in cls._CONTROL_RE.finditer(value):
if _control_marker_is_protocol(value, match):
return match
return None

@classmethod
def _find_action_marker(cls, value: str, *, final: bool) -> re.Match[str] | None:
for match in cls._ACTION_MARKER_RE.finditer(value):
if _action_marker_is_protocol(value, match, final=final):
return match
return None

@staticmethod
def _balanced_end(value: str, start: int, opening: str, closing: str) -> int | None:
depth = 0
Expand Down Expand Up @@ -3981,6 +4047,12 @@ def _generic_block_end(cls, value: str, match: re.Match[str]) -> int | None:
@staticmethod
def _action_marker_end(value: str, match: re.Match[str], *, final: bool) -> int | None:
cursor = match.end()
while cursor < len(value) and value[cursor] in " \t":
cursor += 1
if value[cursor:cursor + 2] == "\r\n":
cursor += 2
elif value[cursor:cursor + 1] == "\n":
cursor += 1
while cursor < len(value) and value[cursor] in " \t":
cursor += 1
if value[cursor:cursor + 3] == "```":
Expand All @@ -3997,10 +4069,10 @@ def _filter_control(self, chunk: str, *, final: bool) -> str:
self._control_buffer += chunk
output: list[str] = []
while self._control_buffer:
control = self._CONTROL_RE.search(self._control_buffer)
control = self._find_control_marker(self._control_buffer, final=final)
generic_open = self._GENERIC_OPEN_RE.search(self._control_buffer)
generic_close = self._GENERIC_CLOSE_RE.search(self._control_buffer)
action_marker = self._ACTION_MARKER_RE.search(self._control_buffer)
action_marker = self._find_action_marker(self._control_buffer, final=final)
starts = [item for item in (control, generic_open, generic_close, action_marker) if item is not None]
if not starts:
if final:
Expand Down Expand Up @@ -4209,9 +4281,7 @@ def _clean_final_response(text: str) -> str:
# any following natural-language answer.
cleaned = re.sub(r"^[ \t]*Thought:[ \t]*.*(?:\n|$)", "", cleaned,
flags=re.IGNORECASE | re.MULTILINE)
cleaned = re.sub(
r"(?i)(?<![\w])(?:Thought|Plan|TaskList|TaskDone|DefineTool)\s*:", "", cleaned,
)
cleaned = _remove_protocol_headings(cleaned)
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
return cleaned.strip()

Expand Down
63 changes: 63 additions & 0 deletions tests/test_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ def chat_stream(self, _messages, _model=None):
)


class _ToolNameProseProvider:
config = ProviderConfig(provider="deepseek", model_simple="deepseek-v4-flash")

def chat_stream(self, _messages, _model=None):
# Tool names in explanations and examples are ordinary user-facing prose.
yield from (
"Use run_cmd: to execute a command.\n",
"The tool name is read_file: and it reads text.\n",
"For example, browser_open: accepts a URL.\n",
"The following is a label, not a call: write_file: README.md\n",
"Use plan: for a project plan.\n",
"工具名称是 read_file:,用于读取文本。\n",
"run_cmd: is a shell command tool.",
)


class StreamingEndpointTests(unittest.TestCase):
def test_first_sse_content_arrives_before_provider_stream_completes(self):
session_id = "stream-timing-regression"
Expand Down Expand Up @@ -298,6 +314,53 @@ def text(item):
]
self.assertEqual(assistant_messages[-1]["content"], main._clean_final_response(content))

def test_tool_name_prose_survives_sse_and_persistence(self):
session_id = "stream-tool-name-prose-regression"
provider = _ToolNameProseProvider()

async def exercise():
response = await server.api_chat_stream(_Request({
"message": "explain the tools", "session_id": session_id,
}))
iterator = response.body_iterator.__aiter__()
frames = []
try:
while True:
frames.append(await asyncio.wait_for(anext(iterator), timeout=2))
except StopAsyncIteration:
return frames

with tempfile.TemporaryDirectory(prefix="openkyrozen-tool-name-prose-") as directory:
memory = MemoryBank(Path(directory) / "state.sqlite3", workspace_id="tool-name-prose-test")
original_sessions = server._sessions
server._sessions = {}
try:
with (patch.object(server._agent, "memory_bank", memory),
patch.object(server._agent, "llm_provider", provider),
patch.object(server._agent, "DEEPSEEK_MODEL", "deepseek-v4-flash"),
patch.object(server._agent, "_chat_turn", side_effect=lambda message, **_: (
main._call_llm_with_spinner([{"role": "user", "content": message}])
))):
frames = asyncio.run(exercise())
finally:
server._sessions = original_sessions

def text(item):
return item.decode() if isinstance(item, bytes) else item

payloads = [json.loads(text(frame).split("data: ", 1)[1].splitlines()[0])
for frame in frames
if text(frame).startswith("data: ") and text(frame) != "data: [DONE]\n\n"]
content = "".join(item["chunk"] for item in payloads if item.get("event") == "content")
expected = "".join(provider.chat_stream([], None))
self.assertEqual(content, expected)
assistant_messages = [
event["payload"] for event in memory.store.list_events(
"session.message", workspace_id="tool-name-prose-test", session_id=session_id,
) if event["payload"].get("role") == "assistant"
]
self.assertEqual(assistant_messages[-1]["content"], expected)


if __name__ == "__main__":
unittest.main()
16 changes: 16 additions & 0 deletions tests/test_task_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,22 @@ def test_unwrapped_provider_aliases_are_bounded_and_canonicalized(self):
self.assertEqual(malformed["tool_calls"], [])
self.assertIn("No tool was executed", malformed["protocol_error"])

def test_tool_names_in_prose_are_not_filtered_or_executed(self):
prose = (
"Use run_cmd: to execute a command.\n"
"The tool name is read_file: and it reads text.\n"
"For example, browser_open: accepts a URL.\n"
"The following is a label, not a call: write_file: README.md\n"
"Use plan: for a project plan.\n"
"工具名称是 read_file:,用于读取文本。\n"
"run_cmd: is a shell command tool."
)
self.assertEqual(main.DeepSeekDSMLFilter().feed(prose, final=True), prose)
self.assertEqual(main._clean_final_response(prose), prose)
parsed = main._parse_model_response(prose)
self.assertEqual(parsed["tool_calls"], [])
self.assertIsNone(parsed["protocol_error"])

def test_malformed_unwrapped_alias_stops_without_retrying_or_executing(self):
class StubLearning:
def feedback_signal(self, _text):
Expand Down
Loading