agentx-python 0.6.21__tar.gz → 0.6.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {agentx_python-0.6.21 → agentx_python-0.6.22}/PKG-INFO +1 -1
  2. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/datasets.py +12 -0
  3. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/runner.py +5 -0
  4. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/autogen.py +6 -1
  5. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/langchain.py +173 -10
  6. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/tracing/tracer.py +19 -10
  7. agentx_python-0.6.22/agentx/version.py +1 -0
  8. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/PKG-INFO +1 -1
  9. agentx_python-0.6.21/agentx/version.py +0 -1
  10. {agentx_python-0.6.21 → agentx_python-0.6.22}/LICENSE +0 -0
  11. {agentx_python-0.6.21 → agentx_python-0.6.22}/README.md +0 -0
  12. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/__init__.py +0 -0
  13. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/agentx.py +0 -0
  14. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/cli.py +0 -0
  15. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/__init__.py +0 -0
  16. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/_term.py +0 -0
  17. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/adapters/__init__.py +0 -0
  18. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  19. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/adapters/precomputed.py +0 -0
  20. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/adapters/raw.py +0 -0
  21. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/client.py +0 -0
  22. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/evaluation_settings.py +0 -0
  23. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/models.py +0 -0
  24. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/prompts.py +0 -0
  25. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/redaction.py +0 -0
  26. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/reporting.py +0 -0
  27. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/results.py +0 -0
  28. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/evaluations/tracing.py +0 -0
  29. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/exceptions.py +0 -0
  30. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/feedback.py +0 -0
  31. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/__init__.py +0 -0
  32. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/_traced_call.py +0 -0
  33. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/anthropic.py +0 -0
  34. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/crewai.py +0 -0
  35. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/google_adk.py +0 -0
  36. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/google_genai.py +0 -0
  37. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/litellm.py +0 -0
  38. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/llamaindex.py +0 -0
  39. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/moveworks.py +0 -0
  40. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/openai.py +0 -0
  41. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/integrations/openai_agents.py +0 -0
  42. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/__init__.py +0 -0
  43. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/client.py +0 -0
  44. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/models.py +0 -0
  45. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/online_evaluators.py +0 -0
  46. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/patterns.py +0 -0
  47. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/profile.py +0 -0
  48. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/monitor/signals.py +0 -0
  49. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/outcomes.py +0 -0
  50. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/py.typed +0 -0
  51. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/resources/__init__.py +0 -0
  52. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/resources/agent.py +0 -0
  53. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/resources/conversation.py +0 -0
  54. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/resources/workforce.py +0 -0
  55. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/tracing/__init__.py +0 -0
  56. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/tracing/ci_types.py +0 -0
  57. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/tracing/ingest_client.py +0 -0
  58. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx/util.py +0 -0
  59. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/SOURCES.txt +0 -0
  60. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/dependency_links.txt +0 -0
  61. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/entry_points.txt +0 -0
  62. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/not-zip-safe +0 -0
  63. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/requires.txt +0 -0
  64. {agentx_python-0.6.21 → agentx_python-0.6.22}/agentx_python.egg-info/top_level.txt +0 -0
  65. {agentx_python-0.6.21 → agentx_python-0.6.22}/setup.cfg +0 -0
  66. {agentx_python-0.6.21 → agentx_python-0.6.22}/setup.py +0 -0
  67. {agentx_python-0.6.21 → agentx_python-0.6.22}/tests/test_integration.py +0 -0
  68. {agentx_python-0.6.21 → agentx_python-0.6.22}/tests/test_integrations.py +0 -0
  69. {agentx_python-0.6.21 → agentx_python-0.6.22}/tests/test_span_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.21
3
+ Version: 0.6.22
4
4
  Summary: Official Python SDK for AgentX (https://www.agentx.so/)
5
5
  Home-page: https://github.com/AgentX-ai/AgentX-python
6
6
  Author: Robin Wang and AgentX Team
@@ -86,9 +86,19 @@ class DatasetBuilder:
86
86
  judge_guideline: Optional[str] = None,
87
87
  smoke_test_count: Optional[int] = None,
88
88
  smoke_test_guidance: Optional[str] = None,
89
+ expected_tools: Optional[List[str]] = None,
90
+ trajectory_match_mode: str = "strict",
89
91
  ) -> "DatasetBuilder":
