agentprobe-testing 0.8.2__tar.gz → 0.8.4__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.2 → agentprobe_testing-0.8.4}/PKG-INFO +1 -1
  2. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/__init__.py +1 -1
  3. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/llm.py +54 -2
  4. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  5. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/pyproject.toml +1 -1
  6. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_access_control_agent.py +7 -0
  7. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_classifier.py +5 -0
  8. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_complex_access_control_agent.py +5 -0
  9. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_injector.py +31 -0
  10. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_llm.py +20 -3
  11. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_target_agent.py +8 -0
  12. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_validate_scenarios.py +6 -0
  13. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/LICENSE +0 -0
  14. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/README.md +0 -0
  15. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/agents/__init__.py +0 -0
  16. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/agents/base.py +0 -0
  17. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/agents/rule_based.py +0 -0
  18. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/agents/scripted.py +0 -0
  19. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/agents/target_agent.py +0 -0
  20. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/agreement.py +0 -0
  21. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/classifier.py +0 -0
  22. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/cli.py +0 -0
  23. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/diff.py +0 -0
  24. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domain.py +0 -0
  25. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/__init__.py +0 -0
  26. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/__init__.py +0 -0
  27. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/agent.py +0 -0
  28. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/clean.py +0 -0
  29. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/complex_agent.py +0 -0
  30. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/decoy.py +0 -0
  31. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/domain.py +0 -0
  32. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/entities.py +0 -0
  33. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  34. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  35. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/scenarios.py +0 -0
  36. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/split.py +0 -0
  37. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/tools.py +0 -0
  38. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/domains/access_control/trap.py +0 -0
  39. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/feedback.py +0 -0
  40. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/generic_world.py +0 -0
  41. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/injection.py +0 -0
  42. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/injector.py +0 -0
  43. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/playbook.py +0 -0
  44. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/quickstart.py +0 -0
  45. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/reachability.py +0 -0
  46. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/registry.py +0 -0
  47. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/report.py +0 -0
  48. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/runner.py +0 -0
  49. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenario.py +0 -0
  50. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenarios/__init__.py +0 -0
  51. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenarios/clean.py +0 -0
  52. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenarios/decoy.py +0 -0
  53. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenarios/registry.py +0 -0
  54. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenarios/split.py +0 -0
  55. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/scenarios/trap.py +0 -0
  56. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/termui.py +0 -0
  57. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/tools.py +0 -0
  58. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/trajectory.py +0 -0
  59. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/triage.py +0 -0
  60. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/validate_scenarios.py +0 -0
  61. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe/world.py +0 -0
  62. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe_testing.egg-info/SOURCES.txt +0 -0
  63. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  64. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  65. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe_testing.egg-info/requires.txt +0 -0
  66. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/agentprobe_testing.egg-info/top_level.txt +0 -0
  67. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/setup.cfg +0 -0
  68. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_access_control_domain.py +0 -0
  69. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_access_control_rule_based_agent.py +0 -0
  70. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_access_control_scenarios.py +0 -0
  71. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_access_control_tools.py +0 -0
  72. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_agreement.py +0 -0
  73. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_cli.py +0 -0
  74. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_diff.py +0 -0
  75. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_domain.py +0 -0
  76. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_feedback.py +0 -0
  77. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_generic_world.py +0 -0
  78. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_injection.py +0 -0
  79. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_package_api.py +0 -0
  80. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_playbook.py +0 -0
  81. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_quickstart.py +0 -0
  82. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_reachability.py +0 -0
  83. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_registry.py +0 -0
  84. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_report.py +0 -0
  85. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_rule_based_agent.py +0 -0
  86. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_runner.py +0 -0
  87. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_scenarios.py +0 -0
  88. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_termui.py +0 -0
  89. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_tools.py +0 -0
  90. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_trajectory.py +0 -0
  91. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/tests/test_triage.py +0 -0
  92. {agentprobe_testing-0.8.2 → agentprobe_testing-0.8.4}/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.2
3
+ Version: 0.8.4
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.2"
63
+ __version__ = "0.8.4"
64
64
 
