agentx-python 0.6.14__tar.gz → 0.6.15__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 (66) hide show
  1. {agentx_python-0.6.14/agentx_python.egg-info → agentx_python-0.6.15}/PKG-INFO +7 -2
  2. {agentx_python-0.6.14 → agentx_python-0.6.15}/README.md +6 -1
  3. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/_traced_call.py +8 -0
  4. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/anthropic.py +25 -10
  5. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/google_genai.py +12 -0
  6. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/litellm.py +11 -4
  7. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/openai.py +15 -6
  8. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/models.py +7 -0
  9. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/profile.py +8 -2
  10. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/tracing/tracer.py +63 -0
  11. agentx_python-0.6.15/agentx/version.py +1 -0
  12. {agentx_python-0.6.14 → agentx_python-0.6.15/agentx_python.egg-info}/PKG-INFO +7 -2
  13. agentx_python-0.6.14/agentx/version.py +0 -1
  14. {agentx_python-0.6.14 → agentx_python-0.6.15}/LICENSE +0 -0
  15. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/__init__.py +0 -0
  16. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/agentx.py +0 -0
  17. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/cli.py +0 -0
  18. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/__init__.py +0 -0
  19. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/_term.py +0 -0
  20. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/adapters/__init__.py +0 -0
  21. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  22. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/adapters/precomputed.py +0 -0
  23. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/adapters/raw.py +0 -0
  24. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/client.py +0 -0
  25. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/datasets.py +0 -0
  26. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/evaluation_settings.py +0 -0
  27. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/models.py +0 -0
  28. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/prompts.py +0 -0
  29. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/redaction.py +0 -0
  30. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/reporting.py +0 -0
  31. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/results.py +0 -0
  32. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/runner.py +0 -0
  33. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/evaluations/tracing.py +0 -0
  34. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/exceptions.py +0 -0
  35. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/__init__.py +0 -0
  36. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/autogen.py +0 -0
  37. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/crewai.py +0 -0
  38. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/google_adk.py +0 -0
  39. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/langchain.py +0 -0
  40. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/llamaindex.py +0 -0
  41. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/integrations/openai_agents.py +0 -0
  42. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/__init__.py +0 -0
  43. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/client.py +0 -0
  44. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/online_evaluators.py +0 -0
  45. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/patterns.py +0 -0
  46. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/monitor/signals.py +0 -0
  47. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/py.typed +0 -0
  48. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/resources/__init__.py +0 -0
  49. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/resources/agent.py +0 -0
  50. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/resources/conversation.py +0 -0
  51. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/resources/workforce.py +0 -0
  52. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/tracing/__init__.py +0 -0
  53. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/tracing/ci_types.py +0 -0
  54. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/tracing/ingest_client.py +0 -0
  55. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx/util.py +0 -0
  56. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx_python.egg-info/SOURCES.txt +0 -0
  57. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx_python.egg-info/dependency_links.txt +0 -0
  58. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx_python.egg-info/entry_points.txt +0 -0
  59. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx_python.egg-info/not-zip-safe +0 -0
  60. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx_python.egg-info/requires.txt +0 -0
  61. {agentx_python-0.6.14 → agentx_python-0.6.15}/agentx_python.egg-info/top_level.txt +0 -0
  62. {agentx_python-0.6.14 → agentx_python-0.6.15}/setup.cfg +0 -0
  63. {agentx_python-0.6.14 → agentx_python-0.6.15}/setup.py +0 -0
  64. {agentx_python-0.6.14 → agentx_python-0.6.15}/tests/test_integration.py +0 -0
  65. {agentx_python-0.6.14 → agentx_python-0.6.15}/tests/test_integrations.py +0 -0
  66. {agentx_python-0.6.14 → agentx_python-0.6.15}/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.14
3
+ Version: 0.6.15
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
@@ -228,7 +228,12 @@ with tracer.trace("rag-agent", framework="langchain") as span:
228
228
 
229
229
  ### Framework integrations
230
230
 
