mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-02 17:32:39 +00:00
203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
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"
|