agentx-python 0.8.20__tar.gz → 0.8.21__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 (96) hide show
  1. {agentx_python-0.8.20/agentx_python.egg-info → agentx_python-0.8.21}/PKG-INFO +1 -1
  2. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/client.py +25 -8
  3. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/datasets.py +2 -1
  4. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/evaluation_settings.py +2 -1
  5. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/models.py +4 -0
  6. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/prompts.py +3 -3
  7. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/runner.py +5 -0
  8. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/crewai.py +3 -0
  9. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/google_adk.py +6 -2
  10. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/langchain.py +4 -0
  11. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/openai_agents.py +3 -0
  12. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/client.py +7 -2
  13. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/models.py +7 -0
  14. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/profile.py +1 -1
  15. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/tracer.py +42 -28
  16. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/version.py +2 -2
  17. {agentx_python-0.8.20 → agentx_python-0.8.21/agentx_python.egg-info}/PKG-INFO +1 -1
  18. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_runner_features.py +16 -0
  19. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_selfhost_analysis_fallback.py +25 -1
  20. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_span_tree.py +52 -5
  21. {agentx_python-0.8.20 → agentx_python-0.8.21}/LICENSE +0 -0
  22. {agentx_python-0.8.20 → agentx_python-0.8.21}/README.md +0 -0
  23. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/__init__.py +0 -0
  24. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/agentx.py +0 -0
  25. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/cli.py +0 -0
  26. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/__init__.py +0 -0
  27. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/_term.py +0 -0
  28. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/__init__.py +0 -0
  29. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  30. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/precomputed.py +0 -0
  31. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/raw.py +0 -0
  32. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/reporting.py +0 -0
  33. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/results.py +0 -0
  34. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/tool_schemas.py +0 -0
  35. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/tracing.py +0 -0
  36. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/exceptions.py +0 -0
  37. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/export.py +0 -0
  38. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/feedback.py +0 -0
  39. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/__init__.py +0 -0
  40. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/_traced_call.py +0 -0
  41. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/anthropic.py +0 -0
  42. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/autogen.py +0 -0
  43. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/databricks.py +0 -0
  44. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/google_genai.py +0 -0
  45. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/litellm.py +0 -0
  46. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/llamaindex.py +0 -0
  47. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/moveworks.py +0 -0
  48. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/openai.py +0 -0
  49. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/__init__.py +0 -0
  50. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/agents.py +0 -0
  51. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/improvement_groups.py +0 -0
  52. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/judge_scorers.py +0 -0
  53. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/online_evaluators.py +0 -0
  54. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/patterns.py +0 -0
  55. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/review_queue.py +0 -0
  56. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/rules.py +0 -0
  57. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/scorer_groups.py +0 -0
  58. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/scorers.py +0 -0
  59. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/sessions.py +0 -0
  60. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/signals.py +0 -0
  61. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/outcomes.py +0 -0
  62. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/projects.py +0 -0
  63. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/py.typed +0 -0
  64. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/__init__.py +0 -0
  65. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/agent.py +0 -0
  66. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/conversation.py +0 -0
  67. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/workforce.py +0 -0
  68. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/testing.py +0 -0
  69. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/traces.py +0 -0
  70. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/__init__.py +0 -0
  71. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/ci_types.py +0 -0
  72. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/eval_scope.py +0 -0
  73. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/framework_detect.py +0 -0
  74. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/ingest_client.py +0 -0
  75. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/util.py +0 -0
  76. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/SOURCES.txt +0 -0
  77. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/dependency_links.txt +0 -0
  78. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/entry_points.txt +0 -0
  79. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/not-zip-safe +0 -0
  80. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/requires.txt +0 -0
  81. {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/top_level.txt +0 -0
  82. {agentx_python-0.8.20 → agentx_python-0.8.21}/setup.cfg +0 -0
  83. {agentx_python-0.8.20 → agentx_python-0.8.21}/setup.py +0 -0
  84. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_cli_launcher.py +0 -0
  85. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_deep_dive_fixes.py +0 -0
  86. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_docs_match_sdk.py +0 -0
  87. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_eval_scope.py +0 -0
  88. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_framework_detect.py +0 -0
  89. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_integration.py +0 -0
  90. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_integrations.py +0 -0
  91. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_judge_scorers.py +0 -0
  92. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_multi_judge.py +0 -0
  93. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_pairwise.py +0 -0
  94. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_review_queue.py +0 -0
  95. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_selfhost_compat.py +0 -0
  96. {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.20
3
+ Version: 0.8.21
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
@@ -207,6 +207,12 @@ class EvaluationsClient:
207
207
  return resp.json()
208
208
  except Exception:
209
209
  return resp.text
210
+ # A timeout keeps its type: runner._flush_batch catches requests.Timeout specifically
211
+ # (the engine may still be scoring the batch - a blind retry double-bills every judge
212
+ # call), and wrapping it in AgentXEvaluationsError here made that guard unreachable.
213
+ # Applies to retry=False calls too, where the single attempt lands straight here.
214
+ if isinstance(last_exc, requests.Timeout):
215
+ raise last_exc
210
216
  raise AgentXEvaluationsError(f"Request failed after retries: {last_exc}")
211
217
 
212
218
  # ------------------------------------------------------------------
@@ -227,7 +233,9 @@ class EvaluationsClient:
227
233
  # ------------------------------------------------------------------
228
234
 
229
235
  def create_dataset(self, payload: dict) -> Dataset:
230
- data = self._request("POST", "/datasets", json=self._with_workspace(payload))
236
+ # Server-side write: a timeout after the dataset row was created would be
237
+ # retried into a duplicate dataset, so no transport retry.
238
+ data = self._request("POST", "/datasets", json=self._with_workspace(payload), retry=False)
231
239
  return Dataset(**data)
232
240
 
233
241
  def delete_dataset(self, dataset_id: str) -> None:
@@ -255,8 +263,9 @@ class EvaluationsClient:
255
263
  # ------------------------------------------------------------------
256
264
 
257
265
  def create_evaluation_settings(self, payload: dict) -> EvaluationSettings:
266
+ # Server-side write - no transport retry (see init_run's comment).
258
267
  data = self._request(
259
- "POST", "/evaluation-settings", json=self._with_workspace(payload)
268
+ "POST", "/evaluation-settings", json=self._with_workspace(payload), retry=False
260
269
  )
261
270
  return EvaluationSettings(**data)
262
271
 
@@ -281,12 +290,14 @@ class EvaluationsClient:
281
290
 
282
291
  # ------------------------------------------------------------------
283
292
  # Prompt registry endpoints - see agentx.evaluations.prompts.PromptClient for the concept
284
- # (the external-agent analog to native autotune). Deliberately read-mostly: no publish here,
285
- # a new version only ever comes from the dashboard's human-approved propose/publish flow.
293
+ # (the external-agent analog to native autotune). propose_prompt never publishes;
294
+ # publish_prompt_version below IS the explicit approval step - call it only after a human
295
+ # reviewed the proposal.
286
296
  # ------------------------------------------------------------------
287
297
 
288
298
  def create_prompt(self, payload: dict) -> Prompt:
289
- data = self._request("POST", "/prompts", json=self._with_workspace(payload))
299
+ # Server-side write - no transport retry (see init_run's comment).
300
+ data = self._request("POST", "/prompts", json=self._with_workspace(payload), retry=False)
290
301
  return Prompt(**data)
291
302
 
292
303
  def list_prompts(self) -> List[Prompt]:
@@ -322,7 +333,9 @@ class EvaluationsClient:
322
333
  alias and keeps working. ``split`` records the named case subset this run covers.
323
334
  ``additional_scorer_ids`` (self-host): extra judge scorers that each pass their own
324
335
  verdict on every result from the same single agent execution - verdicts land in each
325
- result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``."""
336
+ result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``. When
337
+ ``scorer_group_id`` is set, the engine nulls ``additionalScorerIds`` on the run too -
338
+ the group is the whole grading story, not a layer on top of extra scorers."""
326
339
  from agentx.version import VERSION
327
340
 
328
341
  grader_id = _resolve_scorer_id(scorer_id, evaluation_settings_id)
@@ -343,7 +356,8 @@ class EvaluationsClient:
343
356
  if additional_scorer_ids:
344
357
  payload["additionalScorerIds"] = additional_scorer_ids
345
358
  # Scorer group grading (self-host): the group's weighted 0-10 aggregate fills the rating
346
- # column and member verdicts land per row. Mutually exclusive with scorer_id (group wins).
359
+ # column and member verdicts land per row. Mutually exclusive with scorer_id (group
360
+ # wins), and the engine also nulls additionalScorerIds when a group grades the run.
347
361
  if scorer_group_id:
348
362
  payload["scorerGroupId"] = scorer_group_id
349
363
  if split:
@@ -408,7 +422,10 @@ class EvaluationsClient:
408
422
  # scorer's own per-result verdicts. Unknown names are a hard 400 from the engine.
409
423
  if scorer:
410
424
  params["scorer"] = scorer
411
- return self._request("GET", f"/runs/{run_id}/gate", params=params)
425
+ # record=True is a server-side write despite the GET verb (it persists a gate-history
426
+ # row): a timeout after the row was stored would be retried into a duplicate verdict,
427
+ # so no transport retry - same precedent as init_run.
428
+ return self._request("GET", f"/runs/{run_id}/gate", params=params, retry=not record)
412
429
 
413
430
  def analyze_run(
414
431
  self,
@@ -81,7 +81,8 @@ class DatasetBuilder:
81
81
  self._payload["codeScorers"] = [
82
82
  {
83
83
  "id": scorer.get("id") or _uuid.uuid4().hex[:12],
84
- "name": scorer["name"],
84
+ # Name may be omitted - the engine defaults it, so don't KeyError here.
85
+ "name": scorer.get("name"),
85
86
  "code": scorer["code"],
86
87
  "enabled": scorer.get("enabled", True),
87
88
  }
@@ -64,7 +64,8 @@ class EvaluationSettingsBuilder:
64
64
  if rouge_score:
65
65
  self._payload["rougeScore"] = {"enabled": True}
66
66
  # Sovereignty & Portability - the models to compare when this config runs
67
- # (use client.evaluations.list_models() to discover valid ids).
67
+ # (use client.evaluations.list_models() to discover valid ids). Self-host: accepted
68
+ # on the wire but not acted on by the engine (same caveat as DatasetBuilder's).
68
69
  if sovereignty_models:
69
70
  self._payload["sovereigntyIndex"] = {
70
71
  "enabled": True,
@@ -248,6 +248,10 @@ class LiveStatistics(BaseModel):
248
248
  min_rating: Optional[float] = Field(default=None, alias="minRating")
249
249
  max_rating: Optional[float] = Field(default=None, alias="maxRating")
250
250
  rated_count: int = Field(default=0, alias="ratedCount")
251
+ # Rows the judge could not score / rows submitted with an error - the difference between
252
+ # "everything rated 8" and "half the run never got a verdict".
253
+ skipped_count: int = Field(default=0, alias="skippedCount")
254
+ failed_count: int = Field(default=0, alias="failedCount")
251
255
 
252
256
  class Config:
253
257
  populate_by_name = True
@@ -17,9 +17,9 @@ class PromptClient:
17
17
  existing version-comparison view (``client.evaluations`` run comparisons on a dataset) can
18
18
  tell you which published version actually scored higher.
19
19
 
20
- Deliberately read-mostly from here: there is no ``publish`` on this client. A prompt only
21
- gets a new version through the dashboard's human-approved propose/publish flow, so a
22
- rewritten prompt never reaches your running agent without someone explicitly approving it.
20
+ ``propose()`` never publishes anything; ``publish_version()`` IS the explicit approval
21
+ step - call it only after a human reviewed the proposal, since a published version is
22
+ what your running agent pulls as its live prompt.
23
23
 
24
24
  Example::
25
25
 
@@ -342,6 +342,10 @@ class EvaluationRunContext:
342
342
  # ------------------------------------------------------------------
343
343
 
344
344
  def finalize(self) -> "EvaluationRunContext":
345
+ """Mark the run completed server-side. A failed finalize is raised, not swallowed
346
+ (same fail-loud posture as _flush_batch): it leaves the run in_progress - a state a
347
+ CI pipeline MUST treat as a failure, since gates and baselines only consider
348
+ completed runs."""
345
349
  _say()
346
350
  with Spinner("Finalizing - submitting results"):
347
351
  try:
@@ -353,6 +357,7 @@ class EvaluationRunContext:
353
357
  except Exception as exc:
354
358
  _say(f" {red('✗')} Finalize failed: {dim(str(exc))}")
355
359
  logger.error("Finalize failed: %s", exc)
360
+ raise
356
361
  return self
357
362
 
358
363
  def gate(
@@ -212,6 +212,9 @@ class AgentXCrewObserver:
212
212
  "end_time": end,
213
213
  "input": description,
214
214
  "output": output_text,
215
+ # A CrewAI task is an agent turn, not a model call - without this,
216
+ # _merge_child_run's default stamped every task span "llm".
217
+ "kind": "agent",
215
218
  })
216
219
  if description is not None or task_output_text is not None:
217
220
  tool_calls.append({"name": name, "input": description, "output": task_output_text})
@@ -231,6 +231,7 @@ class AgentXADKPlugin(BasePlugin):
231
231
  model=call_start.get("model") if call_start else None,
232
232
  input_tokens=call_input_tokens,
233
233
  output_tokens=call_output_tokens,
234
+ span_kind="llm",
234
235
  )
235
236
 
236
237
  async def on_model_error_callback(
@@ -261,6 +262,7 @@ class AgentXADKPlugin(BasePlugin):
261
262
  output=f"ERROR: {error}",
262
263
  model=call_start.get("model") if call_start else None,
263
264
  error=str(error),
265
+ span_kind="llm",
264
266
  )
265
267
 
266
268
  # ------------------------------------------------------------------
@@ -290,7 +292,8 @@ class AgentXADKPlugin(BasePlugin):
290
292
  tool_input = _safe_serialize(tool_args)
291
293
  tool_output = str(result) if result is not None else None
292
294
  state["root_span"].child_span(
293
- tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output
295
+ tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output,
296
+ span_kind="tool",
294
297
  )
295
298
 
296
299
  async def on_tool_error_callback(
@@ -311,5 +314,6 @@ class AgentXADKPlugin(BasePlugin):
311
314
  tool_input = _safe_serialize(tool_args)
312
315
  tool_output = f"ERROR: {error}"
313
316
  state["root_span"].child_span(
314
- tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output, error=str(error)
317
+ tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output, error=str(error),
318
+ span_kind="tool",
315
319
  )
@@ -277,6 +277,10 @@ class AgentXCallbackHandler(BaseCallbackHandler):
277
277
  become child spans, and each LLM call / tool call / retrieval becomes a span parented under
278
278
  the node that ran it - so the engine's Execution Timeline shows the actual graph trajectory
279
279
  (which nodes ran, in what order, and what each did), not a flat step list.
280
+
281
+ Retriever runs are stamped ``retrieval`` - LangChain cannot distinguish memory-backed
282
+ retrievers, so a Mem0/Zep-style store exposed as a retriever classifies as retrieval too.
283
+ Use ``tracer.trace_memory`` for lookups that should classify as memory.
280
284
  """
281
285
 
282
286
  def __init__(
@@ -251,6 +251,7 @@ class AgentXTracingProcessor:
251
251
  model=call_model,
252
252
  input_tokens=call_input_tokens,
253
253
  output_tokens=call_output_tokens,
254
+ span_kind="llm",
254
255
  )
255
256
 
256
257
  elif span_type == "response":
@@ -300,6 +301,7 @@ class AgentXTracingProcessor:
300
301
  model=call_model,
301
302
  input_tokens=call_input_tokens,
302
303
  output_tokens=call_output_tokens,
304
+ span_kind="llm",
303
305
  )
304
306
 
305
307
  elif span_type == "function":
@@ -313,6 +315,7 @@ class AgentXTracingProcessor:
313
315
  duration_ms=latency if t0 is None or t1 is None else None,
314
316
  input=span_data.input,
315
317
  output=tool_output,
318
+ span_kind="tool",
316
319
  )
317
320
 
318
321
  def force_flush(self) -> None:
@@ -235,7 +235,9 @@ class MonitorClient:
235
235
  # ------------------------------------------------------------------
236
236
 
237
237
  def create_pattern(self, payload: dict) -> MonitorPattern:
238
- data = self._request("POST", "/patterns", json=self._with_workspace(payload))
238
+ # Server-side write: a timeout after the pattern row was created would be
239
+ # retried into a duplicate pattern, so no transport retry.
240
+ data = self._request("POST", "/patterns", json=self._with_workspace(payload), retry=False)
239
241
  return MonitorPattern(**data["pattern"])
240
242
 
241
243
  def list_patterns(self) -> List[MonitorPattern]:
@@ -253,7 +255,10 @@ class MonitorClient:
253
255
  # ------------------------------------------------------------------
254
256
 
255
257
  def create_online_evaluator(self, payload: dict) -> MonitorOnlineEvaluator:
256
- data = self._request("POST", "/online-evaluators", json=self._with_workspace(payload))
258
+ # Server-side write - no transport retry (see create_pattern's comment).
259
+ data = self._request(
260
+ "POST", "/online-evaluators", json=self._with_workspace(payload), retry=False
261
+ )
257
262
  return MonitorOnlineEvaluator(**data["evaluator"])
258
263
 
259
264
  @property
@@ -12,6 +12,11 @@ class MonitorPattern(BaseModel):
12
12
  A "failure" pattern (the default) raises a signal to triage; a "proper" pattern logs a
13
13
  healthy tally instead. Only one of ``include_terms``/``regex``/``semantic_prompt`` is
14
14
  meaningful at a time, selected by ``detector_kind``.
15
+
16
+ On self-host the engine stores a pattern as a list of ``conditions`` (each with its own
17
+ detector kind and match settings) - the flat ``include_terms``/``regex``/
18
+ ``semantic_prompt`` fields are display-only projections derived from the first condition;
19
+ ``conditions`` is the truth.
15
20
  """
16
21
 
17
22
  id: str = Field(alias="_id")
@@ -26,6 +31,8 @@ class MonitorPattern(BaseModel):
26
31
  exclude_terms: List[str] = Field(default_factory=list, alias="excludeTerms")
27
32
  regex: Optional[str] = None
28
33
  semantic_prompt: Optional[str] = Field(default=None, alias="semanticPrompt")
34
+ # The engine's stored detection rules (self-host) - see the class docstring.
35
+ conditions: List[Dict[str, Any]] = Field(default_factory=list)
29
36
  severity: str = "medium"
30
37
  polarity: str = "failure"
31
38
  enabled: bool = True
@@ -46,7 +46,7 @@ class MonitorProfileClient:
46
46
  """Update (and enable, if not already) this agent's Monitor profile. Only fields passed
47
47
  here are changed; everything else on the existing profile is left as is.
48
48
 
49
- Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``,
49
+ Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``, ``dataset_id``,
50
50
  and ``threshold_overrides["latencyMs"]`` are project-level defaults now (see
51
51
  ``MonitorProfile``'s docstring) - set them via the dashboard's Platform Settings screen
52
52
  instead, passing them here is accepted but has no effect. ``enabled``/
@@ -5,6 +5,7 @@ import concurrent.futures
5
5
  import functools
6
6
  import inspect
7
7
  import contextvars
8
+ import logging
8
9
  import threading
9
10
  import time
10
11
  from contextlib import contextmanager
@@ -17,6 +18,8 @@ from agentx.tracing.ci_types import CIRun, CIRunResult, CIRunStatus, CIQuestionS
17
18
  from agentx.tracing.eval_scope import EVAL_RUN_SOURCE, current_eval_run_id
18
19
  from agentx.tracing.framework_detect import detect_framework
19
20
 
21
+ logger = logging.getLogger(__name__)
22
+
20
23
  F = TypeVar("F", bound=Callable[..., Any])
21
24
 
22
25
 
@@ -313,7 +316,7 @@ class _TraceSpan:
313
316
  their own real per-step identity and timing (LangChain's run_id/parent_run_id,
314
317
  LlamaIndex's parent_id, the OpenAI Agents SDK's own span objects) and want to parent a new
315
318
  child under a specific span they're holding a reference to - not just whatever's on top of
316
- the tracer's thread-local active-span stack.
319
+ the tracer's context-local (ContextVar) active-span stack.
317
320
 
318
321
  Returns the child span (its ``.span_id`` can parent a further-nested grandchild via
319
322
  another ``child_span()`` call on it). The returned span is not pushed onto the
@@ -441,8 +444,10 @@ class _TraceSpan:
441
444
  cache_read_tokens=step.get("cacheReadTokenSize"),
442
445
  cache_write_tokens=step.get("cacheWriteTokenSize"),
443
446
  # Stated, so a step named anything other than "LLM Call N" still classifies -
444
- # the backend's name regex was the only thing holding this together.
445
- span_kind="llm",
447
+ # the backend's name regex was the only thing holding this together. Steps
448
+ # may state their own kind (crewai.py's task steps carry "agent"); the
449
+ # default stays "llm" for callers whose steps are model calls.
450
+ span_kind=step.get("kind") or "llm",
446
451
  )
447
452
  for tc in tool_calls or []:
448
453
  # Some callers' tool_calls dicts (e.g. langchain.py's, which sets these on the
@@ -477,6 +482,7 @@ class _TraceSpan:
477
482
  })
478
483
  for step in [] if not emit_steps else (retrieval_steps or []):
479
484
  self._child_span_count += 1
485
+ doc_count = step.get("doc_count")
480
486
  self.child_span(
481
487
  step.get("name") or f"Retrieval {self._child_span_count}",
482
488
  start_time=step.get("start_time"),
@@ -484,7 +490,7 @@ class _TraceSpan:
484
490
  duration_ms=step.get("duration_ms"),
485
491
  input=step.get("query"),
486
492
  output=step.get("output"),
487
- metadata={"kind": "retrieval"},
493
+ metadata={"kind": "retrieval", **({"doc_count": doc_count} if doc_count is not None else {})},
488
494
  span_kind="retrieval",
489
495
  )
490
496
 
@@ -680,9 +686,11 @@ class Tracer:
680
686
  """
681
687
  Make ``span`` (created on another thread) the active span for the
682
688
  duration of this block, on *this* thread. The active-span stack is
683
- thread-local, so work submitted to a ``ThreadPoolExecutor`` or run on
684
- any other thread doesn't automatically see a span opened on the
685
- calling thread - wrap the worker function body in this to attach it::
689
+ context-local (a ContextVar): bare threads start with an empty stack,
690
+ while asyncio tasks inherit a copy of their creator's. Work submitted
691
+ to a ``ThreadPoolExecutor`` or run on any other thread therefore
692
+ doesn't automatically see a span opened on the calling thread - wrap
693
+ the worker function body in this to attach it::
686
694
 
687
695
  with tracer.trace("orchestrator") as span:
688
696
  def worker():
@@ -719,7 +727,10 @@ class Tracer:
719
727
  loop where the tool executes in plain Python between two
720
728
  ``messages.create()`` calls. Sent as a real child-span row of the active span (see
721
729
  ``current_span``) immediately; queued onto the next trace's plain ``tool_calls`` list if
722
- there's no active span to attach a child to.
730
+ there's no active span to attach a child to. That queue is tracer-wide, not
731
+ per-context: the pending record attaches to the next trace THIS TRACER sends from ANY
732
+ thread or context, so concurrent no-span use can attach it to an unrelated trace -
733
+ wrap the call in ``tracer.trace()`` when that matters.
723
734
 
724
735
  ``success``/``error`` mark a failed call. ``success=False`` is what the engine's built-in
725
736
  "Tool failure" Monitor check and the dashboard's Tool quality column both read; leaving
@@ -824,6 +835,11 @@ class Tracer:
824
835
  (same behavior as ``record_tool_call``, covering the patched-client flow where the
825
836
  retrieval runs just before a standalone ``messages.create()`` /
826
837
  ``chat.completions.create()`` call).
838
+
839
+ The pending queue is tracer-wide, not per-context: a record queued with no active
840
+ span attaches to the next trace THIS TRACER sends from ANY thread or context, so
841
+ concurrent no-span use can attach it to an unrelated trace. Wrap the call in
842
+ ``tracer.trace()`` when that matters.
827
843
  """
828
844
  active_span = self.current_span
829
845
  if active_span is None:
@@ -839,6 +855,7 @@ class Tracer:
839
855
  "query": _safe_serialize(query) if query is not None else None,
840
856
  "output": _safe_serialize(output) if output is not None else None,
841
857
  "duration_ms": latency_ms,
858
+ **({"doc_count": doc_count} if doc_count is not None else {}),
842
859
  })
843
860
  return
844
861
  # The kind marker is what tells the engine (retrieval-context extraction for RAG
@@ -852,7 +869,7 @@ class Tracer:
852
869
  duration_ms=duration_ms,
853
870
  input=query,
854
871
  output=output,
855
- metadata={"kind": "retrieval"},
872
+ metadata={"kind": "retrieval", **({"doc_count": doc_count} if doc_count is not None else {})},
856
873
  span_kind="retrieval",
857
874
  )
858
875
 
@@ -874,27 +891,23 @@ class Tracer:
874
891
  itself stays one value so dashboards and scorers can select all memory activity at once.
875
892
  Deliberately NOT a retrieval: retrieval spans feed the RAG judges' ``{context}``
876
893
  (knowledge grounding), while memory is recalled state - see the engine's spanKind.ts.
894
+
895
+ With no active span the record is DROPPED (with a debug log), not queued: the only
896
+ pending queue rides the next trace's ``retrieval_steps``, and memory content must
897
+ never reach the engine's retrieval-context extraction for RAG judges. Wrap the call
898
+ in ``tracer.trace()`` to keep it. (``record_tool_call``/``record_retrieval`` queue
899
+ instead - see their docstrings.)
877
900
  """
878
901
  active_span = self.current_span
879
902
  if active_span is None:
880
- # Same posture as record_retrieval: queue and merge into the next trace this
881
- # tracer sends (the patched-client flow where the memory op runs just before a
882
- # standalone completions call) instead of silently dropping the record.
883
- latency_ms = (
884
- int(duration_ms)
885
- if duration_ms is not None
886
- else int((end_time - start_time) * 1000)
887
- if start_time is not None and end_time is not None
888
- else None
903
+ # NOT the record_retrieval queue posture: _pending_retrievals rides the next
904
+ # trace's performance_summary.retrieval_steps, which the engine's
905
+ # retrieval-context extraction feeds to RAG judges - recalled memory must never
906
+ # classify as knowledge grounding. Drop, and say so.
907
+ logger.debug(
908
+ "record_memory(%r) called with no active span - wrap the call in tracer.trace(); dropped",
909
+ name,
889
910
  )
890
- self._pending_retrievals.append({
891
- "name": name,
892
- "query": _safe_serialize(query) if query is not None else None,
893
- "output": _safe_serialize(output) if output is not None else None,
894
- "duration_ms": latency_ms,
895
- "kind": "memory",
896
- **({"operation": operation} if operation else {}),
897
- })
898
911
  return
899
912
  active_span.child_span(
900
913
  name,
@@ -923,9 +936,10 @@ class Tracer:
923
936
  error: Optional[str] = None
924
937
  try:
925
938
  yield recorder
926
- except Exception as exc:
939
+ except BaseException as exc:
927
940
  # A memory op that raised must not be recorded as a clean span (trace_tool_call
928
- # precedent) - fold the error into the output and re-raise.
941
+ # precedent, which also catches BaseException) - fold the error into the output
942
+ # and re-raise.
929
943
  error = str(exc)
930
944
  raise
931
945
  finally:
@@ -1,7 +1,7 @@
1
- VERSION = "0.8.20"
1
+ VERSION = "0.8.21"
2
2
 
3
3
  # The AgentX-trace-eval release this SDK version is tested against - what `agentx-trace-eval`
4
4
  # installs and converges to (see agentx/cli.py). Bump together with VERSION when releasing, so
5
5
  # every published SDK names a known-good engine+dashboard pair. Users can override with
6
6
  # AGENTX_TRACE_EVAL_VERSION=<tag|latest>.
7
- ENGINE_VERSION = "v0.3.21"
7
+ ENGINE_VERSION = "v0.3.22"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.20
3
+ Version: 0.8.21
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
@@ -144,3 +144,19 @@ def test_flush_batch_failure_raises_after_one_retry(monkeypatch):
144
144
 
145
145
  with pytest.raises(EvaluationSubmissionError):
146
146
  ctx.execute(lambda case: "x")
147
+
148
+
149
+ def test_finalize_failure_raises(monkeypatch):
150
+ """A failed finalize leaves the run in_progress - swallowing it let CI pipelines pass on
151
+ a run that gates and baselines would never see (same fail-loud posture as _flush_batch)."""
152
+ monkeypatch.setenv("AGENTX_EVAL_QUIET", "1")
153
+ client = FakeClient()
154
+
155
+ def failing_finalize(run_id):
156
+ raise RuntimeError("engine down")
157
+
158
+ client.finalize_run = failing_finalize
159
+ ctx = make_context(client)
160
+
161
+ with pytest.raises(RuntimeError, match="engine down"):
162
+ ctx.finalize()
@@ -198,13 +198,37 @@ def test_the_synchronous_fallback_analyze_is_never_retried():
198
198
 
199
199
  session.request = always_times_out
200
200
 
201
- with pytest.raises(AgentXEvaluationsError):
201
+ # The timeout keeps its type (requests.Timeout, not a generic wrapper) so callers can
202
+ # branch on "the engine may still be doing the billable work" - see _request.
203
+ with pytest.raises(requests.Timeout):
202
204
  client.analyze_run(RUN)
203
205
 
204
206
  dashboard_posts = [u for u in session.urls("POST") if "/evaluate/analyze/" in u]
205
207
  assert len(dashboard_posts) == 1, f"retried a billable request: {dashboard_posts}"
206
208
 
207
209
 
210
+ def test_append_results_timeout_propagates_as_timeout_with_one_post():
211
+ """A read timeout on the batch-scoring POST must surface as requests.Timeout (the type
212
+ runner._flush_batch's double-billing guard catches - wrapping it in
213
+ AgentXEvaluationsError made that guard dead code) and must not be re-POSTed by the
214
+ transport while the engine may still be scoring the first submission."""
215
+ import requests
216
+
217
+ client, session = make_client({})
218
+
219
+ def always_times_out(method, url, **kwargs):
220
+ session.calls.append((method, url, kwargs))
221
+ raise requests.exceptions.ReadTimeout("still scoring")
222
+
223
+ session.request = always_times_out
224
+
225
+ with pytest.raises(requests.Timeout):
226
+ client.append_results(RUN, "batch-1", [])
227
+
228
+ result_posts = [u for u in session.urls("POST") if u.endswith(f"/runs/{RUN}/results")]
229
+ assert len(result_posts) == 1, f"re-POSTed a batch mid-scoring: {result_posts}"
230
+
231
+
208
232
  def test_the_fallback_request_gets_the_long_analysis_timeout():
209
233
  client, session = make_client(
210
234
  {("POST", f"{API_ROOT}/evaluate/analyze/{RUN}"): FakeResponse(200, {"status": "completed"})}
@@ -666,17 +666,64 @@ def test_trace_retrieval_emits_real_child_span():
666
666
  assert child["output"] == "3 matching docs"
667
667
 
668
668
 
669
- def test_record_retrieval_with_no_active_span_is_a_no_op():
670
- """No enclosing `with tracer.trace()` and nothing else to attach a child span to — retrieval
671
- data has no standalone wire representation anymore (that was performance_summary-only),
672
- so this is a documented no-op rather than a silent fabrication."""
669
+ def test_record_retrieval_with_no_active_span_queues_onto_next_trace():
670
+ """No enclosing `with tracer.trace()` - record_retrieval queues onto the tracer-level
671
+ pending list and rides the next trace's performance_summary.retrieval_steps (the shape
672
+ the engine's retrieval-context extraction reads), carrying doc_count along."""
673
673
  tracer = make_tracer()
674
- tracer.record_retrieval("orphan_search", query="x", output="y")
674
+ tracer.record_retrieval("orphan_search", query="x", output="y", doc_count=4)
675
675
  with tracer.trace("agent"):
676
676
  pass
677
677
  wires = enqueued_wires(tracer)
678
678
  assert len(wires) == 1
679
679
  assert wires[0]["name"] == "agent"
680
+ steps = wires[0]["performance_summary"]["retrieval_steps"]
681
+ assert [s["name"] for s in steps] == ["orphan_search"]
682
+ assert steps[0]["doc_count"] == 4
683
+
684
+
685
+ def test_record_retrieval_carries_doc_count_into_child_span_metadata():
686
+ tracer = make_tracer()
687
+ with tracer.trace("agent"):
688
+ tracer.record_retrieval("kb_search", query="q", output="docs", doc_count=3)
689
+ wires = enqueued_wires(tracer)
690
+ child = wires[0]
691
+ assert child["name"] == "kb_search"
692
+ assert child["span_kind"] == "retrieval"
693
+ assert child["metadata"] == {"kind": "retrieval", "doc_count": 3}
694
+
695
+
696
+ def test_record_memory_with_no_active_span_drops_instead_of_queueing():
697
+ """Memory must never ride the pending retrieval queue: it lands in
698
+ performance_summary.retrieval_steps, which feeds the engine's retrieval-context
699
+ extraction for RAG judges - recalled state is not knowledge grounding. With no active
700
+ span the record is dropped (debug-logged), not attached to the next trace."""
701
+ tracer = make_tracer()
702
+ tracer.record_memory("orphan prefs", operation="read", query="u-1", output="secret")
703
+ with tracer.trace("agent"):
704
+ pass
705
+ wires = enqueued_wires(tracer)
706
+ assert len(wires) == 1
707
+ assert wires[0]["name"] == "agent"
708
+ assert_no_performance_summary(wires)
709
+
710
+
711
+ def test_merge_child_run_execution_steps_honor_their_stated_kind():
712
+ """crewai.py stamps its task steps "kind": "agent" - the merge loop must forward that
713
+ instead of unconditionally stamping every step "llm" (autogen/llamaindex steps carry no
714
+ kind and keep classifying as llm)."""
715
+ tracer = make_tracer()
716
+ with tracer.trace("crew") as span:
717
+ span._merge_child_run(
718
+ execution_steps=[
719
+ {"name": "Research task", "duration_ms": 5, "kind": "agent"},
720
+ {"name": "LLM Call 1", "duration_ms": 5},
721
+ ],
722
+ framework="crewai",
723
+ )
724
+ wires = enqueued_wires(tracer)
725
+ kinds = {w["name"]: w.get("span_kind") for w in wires if w["name"] != "crew"}
726
+ assert kinds == {"Research task": "agent", "LLM Call 1": "llm"}
680
727
 
681
728
 
682
729
  def test_record_tool_call_with_no_active_span_still_queues():
File without changes
File without changes
File without changes
File without changes