90
92
  """Add a case. `judge_guideline` is optional grading guidance specific to this question.
91
93
 
94
+ `expected_tools` declares the tool calls a correct run of this case should make. When a
95
+ result links its trace (return `{"output": ..., "trace_id": span.trace_id}` from the
96
+ agent function), the engine matches the trace's actual tool-call sequence against it and
97
+ reports a pass/fail "Trajectory match" scorer row on the result. `trajectory_match_mode`
98
+ follows agentevals semantics: "strict" (same calls, same order), "unordered" (same calls,
99
+ any order), "superset" (all expected present, extras allowed), or "subset" (no unexpected
100
+ calls, missing allowed).
101
+
92
102
  `smoke_test_count`, when set (1-10), asks this question that many extra ways each
93
103
  evaluation run, LLM-paraphrased server-side, to catch agents that are brittle to phrasing
94
104
  rather than genuinely wrong. `smoke_test_guidance` optionally steers what kind of variants
@@ -111,6 +121,8 @@ class DatasetBuilder:
111
121
  main["smokeTest"] = {"enabled": True, "count": smoke_test_count}
112
122
  if smoke_test_guidance:
113
123
  main["smokeTest"]["guidance"] = smoke_test_guidance
124
+ if expected_tools:
125
+ main["expectedTrajectory"] = {"tools": expected_tools, "mode": trajectory_match_mode}
114
126
  self._payload["questions"].append(
115
127
  {
116
128
  "main_question": main,
@@ -271,6 +271,11 @@ class EvaluationRunContext:
271
271
  # UI reads, computed once in the API.
272
272
  # ------------------------------------------------------------------
273
273
 
274
+ @property
275
+ def run_id(self) -> str:
276
+ """The server-side run id - handy for fetching the run's full results afterwards."""
277
+ return self._run.run_id
278
+
274
279
  @property
275
280
  def rated_count(self) -> int:
276
281
  """Number of submitted results that have received a rating so far."""
@@ -178,8 +178,13 @@ class AgentXAutoGenObserver:
178
178
  if input_tokens is not None or out_tokens is not None:
179
179
  total_input_tokens += input_tokens or 0
180
180
  total_output_tokens += out_tokens or 0
