stroma 0.2.7__tar.gz → 0.3.2__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 (33) hide show
  1. {stroma-0.2.7/src/stroma.egg-info → stroma-0.3.2}/PKG-INFO +119 -26
  2. stroma-0.3.2/README.md +244 -0
  3. {stroma-0.2.7 → stroma-0.3.2}/pyproject.toml +4 -2
  4. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/__init__.py +26 -3
  5. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/adapters/__init__.py +4 -0
  6. stroma-0.3.2/src/stroma/adapters/base.py +100 -0
  7. stroma-0.3.2/src/stroma/adapters/crewai.py +95 -0
  8. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/adapters/langgraph.py +1 -22
  9. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/contracts.py +4 -0
  10. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/cost.py +43 -0
  11. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/failures.py +2 -0
  12. stroma-0.3.2/src/stroma/middleware.py +327 -0
  13. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/runner.py +163 -172
  14. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/trace.py +5 -0
  15. {stroma-0.2.7 → stroma-0.3.2/src/stroma.egg-info}/PKG-INFO +119 -26
  16. {stroma-0.2.7 → stroma-0.3.2}/src/stroma.egg-info/SOURCES.txt +5 -0
  17. {stroma-0.2.7 → stroma-0.3.2}/src/stroma.egg-info/requires.txt +4 -0
  18. {stroma-0.2.7 → stroma-0.3.2}/tests/test_cost.py +78 -0
  19. stroma-0.3.2/tests/test_middleware.py +196 -0
  20. {stroma-0.2.7 → stroma-0.3.2}/tests/test_runner.py +114 -3
  21. stroma-0.3.2/tests/test_step_aliases.py +116 -0
  22. stroma-0.2.7/README.md +0 -154
  23. {stroma-0.2.7 → stroma-0.3.2}/LICENSE +0 -0
  24. {stroma-0.2.7 → stroma-0.3.2}/setup.cfg +0 -0
  25. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/adapters/deepagents.py +0 -0
  26. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/checkpoint.py +0 -0
  27. {stroma-0.2.7 → stroma-0.3.2}/src/stroma/py.typed +0 -0
  28. {stroma-0.2.7 → stroma-0.3.2}/src/stroma.egg-info/dependency_links.txt +0 -0
  29. {stroma-0.2.7 → stroma-0.3.2}/src/stroma.egg-info/top_level.txt +0 -0
  30. {stroma-0.2.7 → stroma-0.3.2}/tests/test_checkpoint.py +0 -0
  31. {stroma-0.2.7 → stroma-0.3.2}/tests/test_contracts.py +0 -0
  32. {stroma-0.2.7 → stroma-0.3.2}/tests/test_failures.py +0 -0
  33. {stroma-0.2.7 → stroma-0.3.2}/tests/test_trace.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stroma
3
- Version: 0.2.7
4
- Summary: Reliability primitives for agent pipelines.
3
+ Version: 0.3.2
4
+ Summary: The framework handles the graph. Stroma handles the guarantees.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/jengroff/stroma
7
7
  Project-URL: Repository, https://github.com/jengroff/stroma
@@ -18,11 +18,14 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Requires-Python: >=3.12
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Requires-Dist: crewai>=1.13.0
21
22
  Requires-Dist: pydantic>=2.0
22
23
  Provides-Extra: redis
23
24
  Requires-Dist: redis>=5.0; extra == "redis"
24
25
  Provides-Extra: langgraph
25
26
  Requires-Dist: langgraph>=0.2; extra == "langgraph"
27
+ Provides-Extra: crewai
28
+ Requires-Dist: crewai>=0.70; extra == "crewai"
26
29
  Provides-Extra: deepagents
27
30
  Requires-Dist: deepagents>=0.4.0; extra == "deepagents"
28
31
  Requires-Dist: langgraph>=0.2; extra == "deepagents"
@@ -42,44 +45,103 @@ Dynamic: license-file
42
45
 
43
46
  **dbt didn't replace your data warehouse. Stroma doesn't replace your agent framework.**
