Skip to content

Commit 68685cd

Browse files
authored
feat!: Add per-execution runId, at-most-once tracking, and cross-process tracker resumption (#133)
1 parent 05758a7 commit 68685cd

24 files changed

Lines changed: 1003 additions & 636 deletions

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_agent_graph_runner.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,7 @@ def route(state: WorkflowState) -> str:
260260

261261
self._graph.traverse(fn=handle_traversal)
262262

263-
tracker = self._graph.get_tracker()
264-
graph_key_str = tracker.graph_key if tracker else 'unknown'
263+
graph_key_str = self._graph._agent_graph.key or 'unknown'
265264
log.debug(
266265
f"LangGraphAgentGraphRunner: graph='{graph_key_str}', root='{root_key}', "
267266
f"structure: {' | '.join(graph_structure)}"
@@ -281,7 +280,7 @@ async def run(self, input: Any) -> AgentGraphResult:
281280
:param input: The string prompt to send to the agent graph
282281
:return: AgentGraphResult with the final output and metrics
283282
"""
284-
tracker = self._graph.get_tracker()
283+
tracker = self._graph.create_tracker() if self._graph.create_tracker is not None else None
285284
start_ns = time.perf_counter_ns()
286285

287286
try:

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_callback_handler.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,17 @@ def flush(self, graph: AgentGraphDefinition) -> None:
196196
197197
:param graph: The AgentGraphDefinition whose nodes hold the LD config trackers.
198198
"""
199+
node_trackers: Dict[str, Any] = {}
199200
for node_key in self._path:
201+
if node_key in node_trackers:
202+
continue
200203
node = graph.get_node(node_key)
201204
if not node:
202205
continue
203-
config_tracker = node.get_config().tracker
206+
config_tracker = node.get_config().create_tracker()
204207
if not config_tracker:
205208
continue
209+
node_trackers[node_key] = config_tracker
206210

207211
usage = self._node_tokens.get(node_key)
208212
if usage:

packages/ai-providers/server-ai-langchain/tests/test_langchain_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,13 +530,13 @@ def sync_tool(x: str = '') -> str:
530530
cfg = AIAgentConfig(
531531
key='n',
532532
enabled=True,
533+
create_tracker=MagicMock(),
533534
model=ModelConfig(
534535
name='gpt-4',
535536
parameters={'tools': [{'name': 'my_tool', 'type': 'function', 'parameters': {}}]},
536537
),
537538
provider=ProviderConfig(name='openai'),
538539
instructions='',
539-
tracker=MagicMock(),
540540
)
541541
tools = build_structured_tools(cfg, {'my_tool': sync_tool})
542542
assert len(tools) == 1
@@ -553,13 +553,13 @@ async def async_tool(x: str = '') -> str:
553553
cfg = AIAgentConfig(
554554
key='n',
555555
enabled=True,
556+
create_tracker=MagicMock(),
556557
model=ModelConfig(
557558
name='gpt-4',
558559
parameters={'tools': [{'name': 'my_tool', 'type': 'function', 'parameters': {}}]},
559560
),
560561
provider=ProviderConfig(name='openai'),
561562
instructions='',
562-
tracker=MagicMock(),
563563
)
564564
tools = build_structured_tools(cfg, {'my_tool': async_tool})
565565
assert len(tools) == 1

packages/ai-providers/server-ai-langchain/tests/test_langgraph_agent_graph_runner.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111

1212

1313
def _make_graph(enabled: bool = True) -> AgentGraphDefinition:
14+
graph_tracker = MagicMock()
15+
node_tracker = MagicMock()
1416
root_config = AIAgentConfig(
1517
key='root-agent',
1618
enabled=enabled,
19+
create_tracker=MagicMock(return_value=node_tracker),
1720
model=ModelConfig(name='gpt-4'),
1821
provider=ProviderConfig(name='openai'),
1922
instructions='You are a helpful assistant.',
20-
tracker=MagicMock(),
2123
)
2224
graph_config = AIAgentGraphConfig(
2325
key='test-graph',
@@ -31,7 +33,7 @@ def _make_graph(enabled: bool = True) -> AgentGraphDefinition:
3133
nodes=nodes,
3234
context=MagicMock(),
3335
enabled=enabled,
34-
tracker=MagicMock(),
36+
create_tracker=lambda: graph_tracker,
3537
)
3638

3739

@@ -78,7 +80,7 @@ async def test_langgraph_runner_run_raises_when_langgraph_not_installed():
7880
@pytest.mark.asyncio
7981
async def test_langgraph_runner_run_tracks_failure_on_exception():
8082
graph = _make_graph()
81-
tracker = graph.get_tracker()
83+
tracker = graph.create_tracker()
8284
runner = LangGraphAgentGraphRunner(graph, {})
8385

8486
with patch.dict('sys.modules', {'langgraph': None, 'langgraph.graph': None}):
@@ -92,7 +94,7 @@ async def test_langgraph_runner_run_tracks_failure_on_exception():
9294
@pytest.mark.asyncio
9395
async def test_langgraph_runner_run_success():
9496
graph = _make_graph()
95-
tracker = graph.get_tracker()
97+
tracker = graph.create_tracker()
9698

9799
mock_message = MagicMock()
98100
mock_message.content = "langgraph answer"

packages/ai-providers/server-ai-langchain/tests/test_langgraph_callback_handler.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def _make_graph(mock_ld_client: MagicMock, node_key: str = 'root-agent', graph_k
3535
model_name='gpt-4',
3636
provider_name='openai',
3737
context=context,
38+
run_id='test-run-id',
3839
graph_key=graph_key,
3940
)
4041
graph_tracker = AIGraphTracker(
@@ -50,7 +51,7 @@ def _make_graph(mock_ld_client: MagicMock, node_key: str = 'root-agent', graph_k
5051
model=ModelConfig(name='gpt-4', parameters={}),
5152
provider=ProviderConfig(name='openai'),
5253
instructions='Be helpful.',
53-
tracker=node_tracker,
54+
create_tracker=lambda: node_tracker,
5455
)
5556
graph_config = AIAgentGraphConfig(
5657
key=graph_key,
@@ -64,7 +65,7 @@ def _make_graph(mock_ld_client: MagicMock, node_key: str = 'root-agent', graph_k
6465
nodes=nodes,
6566
context=context,
6667
enabled=True,
67-
tracker=graph_tracker,
68+
create_tracker=lambda: graph_tracker,
6869
)
6970

7071

@@ -320,7 +321,7 @@ def test_flush_emits_token_events_to_ld_tracker():
320321
"""flush() calls track_tokens on the node's config tracker."""
321322
mock_ld_client = MagicMock()
322323
graph = _make_graph(mock_ld_client, node_key='root-agent', graph_key='g1')
323-
tracker = graph.get_tracker()
324+
tracker = graph.create_tracker()
324325

325326
handler = LDMetricsCallbackHandler({'root-agent'}, {})
326327
node_run_id = uuid4()
@@ -339,7 +340,7 @@ def test_flush_emits_duration():
339340
"""flush() calls track_duration when duration was recorded."""
340341
mock_ld_client = MagicMock()
341342
graph = _make_graph(mock_ld_client)
342-
tracker = graph.get_tracker()
343+
tracker = graph.create_tracker()
343344

344345
handler = LDMetricsCallbackHandler({'root-agent'}, {})
345346
run_id = uuid4()
@@ -355,7 +356,7 @@ def test_flush_emits_tool_calls():
355356
"""flush() calls track_tool_call for each recorded tool invocation."""
356357
mock_ld_client = MagicMock()
357358
graph = _make_graph(mock_ld_client)
358-
tracker = graph.get_tracker()
359+
tracker = graph.create_tracker()
359360

360361
handler = LDMetricsCallbackHandler({'root-agent'}, {'fn_search': 'search'})
361362
# The agent node must be started first so it appears in the path for flush()
@@ -377,7 +378,7 @@ def test_flush_includes_graph_key_in_node_events():
377378
"""flush() passes graph_key to the node tracker so graphKey appears in events."""
378379
mock_ld_client = MagicMock()
379380
graph = _make_graph(mock_ld_client, graph_key='my-graph')
380-
tracker = graph.get_tracker()
381+
tracker = graph.create_tracker()
381382

382383
handler = LDMetricsCallbackHandler({'root-agent'}, {})
383384
node_run_id = uuid4()
@@ -402,14 +403,15 @@ def test_flush_with_no_graph_key_on_node_tracker():
402403
model_name='gpt-4',
403404
provider_name='openai',
404405
context=context,
406+
run_id='test-run-id',
405407
)
406408
node_config = AIAgentConfig(
407409
key='root-agent',
408410
enabled=True,
409411
model=ModelConfig(name='gpt-4', parameters={}),
410412
provider=ProviderConfig(name='openai'),
411413
instructions='Be helpful.',
412-
tracker=node_tracker,
414+
create_tracker=lambda: node_tracker,
413415
)
414416
graph_config = AIAgentGraphConfig(
415417
key='test-graph',
@@ -423,7 +425,7 @@ def test_flush_with_no_graph_key_on_node_tracker():
423425
nodes=nodes,
424426
context=context,
425427
enabled=True,
426-
tracker=None,
428+
create_tracker=lambda: None,
427429
)
428430

429431
handler = LDMetricsCallbackHandler({'root-agent'}, {})
@@ -441,7 +443,7 @@ def test_flush_skips_nodes_not_in_path():
441443
"""flush() only emits events for nodes that were actually executed."""
442444
mock_ld_client = MagicMock()
443445
graph = _make_graph(mock_ld_client)
444-
tracker = graph.get_tracker()
446+
tracker = graph.create_tracker()
445447

446448
# Handler with 'root-agent' in node_keys but never started
447449
handler = LDMetricsCallbackHandler({'root-agent'}, {})
@@ -460,10 +462,10 @@ def test_flush_skips_node_without_tracker():
460462
node_config_no_tracker = AIAgentConfig(
461463
key='no-track',
462464
enabled=True,
465+
create_tracker=lambda: None,
463466
model=ModelConfig(name='gpt-4', parameters={}),
464467
provider=ProviderConfig(name='openai'),
465468
instructions='',
466-
tracker=None,
467469
)
468470
graph_config = AIAgentGraphConfig(
469471
key='g', root_config_key='no-track', edges=[], enabled=True
@@ -474,7 +476,7 @@ def test_flush_skips_node_without_tracker():
474476
nodes=nodes,
475477
context=context,
476478
enabled=True,
477-
tracker=None,
479+
create_tracker=lambda: None,
478480
)
479481

480482
handler = LDMetricsCallbackHandler({'no-track'}, {})

0 commit comments

Comments
 (0)