231
- Each integration auto-captures LLM calls, tool calls, and token usage — install the matching extra:
231
+ Each integration auto-captures LLM calls, tool calls, and token usage — including prompt-caching
232
+ token counts (Anthropic's cache write/read, OpenAI/LiteLLM's cached tokens, Google GenAI's cached
233
+ content), reported as their own `cache_read_tokens`/`cache_write_tokens` fields alongside the
234
+ regular totals, no extra config needed. Self-host's cost estimate prices these separately from a
235
+ regular input token when you've set optional cache rates on that model. Install the matching
236
+ extra:
232
237
 
233
238
  | Framework | Install | Integration |
234
239
  | --------------------- | -------------------------------------------- | ------------------------ |
@@ -166,7 +166,12 @@ with tracer.trace("rag-agent", framework="langchain") as span:
166
166
 
167
167
  ### Framework integrations
168
168
 
169
- Each integration auto-captures LLM calls, tool calls, and token usage — install the matching extra:
169
+ Each integration auto-captures LLM calls, tool calls, and token usage — including prompt-caching
170
+ token counts (Anthropic's cache write/read, OpenAI/LiteLLM's cached tokens, Google GenAI's cached
171
+ content), reported as their own `cache_read_tokens`/`cache_write_tokens` fields alongside the
172
+ regular totals, no extra config needed. Self-host's cost estimate prices these separately from a
173
+ regular input token when you've set optional cache rates on that model. Install the matching
174
+ extra:
170
175
 
171
176
  | Framework | Install | Integration |
172
177
  | --------------------- | -------------------------------------------- | ------------------------ |
@@ -77,6 +77,8 @@ def finish_llm_call(
77
77
  input_tokens: Optional[int],
78
78
  output_tokens: Optional[int],
79
79
  error: Optional[str],
80
+ cache_read_tokens: Optional[int] = None,
81
+ cache_write_tokens: Optional[int] = None,
80
82
  ) -> None:
81
83
  """
82
84
  Close out one raw-client LLM call — shared by the ``on_finish``/exit
@@ -107,6 +109,8 @@ def finish_llm_call(
107
109
  model=model,
108
110
  input_tokens=input_tokens,
109
111
  output_tokens=output_tokens,
112
+ cache_read_tokens=cache_read_tokens,
113
+ cache_write_tokens=cache_write_tokens,
110
114
  )
111
115
  return
112
116
 
@@ -121,4 +125,8 @@ def finish_llm_call(
121
125
  span._input_tokens = input_tokens
122
126
  if output_tokens:
123
127
  span._output_tokens = output_tokens
128
+ if cache_read_tokens:
129
+ span._cache_read_tokens = cache_read_tokens
130
+ if cache_write_tokens:
131
+ span._cache_write_tokens = cache_write_tokens
124
132
  span.__exit__(None, None, None)
@@ -70,23 +70,26 @@ def _prepend_system(messages: Any, system: Any) -> Any:
70
70
  return [system_entry, messages]
71
71
 
72
72
 
73
- def _extract_usage_tokens(usage: Any) -> Tuple[Optional[int], Optional[int]]:
73
+ def _extract_usage_tokens(
74
+ usage: Any,
75
+ ) -> Tuple[Optional[int], Optional[int], Optional[int], Optional[int]]:
74
76
  """