44
47
 
45
- Typed node contracts, formal failure classification, and cost-aware execution — portable across whatever orchestration framework you're building on. The framework handles the graph. Stroma handles the guarantees.
48
+ dbt gave you typed models, tested transformations, and documented lineage — a software engineering layer that worked regardless of which warehouse you were running. Stroma does the same thing for agent execution graphs: typed node contracts, formal failure classification, and cost-aware execution — portable across whatever orchestration framework you're building on.
49
+
50
+ The framework handles the graph. Stroma handles the guarantees.
51
+
52
+ ## The problem it solves
53
+
54
+ LLM pipelines fail in ways that traditional software doesn't. A node returns malformed data and the error surfaces three steps later. A transient timeout kills a 20-minute run and you start over from scratch. Costs spiral past budget with no enforcement mechanism. Failures are silent until they're catastrophic.
55
+
56
+ Stroma gives you the building blocks to handle this — without locking you into a framework.
57
+
58
+ ## See it in action
59
+
60
+ The scenario below is the kind that breaks raw LangGraph pipelines: a multi-step run that crashes midway, resumes from checkpoint, and gives you a diff of what changed between the failed and successful run.
46
61
 
47
62
  ```python
63
+ import asyncio
48
64
  from pydantic import BaseModel
49
- from stroma import StromaRunner
65
+ from stroma import (
66
+ AsyncInMemoryStore,
67
+ CheckpointManager,
68
+ ContractRegistry,
69
+ NodeContract,
70
+ RunConfig,
71
+ StromaRunner,
72
+ stroma_node,
73
+ )
50
74
 
51
- class Input(BaseModel):
52
- value: int
53
75
 
54
- class Output(BaseModel):
55
- result: int
76
+ class Document(BaseModel):
77
+ text: str
56
78
 
57
- runner = StromaRunner.quick()
58
79
 
59
- @runner.node("double", input=Input, output=Output)
60
- async def double(state: Input) -> dict:
61
- return {"result": state.value * 2}
80
+ class Extracted(BaseModel):
81
+ entities: list[str]
62
82
 
63
- result = await runner.run([double], Input(value=5))
64
- print(result.final_state) # result=10
65
- ```
66
83
 
67
- ## Install
84
+ class Summary(BaseModel):
85
+ entities: list[str]
86
+ count: int
68
87
 
69
- Requires **Python 3.12+**.
70
88
 
71
- ```bash
72
- uv add stroma
73
- ```
89
+ registry = ContractRegistry()
90
+ store = AsyncInMemoryStore()
91
+ manager = CheckpointManager(store)
74
92
 
75
- Optional extras:
93
+ c1 = NodeContract(node_id="extract", input_schema=Document, output_schema=Extracted)
94
+ c2 = NodeContract(node_id="summarize", input_schema=Extracted, output_schema=Summary)
95
+ registry.register(c1)
96
+ registry.register(c2)
76
97
 
77
- ```bash
78
- uv add stroma[redis] # Redis-backed checkpointing
79
- uv add stroma[langgraph] # LangGraph adapter
80
- uv add stroma[deepagents] # DeepAgents adapter
98
+
99
+ @stroma_node("extract", c1)
100
+ async def extract(state: Document) -> dict:
101
+ return {"entities": state.text.split()}
102
+
103
+
104
+ @stroma_node("summarize", c2)
105
+ async def summarize_failing(state: Extracted) -> dict:
106
+ raise TimeoutError("downstream API unavailable") # (1)!
107
+
108
+
109
+ @stroma_node("summarize", c2)
110
+ async def summarize_fixed(state: Extracted) -> dict:
111
+ return {"entities": state.entities, "count": len(state.entities)}
112
+
113
+
114
+ async def main():
115
+ config1 = RunConfig(run_id="doc-run-1")
116
+ runner1 = StromaRunner(registry, manager, config1)
117
+ result1 = await runner1.run(
118
+ [extract, summarize_failing],
119
+ Document(text="Stroma adds reliability to agent pipelines"),
120
+ )
121
+ print(result1.status) # PARTIAL — extract checkpointed, summarize exhausted retries
122
+
123
+ config2 = RunConfig(run_id="doc-run-1", resume_from="summarize") # (2)!
124
+ runner2 = StromaRunner(registry, manager, config2)
125
+ result2 = await runner2.run(
126
+ [extract, summarize_fixed],
127
+ Document(text="Stroma adds reliability to agent pipelines"),
128
+ )
129
+ print(result2.status) # RESUMED — extract skipped, loaded from checkpoint
130
+ print(result2.final_state) # entities=[...] count=6
131
+
132
+ diffs = result1.trace.diff(result2.trace) # (3)!
133
+ for d in diffs:
134
+ print(d)
135
+
136
+
137
+ asyncio.run(main())
138
+ # In a Jupyter notebook, replace the line above with: await main()
81
139
  ```
