# retoor import json from devplacepy.services.devii.agentic.compaction import ( compact_messages, find_compaction_split, is_context_length_error, ) from devplacepy.services.devii.errors import LLMError from tests.conftest import run_async def _error(status, body): return LLMError("Model endpoint returned error", status=status, body=body) def test_openrouter_style_message_detected(): body = json.dumps( { "error": { "message": ( "This endpoint's maximum context length is 131072 tokens. " "However, you requested about 403355 tokens (349784 of text " "input, 53571 of tool input). Please reduce the length of " "either one, or use the context-compression plugin." ), "code": 400, "metadata": {"provider_name": None}, } } ) assert is_context_length_error(_error(400, body)) is True def test_openai_style_code_detected(): body = json.dumps( { "error": { "message": "This model's maximum context length is 128000 tokens.", "type": "invalid_request_error", "param": None, "code": "context_length_exceeded", } } ) assert is_context_length_error(_error(400, body)) is True def test_unrelated_400_not_detected(): body = json.dumps({"error": {"message": "Invalid API key.", "code": 400}}) assert is_context_length_error(_error(400, body)) is False def test_non_400_status_not_detected_even_with_matching_text(): body = json.dumps( {"error": {"message": "maximum context length is 131072 tokens"}} ) assert is_context_length_error(_error(429, body)) is False def test_malformed_body_falls_back_to_phrase_match(): truncated = "maximum context length is 131072 tokens, please reduce the length" assert is_context_length_error(_error(400, truncated)) is True def test_malformed_body_with_no_match_is_false(): assert is_context_length_error(_error(400, "not valid json at all")) is False class _StubLlm: def __init__(self, summary="a concise summary of the earlier turns"): self.summary = summary self.calls = 0 async def summarize(self, prompt): self.calls += 1 return self.summary def _tool_call_message(name="run_tool"): return { "role": "assistant", "content": "", "tool_calls": [ {"id": "c1", "function": {"name": name, "arguments": "{}"}} ], } def _tool_result_message(content="result"): return {"role": "tool", "tool_call_id": "c1", "name": "run_tool", "content": content} def _long_tool_heavy_conversation(rounds=20): messages = [ {"role": "system", "content": "sys"}, {"role": "user", "content": "start the long task"}, ] for i in range(rounds): messages.append(_tool_call_message()) messages.append(_tool_result_message(f"result {i}" * 200)) return messages def test_find_compaction_split_prefers_a_user_message(): messages = [ {"role": "system", "content": "sys"}, {"role": "user", "content": "first"}, {"role": "assistant", "content": "reply"}, {"role": "user", "content": "second"}, {"role": "assistant", "content": "reply2"}, {"role": "user", "content": "third"}, {"role": "assistant", "content": "reply3"}, ] split = find_compaction_split(messages, keep_tail=2) assert messages[split]["role"] == "user" def test_find_compaction_split_falls_back_to_a_non_tool_boundary_without_a_recent_user_message(): messages = _long_tool_heavy_conversation(rounds=20) split = find_compaction_split(messages, keep_tail=4) assert split > 1 assert messages[split].get("role") != "tool" def test_find_compaction_split_never_lands_inside_a_tool_result_run(): messages = _long_tool_heavy_conversation(rounds=30) for keep_tail in (2, 3, 4, 5, 8, 10, 15): split = find_compaction_split(messages, keep_tail) assert messages[split].get("role") != "tool", ( f"keep_tail={keep_tail} split at a tool message, orphaning its tool_calls" ) def test_compact_messages_shrinks_a_tool_heavy_conversation_with_no_recent_user_message(): messages = _long_tool_heavy_conversation(rounds=20) original_len = len(messages) llm = _StubLlm() compacted = run_async(compact_messages(llm, messages, keep_tail=4)) assert llm.calls == 1 assert len(compacted) < original_len assert compacted[0]["role"] == "system" assert "[compacted earlier turns]" in compacted[1]["content"] assert compacted[-1] == messages[-1] def test_compact_messages_tail_never_starts_with_a_dangling_tool_result(): messages = _long_tool_heavy_conversation(rounds=25) llm = _StubLlm() compacted = run_async(compact_messages(llm, messages, keep_tail=6)) tail = compacted[2:] assert tail assert tail[0].get("role") != "tool"