from nanobot.agent.context import ContextBuilder from nanobot.agent.loop import AgentLoop from nanobot.session.manager import Session def _mk_loop() -> AgentLoop: loop = AgentLoop.__new__(AgentLoop) from nanobot.config.schema import AgentDefaults loop.max_tool_result_chars = AgentDefaults().max_tool_result_chars return loop def test_save_turn_skips_multimodal_user_when_only_runtime_context() -> None: loop = _mk_loop() session = Session(key="test:runtime-only") runtime = ContextBuilder._RUNTIME_CONTEXT_TAG + "\nCurrent Time: now (UTC)" loop._save_turn( session, [{"role": "user", "content": [{"type": "text", "text": runtime}]}], skip=0, ) assert session.messages == [] def test_save_turn_keeps_image_placeholder_with_path_after_runtime_strip() -> None: loop = _mk_loop() session = Session(key="test:image") runtime = ContextBuilder._RUNTIME_CONTEXT_TAG + "\nCurrent Time: now (UTC)" loop._save_turn( session, [{ "role": "user", "content": [ {"type": "text", "text": runtime}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}, "_meta": {"path": "/media/feishu/photo.jpg"}}, ], }], skip=0, ) assert session.messages[0]["content"] == [{"type": "text", "text": "[image: /media/feishu/photo.jpg]"}] def test_save_turn_keeps_image_placeholder_without_meta() -> None: loop = _mk_loop() session = Session(key="test:image-no-meta") runtime = ContextBuilder._RUNTIME_CONTEXT_TAG + "\nCurrent Time: now (UTC)" loop._save_turn( session, [{ "role": "user", "content": [ {"type": "text", "text": runtime}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}, ], }], skip=0, ) assert session.messages[0]["content"] == [{"type": "text", "text": "[image]"}] def test_save_turn_keeps_tool_results_under_16k() -> None: loop = _mk_loop() session = Session(key="test:tool-result") content = "x" * 12_000 loop._save_turn( session, [{"role": "tool", "tool_call_id": "call_1", "name": "read_file", "content": content}], skip=0, ) assert session.messages[0]["content"] == content def test_restore_runtime_checkpoint_rehydrates_completed_and_pending_tools() -> None: loop = _mk_loop() session = Session( key="test:checkpoint", metadata={ AgentLoop._RUNTIME_CHECKPOINT_KEY: { "assistant_message": { "role": "assistant", "content": "working", "tool_calls": [ { "id": "call_done", "type": "function", "function": {"name": "read_file", "arguments": "{}"}, }, { "id": "call_pending", "type": "function", "function": {"name": "exec", "arguments": "{}"}, }, ], }, "completed_tool_results": [ { "role": "tool", "tool_call_id": "call_done", "name": "read_file", "content": "ok", } ], "pending_tool_calls": [ { "id": "call_pending", "type": "function", "function": {"name": "exec", "arguments": "{}"}, } ], } }, ) restored = loop._restore_runtime_checkpoint(session) assert restored is True assert session.metadata.get(AgentLoop._RUNTIME_CHECKPOINT_KEY) is None assert session.messages[0]["role"] == "assistant" assert session.messages[1]["tool_call_id"] == "call_done" assert session.messages[2]["tool_call_id"] == "call_pending" assert "interrupted before this tool finished" in session.messages[2]["content"].lower() def test_restore_runtime_checkpoint_dedupes_overlapping_tail() -> None: loop = _mk_loop() session = Session( key="test:checkpoint-overlap", messages=[ { "role": "assistant", "content": "working", "tool_calls": [ { "id": "call_done", "type": "function", "function": {"name": "read_file", "arguments": "{}"}, }, { "id": "call_pending", "type": "function", "function": {"name": "exec", "arguments": "{}"}, }, ], }, { "role": "tool", "tool_call_id": "call_done", "name": "read_file", "content": "ok", }, ], metadata={ AgentLoop._RUNTIME_CHECKPOINT_KEY: { "assistant_message": { "role": "assistant", "content": "working", "tool_calls": [ { "id": "call_done", "type": "function", "function": {"name": "read_file", "arguments": "{}"}, }, { "id": "call_pending", "type": "function", "function": {"name": "exec", "arguments": "{}"}, }, ], }, "completed_tool_results": [ { "role": "tool", "tool_call_id": "call_done", "name": "read_file", "content": "ok", } ], "pending_tool_calls": [ { "id": "call_pending", "type": "function", "function": {"name": "exec", "arguments": "{}"}, } ], } }, ) restored = loop._restore_runtime_checkpoint(session) assert restored is True assert session.metadata.get(AgentLoop._RUNTIME_CHECKPOINT_KEY) is None assert len(session.messages) == 3 assert session.messages[0]["role"] == "assistant" assert session.messages[1]["tool_call_id"] == "call_done" assert session.messages[2]["tool_call_id"] == "call_pending"