75
- Pull input/output token counts off a ``response.usage`` object, folding
76
- prompt-caching tokens (``cache_creation_input_tokens`` /
77
- ``cache_read_input_tokens``) into the input total they're still real
78
- input tokens for cost/context-window purposes, and the backend has no
79
- separate column for them today.
77
+ Pull input/output/cache token counts off a ``response.usage`` object.
78
+ ``input_tokens`` stays the *total* (base + cache_creation + cache_read) —
79
+ still real input tokens for cost/context-window purposes — while
80
+ ``cache_read``/``cache_write`` are reported alongside as the subset of
81
+ that total the provider actually billed at a different (cache) rate, so
82
+ the backend can price them separately instead of at the full input rate.
80
83
  """
81
84
  if usage is None:
82
- return None, None
85
+ return None, None, None, None
83
86
  input_tokens = getattr(usage, "input_tokens", None)
84
87
  output_tokens = getattr(usage, "output_tokens", None)
85
88
  cache_creation = getattr(usage, "cache_creation_input_tokens", None)
86
89
  cache_read = getattr(usage, "cache_read_input_tokens", None)
87
90
  if cache_creation or cache_read:
88
91
  input_tokens = (input_tokens or 0) + (cache_creation or 0) + (cache_read or 0)
89
- return input_tokens, output_tokens
92
+ return input_tokens, output_tokens, cache_read, cache_creation
90
93
 
91
94
 
92
95
  def patch_anthropic_client(
@@ -141,10 +144,14 @@ def _patch_create(
141
144
  output = None
142
145
  input_tokens = None
143
146
  output_tokens = None
147
+ cache_read_tokens = None
148
+ cache_write_tokens = None
144
149
  if response is not None:
145
150
  output = _extract_output_text(response)
146
151
  try:
147
- input_tokens, output_tokens = _extract_usage_tokens(getattr(response, "usage", None))
152
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens = _extract_usage_tokens(
153
+ getattr(response, "usage", None)
154
+ )
148
155
  except Exception:
149
156
  pass
150
157
 
@@ -161,6 +168,8 @@ def _patch_create(
161
168
  model=model,
162
169
  input_tokens=input_tokens,
163
170
  output_tokens=output_tokens,
171
+ cache_read_tokens=cache_read_tokens,
172
+ cache_write_tokens=cache_write_tokens,
164
173
  error=error,
165
174
  )
166
175
 
@@ -195,10 +204,14 @@ def _patch_stream(
195
204
  output = None
196
205
  input_tokens = None
197
206
  output_tokens = None
207
+ cache_read_tokens = None
208
+ cache_write_tokens = None
198
209
  if final_message is not None:
199
210
  output = _extract_output_text(final_message)
200
211
  try:
201
- input_tokens, output_tokens = _extract_usage_tokens(getattr(final_message, "usage", None))
212
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens = _extract_usage_tokens(
213
+ getattr(final_message, "usage", None)
214
+ )
202
215
  except Exception:
203
216
  pass
204
217
  finish_llm_call(
@@ -214,6 +227,8 @@ def _patch_stream(
214
227
  model=model,
215
228
  input_tokens=input_tokens,
216
229
  output_tokens=output_tokens,
230
+ cache_read_tokens=cache_read_tokens,
231
+ cache_write_tokens=cache_write_tokens,
217
232
  error=error,
218
233
  )
219
234
 
@@ -112,11 +112,16 @@ def _patch_generate_content(
112
112
  output = _extract_response_text(response) if response is not None else None
113
113
  input_tokens = None
114
114
  output_tokens = None
115
+ cache_read_tokens = None
115
116
  if response is not None:
116
117
  usage = getattr(response, "usage_metadata", None)
117
118
  if usage is not None:
118
119
  input_tokens = getattr(usage, "prompt_token_count", None)
119
120
  output_tokens = getattr(usage, "candidates_token_count", None)
121
+ # prompt_token_count already includes this — a discount breakdown, same
122
+ # "total unchanged, cache portion reported alongside" posture as OpenAI's
123
+ # prompt_tokens_details.cached_tokens.
124
+ cache_read_tokens = getattr(usage, "cached_content_token_count", None)
120
125
  finish_llm_call(
121
126
  tracer,
122
127
  name=name,
@@ -130,6 +135,7 @@ def _patch_generate_content(
130
135
  model=str(model) if model else None,
131
136
  input_tokens=input_tokens,
132
137
  output_tokens=output_tokens,
138
+ cache_read_tokens=cache_read_tokens,
133
139
  error=error,
134
140
  )
135
141
 
@@ -195,9 +201,11 @@ def _patch_sync_generate_content_stream(
195
201
  end_t = time.time()
196
202
  input_tokens = None
197
203
  output_tokens = None
204
+ cache_read_tokens = None
198
205
  if last_usage_metadata is not None:
199
206
  input_tokens = getattr(last_usage_metadata, "prompt_token_count", None)
200
207
  output_tokens = getattr(last_usage_metadata, "candidates_token_count", None)
208
+ cache_read_tokens = getattr(last_usage_metadata, "cached_content_token_count", None)
201
209
  output_repr = "".join(accumulated_text) or None
202
210
  finish_llm_call(
203
211
  tracer,
@@ -212,6 +220,7 @@ def _patch_sync_generate_content_stream(
212
220
  model=model,
213
221
  input_tokens=input_tokens,
214
222
  output_tokens=output_tokens,
223
+ cache_read_tokens=cache_read_tokens,
215
224
  error=error,
216
225
  )
217
226
 
@@ -260,9 +269,11 @@ def _patch_async_generate_content_stream(
260
269
  end_t = time.time()
261
270
  input_tokens = None
262
271
  output_tokens = None
272
+ cache_read_tokens = None
263
273
  if last_usage_metadata is not None:
264
274
  input_tokens = getattr(last_usage_metadata, "prompt_token_count", None)
265
275
  output_tokens = getattr(last_usage_metadata, "candidates_token_count", None)
276
+ cache_read_tokens = getattr(last_usage_metadata, "cached_content_token_count", None)
266
277
  output_repr = "".join(accumulated_text) or None
267
278
  finish_llm_call(
268
279
  tracer,
@@ -277,6 +288,7 @@ def _patch_async_generate_content_stream(
277
288
  model=model,
278
289
  input_tokens=input_tokens,
279
290
  output_tokens=output_tokens,
291
+ cache_read_tokens=cache_read_tokens,
280
292
  error=error,
281
293
  )
282
294
 
@@ -56,11 +56,16 @@ def _extract_output_text(response: Any) -> Optional[str]:
56
56
  return None
57
57
 
58
58
 
59
- def _extract_usage_tokens(response: Any) -> Tuple[Optional[int], Optional[int]]:
59
+ def _extract_usage_tokens(response: Any) -> Tuple[Optional[int], Optional[int], Optional[int]]:
60
+ """Same posture as agentx.integrations.openai's own _extract_usage_tokens — LiteLLM normalizes
61
+ every provider's response to an OpenAI-shaped ModelResponse, so prompt_tokens_details.cached_tokens
62
+ is the right field here too regardless of which underlying provider actually served the call."""
60
63
  usage = getattr(response, "usage", None)
61
64
  if usage is None:
62
- return None, None
63
- return getattr(usage, "prompt_tokens", None), getattr(usage, "completion_tokens", None)
65
+ return None, None, None
66
+ details = getattr(usage, "prompt_tokens_details", None)
67
+ cached_tokens = getattr(details, "cached_tokens", None) if details is not None else None
68
+ return getattr(usage, "prompt_tokens", None), getattr(usage, "completion_tokens", None), cached_tokens
64
69
 
65
70
 
66
71
  class AgentXLiteLLMLogger(CustomLogger):
@@ -96,9 +101,10 @@ class AgentXLiteLLMLogger(CustomLogger):
96
101
  output = None
97
102
  input_tokens = None
98
103
  output_tokens = None
104
+ cache_read_tokens = None
99
105
  if error is None and response_obj is not None:
100
106
  output = _extract_output_text(response_obj)
101
- input_tokens, output_tokens = _extract_usage_tokens(response_obj)
107
+ input_tokens, output_tokens, cache_read_tokens = _extract_usage_tokens(response_obj)
102
108
 
103
109
  finish_llm_call(
104
110
  self._tracer,
@@ -113,6 +119,7 @@ class AgentXLiteLLMLogger(CustomLogger):
113
119
  model=model,
114
120
  input_tokens=input_tokens,
115
121
  output_tokens=output_tokens,
122
+ cache_read_tokens=cache_read_tokens,
116
123
  error=error,
117
124
  )
118
125
 
@@ -58,16 +58,21 @@ def _extract_output_text(response: Any) -> Optional[str]:
58
58
  return None
59
59
 
60
60
 
61
- def _extract_usage_tokens(usage: Any) -> Tuple[Optional[int], Optional[int]]:
61
+ def _extract_usage_tokens(usage: Any) -> Tuple[Optional[int], Optional[int], Optional[int]]:
62
62
  """