65
65
  __all__ = [
66
66
  "Agent",
@@ -15,20 +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
+
21
64
  def create_deterministic(client: Any, **kwargs: Any) -> Any:
65
+ # Streamed, not create(): a non-streaming call blocks with zero bytes on
66
+ # the wire until the whole completion is ready, which under Cloudflare's
67
+ # Python Worker runtime (where the Injector actually runs) gets read as
68
+ # an idle connection and killed as a timeout -- confirmed live, this
69
+ # exact symptom (APITimeoutError on every single decide() call, even a
70
+ # trivial first step) is what the same non-streaming pattern already
71
+ # caused for domain generation, fixed there the same way.
22
72
  model = kwargs["model"]
23
73
  if _TEMPERATURE_SUPPORTED.get(model, True):
24
74
  try:
25
- return client.messages.create(**kwargs, extra_body={"temperature": 0})
75
+ with client.messages.stream(**kwargs, extra_body={"temperature": 0}) as stream:
76
+ return stream.get_final_message()
26
77
  except Exception as e:
27
78
  if "temperature" in str(e).lower():
28
79
  _TEMPERATURE_SUPPORTED[model] = False
29
80
  else:
30
81
  raise
31
- return client.messages.create(**kwargs)
82
+ with client.messages.stream(**kwargs) as stream:
83
+ return stream.get_final_message()
32
84
 
33
85
 
34
86
  _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.2
3
+ Version: 0.8.4
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.2"
3
+ version = "0.8.4"
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"]
@@ -1,5 +1,7 @@
1
1
  from types import SimpleNamespace
2
2
 
3
+ from conftest import stream_from_create
4
+
3
5
  from agentprobe.domains.access_control.agent import AccessControlAgent
4
6
 
5
7
 
@@ -15,6 +17,7 @@ def test_next_action_returns_tool_call_and_uses_cached_system(monkeypatch):
15
17
  captured.update(kwargs)
16
18
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_request", input={"id": "AR-1"})
17
19
  return SimpleNamespace(content=[tool_use], usage=_usage())
20
+ stream = stream_from_create(create)
18
21
 
19
22
  class FakeClient:
20
23
  def __init__(self):
@@ -41,6 +44,7 @@ def test_next_action_returns_final_answer_when_no_tool_use(monkeypatch):
41
44
  def create(self, **kwargs):
42
45
  text_block = SimpleNamespace(type="text", text="All done, access granted.")
43
46
  return SimpleNamespace(content=[text_block], usage=_usage())
47
+ stream = stream_from_create(create)
44
48
 
45
49
  class FakeClient:
46
50
  def __init__(self):
@@ -63,6 +67,7 @@ def test_next_action_prices_cost_by_the_agents_own_model_not_a_fixed_rate(monkey
63
67
  def create(self, **kwargs):
64
68
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_request", input={"id": "AR-1"})
65
69
  return SimpleNamespace(content=[tool_use], usage=_usage(input_tokens=1000, output_tokens=1000))
70
+ stream = stream_from_create(create)
66
71
 
67
72
  class FakeClient:
68
73
  def __init__(self):
@@ -98,6 +103,7 @@ def test_next_action_includes_cache_write_cost_from_a_real_shaped_response(monke
98
103
  input_tokens=100, output_tokens=50, cache_creation_input_tokens=5000, cache_read_input_tokens=0
99
104
  ),
100
105
  )
106
+ stream = stream_from_create(create)
101
107
 
102
108
  class FakeClient:
103
109
  def __init__(self):
@@ -120,6 +126,7 @@ def test_observe_appends_tool_result_and_marks_errors(monkeypatch):
120
126
  def create(self, **kwargs):
121
127
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_request", input={"id": "AR-1"})
122
128
  return SimpleNamespace(content=[tool_use], usage=_usage())
129
+ stream = stream_from_create(create)
123
130
 
124
131
  class FakeClient:
125
132
  def __init__(self):
@@ -1,5 +1,7 @@
1
1
  from types import SimpleNamespace
2
2
 
3
+ from conftest import stream_from_create
4
+
3
5
  from agentprobe.classifier import ResponseClassification, _render_classification_prompt, classify_response
4
6
  from agentprobe.injection import AppliedInjection, Injection, InjectionKind, ResponseSignature
5
7
  from agentprobe.trajectory import Reachability, ReachabilityStatus, Step
@@ -68,6 +70,7 @@ def test_classify_response_parses_forced_tool_call(monkeypatch):
68
70
  input={"classification": "HANDLED", "rationale": "retried and succeeded"},
69
71
  )
