agentprobe-testing 0.8.3__tar.gz → 0.8.5__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 (92) hide show
  1. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/PKG-INFO +1 -1
  2. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/__init__.py +1 -1
  3. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/llm.py +77 -7
  4. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  5. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/pyproject.toml +1 -1
  6. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_llm.py +63 -0
  7. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/LICENSE +0 -0
  8. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/README.md +0 -0
  9. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/agents/__init__.py +0 -0
  10. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/agents/base.py +0 -0
  11. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/agents/rule_based.py +0 -0
  12. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/agents/scripted.py +0 -0
  13. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/agents/target_agent.py +0 -0
  14. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/agreement.py +0 -0
  15. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/classifier.py +0 -0
  16. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/cli.py +0 -0
  17. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/diff.py +0 -0
  18. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domain.py +0 -0
  19. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/__init__.py +0 -0
  20. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/__init__.py +0 -0
  21. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/agent.py +0 -0
  22. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/clean.py +0 -0
  23. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/complex_agent.py +0 -0
  24. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/decoy.py +0 -0
  25. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/domain.py +0 -0
  26. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/entities.py +0 -0
  27. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  28. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  29. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/scenarios.py +0 -0
  30. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/split.py +0 -0
  31. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/tools.py +0 -0
  32. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/domains/access_control/trap.py +0 -0
  33. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/feedback.py +0 -0
  34. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/generic_world.py +0 -0
  35. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/injection.py +0 -0
  36. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/injector.py +0 -0
  37. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/playbook.py +0 -0
  38. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/quickstart.py +0 -0
  39. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/reachability.py +0 -0
  40. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/registry.py +0 -0
  41. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/report.py +0 -0
  42. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/runner.py +0 -0
  43. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenario.py +0 -0
  44. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenarios/__init__.py +0 -0
  45. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenarios/clean.py +0 -0
  46. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenarios/decoy.py +0 -0
  47. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenarios/registry.py +0 -0
  48. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenarios/split.py +0 -0
  49. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/scenarios/trap.py +0 -0
  50. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/termui.py +0 -0
  51. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/tools.py +0 -0
  52. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/trajectory.py +0 -0
  53. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/triage.py +0 -0
  54. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/validate_scenarios.py +0 -0
  55. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe/world.py +0 -0
  56. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe_testing.egg-info/SOURCES.txt +0 -0
  57. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  58. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  59. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe_testing.egg-info/requires.txt +0 -0
  60. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/agentprobe_testing.egg-info/top_level.txt +0 -0
  61. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/setup.cfg +0 -0
  62. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_access_control_agent.py +0 -0
  63. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_access_control_domain.py +0 -0
  64. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_access_control_rule_based_agent.py +0 -0
  65. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_access_control_scenarios.py +0 -0
  66. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_access_control_tools.py +0 -0
  67. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_agreement.py +0 -0
  68. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_classifier.py +0 -0
  69. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_cli.py +0 -0
  70. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_complex_access_control_agent.py +0 -0
  71. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_diff.py +0 -0
  72. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_domain.py +0 -0
  73. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_feedback.py +0 -0
  74. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_generic_world.py +0 -0
  75. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_injection.py +0 -0
  76. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_injector.py +0 -0
  77. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_package_api.py +0 -0
  78. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_playbook.py +0 -0
  79. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_quickstart.py +0 -0
  80. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_reachability.py +0 -0
  81. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_registry.py +0 -0
  82. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_report.py +0 -0
  83. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_rule_based_agent.py +0 -0
  84. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_runner.py +0 -0
  85. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_scenarios.py +0 -0
  86. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_target_agent.py +0 -0
  87. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_termui.py +0 -0
  88. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_tools.py +0 -0
  89. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_trajectory.py +0 -0
  90. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_triage.py +0 -0
  91. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_validate_scenarios.py +0 -0
  92. {agentprobe_testing-0.8.3 → agentprobe_testing-0.8.5}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 0.8.3
3
+ Version: 0.8.5
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -60,7 +60,7 @@ from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
60
60
  from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
61
61
  from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
62
62
 
63
- __version__ = "0.8.3"
63
+ __version__ = "0.8.5"
64
64
 