63
- Pull input/output token counts off a ``response.usage`` object.
63
+ Pull input/output/cached token counts off a ``response.usage`` object.
64
64
  ``prompt_tokens`` already includes cached tokens (``prompt_tokens_details
65
65
  .cached_tokens`` is a discount breakdown, not an addition), so — unlike
66
- Anthropic's cache accounting — no extra folding is needed here.
66
+ Anthropic's cache accounting — no extra folding is needed for the input
67
+ total; ``cached_tokens`` is reported alongside it so the backend can
68
+ price that subset at its own (cheaper) cache rate instead of the full
69
+ input rate. OpenAI has no cache-*write* concept to report.
67
70
  """
68
71
  if usage is None:
69
- return None, None
70
- return getattr(usage, "prompt_tokens", None), getattr(usage, "completion_tokens", None)
72
+ return None, None, None
73
+ details = getattr(usage, "prompt_tokens_details", None)
74
+ cached_tokens = getattr(details, "cached_tokens", None) if details is not None else None
75
+ return getattr(usage, "prompt_tokens", None), getattr(usage, "completion_tokens", None), cached_tokens
71
76
 
72
77
 
73
78
  def patch_openai_client(
@@ -129,10 +134,13 @@ def _patch_chat_completions_create(
129
134
  output = None
130
135
  input_tokens = None
131
136
  output_tokens = None
137
+ cache_read_tokens = None
132
138
  if response is not None:
133
139
  output = _extract_output_text(response)
134
140
  try:
135
- input_tokens, output_tokens = _extract_usage_tokens(getattr(response, "usage", None))
141
+ input_tokens, output_tokens, cache_read_tokens = _extract_usage_tokens(
142
+ getattr(response, "usage", None)
143
+ )
136
144
  except Exception:
137
145
  pass
138
146
 
@@ -149,6 +157,7 @@ def _patch_chat_completions_create(
149
157
  model=model,
150
158
  input_tokens=input_tokens,
151
159
  output_tokens=output_tokens,
160
+ cache_read_tokens=cache_read_tokens,
152
161
  error=error,
153
162
  )
154
163
 
@@ -117,6 +117,13 @@ class MonitorProfile(BaseModel):
117
117
  approval policy gating autotune actions. See ``client.monitor.profile.get()/update()``.
118
118
  ``None`` from ``get()`` means this agent has never been configured and is running on
119
119
  platform defaults (e.g. the built-in latency threshold defaults to 20000ms).
120
+
121
+ Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``/``redaction_mode``, and
122
+ ``threshold_overrides["latencyMs"]`` are project-level defaults now (set once for every agent
123
+ via the dashboard's Platform Settings screen), not real per-agent settings — this model and
124
+ ``update()`` still accept/return them for wire compatibility, but a self-host engine no longer
125
+ reads the stored per-agent values for any behavior. ``enabled``/``failure_detection_enabled``/
126
+ ``info_detection_enabled``/``channels`` remain real per-agent settings on self-host too.
120
127
  """