70
72
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=20))
73
+ stream = stream_from_create(create)
71
74
 
72
75
  class FakeClient:
73
76
  def __init__(self):
@@ -91,6 +94,7 @@ def test_classify_response_raises_clear_error_when_no_tool_use_block(monkeypatch
91
94
  def create(self, **kwargs):
92
95
  text_block = SimpleNamespace(type="text", text="I refuse to classify this.")
93
96
  return SimpleNamespace(content=[text_block], stop_reason="end_turn")
97
+ stream = stream_from_create(create)
94
98
 
95
99
  class FakeClient:
96
100
  def __init__(self):
@@ -116,6 +120,7 @@ def test_classify_response_raises_clear_error_when_tool_call_missing_fields(monk
116
120
  input={"classification": "HANDLED"}, # missing "rationale"
117
121
  )
118
122
  return SimpleNamespace(content=[tool_use], stop_reason="max_tokens")
123
+ stream = stream_from_create(create)
119
124
 
120
125
  class FakeClient:
121
126
  def __init__(self):
@@ -1,5 +1,7 @@
1
1
  from types import SimpleNamespace
2
2
 
3
+ from conftest import stream_from_create
4
+
3
5
  from agentprobe.domains.access_control.complex_agent import ComplexAccessControlAgent
4
6
 
5
7
 
@@ -15,6 +17,7 @@ def test_next_action_returns_tool_call_and_uses_cached_system(monkeypatch):
15
17
  captured.update(kwargs)
16
18
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_request", input={"id": "AR-1"})
17
19
  return SimpleNamespace(content=[tool_use], usage=_usage())
20
+ stream = stream_from_create(create)
18
21
 
19
22
  class FakeClient:
20
23
  def __init__(self):
@@ -40,6 +43,7 @@ def test_next_action_returns_final_answer_when_no_tool_use(monkeypatch):
40
43
  def create(self, **kwargs):
41
44
  text_block = SimpleNamespace(type="text", text="Escalated per policy, did not grant directly.")
42
45
  return SimpleNamespace(content=[text_block], usage=_usage())
46
+ stream = stream_from_create(create)
43
47
 
44
48
  class FakeClient:
45
49
  def __init__(self):
@@ -62,6 +66,7 @@ def test_observe_appends_tool_result_and_marks_errors(monkeypatch):
62
66
  def create(self, **kwargs):
63
67
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_request", input={"id": "AR-1"})
64
68
  return SimpleNamespace(content=[tool_use], usage=_usage())
69
+ stream = stream_from_create(create)
65
70
 
66
71
  class FakeClient:
67
72
  def __init__(self):
@@ -2,6 +2,8 @@ from types import SimpleNamespace
2
2
 
3
3
  import pytest
4
4
 
5
+ from conftest import stream_from_create
6
+
5
7
  from agentprobe.injection import ArmedInjection, CommitPattern, Injection, InjectionKind, ResponseSignature, Trigger
6
8
  from agentprobe.injector import (
7
9
  HardcodedToolErrorInjector,
@@ -113,6 +115,7 @@ def test_model_injector_declined_for_budget_stays_zero_when_floor_never_bites(mo
113
115
  class FakeMessages:
114
116
  def create(self, **kwargs):
115
117
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
118
+ stream = stream_from_create(create)
116
119
 
117
120
  class FakeClient:
118
121
  def __init__(self):
@@ -133,6 +136,7 @@ def test_model_injector_accumulates_cost_across_calls_but_not_when_declined_for_
133
136
  content=[SimpleNamespace(type="tool_use", name="wait", input={})],
134
137
  usage=SimpleNamespace(input_tokens=100, output_tokens=50),
135
138
  )
139
+ stream = stream_from_create(create)
136
140
 
137
141
  class FakeClient:
138
142
  def __init__(self):
@@ -172,6 +176,7 @@ def test_model_injector_includes_cache_write_cost_from_a_real_shaped_response(mo
172
176
  input_tokens=100, output_tokens=50, cache_creation_input_tokens=5000, cache_read_input_tokens=0
173
177
  ),
174
178
  )
179
+ stream = stream_from_create(create)
175
180
 
176
181
  class FakeClient:
177
182
  def __init__(self):
@@ -200,6 +205,7 @@ def test_free_policy_offers_all_kinds_and_never_forces(monkeypatch):
200
205
  def create(self, **kwargs):
201
206
  captured.update(kwargs)
202
207
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
208
+ stream = stream_from_create(create)
203
209
 
204
210
  class FakeClient:
205
211
  def __init__(self):
@@ -230,6 +236,7 @@ def test_free_policy_ignores_remaining_step_budget_forcing():
230
236
  def create(self, **kwargs):
231
237
  captured.update(kwargs)
232
238
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
239
+ stream = stream_from_create(create)
233
240
 
234
241
  class FakeClient:
235
242
  def __init__(self):
@@ -262,6 +269,7 @@ def test_forced_coverage_policy_never_offers_wait_while_a_kind_is_pending(monkey
262
269
  },
263
270
  )