82
140
 
141
+ 1. `TimeoutError` is classified as `RECOVERABLE`. Stroma retries with jittered backoff. After exhausting retries, the run fails — but `extract`'s output is already checkpointed.
142
+ 2. Same `run_id`, `resume_from="summarize"`. The runner loads `extract`'s checkpoint and skips re-running it entirely.
143
+ 3. `diff()` compares both traces — node IDs, attempts, inputs, outputs, failure states — so you can see exactly what changed between the failed run and the successful one.
144
+
83
145
  ## What You Get
84
146
 
85
147
  - **Contracts** — Pydantic-based input/output validation at every node boundary
@@ -87,16 +149,36 @@ uv add stroma[deepagents] # DeepAgents adapter
87
149
  - **Retry policies** — configurable retries with jittered backoff, per failure class or per node
88
150
  - **Checkpointing** — async-first save and resume across crashes (in-memory or Redis)
89
151
  - **Cost estimation** — model-aware USD cost tracking via `KNOWN_MODELS` and token/dollar/latency budgets
90
- - **Parallel execution** — fan out work to concurrent nodes with `parallel()`, per-child contract validation, and merged output
152
+ - **Per-node timeouts** — configurable `node_timeouts` with `asyncio.wait_for`; timeouts are classified as recoverable and retried automatically
153
+ - **Parallel execution** — fan out work to concurrent nodes with `parallel()`, per-child contract validation, merged output, and full retry support
91
154
  - **Node hooks** — async `on_node_start`, `on_node_success`, and `on_node_failure` callbacks
92
155
  - **Shared context** — pass a mutable `context` dict through `RunConfig` to every node
93
156
  - **Execution tracing** — full record of every node attempt, with diffing and JSON export
94
157
  - **Per-run logging** — structured `LoggerAdapter` with `run_id` in every log line
95
158
  - **Fluent builder API** — configure runners with chained `.with_budget()`, `.with_hooks()`, `.with_redis()`, etc.
96
159
  - **LangGraph adapter** — apply contracts to existing LangGraph graphs
160
+ - **CrewAI adapter** — contract validation for CrewAI Flow methods
97
161
  - **DeepAgents adapter** — contract validation and cost tracking for deepagents graphs
162
+ - **Universal reliability middleware** — `execute_step()` and `StromaStep` apply contracts, retries, cost tracking, and checkpointing to any async callable, independent of any framework
98
163
  - **Framework-agnostic** — works with any async Python code, no framework lock-in
99
164
 
165
+ ## Install
166
+
167
+ Requires **Python 3.12+**.
168
+
169
+ ```bash
170
+ uv add stroma
171
+ ```
172
+
173
+ Optional extras:
174
+
175
+ ```bash
176
+ uv add stroma[redis] # Redis-backed checkpointing
177
+ uv add stroma[langgraph] # LangGraph adapter
178
+ uv add stroma[crewai] # CrewAI adapter
179
+ uv add stroma[deepagents] # DeepAgents adapter
180
+ ```
181
+
100
182
  ## Quick Examples
101
183
 