121
128
 
122
129
  id: Optional[str] = Field(default=None, alias="_id")
@@ -47,9 +47,15 @@ class MonitorProfileClient:
47
47
  """Update (and enable, if not already) this agent's Monitor profile. Only fields passed
48
48
  here are changed; everything else on the existing profile is left as is.
49
49
 
50
- Example, overriding the built-in "Latency regression" pattern's threshold for one agent::
50
+ Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``/``redaction_mode``,
51
+ and ``threshold_overrides["latencyMs"]`` are project-level defaults now (see
52
+ ``MonitorProfile``'s docstring) — set them via the dashboard's Platform Settings screen
53
+ instead, passing them here is accepted but has no effect. ``enabled``/
54
+ ``failure_detection_enabled``/``info_detection_enabled``/``channels`` still work per-agent.
51
55
 
52
- client.monitor.profile.update("agent_123", threshold_overrides={"latencyMs": 15000})
56
+ Example (hosted, or self-host's still-per-agent fields)::
57
+
58
+ client.monitor.profile.update("agent_123", info_detection_enabled=False)
53
59
  """
54
60
  payload: Dict[str, Any] = {
55
61
  "enabled": enabled,
@@ -70,6 +70,7 @@ class _TraceSpan:
70
70
  sync: bool = False,
71
71
  monitor: bool = False,
72
72
  pattern_ids: Optional[List[str]] = None,
73
+ agent_id: Optional[str] = None,
73
74
  ) -> None:
74
75
  self._tracer = tracer
75
76
  self.name = name
@@ -79,6 +80,10 @@ class _TraceSpan:
79
80
  self._framework = framework
80
81
  self._model = model
81
82
  self._session_id = session_id
83
+ # Disambiguator for when `name` alone isn't enough — pass an already-known agent id (e.g.
84
+ # from a prior GET /agents lookup) to pin this trace to that exact agent. None (the
85
+ # default) resolves from `name` alone server-side, one stable agent per distinct name.
86
+ self._agent_id = agent_id
82
87
  # When True, __exit__ sends synchronously (blocking) instead of enqueueing, so trace_id
83
88
  # is populated by the time the `with` block exits — see Tracer.trace()'s sync param.
84
89
  self._sync = sync
@@ -113,6 +118,10 @@ class _TraceSpan:
113
118
  self._captured_framework: Optional[str] = None
114
119
  self._input_tokens: int = 0
115
120
  self._output_tokens: int = 0
121
+ # Subsets of _input_tokens (not additional tokens) — a prompt-caching write/read, when the
122
+ # provider reports one. See _record_llm_call/child_span for where these get populated.
123
+ self._cache_read_tokens: int = 0
124
+ self._cache_write_tokens: int = 0
116
125
  # Guards _merge_child_run — with Tracer.use_span(), multiple threads
117
126
  # (e.g. a ThreadPoolExecutor) can merge into this span concurrently.
118
127
  self._merge_lock = threading.Lock()
@@ -146,6 +155,7 @@ class _TraceSpan:
146
155
  monitor=self._monitor or None,
147
156
  pattern_ids=self._pattern_ids,
148
157
  name=self.name,
158
+ agent_id=self._agent_id,
149
159
  input=_safe_serialize(self.input) if self.input is not None else None,
150
160
  output=_safe_serialize(self.output) if self.output is not None else None,
151
161
  latency_ms=latency_ms,
@@ -157,6 +167,8 @@ class _TraceSpan:
157
167
  session_id=self._session_id,
158
168
  input_tokens=self._input_tokens or None,
159
169
  output_tokens=self._output_tokens or None,
170
+ cache_read_tokens=self._cache_read_tokens or None,
171
+ cache_write_tokens=self._cache_write_tokens or None,
160
172
  span_id=self._span_id,
161
173
  parent_span_id=self._parent_span_id,
162
174
  started_at_unix_nano=str(int(self._start * 1_000_000_000)) if self._start else None,
@@ -193,6 +205,8 @@ class _TraceSpan:
193
205
  model: Optional[str] = None,
194
206
  input_tokens: Optional[int] = None,
195
207
  output_tokens: Optional[int] = None,
208
+ cache_read_tokens: Optional[int] = None,
209
+ cache_write_tokens: Optional[int] = None,
196
210
  ) -> None:
197
211
  """Record one LLM-call child span (e.g. one patched Anthropic call) under this span —
198
212
  name left unset so _merge_child_run auto-numbers it "LLM Call N"."""
@@ -206,12 +220,16 @@ class _TraceSpan:
206
220
  "output": output,
207
221
  "inputTokenSize": input_tokens,
208
222
  "outputTokenSize": output_tokens,