181
+ # Prefer the speaking agent's name (message.source) over a generic counter -
182
+ # in a multi-agent team the step sequence then reads as the actual
183
+ # agent-turn trajectory ("planner" -> "coder" -> "reviewer"), which is what
184
+ # trajectory evaluation cares about.
185
+ source = getattr(message, "source", None)
181
186
  execution_steps.append({
182
- "name": f"LLM Call {len(execution_steps) + 1}",
187
+ "name": str(source) if source else f"LLM Call {len(execution_steps) + 1}",
183
188
  "duration_ms": (end_t - start_t) * 1000,
184
189
  "start_time": start_t,
185
190
  "end_time": end_t,
@@ -231,6 +231,39 @@ def _extract_llm_output(response: "LLMResult") -> Optional[str]:
231
231
  return None
232
232
 
233
233
 
234
+ # Nested chain runs that are plumbing, not agent structure - LCEL composition wrappers,
235
+ # prompt/parse steps, and LangGraph's internal channel machinery. Skipped when deciding which
236
+ # chain runs become "node" child spans; their LLM/tool descendants re-parent to the nearest
237
+ # non-noise ancestor (see _emit_span_tree's resolve_parent walk).
238
+ _NOISE_CHAIN_NAMES = frozenset({
239
+ "RunnableSequence",
240
+ "RunnableParallel",
241
+ "RunnableLambda",
242
+ "RunnableAssign",
243
+ "RunnablePick",
244
+ "RunnableBinding",
245
+ "RunnableWithFallbacks",
246
+ "RunnableWithMessageHistory",
247
+ "RunnableBranch",
248
+ "ChatPromptTemplate",
249
+ "PromptTemplate",
250
+ "StrOutputParser",
251
+ "JsonOutputParser",
252
+ "ToolsAgentOutputParser",
253
+ "OpenAIFunctionsAgentOutputParser",
254
+ "LangGraph",
255
+ "CompiledStateGraph",
256
+ "Prompt",
257
+ "_Exception",
258
+ })
259
+
260
+ _NOISE_CHAIN_PREFIXES = ("ChannelWrite", "ChannelRead", "Branch<", "RunnableParallel<", "_")
261
+
262
+
263
+ def _is_noise_chain(name: str) -> bool:
264
+ return name in _NOISE_CHAIN_NAMES or name.startswith(_NOISE_CHAIN_PREFIXES)
265
+
266
+
234
267
  class AgentXCallbackHandler(BaseCallbackHandler):
235
268
  """
236
269
  LangChain callback handler that captures the top-level chain run and all
@@ -238,6 +271,11 @@ class AgentXCallbackHandler(BaseCallbackHandler):
238
271
 
239
272
  Compatible with AgentExecutor, LCEL chains, and LangGraph agents
240
273
  (``create_agent``, ``create_react_agent``).
274
+
275
+ The trace is a real span tree: LangGraph graph nodes (and named non-plumbing sub-chains)
276
+ become child spans, and each LLM call / tool call / retrieval becomes a span parented under
277
+ the node that ran it - so the engine's Execution Timeline shows the actual graph trajectory
278
+ (which nodes ran, in what order, and what each did), not a flat step list.
241
279
  """
242
280
 
243
281
  def __init__(
@@ -331,7 +369,122 @@ class AgentXCallbackHandler(BaseCallbackHandler):
331
369
  "retrieval_steps": pending,
332
370
  "input_tokens": 0,
333
371
  "output_tokens": 0,
372
+ # Graph structure captured under this top-level run: named nested chain runs
373
+ # (LangGraph nodes, sub-agents) keyed by run_id, and the FULL nested-chain
374
+ # parent map (noise chains included) so _emit_span_tree can walk through
375
+ # skipped plumbing runs to the nearest emitted ancestor.
376
+ "node_runs": {},
377
+ "chain_parents": {},
334
378
  }
379
+ else:
380
+ top = self._find_top_ancestor(parent_run_id)
381
+ if top is None:
382
+ return
383
+ state = self._runs.get(top)
384
+ if state is None:
385
+ return
386
+ name = kwargs.get("name") or (serialized or {}).get("name")
387
+ # LangGraph stamps its node runs with metadata.langgraph_node - trust that over the
388
+ # noise heuristic when present (a user's node could legitimately be named
389
+ # "RunnableLambda"-style by a wrapper).
390
+ run_meta = kwargs.get("metadata") or {}
391
+ is_node = bool(name) and (run_meta.get("langgraph_node") == name or not _is_noise_chain(str(name)))
392
+ with self._state_lock:
393
+ state["chain_parents"][run_id] = parent_run_id
394
+ if is_node:
395
+ state["node_runs"][run_id] = {
396
+ "name": str(name),
397
+ "start": time.time(),
398
+ "end": None,
399
+ "input": _extract_input(inputs),
400
+ "output": None,
401
+ "error": None,
402
+ "parent": parent_run_id,
403
+ }
404
+
405
+ def _finalize_node(self, run_id: UUID, *, output: Any = None, error: Optional[str] = None) -> None:
406
+ """Close a nested chain run's node record (if it became one) with end time + result."""
407
+ top = self._find_top_ancestor(run_id)
408
+ if top is None:
409
+ return
410
+ state = self._runs.get(top)
411
+ if state is None:
412
+ return
413
+ with self._state_lock:
414
+ node = state.get("node_runs", {}).get(run_id)
415
+ if node is None:
416
+ return
417
+ node["end"] = time.time()
418
+ if output is not None:
419
+ node["output"] = output
420
+ if error is not None:
421
+ node["error"] = error
422
+
423
+ def _emit_span_tree(self, span, state: Dict[str, Any], tool_calls: List[Dict[str, Any]]) -> None:
424
+ """
425
+ Emit the recorded run as a hierarchical span tree under ``span``: node runs first (in
426
+ start order, parented to their nearest emitted ancestor), then every LLM call, tool
427
+ call, and retrieval parented under the node that ran it. Records whose parent chain is
428
+ entirely noise (or missing, e.g. pre-run retrievals) land directly under the root span.
429
+ """
430
+ emitted: Dict[Any, Any] = {}
431
+ chain_parents: Dict[Any, Any] = state.get("chain_parents", {})
432
+ node_runs: Dict[Any, Dict[str, Any]] = state.get("node_runs", {})
433
+
434
+ def resolve_parent(parent_id: Any):
435
+ seen: set = set()
436
+ current = parent_id
437
+ while current is not None and current not in seen:
438
+ seen.add(current)
439
+ if current in emitted:
440
+ return emitted[current]
441
+ current = chain_parents.get(current)
442
+ return span
443
+
444
+ for run_id, node in sorted(node_runs.items(), key=lambda kv: kv[1]["start"]):
445
+ parent_span = resolve_parent(node.get("parent"))
446
+ emitted[run_id] = parent_span.child_span(
447
+ node["name"],
448
+ start_time=node["start"],
449
+ end_time=node.get("end") or node["start"],
450
+ input=node.get("input"),
451
+ output=node.get("output"),
452
+ error=node.get("error"),
453
+ )
454
+
455
+ llm_count = 0
456
+ for step in state.get("execution_steps", []):
457
+ llm_count += 1
458
+ resolve_parent(step.get("parent_run_id")).child_span(
459
+ step.get("name") or f"LLM Call {llm_count}",
460
+ start_time=step.get("start_time"),
461
+ end_time=step.get("end_time"),
462
+ duration_ms=step.get("duration_ms"),
463
+ input=step.get("input"),
464
+ output=step.get("output"),
465
+ model=step.get("model"),
466
+ input_tokens=step.get("inputTokenSize"),
467
+ output_tokens=step.get("outputTokenSize"),
468
+ )
469
+ for tc in tool_calls:
470
+ resolve_parent(tc.get("parent_run_id")).child_span(
471
+ tc.get("name") or "Tool call",
472
+ start_time=tc.get("start_time"),
473
+ end_time=tc.get("end_time"),
474
+ duration_ms=tc.get("latency_ms"),
475
+ input=tc.get("input"),
476
+ output=tc.get("output"),
477
+ error=None if tc.get("success", True) else str(tc.get("output") or "Tool call failed"),
478
+ )
479
+ for step in state.get("retrieval_steps", []):
480
+ resolve_parent(step.get("parent_run_id")).child_span(
481
+ step.get("name") or "Retrieval",
482
+ start_time=step.get("start_time"),
483
+ end_time=step.get("end_time"),
484
+ duration_ms=step.get("duration_ms"),
485
+ input=step.get("query"),
486
+ output=step.get("output"),
487
+ )
335
488
 
336
489
  def on_chain_end(
337
490
  self,
@@ -347,9 +500,13 @@ class AgentXCallbackHandler(BaseCallbackHandler):
347
500
  # entries behind here would leak forever in a long-lived singleton
348
501
  # handler, since nothing else ever cleans up a non-top-level run_id.
349
502
  is_top = self._top_level.pop(run_id, None)
350
- self._parents.pop(run_id, None)
351
503
  if not is_top:
504
+ # Close the node record before dropping this run's _parents entry - the top-ancestor
505
+ # walk inside _finalize_node still needs it.
506
+ self._finalize_node(run_id, output=_extract_output(outputs))
507
+ self._parents.pop(run_id, None)
352
508
  return
509
+ self._parents.pop(run_id, None)
353
510
  state = self._runs.pop(run_id, None)
354
511
  if state is None:
355
512
  return
@@ -369,16 +526,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
369
526
  # spanning several chain/agent/retriever calls) - fold this
370
527
  # top-level run into it instead of sending an independent trace.
371
528
  active_span._merge_child_run(
372
- execution_steps=state["execution_steps"],
373
529
  tool_calls=tool_calls,
374
- retrieval_steps=state["retrieval_steps"],
375
530
  input=state["input"],
376
531
  output=output,
377
532
  model=state.get("model"),
378
533
  framework="langchain",
379
534
  input_tokens=state["input_tokens"] or None,
380
535
  output_tokens=state["output_tokens"] or None,
536
+ emit_steps=False,
381
537
  )
538
+ self._emit_span_tree(active_span, state, tool_calls)
382
539
  else:
383
540
  # Standalone usage (no enclosing `with tracer.trace()`): open a real root span for
384
541
  # this chain invocation and let _merge_child_run explode its accumulated
@@ -388,16 +545,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
388
545
  # see llamaindex.py's _send_trace for the identical fix and full rationale.
389
546
  span._start = state["start"]
390
547
  span._merge_child_run(
391
- execution_steps=state["execution_steps"],
392
548
  tool_calls=tool_calls,
393
- retrieval_steps=state["retrieval_steps"],
394
549
  input=state["input"],
395
550
  output=output,
396
551
  model=state.get("model"),
397
552
  framework="langchain",
398
553
  input_tokens=state["input_tokens"] or None,
399
554
  output_tokens=state["output_tokens"] or None,
555
+ emit_steps=False,
400
556
  )
557
+ self._emit_span_tree(span, state, tool_calls)
401
558
 
402
559
  def on_chain_error(
403
560
  self,
@@ -409,9 +566,11 @@ class AgentXCallbackHandler(BaseCallbackHandler):
409
566
  ) -> None:
410
567
  # See on_chain_end's comment - pop for every chain run, not just top-level.
411
568
  is_top = self._top_level.pop(run_id, None)
412
- self._parents.pop(run_id, None)
413
569
  if not is_top:
570
+ self._finalize_node(run_id, error=str(error))
571
+ self._parents.pop(run_id, None)
414
572
  return
573
+ self._parents.pop(run_id, None)
415
574
  state = self._runs.pop(run_id, None)
416
575
  if state is None:
417
576
  return
@@ -420,30 +579,30 @@ class AgentXCallbackHandler(BaseCallbackHandler):
420
579
  if active_span is not None:
421
580
  active_span.set_error(str(error))
422
581
  active_span._merge_child_run(
423
- execution_steps=state["execution_steps"],
424
582
  tool_calls=state["tool_calls"],
425
- retrieval_steps=state["retrieval_steps"],
426
583
  input=state["input"],
427
584
  model=state.get("model"),
428
585
  framework="langchain",
429
586
  input_tokens=state["input_tokens"] or None,
430
587
  output_tokens=state["output_tokens"] or None,
588
+ emit_steps=False,
431
589
  )
590
+ self._emit_span_tree(active_span, state, state["tool_calls"])
432
591
  else:
433
592
  # See on_chain_end's matching branch - same standalone-usage handling.
434
593
  with self._tracer.trace(self._name, metadata=self._metadata, session_id=self._session_id) as span:
435
594
  span._start = state["start"]
436
595
  span.set_error(str(error))
437
596
  span._merge_child_run(
438
- execution_steps=state["execution_steps"],
439
597
  tool_calls=state["tool_calls"],
440
- retrieval_steps=state["retrieval_steps"],
441
598
  input=state["input"],
442
599
  model=state.get("model"),
443
600
  framework="langchain",
444
601
  input_tokens=state["input_tokens"] or None,
445
602
  output_tokens=state["output_tokens"] or None,
603
+ emit_steps=False,
446
604
  )
605
+ self._emit_span_tree(span, state, state["tool_calls"])
447
606
 
448
607
  # ------------------------------------------------------------------
449
608
  # LLM lifecycle
@@ -545,6 +704,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
545
704
  if start_t is not None and top and top in self._runs:
546
705
  steps = self._runs[top]["execution_steps"]
547
706
  steps.append({
707
+ "parent_run_id": parent_run_id,
548
708
  "name": f"LLM Call {len(steps) + 1}",
549
709
  "duration_ms": (end_t - start_t) * 1000,
550
710
  "start_time": start_t,
@@ -592,6 +752,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
592
752
  start_t = state["start"]
593
753
  latency_ms = int((end_t - start_t) * 1000)
594
754
  tool_call = {
755
+ "parent_run_id": parent_run_id,
595
756
  "name": state["tool_name"],
596
757
  "input": state["tool_input"],
597
758
  "output": str(output),
@@ -623,6 +784,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
623
784
  end_t = time.time()
624
785
  start_t = state.get("start", end_t)
625
786
  tool_call = {
787
+ "parent_run_id": parent_run_id,
626
788
  "name": state.get("tool_name", "unknown"),
627
789
  "input": state.get("tool_input"),
628
790
  "output": f"ERROR: {error}",
@@ -669,6 +831,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
669
831
  query: Optional[str] = state["query"] or None
670
832
  doc_count = len(documents) if hasattr(documents, "__len__") else None
671
833
  step: Dict[str, Any] = {
834
+ "parent_run_id": parent_run_id,
672
835
  "name": "Retrieval 1", # renumbered below
673
836
  "duration_ms": (end_t - start_t) * 1000,
674
837
  "start_time": start_t,
@@ -338,6 +338,7 @@ class _TraceSpan:
338
338
  output_tokens: Optional[int] = None,
339
339
  cache_read_tokens: Optional[int] = None,
340
340
  cache_write_tokens: Optional[int] = None,
341
+ emit_steps: bool = True,
341
342
  ) -> None:
342
343
  """
343
344
  Explode a whole auto-instrumented sub-run (e.g. one top-level LangChain
@@ -348,9 +349,15 @@ class _TraceSpan:
348
349
  tool_calls entries here carry no start/end timing (only latency_ms, see callers e.g.
349
350
  langchain.py) - their child span falls back to offset-0 positioning in the tree panel;
350
351
  execution_steps (LLM calls) always carry real timing and position correctly.
352
+
353
+ ``emit_steps=False`` skips the flat child-span emission but keeps everything else (the
354
+ flat tool_calls mirror for the Monitor tool-failure check, and the summary adoption) -
355
+ for callers that emit their own HIERARCHICAL span tree instead (langchain.py's
356
+ _emit_span_tree parents steps under the graph node that ran them, rather than flat
357
+ under this span).
351
358
  """
352
359
  with self._merge_lock:
353
- for step in execution_steps or []:
360
+ for step in [] if not emit_steps else (execution_steps or []):
354
361
  self._child_span_count += 1
355
362
  self.child_span(
356
363
  step.get("name") or f"LLM Call {self._child_span_count}",
@@ -371,14 +378,16 @@ class _TraceSpan:
371
378
  # start_time/end_time; prefer those over latency_ms alone when both know duration
372
379
  # since they also let this child span position correctly in the tree panel
373
380
  # instead of defaulting to offset 0.
374
- self.child_span(
375
- tc.get("name") or "Tool call",
376
- start_time=tc.get("start_time"),
377
- end_time=tc.get("end_time"),
378
- duration_ms=tc.get("latency_ms"),
379
- input=tc.get("input"),
380
- output=tc.get("output"),
381
- )
381
+ if emit_steps:
382
+ self.child_span(
383
+ tc.get("name") or "Tool call",
384
+ start_time=tc.get("start_time"),
385
+ end_time=tc.get("end_time"),
386
+ duration_ms=tc.get("latency_ms"),
387
+ input=tc.get("input"),
388
+ output=tc.get("output"),
389
+ error=None if tc.get("success", True) else str(tc.get("output") or "Tool call failed"),
390
+ )
382
391
  # Also mirror onto this span's own flat tool_calls list, sent in this span's own
383
392
  # wire payload on __exit__ (see tool_calls=self.tool_calls or None below). The
384
393
  # child span above is only for the trace detail view's span tree; the engine's
@@ -393,7 +402,7 @@ class _TraceSpan:
393
402
  "latency_ms": tc.get("latency_ms"),
394
403
  "success": tc.get("success", True),
395
404
  })
396
- for step in retrieval_steps or []:
405
+ for step in [] if not emit_steps else (retrieval_steps or []):
397
406
  self._child_span_count += 1
398
407
  self.child_span(
399
408
  step.get("name") or f"Retrieval {self._child_span_count}",
@@ -0,0 +1 @@
1
+ VERSION = "0.6.22"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.21
3
+ Version: 0.6.22
4
4
  Summary: Official Python SDK for AgentX (https://www.agentx.so/)
5
5
  Home-page: https://github.com/AgentX-ai/AgentX-python
6
6
  Author: Robin Wang and AgentX Team
@@ -1 +0,0 @@
1
- VERSION = "0.6.21"
File without changes
File without changes
File without changes
File without changes