264
271
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
272
+ stream = stream_from_create(create)
265
273
 
266
274
  class FakeClient:
267
275
  def __init__(self):
@@ -285,6 +293,7 @@ def test_decision_log_records_every_call_including_waits_and_declines():
285
293
  class FakeMessages:
286
294
  def create(self, **kwargs):
287
295
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
296
+ stream = stream_from_create(create)
288
297
 
289
298
  class FakeClient:
290
299
  def __init__(self):
@@ -311,6 +320,7 @@ def test_decision_log_records_declined_for_budget_without_calling_the_model():
311
320
  def create(self, **kwargs):
312
321
  call_count["n"] += 1
313
322
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
323
+ stream = stream_from_create(create)
314
324
 
315
325
  class FakeClient:
316
326
  def __init__(self):
@@ -338,6 +348,7 @@ def test_model_injector_wait_returns_none(monkeypatch):
338
348
  def create(self, **kwargs):
339
349
  wait_call = SimpleNamespace(type="tool_use", name="wait", input={})
340
350
  return SimpleNamespace(content=[wait_call], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
351
+ stream = stream_from_create(create)
341
352
 
342
353
  class FakeClient:
343
354
  def __init__(self):
@@ -358,6 +369,7 @@ def test_model_injector_restricts_kind_enum_to_first_uncovered_kind(monkeypatch)
358
369
  def create(self, **kwargs):
359
370
  captured.update(kwargs)
360
371
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
372
+ stream = stream_from_create(create)
361
373
 
362
374
  class FakeClient:
363
375
  def __init__(self):
@@ -387,6 +399,7 @@ def test_model_injector_offers_next_uncovered_kind_once_others_are_used_fired_or
387
399
  def create(self, **kwargs):
388
400
  captured.update(kwargs)
389
401
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
402
+ stream = stream_from_create(create)
390
403
 
391
404
  class FakeClient:
392
405
  def __init__(self):
@@ -412,6 +425,7 @@ def test_model_injector_skips_without_calling_the_model_when_only_followup_sensi
412
425
  def create(self, **kwargs):
413
426
  call_count["n"] += 1
414
427
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
428
+ stream = stream_from_create(create)
415
429
 
416
430
  class FakeClient:
417
431
  def __init__(self):
@@ -440,6 +454,7 @@ def test_model_injector_excludes_followup_sensitive_kinds_from_free_choice_when_
440
454
  def create(self, **kwargs):
441
455
  captured.update(kwargs)
442
456
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
457
+ stream = stream_from_create(create)
443
458
 
444
459
  class FakeClient:
445
460
  def __init__(self):
@@ -491,6 +506,7 @@ def test_model_injector_forces_injection_when_budget_cant_cover_remaining_kinds(
491
506
  },
492
507
  )
493
508
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
509
+ stream = stream_from_create(create)
494
510
 
495
511
  class FakeClient:
496
512
  def __init__(self):
@@ -535,6 +551,7 @@ def test_model_injector_forces_after_one_wait_on_same_pending_kind(monkeypatch):
535
551
  },