223
+ "cacheReadTokenSize": cache_read_tokens,
224
+ "cacheWriteTokenSize": cache_write_tokens,
209
225
  }],
210
226
  input=input,
211
227
  output=output,
212
228
  model=model,
213
229
  input_tokens=input_tokens,
214
230
  output_tokens=output_tokens,
231
+ cache_read_tokens=cache_read_tokens,
232
+ cache_write_tokens=cache_write_tokens,
215
233
  )
216
234
 
217
235
  def child_span(
@@ -227,6 +245,8 @@ class _TraceSpan:
227
245
  framework: Optional[str] = None,
228
246
  input_tokens: Optional[int] = None,
229
247
  output_tokens: Optional[int] = None,
248
+ cache_read_tokens: Optional[int] = None,
249
+ cache_write_tokens: Optional[int] = None,
230
250
  error: Optional[str] = None,
231
251
  tool_calls: Optional[List[Dict[str, Any]]] = None,
232
252
  ) -> "_TraceSpan":
@@ -294,6 +314,10 @@ class _TraceSpan:
294
314
  wire["input_tokens"] = input_tokens
295
315
  if output_tokens:
296
316
  wire["output_tokens"] = output_tokens
317
+ if cache_read_tokens:
318
+ wire["cache_read_tokens"] = cache_read_tokens
319
+ if cache_write_tokens:
320
+ wire["cache_write_tokens"] = cache_write_tokens
297
321
  child._trace_id = self._tracer._dispatch(wire)
298
322
  return child
299
323
 
@@ -309,6 +333,8 @@ class _TraceSpan:
309
333
  framework: Optional[str] = None,
310
334
  input_tokens: Optional[int] = None,
311
335
  output_tokens: Optional[int] = None,
336
+ cache_read_tokens: Optional[int] = None,
337
+ cache_write_tokens: Optional[int] = None,
312
338
  ) -> None:
313
339
  """
314
340
  Explode a whole auto-instrumented sub-run (e.g. one top-level LangChain
@@ -333,6 +359,8 @@ class _TraceSpan:
333
359
  model=step.get("model"),
334
360
  input_tokens=step.get("inputTokenSize"),
335
361
  output_tokens=step.get("outputTokenSize"),
362
+ cache_read_tokens=step.get("cacheReadTokenSize"),
363
+ cache_write_tokens=step.get("cacheWriteTokenSize"),
336
364
  )
337
365
  for tc in tool_calls or []:
338
366
  # Some callers' tool_calls dicts (e.g. langchain.py's, which sets these on the
@@ -348,6 +376,20 @@ class _TraceSpan:
348
376
  input=tc.get("input"),
349
377
  output=tc.get("output"),
350
378
  )
379
+ # Also mirror onto this span's own flat tool_calls list, sent in this span's own
380
+ # wire payload on __exit__ (see tool_calls=self.tool_calls or None below). The
381
+ # child span above is only for the trace detail view's span tree; the engine's
382
+ # built-in "Tool failure" Monitor check reads *this* flat list specifically,
383
+ # looking for a `success: false` entry — a child span row has no such field, so
384
+ # without this a failed tool call recorded via a framework integration (e.g.
385
+ # langchain.py's on_tool_error) would silently never trip that check.
386
+ self.tool_calls.append({
387
+ "name": tc.get("name") or "Tool call",
388
+ "input": tc.get("input"),
389
+ "output": tc.get("output"),
390
+ "latency_ms": tc.get("latency_ms"),
391
+ "success": tc.get("success", True),
392
+ })
351
393
  for step in retrieval_steps or []:
352
394
  self._child_span_count += 1
353
395
  self.child_span(
@@ -371,6 +413,10 @@ class _TraceSpan:
371
413
  self._input_tokens += input_tokens
372
414
  if output_tokens:
373
415
  self._output_tokens += output_tokens
416
+ if cache_read_tokens:
417
+ self._cache_read_tokens += cache_read_tokens
418
+ if cache_write_tokens:
419
+ self._cache_write_tokens += cache_write_tokens
374
420
 
375
421
  # ------------------------------------------------------------------
376
422
  # Context manager helpers
@@ -675,6 +721,7 @@ class Tracer:
675
721
  sync: bool = False,
676
722
  monitor: bool = False,
677
723
  pattern_ids: Optional[List[str]] = None,
724
+ agent_id: Optional[str] = None,
678
725
  ) -> _TraceSpan:
679
726
  """