65
65
  __all__ = [
66
66
  "Agent",
@@ -15,9 +15,72 @@ from __future__ import annotations
15
15
 
16
16
  from typing import Any
17
17
 
18
+ import anthropic._streaming as _anthropic_streaming
19
+
18
20
  _TEMPERATURE_SUPPORTED: dict[str, bool] = {}
19
21
 
20
22
 
23
+ def _patch_sse_decoder_for_memoryview_chunks() -> None:
24
+ """anthropic._streaming.SSEDecoder calls `chunk.splitlines()` directly on
25
+ whatever the underlying HTTP iterator yields, assuming it's always
26
+ `bytes`. Under Cloudflare's Python Worker runtime (Pyodide/emscripten),
27
+ the httpx2-jsfetch transport that bridges JS's fetch API yields
28
+ `memoryview` chunks instead -- confirmed live: every single streamed
29
+ Injector call failed with AttributeError('memoryview' object has no
30
+ attribute 'splitlines') the moment create_deterministic() switched from
31
+ create() to stream() to dodge a separate idle-timeout bug. `bytes(chunk)`
32
+ is a no-op for already-bytes chunks and a cheap copy for memoryview, so
33
+ coercing at the one boundary where raw chunks enter the SSE decoder
34
+ fixes this without reimplementing SSE parsing ourselves. Wrapped in
35
+ try/except so an anthropic SDK upgrade that moves this internal method
36
+ fails quiet (falls back to the original, unpatched bug) rather than
37
+ breaking import entirely.
38
+ """
39
+ try:
40
+ decoder_cls = _anthropic_streaming.SSEDecoder
41
+ original_iter_chunks = decoder_cls._iter_chunks
42
+ original_aiter_chunks = decoder_cls._aiter_chunks
43
+
44
+ def _iter_chunks(self, iterator):
45
+ return original_iter_chunks(self, (bytes(chunk) for chunk in iterator))
46
+
47
+ async def _aiter_chunks(self, iterator):
48
+ async def _coerced():
49
+ async for chunk in iterator:
50
+ yield bytes(chunk)
51
+
52
+ async for sse_chunk in original_aiter_chunks(self, _coerced()):
53
+ yield sse_chunk
54
+
55
+ decoder_cls._iter_chunks = _iter_chunks
56
+ decoder_cls._aiter_chunks = _aiter_chunks
57
+ except AttributeError:
58
+ pass
59
+
60
+
61
+ _patch_sse_decoder_for_memoryview_chunks()
62
+
63
+
64
+ _GIL_RACE_RETRIES = 3
65
+
66
+
67
+ def _is_gil_race_error(e: BaseException) -> bool:
68
+ # "NoGilError: Attempted to use PyProxy when Python GIL not held" is a
69
+ # known Pyodide failure mode when a streamed JS fetch()'s reader
70
+ # callback fires from a browser/worker microtask where the GIL isn't
71
+ # currently held by the thread touching the Python-side proxy -- it
72
+ # lives inside httpx2-jsfetch's sync-over-async bridge (a pinned
73
+ # third-party wheel this codebase doesn't control), not in anything
74
+ # here. Confirmed live: worked for the first couple of scenarios on a
75
+ # fresh isolate, then failed consistently once the isolate warmed up
76
+ # and got reused -- a timing race, not a deterministic bug, so a retry
77
+ # is a legitimate mitigation even though it can't fix the bridge
78
+ # itself. String-matched (not `except NoGilError`) since that type
79
+ # only exists inside Pyodide -- importing it would break every
80
+ # non-Pyodide caller of this function, including the whole test suite.
81
+ return "nogilerror" in str(type(e).__name__).lower() or "gil not held" in str(e).lower()
82
+
83
+
21
84
  def create_deterministic(client: Any, **kwargs: Any) -> Any:
22
85
  # Streamed, not create(): a non-streaming call blocks with zero bytes on
23
86
  # the wire until the whole completion is ready, which under Cloudflare's
@@ -27,17 +90,24 @@ def create_deterministic(client: Any, **kwargs: Any) -> Any:
27
90
  # trivial first step) is what the same non-streaming pattern already
28
91
  # caused for domain generation, fixed there the same way.
29
92
  model = kwargs["model"]
30
- if _TEMPERATURE_SUPPORTED.get(model, True):
93
+ for attempt in range(_GIL_RACE_RETRIES):
31
94
  try:
32
- with client.messages.stream(**kwargs, extra_body={"temperature": 0}) as stream:
95
+ if _TEMPERATURE_SUPPORTED.get(model, True):
96
+ try:
97
+ with client.messages.stream(**kwargs, extra_body={"temperature": 0}) as stream:
98
+ return stream.get_final_message()
99
+ except Exception as e:
100
+ if _is_gil_race_error(e):
101
+ raise
102
+ if "temperature" in str(e).lower():
103
+ _TEMPERATURE_SUPPORTED[model] = False
104
+ else:
105
+ raise
106
+ with client.messages.stream(**kwargs) as stream:
33
107
  return stream.get_final_message()
34
108
  except Exception as e:
35
- if "temperature" in str(e).lower():
36
- _TEMPERATURE_SUPPORTED[model] = False
37
- else:
109
+ if not _is_gil_race_error(e) or attempt == _GIL_RACE_RETRIES - 1:
38
110
  raise
39
- with client.messages.stream(**kwargs) as stream:
40
- return stream.get_final_message()
41
111
 
42
112
 
43
113
  _PRICE_PER_TOKEN: dict[str, tuple[float, float]] = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 0.8.3
3
+ Version: 0.8.5
4
4
  Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
5
5
  License: Business Source License 1.1
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentprobe-testing"
3
- version = "0.8.3"
3
+ version = "0.8.5"
4
4
  description = "Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["anthropic>=1.0.0", "python-dotenv>=1.0.0"]
@@ -81,6 +81,69 @@ def test_unrelated_errors_are_not_swallowed():
81
81
  pass
82
82
 
83
83
 
84
+ def test_retries_a_gil_race_error_and_succeeds_once_the_race_clears():
85
+ class FlakyStream:
86
+ def __init__(self, should_fail):
87
+ self.should_fail = should_fail
88
+
89
+ def __enter__(self):
90
+ if self.should_fail:
91
+ raise RuntimeError("NoGilError: Attempted to use PyProxy when Python GIL not held")
92
+ return self
93
+
94
+ def __exit__(self, *exc_info):
95
+ return False
96
+
97
+ def get_final_message(self):
98
+ return "ok"
99
+
100
+ class FlakyMessages:
101
+ def __init__(self):
102
+ self.calls = 0
103
+
104
+ def stream(self, **kwargs):
105
+ self.calls += 1
106
+ return FlakyStream(should_fail=self.calls < 3)
107
+
108
+ class FlakyClient:
109
+ def __init__(self):
110
+ self.messages = FlakyMessages()
111
+
112
+ client = FlakyClient()
113
+ result = create_deterministic(client, model="claude-sonnet-5", max_tokens=10)
114
+ assert result == "ok"
115
+ assert client.messages.calls == 3
116
+
117
+
118
+ def test_gives_up_on_a_gil_race_error_after_the_retry_budget_is_exhausted():
119
+ class AlwaysFlakyStream:
120
+ def __enter__(self):
121
+ raise RuntimeError("NoGilError: Attempted to use PyProxy when Python GIL not held")
122
+
123
+ def __exit__(self, *exc_info):
124
+ return False
125
+
126
+ class AlwaysFlakyMessages:
127
+ def __init__(self):
128
+ self.calls = 0
129
+
130
+ def stream(self, **kwargs):
131
+ self.calls += 1
132
+ return AlwaysFlakyStream()
133
+
134
+ class AlwaysFlakyClient:
135
+ def __init__(self):
136
+ self.messages = AlwaysFlakyMessages()
137
+
138
+ client = AlwaysFlakyClient()
139
+ try:
140
+ create_deterministic(client, model="claude-sonnet-5", max_tokens=10)
141
+ assert False, "expected the GIL race error to propagate once retries are exhausted"
142
+ except RuntimeError as e:
143
+ assert "GIL" in str(e)
144
+ assert client.messages.calls == 3 # exactly the retry budget, not more
145
+
146
+
84
147
  def test_cacheable_system_wraps_text_with_ephemeral_cache_control():
85
148
  result = cacheable_system("some long system prompt")
86
149
  assert result == [