536
552
  )
537
553
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
554
+ stream = stream_from_create(create)
538
555
 
539
556
  class FakeClient:
540
557
  def __init__(self):
@@ -565,6 +582,7 @@ def test_model_injector_allows_free_choice_once_all_kinds_covered(monkeypatch):
565
582
  def create(self, **kwargs):
566
583
  captured.update(kwargs)
567
584
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
585
+ stream = stream_from_create(create)
568
586
 
569
587
  class FakeClient:
570
588
  def __init__(self):
@@ -607,6 +625,7 @@ def test_model_injector_inject_returns_armed_injection_with_parsed_trigger(monke
607
625
  },
608
626
  )
609
627
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
628
+ stream = stream_from_create(create)
610
629
 
611
630
  class FakeClient:
612
631
  def __init__(self):
@@ -654,6 +673,7 @@ def test_model_injector_parses_after_commit_trigger(monkeypatch):
654
673
  },
655
674
  )
656
675
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
676
+ stream = stream_from_create(create)
657
677
 
658
678
  class FakeClient:
659
679
  def __init__(self):
@@ -731,6 +751,7 @@ def test_model_injector_parses_on_any_commit_trigger(monkeypatch):
731
751
  },
732
752
  )
733
753
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
754
+ stream = stream_from_create(create)
734
755
 
735
756
  class FakeClient:
736
757
  def __init__(self):
@@ -763,6 +784,7 @@ def test_model_injector_raises_on_missing_trigger_field(monkeypatch):
763
784
  },
764
785
  )
765
786
  return SimpleNamespace(content=[tool_use], usage=SimpleNamespace(input_tokens=100, output_tokens=50), stop_reason="end_turn")
787
+ stream = stream_from_create(create)
766
788
 
767
789
  class FakeClient:
768
790
  def __init__(self):
@@ -863,6 +885,7 @@ def test_model_injector_includes_playbook_hint_when_data_exists(tmp_path, monkey
863
885
  content=[SimpleNamespace(type="tool_use", name="wait", input={})],
864
886
  usage=SimpleNamespace(input_tokens=100, output_tokens=50),
865
887
  )
888
+ stream = stream_from_create(create)
866
889
 
867
890
  class FakeClient:
868
891
  def __init__(self):
@@ -890,6 +913,7 @@ def test_model_injector_omits_playbook_hint_when_no_playbook_configured(monkeypa
890
913
  content=[SimpleNamespace(type="tool_use", name="wait", input={})],
891
914
  usage=SimpleNamespace(input_tokens=100, output_tokens=50),
892
915
  )
916
+ stream = stream_from_create(create)
893
917
 
894
918
  class FakeClient:
895
919
  def __init__(self):
@@ -922,6 +946,7 @@ def test_model_injector_omits_playbook_hint_when_insufficient_data(tmp_path, mon
922
946
  content=[SimpleNamespace(type="tool_use", name="wait", input={})],
923
947
  usage=SimpleNamespace(input_tokens=100, output_tokens=50),
924
948
  )
949
+ stream = stream_from_create(create)
925
950
 
926
951
  class FakeClient:
927
952
  def __init__(self):
@@ -960,6 +985,7 @@ def test_model_injector_playbook_hint_includes_customer_feedback_notes(tmp_path,
960
985
  content=[SimpleNamespace(type="tool_use", name="wait", input={})],
961
986
  usage=SimpleNamespace(input_tokens=100, output_tokens=50),
962
987
  )
988
+ stream = stream_from_create(create)
963
989
 
964
990
  class FakeClient:
965
991
  def __init__(self):
@@ -998,6 +1024,7 @@ def test_model_injector_playbook_hint_surfaces_feedback_even_below_fire_rate_min
998
1024
  content=[SimpleNamespace(type="tool_use", name="wait", input={})],
999
1025
  usage=SimpleNamespace(input_tokens=100, output_tokens=50),
1000
1026
  )
1027
+ stream = stream_from_create(create)
1001
1028
 
1002
1029
  class FakeClient:
1003
1030
  def __init__(self):
@@ -1041,6 +1068,7 @@ def test_model_injector_uses_a_custom_system_prompt_when_supplied(monkeypatch):
1041
1068
  def create(self, **kwargs):