680
727
  Return a :class:`_TraceSpan` that works as both a decorator and a
@@ -713,6 +760,15 @@ class Tracer:
713
760
 
714
761
  with client.tracer.trace("support_agent_call", monitor=True, pattern_ids=[pattern.id]) as span:
715
762
  span.output = call_llm(...)
763
+
764
+ ``agent_id`` disambiguates when ``name`` alone isn't enough — pass an already-known agent
765
+ id (e.g. one you've seen in the dashboard's Overview tab, or from a direct ``GET /agents``
766
+ call) to pin this trace to that exact agent instead of resolving by name. Omit it (the
767
+ default) and this resolves from ``name`` alone — one stable agent per distinct name,
768
+ created on first use::
769
+
770
+ with client.tracer.trace("support-agent", agent_id="ag_123", sync=True) as span:
771
+ span.output = call_llm(...)
716
772
  """
717
773
  return _TraceSpan(
718
774
  tracer=self,
@@ -725,6 +781,7 @@ class Tracer:
725
781
  sync=sync,
726
782
  monitor=monitor,
727
783
  pattern_ids=pattern_ids,
784
+ agent_id=agent_id,
728
785
  )
729
786
 
730
787
  def flush(self, timeout: float = 5.0) -> None:
@@ -946,6 +1003,10 @@ class Tracer:
946
1003
  wire["input_tokens"] = payload["input_tokens"]
947
1004
  if "output_tokens" in payload:
948
1005
  wire["output_tokens"] = payload["output_tokens"]
1006
+ if "cache_read_tokens" in payload:
1007
+ wire["cache_read_tokens"] = payload["cache_read_tokens"]
1008
+ if "cache_write_tokens" in payload:
1009
+ wire["cache_write_tokens"] = payload["cache_write_tokens"]
949
1010
  if "monitor" in payload:
950
1011
  wire["monitor"] = payload["monitor"]
951
1012
  if "pattern_ids" in payload:
@@ -956,6 +1017,8 @@ class Tracer:
956
1017
  wire["parent_span_id"] = payload["parent_span_id"]
957
1018
  if "started_at_unix_nano" in payload:
958
1019
  wire["started_at_unix_nano"] = payload["started_at_unix_nano"]
1020
+ if "agent_id" in payload:
1021
+ wire["agent_id"] = payload["agent_id"]
959
1022
 
960
1023
  pending_tool_calls, self._pending_tool_calls = self._pending_tool_calls, []
961
1024
  if pending_tool_calls:
@@ -0,0 +1 @@
1
+ VERSION = "0.6.15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.14
3
+ Version: 0.6.15
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
@@ -228,7 +228,12 @@ with tracer.trace("rag-agent", framework="langchain") as span:
228
228
 
229
229
  ### Framework integrations
230
230
 
231
- Each integration auto-captures LLM calls, tool calls, and token usage — install the matching extra:
231
+ Each integration auto-captures LLM calls, tool calls, and token usage — including prompt-caching
232
+ token counts (Anthropic's cache write/read, OpenAI/LiteLLM's cached tokens, Google GenAI's cached
233
+ content), reported as their own `cache_read_tokens`/`cache_write_tokens` fields alongside the
234
+ regular totals, no extra config needed. Self-host's cost estimate prices these separately from a
235
+ regular input token when you've set optional cache rates on that model. Install the matching
236
+ extra:
232
237
 
233
238
  | Framework | Install | Integration |
234
239
  | --------------------- | -------------------------------------------- | ------------------------ |
@@ -1 +0,0 @@
1
- VERSION = "0.6.14"
File without changes
File without changes
File without changes