77
88from ldai import LDAIClient , ManagedAgentGraph , ManagedGraphResult
99from ldai .providers .types import AgentGraphRunnerResult , GraphMetrics , LDAIMetrics
10- from ldai .providers import AgentGraphResult , AgentGraphRunner , ToolRegistry
10+ from ldai .providers import AgentGraphRunner , ToolRegistry
1111from ldai .tracker import TokenUsage
1212
1313
1414# --- Test doubles ---
1515
1616class StubAgentGraphRunner (AgentGraphRunner ):
17- """Legacy runner that returns AgentGraphResult (old shape)."""
18- def __init__ (self , output : str = "stub output" ):
19- self ._output = output
17+ """Runner that returns AgentGraphRunnerResult (new shape)."""
18+ def __init__ (self , content : str = "stub output" ):
19+ self ._content = content
2020
21- async def run (self , input ) -> AgentGraphResult :
22- return AgentGraphResult (
23- output = self ._output ,
21+ async def run (self , input ) -> AgentGraphRunnerResult :
22+ return AgentGraphRunnerResult (
23+ content = self ._content ,
2424 raw = {"input" : input },
25- metrics = LDAIMetrics (success = True ),
25+ metrics = GraphMetrics (success = True ),
2626 )
2727
2828
29- class StubNewShapeRunner (AgentGraphRunner ):
30- """New-shape runner that returns AgentGraphRunnerResult."""
29+ class StubRunnerWithMetrics (AgentGraphRunner ):
30+ """Runner that returns AgentGraphRunnerResult with full GraphMetrics ."""
3131 def __init__ (self , content : str = "new shape output" ):
3232 self ._content = content
3333
@@ -39,17 +39,28 @@ async def run(self, input) -> AgentGraphRunnerResult:
3939 path = ["root" , "specialist" ],
4040 duration_ms = 42 ,
4141 usage = TokenUsage (total = 10 , input = 5 , output = 5 ),
42- node_metrics = {},
42+ node_metrics = {
43+ "root" : LDAIMetrics (
44+ success = True ,
45+ usage = TokenUsage (total = 5 , input = 3 , output = 2 ),
46+ duration_ms = 20 ,
47+ ),
48+ "specialist" : LDAIMetrics (
49+ success = True ,
50+ usage = TokenUsage (total = 5 , input = 2 , output = 3 ),
51+ duration_ms = 22 ,
52+ ),
53+ },
4354 ),
4455 raw = {"input" : input },
4556 )
4657
4758
48- # --- ManagedAgentGraph unit tests (legacy shape) ---
59+ # --- ManagedAgentGraph unit tests ---
4960
5061@pytest .mark .asyncio
5162async def test_managed_agent_graph_run_delegates_to_runner ():
52- """Legacy AgentGraphResult shape: content comes from output field ."""
63+ """Runner result content is surfaced correctly ."""
5364 runner = StubAgentGraphRunner ("hello world" )
5465 managed = ManagedAgentGraph (runner )
5566 result = await managed .run ("test input" )
@@ -64,15 +75,14 @@ def test_managed_agent_graph_get_runner():
6475 assert managed .get_agent_graph_runner () is runner
6576
6677
67- # --- ManagedAgentGraph unit tests (new AgentGraphRunnerResult shape) ---
68-
6978@pytest .mark .asyncio
70- async def test_managed_agent_graph_run_handles_new_shape ():
71- """New AgentGraphRunnerResult shape: content and GraphMetrics are surfaced ."""
72- runner = StubNewShapeRunner ("final answer" )
79+ async def test_managed_agent_graph_run_surfaces_graph_metrics ():
80+ """GraphMetrics fields are reflected in GraphMetricSummary ."""
81+ runner = StubRunnerWithMetrics ("final answer" )
7382 mock_graph = MagicMock ()
7483 mock_tracker = MagicMock ()
7584 mock_graph .create_tracker = MagicMock (return_value = mock_tracker )
85+ mock_graph .get_node = MagicMock (return_value = None ) # no nodes for this test
7686
7787 managed = ManagedAgentGraph (runner , graph = mock_graph )
7888 result = await managed .run ("test input" )
@@ -87,12 +97,13 @@ async def test_managed_agent_graph_run_handles_new_shape():
8797
8898
8999@pytest .mark .asyncio
90- async def test_managed_agent_graph_new_shape_drives_tracking ():
91- """New shape: managed layer calls tracker methods from result.metrics."""
92- runner = StubNewShapeRunner ()
100+ async def test_managed_agent_graph_drives_graph_level_tracking ():
101+ """Managed layer calls graph tracker methods from result.metrics."""
102+ runner = StubRunnerWithMetrics ()
93103 mock_graph = MagicMock ()
94104 mock_tracker = MagicMock ()
95105 mock_graph .create_tracker = MagicMock (return_value = mock_tracker )
106+ mock_graph .get_node = MagicMock (return_value = None )
96107
97108 managed = ManagedAgentGraph (runner , graph = mock_graph )
98109 await managed .run ("test input" )
@@ -104,16 +115,73 @@ async def test_managed_agent_graph_new_shape_drives_tracking():
104115
105116
106117@pytest .mark .asyncio
107- async def test_managed_agent_graph_new_shape_no_graph_skips_tracking ():
108- """New shape without graph: no tracking called (graph not available)."""
109- runner = StubNewShapeRunner ()
118+ async def test_managed_agent_graph_drives_per_node_tracking ():
119+ """Managed layer creates per-node trackers and fires node-level events."""
120+ runner = StubRunnerWithMetrics ()
121+ mock_graph = MagicMock ()
122+ mock_graph_tracker = MagicMock ()
123+ mock_graph .create_tracker = MagicMock (return_value = mock_graph_tracker )
124+
125+ root_tracker = MagicMock ()
126+ specialist_tracker = MagicMock ()
127+
128+ root_node = MagicMock ()
129+ root_node .get_config .return_value .create_tracker = MagicMock (return_value = root_tracker )
130+ specialist_node = MagicMock ()
131+ specialist_node .get_config .return_value .create_tracker = MagicMock (return_value = specialist_tracker )
132+
133+ def get_node (key ):
134+ return {"root" : root_node , "specialist" : specialist_node }.get (key )
135+
136+ mock_graph .get_node = get_node
137+
138+ managed = ManagedAgentGraph (runner , graph = mock_graph )
139+ await managed .run ("test input" )
140+
141+ # root node tracking
142+ root_tracker .track_tokens .assert_called_once ()
143+ root_tracker .track_duration .assert_called_once_with (20 )
144+ root_tracker .track_success .assert_called_once ()
145+
146+ # specialist node tracking
147+ specialist_tracker .track_tokens .assert_called_once ()
148+ specialist_tracker .track_duration .assert_called_once_with (22 )
149+ specialist_tracker .track_success .assert_called_once ()
150+
151+
152+ @pytest .mark .asyncio
153+ async def test_managed_agent_graph_no_graph_skips_tracking ():
154+ """Without a graph reference, no tracking is called but run succeeds."""
155+ runner = StubRunnerWithMetrics ()
110156 managed = ManagedAgentGraph (runner , graph = None )
111- # Should not raise even without a graph reference
112157 result = await managed .run ("test input" )
113158 assert result .content == "new shape output"
114159 assert result .metrics .success is True
115160
116161
162+ @pytest .mark .asyncio
163+ async def test_managed_agent_graph_failure_calls_track_invocation_failure ():
164+ """On a failed run, track_invocation_failure is called instead of success."""
165+
166+ class FailingRunner (AgentGraphRunner ):
167+ async def run (self , input ) -> AgentGraphRunnerResult :
168+ return AgentGraphRunnerResult (
169+ content = '' ,
170+ raw = None ,
171+ metrics = GraphMetrics (success = False , duration_ms = 5 ),
172+ )
173+
174+ mock_graph = MagicMock ()
175+ mock_tracker = MagicMock ()
176+ mock_graph .create_tracker = MagicMock (return_value = mock_tracker )
177+ mock_graph .get_node = MagicMock (return_value = None )
178+
179+ managed = ManagedAgentGraph (FailingRunner (), graph = mock_graph )
180+ result = await managed .run ("test input" )
181+
182+ assert result .metrics .success is False
183+ mock_tracker .track_invocation_failure .assert_called_once ()
184+ mock_tracker .track_invocation_success .assert_not_called ()
117185
118186
119187# --- LDAIClient.create_agent_graph() integration tests ---
0 commit comments