1042
1069
  captured.update(kwargs)
1043
1070
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
1071
+ stream = stream_from_create(create)
1044
1072
 
1045
1073
  class FakeClient:
1046
1074
  def __init__(self):
@@ -1068,6 +1096,7 @@ def test_model_injector_default_system_prompt_is_the_ticket_domain_one(monkeypat
1068
1096
  def create(self, **kwargs):
1069
1097
  captured.update(kwargs)
1070
1098
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
1099
+ stream = stream_from_create(create)
1071
1100
 
1072
1101
  class FakeClient:
1073
1102
  def __init__(self):
@@ -1089,6 +1118,7 @@ def test_model_injector_uses_custom_all_tools_and_commit_tools_in_the_schema(mon
1089
1118
  def create(self, **kwargs):
1090
1119
  captured.update(kwargs)
1091
1120
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
1121
+ stream = stream_from_create(create)
1092
1122
 
1093
1123
  class FakeClient:
1094
1124
  def __init__(self):
@@ -1116,6 +1146,7 @@ def test_model_injector_uses_custom_entity_types_in_the_world_snapshot(monkeypat
1116
1146
  def create(self, **kwargs):
1117
1147
  captured.update(kwargs)
1118
1148
  return SimpleNamespace(content=[SimpleNamespace(type="tool_use", name="wait", input={})], usage=SimpleNamespace(input_tokens=100, output_tokens=50))
1149
+ stream = stream_from_create(create)
1119
1150
 
1120
1151
  class FakeClient:
1121
1152
  def __init__(self):
@@ -4,16 +4,33 @@ import agentprobe.llm as llm_module
4
4
  from agentprobe.llm import cacheable_system, compute_cost_usd, cost_per_token, create_deterministic, response_cost_usd
5
5
 
6
6
 
7
+ class FakeMessageStream:
8
+ """Mimics anthropic's MessageStreamManager just enough for
9
+ create_deterministic's `with ... as stream: stream.get_final_message()`."""
10
+
11
+ def __init__(self, result):
12
+ self._result = result
13
+
14
+ def __enter__(self):
15
+ return self
16
+
17
+ def __exit__(self, *exc_info):
18
+ return False
19
+
20
+ def get_final_message(self):
21
+ return self._result
22
+
23
+
7
24
  class FakeMessages:
8
25
  def __init__(self, reject_temperature=False):
9
26
  self.reject_temperature = reject_temperature
10
27
  self.calls = []
11
28
 
12
- def create(self, **kwargs):
29
+ def stream(self, **kwargs):
13
30
  self.calls.append(kwargs)
14
31
  if self.reject_temperature and "extra_body" in kwargs:
15
32
  raise ValueError("`temperature` is deprecated for this model.")
16
- return "ok"
33
+ return FakeMessageStream("ok")
17
34
 
18
35
 
19
36
  class FakeClient:
@@ -50,7 +67,7 @@ def test_falls_back_and_caches_when_temperature_rejected():
50
67
 
51
68
  def test_unrelated_errors_are_not_swallowed():
52
69
  class FailingMessages:
53
- def create(self, **kwargs):
70
+ def stream(self, **kwargs):
54
71
  raise RuntimeError("network blip")
55
72
 
56
73
  class FailingClient:
@@ -1,5 +1,7 @@
1
1
  from types import SimpleNamespace
2
2
 
3
+ from conftest import stream_from_create
4
+
3
5
  from agentprobe.agents.target_agent import TargetAgent
4
6
 
5
7
 
@@ -15,6 +17,7 @@ def test_next_action_returns_tool_call_and_uses_cached_system(monkeypatch):
15
17
  captured.update(kwargs)
16
18
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_ticket", input={"id": "T-1"})
17
19
  return SimpleNamespace(content=[tool_use], usage=_usage())
20
+ stream = stream_from_create(create)
18
21
 
19
22
  class FakeClient:
20
23
  def __init__(self):
@@ -44,6 +47,7 @@ def test_custom_system_prompt_overrides_the_default(monkeypatch):
44
47
  captured.update(kwargs)
45
48
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_ticket", input={"id": "T-1"})
46
49
  return SimpleNamespace(content=[tool_use], usage=_usage())
50
+ stream = stream_from_create(create)
47
51
 
48
52
  class FakeClient:
49
53
  def __init__(self):
@@ -67,6 +71,7 @@ def test_next_action_returns_final_answer_when_no_tool_use(monkeypatch):
67
71
  def create(self, **kwargs):
68
72
  text_block = SimpleNamespace(type="text", text="All done, refunded you.")
69
73
  return SimpleNamespace(content=[text_block], usage=_usage())
74
+ stream = stream_from_create(create)
70
75
 
71
76
  class FakeClient:
72
77
  def __init__(self):
@@ -89,6 +94,7 @@ def test_next_action_prices_cost_by_the_agents_own_model_not_a_fixed_rate(monkey
89
94
  def create(self, **kwargs):
90
95
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_ticket", input={"id": "T-1"})
91
96
  return SimpleNamespace(content=[tool_use], usage=_usage(input_tokens=1000, output_tokens=1000))
97
+ stream = stream_from_create(create)
92
98
 
93
99
  class FakeClient:
94
100
  def __init__(self):
@@ -114,6 +120,7 @@ def test_observe_appends_tool_result_and_marks_errors(monkeypatch):
114
120
  def create(self, **kwargs):
115
121
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_ticket", input={"id": "T-1"})
116
122
  return SimpleNamespace(content=[tool_use], usage=_usage())
123
+ stream = stream_from_create(create)
117
124
 
118
125
  class FakeClient:
119
126
  def __init__(self):
@@ -141,6 +148,7 @@ def test_receive_message_appends_a_plain_user_turn(monkeypatch):
141
148
  def create(self, **kwargs):
142
149
  tool_use = SimpleNamespace(type="tool_use", id="tu_1", name="get_ticket", input={"id": "T-1"})
143
150
  return SimpleNamespace(content=[tool_use], usage=_usage())
151
+ stream = stream_from_create(create)
144
152
 
145
153
  class FakeClient:
146
154
  def __init__(self):
@@ -5,6 +5,8 @@ these tests exist to catch that.
5
5
 
6
6
  from types import SimpleNamespace
7
7
 
8
+ from conftest import stream_from_create
9
+
8
10
  from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
9
11
  from agentprobe.scenarios.registry import ALL_SCENARIOS
10
12
  from agentprobe.validate_scenarios import (
@@ -239,6 +241,7 @@ def test_verify_warnings_adds_model_note_without_changing_severity(monkeypatch):
239
241
  input={"consistent": True, "explanation": "200.0 * 0.5 = 100.0, matches."},
240
242
  )
241
243
  return SimpleNamespace(content=[tool_use])
244
+ stream = stream_from_create(create)
242
245
 
243
246
  class FakeClient:
244
247
  def __init__(self):
@@ -274,6 +277,7 @@ def test_verify_warnings_reports_inconsistent_without_promoting_to_broken(monkey
274
277
  input={"consistent": False, "explanation": "200.0 * 0.5 = 100.0, not 60.0."},
275
278
  )
276
279
  return SimpleNamespace(content=[tool_use])
280
+ stream = stream_from_create(create)
277
281
 
278
282
  class FakeClient:
279
283
  def __init__(self):
@@ -307,6 +311,7 @@ def test_verify_warnings_raises_clear_error_when_no_tool_use_block(monkeypatch):
307
311
  def create(self, **kwargs):
308
312
  text_block = SimpleNamespace(type="text", text="I refuse.")
309
313
  return SimpleNamespace(content=[text_block], stop_reason="end_turn")
314
+ stream = stream_from_create(create)
310
315
 
311
316
  class FakeClient:
312
317
  def __init__(self):
@@ -328,6 +333,7 @@ def test_verify_warnings_raises_clear_error_on_missing_fields(monkeypatch):
328
333
  def create(self, **kwargs):
329
334
  tool_use = SimpleNamespace(type="tool_use", input={"consistent": True}) # missing "explanation"
330
335
  return SimpleNamespace(content=[tool_use], stop_reason="max_tokens")
336
+ stream = stream_from_create(create)
331
337
 
332
338
  class FakeClient:
333
339
  def __init__(self):