102
184
  ### Cost estimation
@@ -165,6 +247,17 @@ config = RunConfig(
165
247
  )
166
248
  ```
167
249
 
250
+ ### Per-node timeouts
251
+
252
+ Guard against hanging LLM calls with per-node timeouts. Timeouts raise `TimeoutError`, which is classified as recoverable and retried automatically:
253
+
254
+ ```python
255
+ runner = StromaRunner.quick().with_node_timeouts({
256
+ "llm_call": 30_000, # 30 seconds
257
+ "embedding": 10_000, # 10 seconds
258
+ })
259
+ ```
260
+
168
261
  ### Async checkpointing
169
262
 
170
263
  The default store is now async. For distributed pipelines, use the async Redis store:
stroma-0.3.2/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # Stroma
2
+
3
+ **dbt didn't replace your data warehouse. Stroma doesn't replace your agent framework.**
4
+
5
+ dbt gave you typed models, tested transformations, and documented lineage — a software engineering layer that worked regardless of which warehouse you were running. Stroma does the same thing for agent execution graphs: typed node contracts, formal failure classification, and cost-aware execution — portable across whatever orchestration framework you're building on.
6
+
7
+ The framework handles the graph. Stroma handles the guarantees.
8
+
9
+ ## The problem it solves
10
+
11
+ LLM pipelines fail in ways that traditional software doesn't. A node returns malformed data and the error surfaces three steps later. A transient timeout kills a 20-minute run and you start over from scratch. Costs spiral past budget with no enforcement mechanism. Failures are silent until they're catastrophic.
12
+
13
+ Stroma gives you the building blocks to handle this — without locking you into a framework.
14
+
15
+ ## See it in action
16
+
17
+ The scenario below is the kind that breaks raw LangGraph pipelines: a multi-step run that crashes midway, resumes from checkpoint, and gives you a diff of what changed between the failed and successful run.
18
+
19
+ ```python
20
+ import asyncio
21
+ from pydantic import BaseModel
22
+ from stroma import (
23
+ AsyncInMemoryStore,
24
+ CheckpointManager,
25
+ ContractRegistry,
26
+ NodeContract,
27
+ RunConfig,
28
+ StromaRunner,
29
+ stroma_node,
30
+ )
31
+
32
+
33
+ class Document(BaseModel):
34
+ text: str
35
+
36
+
37
+ class Extracted(BaseModel):
38
+ entities: list[str]
39
+
40
+
41
+ class Summary(BaseModel):
42
+ entities: list[str]
43
+ count: int
44
+
45
+
46
+ registry = ContractRegistry()
47
+ store = AsyncInMemoryStore()
48
+ manager = CheckpointManager(store)
49
+
50
+ c1 = NodeContract(node_id="extract", input_schema=Document, output_schema=Extracted)
51
+ c2 = NodeContract(node_id="summarize", input_schema=Extracted, output_schema=Summary)
52
+ registry.register(c1)
53
+ registry.register(c2)
54
+
55
+
56
+ @stroma_node("extract", c1)
57
+ async def extract(state: Document) -> dict:
58
+ return {"entities": state.text.split()}
59
+
60
+
61
+ @stroma_node("summarize", c2)
62
+ async def summarize_failing(state: Extracted) -> dict:
63
+ raise TimeoutError("downstream API unavailable") # (1)!
64
+
65
+
66
+ @stroma_node("summarize", c2)
67
+ async def summarize_fixed(state: Extracted) -> dict:
68
+ return {"entities": state.entities, "count": len(state.entities)}
69
+
70
+
71
+ async def main():
72
+ config1 = RunConfig(run_id="doc-run-1")
73
+ runner1 = StromaRunner(registry, manager, config1)
74
+ result1 = await runner1.run(
75
+ [extract, summarize_failing],
76
+ Document(text="Stroma adds reliability to agent pipelines"),
77
+ )
78
+ print(result1.status) # PARTIAL — extract checkpointed, summarize exhausted retries
79
+
80
+ config2 = RunConfig(run_id="doc-run-1", resume_from="summarize") # (2)!
81
+ runner2 = StromaRunner(registry, manager, config2)
82
+ result2 = await runner2.run(
83
+ [extract, summarize_fixed],
84
+ Document(text="Stroma adds reliability to agent pipelines"),
85
+ )
86
+ print(result2.status) # RESUMED — extract skipped, loaded from checkpoint
87
+ print(result2.final_state) # entities=[...] count=6
88
+
89
+ diffs = result1.trace.diff(result2.trace) # (3)!
90
+ for d in diffs:
91
+ print(d)
92
+
93
+
94
+ asyncio.run(main())
95
+ # In a Jupyter notebook, replace the line above with: await main()
96
+ ```
97
+
98
+ 1. `TimeoutError` is classified as `RECOVERABLE`. Stroma retries with jittered backoff. After exhausting retries, the run fails — but `extract`'s output is already checkpointed.
99
+ 2. Same `run_id`, `resume_from="summarize"`. The runner loads `extract`'s checkpoint and skips re-running it entirely.
100
+ 3. `diff()` compares both traces — node IDs, attempts, inputs, outputs, failure states — so you can see exactly what changed between the failed run and the successful one.
101
+
102
+ ## What You Get
103
+
104
+ - **Contracts** — Pydantic-based input/output validation at every node boundary
105
+ - **Failure classification** — automatic categorization of errors as recoverable, terminal, or ambiguous
106
+ - **Retry policies** — configurable retries with jittered backoff, per failure class or per node
107
+ - **Checkpointing** — async-first save and resume across crashes (in-memory or Redis)
108
+ - **Cost estimation** — model-aware USD cost tracking via `KNOWN_MODELS` and token/dollar/latency budgets
109
+ - **Per-node timeouts** — configurable `node_timeouts` with `asyncio.wait_for`; timeouts are classified as recoverable and retried automatically
110
+ - **Parallel execution** — fan out work to concurrent nodes with `parallel()`, per-child contract validation, merged output, and full retry support
111
+ - **Node hooks** — async `on_node_start`, `on_node_success`, and `on_node_failure` callbacks
112
+ - **Shared context** — pass a mutable `context` dict through `RunConfig` to every node
113
+ - **Execution tracing** — full record of every node attempt, with diffing and JSON export
114
+ - **Per-run logging** — structured `LoggerAdapter` with `run_id` in every log line
115
+ - **Fluent builder API** — configure runners with chained `.with_budget()`, `.with_hooks()`, `.with_redis()`, etc.
116
+ - **LangGraph adapter** — apply contracts to existing LangGraph graphs
117
+ - **CrewAI adapter** — contract validation for CrewAI Flow methods
118
+ - **DeepAgents adapter** — contract validation and cost tracking for deepagents graphs
119
+ - **Universal reliability middleware** — `execute_step()` and `StromaStep` apply contracts, retries, cost tracking, and checkpointing to any async callable, independent of any framework
120
+ - **Framework-agnostic** — works with any async Python code, no framework lock-in
121
+
122
+ ## Install
123
+
124
+ Requires **Python 3.12+**.
125
+
126
+ ```bash
127
+ uv add stroma
128
+ ```
129
+
130
+ Optional extras:
131
+
132
+ ```bash
133
+ uv add stroma[redis] # Redis-backed checkpointing
134
+ uv add stroma[langgraph] # LangGraph adapter
135
+ uv add stroma[crewai] # CrewAI adapter
136
+ uv add stroma[deepagents] # DeepAgents adapter
137
+ ```
138
+
139
+ ## Quick Examples
140
+
141
+ ### Cost estimation
142
+
143
+ Nodes can return token counts and a model name. Stroma computes USD cost automatically from built-in pricing data:
144
+
145
+ ```python
146
+ @runner.node("summarize", input=DocInput, output=Summary)
147
+ async def summarize(state: DocInput) -> tuple:
148
+ # call your LLM here...
149
+ return ({"text": response}, input_tokens, output_tokens, "gpt-4o")
150
+ ```
151
+
152
+ ### Parallel execution
153
+
154
+ Run independent nodes concurrently and merge their outputs:
155
+
156
+ ```python
157
+ from stroma import parallel
158
+
159
+ result = await runner.run(
160
+ [parallel(fetch_metadata, fetch_embeddings), merge_node],
161
+ initial_state,
162
+ )
163
+ ```
164
+
165
+ ### Node hooks
166
+
167
+ Attach lifecycle callbacks to observe node execution:
168
+
169
+ ```python
170
+ from stroma import NodeHooks, RunConfig
171
+
172
+ async def on_start(run_id, node_id, input_dict):
173
+ print(f"Starting {node_id}")
174
+
175
+ config = RunConfig(hooks=NodeHooks(on_node_start=on_start))
176
+ ```
177
+
178
+ ### Shared context
179
+
180
+ Pass runtime configuration to nodes that accept a second argument:
181
+
182
+ ```python
183
+ @runner.node("enrich", input=Input, output=Output)
184
+ async def enrich(state: Input, ctx: dict) -> dict:
185
+ api_key = ctx["api_key"]
186
+ # ...
187
+
188
+ config = RunConfig(context={"api_key": "sk-..."})
189
+ ```
190
+
191
+ ### Per-node retry policies
192
+
193
+ Override the global retry policy for specific nodes:
194
+
195
+ ```python
196
+ from stroma import FailureClass, FailurePolicy
197
+
198
+ config = RunConfig(
199
+ node_policies={
200
+ "flaky_node": {
201
+ FailureClass.RECOVERABLE: FailurePolicy(max_retries=5, backoff_seconds=2.0),
202
+ }
203
+ }
204
+ )
205
+ ```
206
+
207
+ ### Per-node timeouts
208
+
209
+ Guard against hanging LLM calls with per-node timeouts. Timeouts raise `TimeoutError`, which is classified as recoverable and retried automatically:
210
+
211
+ ```python
212
+ runner = StromaRunner.quick().with_node_timeouts({
213
+ "llm_call": 30_000, # 30 seconds
214
+ "embedding": 10_000, # 10 seconds
215
+ })
216
+ ```
217
+
218
+ ### Async checkpointing
219
+
220
+ The default store is now async. For distributed pipelines, use the async Redis store:
221
+
222
+ ```python
223
+ from stroma import RedisStore, CheckpointManager
224
+
225
+ store = RedisStore("redis://localhost:6379", ttl_seconds=7200)
226
+ manager = CheckpointManager(store)
227
+ ```
228
+
229
+ The original synchronous Redis store is still available as `SyncRedisStore`.
230
+
231
+ ## Documentation
232
+
233
+ Full documentation including a tutorial and API reference is available at the [docs site](https://jengroff.github.io/stroma).
234
+
235
+ ## Development
236
+
237
+ ```bash
238
+ uv sync --extra dev
239
+ uv run pytest tests/ -v --cov=stroma --cov-fail-under=85
240
+ ```
241
+
242
+ ## License
243
+
244
+ MIT
@@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stroma"
7
- version = "0.2.7"
8
- description = "Reliability primitives for agent pipelines."
7
+ version = "0.3.2"
8
+ description = "The framework handles the graph. Stroma handles the guarantees."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
  dependencies = [
12
+ "crewai>=1.13.0",
12
13
  "pydantic>=2.0",
13
14
  ]
14
15
  license = "MIT"
@@ -33,6 +34,7 @@ Issues = "https://github.com/jengroff/stroma/issues"
33
34
  [project.optional-dependencies]
34
35
  redis = ["redis>=5.0"]
35
36
  langgraph = ["langgraph>=0.2"]
37
+ crewai = ["crewai>=0.70"]
36
38
  deepagents = ["deepagents>=0.4.0", "langgraph>=0.2"]
37
39
  dev = ["pytest", "pytest-asyncio>=0.23", "pytest-cov", "ruff", "ty"]
38
40
  docs = ["mkdocs>=1.6.1", "mkdocs-material", "mkdocstrings[python]"]
@@ -7,15 +7,25 @@ from stroma.checkpoint import (
7
7
  RedisStore,
8
8
  SyncRedisStore,
9
9
  )
10
- from stroma.contracts import BoundaryValidator, ContractRegistry, ContractViolation, NodeContract
10
+ from stroma.contracts import (
11
+ BoundaryValidator,
12
+ ContractRegistry,
13
+ ContractViolation,
14
+ NodeContract,
15
+ StepContract,
16
+ StepViolation,
17
+ )
11
18
  from stroma.cost import (
12
19
  KNOWN_MODELS,
13
20
  BudgetExceeded,
14
21
  CostTracker,
15
22
  ExecutionBudget,
23
+ FallbackPolicy,
16
24
  ModelHint,
17
25
  NodeUsage,
26
+ StepUsage,
18
27
  estimate_cost_usd,
28
+ resolve_model,
19
29
  )
20
30
  from stroma.failures import (
21
31
  Classifier,
@@ -23,13 +33,15 @@ from stroma.failures import (
23
33
  FailurePolicy,
24
34
  NodeContext,
25
35
  RetryBudget,
36
+ StepContext,
26
37
  classify,
27
38
  default_policy_map,
28
39
  )
29
- from stroma.runner import NodeHooks, RunConfig, StromaRunner, parallel, stroma_node
40
+ from stroma.middleware import ReliabilityContext, StromaStep, execute_step
41
+ from stroma.runner import NodeHooks, RunConfig, StepHooks, StromaRunner, parallel, stroma_node, stroma_step
30
42
  from stroma.trace import ExecutionResult, ExecutionTrace, RunStatus, TraceEvent
31
43
 
32
- __version__ = "0.2.7"
44
+ __version__ = "0.3.2"
33
45
 
34
46
  __all__ = [
35
47
  "KNOWN_MODELS",
@@ -48,6 +60,7 @@ __all__ = [
48
60
  "ExecutionTrace",
49
61
  "FailureClass",
50
62
  "FailurePolicy",
63
+ "FallbackPolicy",
51
64
  "InMemoryStore",
52
65
  "ModelHint",
53
66
  "NodeContext",
@@ -55,15 +68,25 @@ __all__ = [
55
68
  "NodeHooks",
56
69
  "NodeUsage",
57
70
  "RedisStore",
71
+ "ReliabilityContext",
58
72
  "RetryBudget",
59
73
  "RunConfig",
60
74
  "RunStatus",
75
+ "StepContext",
76
+ "StepContract",
77
+ "StepHooks",
78
+ "StepUsage",
79
+ "StepViolation",
61
80
  "StromaRunner",
81
+ "StromaStep",
62
82
  "SyncRedisStore",
63
83
  "TraceEvent",
64
84
  "classify",
65
85
  "default_policy_map",
66
86
  "estimate_cost_usd",
87
+ "execute_step",
67
88
  "parallel",
89
+ "resolve_model",
68
90
  "stroma_node",
91
+ "stroma_step",
69
92
  ]
@@ -7,3 +7,7 @@ with contextlib.suppress(ImportError):
7
7
  with contextlib.suppress(ImportError):
8
8
  from stroma.adapters.deepagents import DeepAgentsAdapter as DeepAgentsAdapter
9
9
  from stroma.adapters.deepagents import stroma_deepagents_node as stroma_deepagents_node
10
+
11
+ with contextlib.suppress(ImportError):
12
+ from stroma.adapters.crewai import CrewAIAdapter as CrewAIAdapter
13
+ from stroma.adapters.crewai import stroma_crewai_step as stroma_crewai_step
@@ -0,0 +1,100 @@
1
+ from collections.abc import Callable
2
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from stroma.contracts import NodeContract
7
+
8
+ if TYPE_CHECKING:
9
+ from stroma.middleware import ReliabilityContext
10
+
11
+
12
+ @runtime_checkable
13
+ class FrameworkAdapter(Protocol):
14
+ """Protocol for graph-based framework adapters (LangGraph, CrewAI Flows).
15
+
16
+ Implementations provide two capabilities: wrapping an entire framework
17
+ graph with contract validation, and producing a decorator that attaches
18
+ stroma metadata to individual node functions.
19
+ """
20
+
21
+ def wrap(self, graph: Any) -> Any:
22
+ """Attach contract validation to a framework graph or flow instance."""
23
+ ...
24
+
25
+ def node_decorator(self, node_id: str, contract: NodeContract) -> Callable[..., Any]:
26
+ """Return a decorator that attaches stroma metadata to a framework node function."""
27
+ ...
28
+
29
+
30
+ @runtime_checkable
31
+ class StepInterceptor(Protocol):
32
+ """Protocol for adapters that wrap individual step functions with reliability middleware.
33
+
34
+ Unlike `FrameworkAdapter` (which discovers and wraps nodes on a graph
35
+ object), a `StepInterceptor` wraps one function at a time. This is the
36
+ right shape for paradigms where there is no single graph object to wrap
37
+ — e.g. composing steps in a notebook, a custom orchestrator, or a
38
+ framework that doesn't expose an inspectable graph.
39
+ """
40
+
41
+ def wrap_step(
42
+ self, step_id: str, func: Callable[..., Any], contract: NodeContract | None = None
43
+ ) -> Callable[..., Any]:
44
+ """Wrap *func* with reliability instrumentation for the given *step_id*."""
45
+ ...
46
+
47
+
48
+ @runtime_checkable
49
+ class LoopAdapter(Protocol):
50
+ """Protocol for agentic-loop paradigms (Claude Agent SDK, OpenAI Agents SDK).
51
+
52
+ In an agentic loop the model decides what to do next via tool calls.
53
+ The adapter intercepts tool call boundaries to apply contract validation,
54
+ cost tracking, and failure handling.
55
+ """
56
+
57
+ def on_tool_call(self, tool_name: str, args: dict[str, Any], ctx: "ReliabilityContext") -> dict[str, Any]:
58
+ """Intercept an outbound tool call before execution."""
59
+ ...
60
+
61
+ def on_tool_result(self, tool_name: str, result: Any, ctx: "ReliabilityContext") -> Any:
62
+ """Intercept a tool result after execution."""
63
+ ...
64
+
65
+
66
+ @runtime_checkable
67
+ class TurnAdapter(Protocol):
68
+ """Protocol for conversation-driven paradigms (AutoGen).
69
+
70
+ In a conversation-driven system, agents exchange messages in turns.
71
+ The adapter intercepts turn boundaries to apply reliability primitives.
72
+ """
73
+
74
+ def on_turn_start(self, turn_id: str, message: Any, ctx: "ReliabilityContext") -> Any:
75
+ """Intercept the start of a conversation turn."""
76
+ ...
77
+
78
+ def on_turn_end(self, turn_id: str, result: Any, ctx: "ReliabilityContext") -> Any:
79
+ """Intercept the end of a conversation turn."""
80
+ ...
81
+
82
+
83
+ def extract_state_dict(state: Any) -> dict[str, Any]:
84
+ """Convert a framework state object to a plain dict.
85
+
86
+ Supports dicts, Pydantic models, objects with a `.dict()` method,
87
+ and plain objects with `__dict__`. Raises `TypeError` if the state
88
+ cannot be converted.
89
+ """
90
+ if isinstance(state, dict):
91
+ return dict(state)
92
+ if isinstance(state, BaseModel):
93
+ return state.model_dump()
94
+ if hasattr(state, "dict"):
95
+ result = state.dict()
96
+ if isinstance(result, dict):
97
+ return result
98
+ if hasattr(state, "__dict__"):
99
+ return {k: v for k, v in vars(state).items() if not k.startswith("_")}
100
+ raise TypeError("Unable to extract state dict from state object")