|
|
@@ -9,7 +9,7 @@ from agent_lab.application.contracts import AgentParams, DebugRunRequest, EventA
|
|
|
from agent_lab.application.runtime import DebugRuntime
|
|
|
from agent_lab.application.tools import ToolDefinition, ToolRegistry
|
|
|
from agent_lab.domain.events import ToolCallEvent
|
|
|
-from agent_lab.domain.messages import ChatMessage, StreamItem
|
|
|
+from agent_lab.domain.messages import ChatMessage, StreamItem, TokenUsage
|
|
|
|
|
|
|
|
|
def _runtime_queues_class():
|
|
|
@@ -111,6 +111,55 @@ class ToolCapturingChatClient:
|
|
|
yield StreamItem.message_delta("final answer")
|
|
|
|
|
|
|
|
|
+class RoundStatsChatClient:
|
|
|
+ async def stream_chat(
|
|
|
+ self,
|
|
|
+ messages: list[ChatMessage],
|
|
|
+ tools: list[dict],
|
|
|
+ params: AgentParams,
|
|
|
+ ) -> AsyncIterator[StreamItem]:
|
|
|
+ yield StreamItem.message_delta("hello")
|
|
|
+ yield StreamItem.usage_item(
|
|
|
+ TokenUsage(
|
|
|
+ prompt_tokens=10,
|
|
|
+ completion_tokens=20,
|
|
|
+ total_tokens=30,
|
|
|
+ cached_tokens=5,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class EventRoundStatsChatClient:
|
|
|
+ def __init__(self) -> None:
|
|
|
+ self.calls = 0
|
|
|
+
|
|
|
+ async def stream_chat(
|
|
|
+ self,
|
|
|
+ messages: list[ChatMessage],
|
|
|
+ tools: list[dict],
|
|
|
+ params: AgentParams,
|
|
|
+ ) -> AsyncIterator[StreamItem]:
|
|
|
+ self.calls += 1
|
|
|
+ if self.calls == 1:
|
|
|
+ yield StreamItem.event(
|
|
|
+ ToolCallEvent(
|
|
|
+ id="call_1",
|
|
|
+ name="handoff_note",
|
|
|
+ arguments={"message": "need event agent"},
|
|
|
+ raw_arguments='{"message":"need event agent"}',
|
|
|
+ )
|
|
|
+ )
|
|
|
+ yield StreamItem.usage_item(
|
|
|
+ TokenUsage(prompt_tokens=3, completion_tokens=0, total_tokens=3)
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ yield StreamItem.message_delta("final answer")
|
|
|
+ yield StreamItem.usage_item(
|
|
|
+ TokenUsage(prompt_tokens=4, completion_tokens=6, total_tokens=10)
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
def test_runtime_queues_exposes_input_output_and_events_queues():
|
|
|
RuntimeQueues = _runtime_queues_class()
|
|
|
|
|
|
@@ -143,11 +192,13 @@ async def test_runtime_routes_chat_events_through_event_agent_then_continues_cha
|
|
|
"session_started",
|
|
|
"event",
|
|
|
"tool_result",
|
|
|
+ "round_stats",
|
|
|
"message_delta",
|
|
|
+ "round_stats",
|
|
|
"done",
|
|
|
]
|
|
|
assert outputs[1]["event"]["name"] == "handoff_note"
|
|
|
- assert outputs[3]["content"] == "final answer"
|
|
|
+ assert outputs[4]["content"] == "final answer"
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
@@ -174,7 +225,9 @@ async def test_runtime_uses_event_and_input_queues_for_event_agent_handoff():
|
|
|
"session_started",
|
|
|
"event",
|
|
|
"tool_result",
|
|
|
+ "round_stats",
|
|
|
"message_delta",
|
|
|
+ "round_stats",
|
|
|
"done",
|
|
|
]
|
|
|
assert queue_log.index(("input", "put", "user")) < queue_log.index(
|
|
|
@@ -215,7 +268,9 @@ async def test_runtime_yields_existing_output_order_from_output_queue():
|
|
|
"session_started",
|
|
|
"event",
|
|
|
"tool_result",
|
|
|
+ "round_stats",
|
|
|
"message_delta",
|
|
|
+ "round_stats",
|
|
|
"done",
|
|
|
]
|
|
|
assert [
|
|
|
@@ -229,8 +284,12 @@ async def test_runtime_yields_existing_output_order_from_output_queue():
|
|
|
("output", "get", "output:event"),
|
|
|
("output", "put", "output:tool_result"),
|
|
|
("output", "get", "output:tool_result"),
|
|
|
+ ("output", "put", "output:round_stats"),
|
|
|
+ ("output", "get", "output:round_stats"),
|
|
|
("output", "put", "output:message_delta"),
|
|
|
("output", "get", "output:message_delta"),
|
|
|
+ ("output", "put", "output:round_stats"),
|
|
|
+ ("output", "get", "output:round_stats"),
|
|
|
("output", "put", "output:done"),
|
|
|
("output", "get", "output:done"),
|
|
|
]
|
|
|
@@ -323,3 +382,80 @@ async def test_runtime_passes_selected_tool_schema_from_registry_to_chat_agent()
|
|
|
},
|
|
|
}
|
|
|
]
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_runtime_emits_round_stats_with_clock_and_usage_after_model_turn():
|
|
|
+ request = DebugRunRequest(
|
|
|
+ user_message="debug this",
|
|
|
+ system_prompts=[],
|
|
|
+ pre_messages=[],
|
|
|
+ chat_agent=AgentParams(model="fake-model", temperature=0.1, max_tokens=200),
|
|
|
+ event_agent=EventAgentParams(enabled_tools=[], max_event_loops=1),
|
|
|
+ )
|
|
|
+ ticks = iter([1.0, 1.123, 1.456])
|
|
|
+ runtime = DebugRuntime(RoundStatsChatClient(), clock=lambda: next(ticks))
|
|
|
+
|
|
|
+ outputs = [message async for message in runtime.run(request)]
|
|
|
+
|
|
|
+ assert [message["type"] for message in outputs] == [
|
|
|
+ "session_started",
|
|
|
+ "message_delta",
|
|
|
+ "usage",
|
|
|
+ "round_stats",
|
|
|
+ "done",
|
|
|
+ ]
|
|
|
+ assert outputs[3] == {
|
|
|
+ "type": "round_stats",
|
|
|
+ "round_index": 1,
|
|
|
+ "ttft_ms": 123,
|
|
|
+ "elapsed_ms": 456,
|
|
|
+ "prompt_tokens": 10,
|
|
|
+ "completion_tokens": 20,
|
|
|
+ "total_tokens": 30,
|
|
|
+ "cached_tokens": 5,
|
|
|
+ "had_event": False,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_runtime_emits_round_stats_for_each_chat_call_in_event_handoff():
|
|
|
+ request = DebugRunRequest(
|
|
|
+ user_message="debug this",
|
|
|
+ system_prompts=[],
|
|
|
+ pre_messages=[],
|
|
|
+ chat_agent=AgentParams(model="fake-model", temperature=0.1, max_tokens=200),
|
|
|
+ event_agent=EventAgentParams(enabled_tools=["handoff_note"], max_event_loops=2),
|
|
|
+ )
|
|
|
+ ticks = iter([2.0, 2.25, 3.0, 3.05, 3.2])
|
|
|
+ client = EventRoundStatsChatClient()
|
|
|
+ runtime = DebugRuntime(client, clock=lambda: next(ticks))
|
|
|
+
|
|
|
+ outputs = [message async for message in runtime.run(request)]
|
|
|
+ stats = [message for message in outputs if message["type"] == "round_stats"]
|
|
|
+
|
|
|
+ assert client.calls == 2
|
|
|
+ assert stats == [
|
|
|
+ {
|
|
|
+ "type": "round_stats",
|
|
|
+ "round_index": 1,
|
|
|
+ "ttft_ms": None,
|
|
|
+ "elapsed_ms": 250,
|
|
|
+ "prompt_tokens": 3,
|
|
|
+ "completion_tokens": 0,
|
|
|
+ "total_tokens": 3,
|
|
|
+ "cached_tokens": 0,
|
|
|
+ "had_event": True,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "type": "round_stats",
|
|
|
+ "round_index": 2,
|
|
|
+ "ttft_ms": 50,
|
|
|
+ "elapsed_ms": 200,
|
|
|
+ "prompt_tokens": 4,
|
|
|
+ "completion_tokens": 6,
|
|
|
+ "total_tokens": 10,
|
|
|
+ "cached_tokens": 0,
|
|
|
+ "had_event": False,
|
|
|
+ },
|
|
|
+ ]
|