ask-graph 0.7.1 → 0.7.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f25d9d9ca750cdb41cc6209ac0adf69346fc3da74a4a2a1cde787e27b13c81c3
4
- data.tar.gz: 521f8841441fdf6efa497502588c772ace146585aa8ae1221f735d6838ee0429
3
+ metadata.gz: d76192a260425897adec826a504bcdf81b773abfd1d5d479945ad9e39dfa2387
4
+ data.tar.gz: d368fbc3f35957b443ab9f5795824b8715633c0ea77ca74484aeb8f9f96e3a6b
5
5
  SHA512:
6
- metadata.gz: b23119776fe5892d3574c5fe0a9721742128b52a6205ee8d2b1fd5c457b9dc7b13a1a0dddc8a99bfda9d533e58a12dd4045f125b42c36b7b4f3e385b5006e5e0
7
- data.tar.gz: 40943d5fc7cf5e09ad3cc6d676220141b93e88d8995b178a11a3a4130c12d5c9d25126d1b9602f53d4d313cb71d01f3313e99aecc2a7afa0af77f25df92fa2bb
6
+ metadata.gz: e963722c00c2fb40bcdaf05d9686a2aea00178c9847db2e1f6feaaf44f17c2917def642315846b8043f6826dab6e0e9c21f27951c8ad09afbaaa1961e5de0a89
7
+ data.tar.gz: c4751c4291f6f07c3cca869dbf368526701a8de74a0c2bfc358e5d3c4d37d75103e8b4d8a2581ffd2ff068760503f32c7bf97c29e7912acc047677b58d18cdf3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.7.2] - 2026-08-04
2
+
3
+ ### Fixed
4
+
5
+ - **Parallel steps inherit the caller's thread-local state.** `Runner#run_parallel` now copies `Thread.current` locals into each worker thread, so Rails `CurrentAttributes` and similar per-request context reach parallel steps instead of being nil.
6
+
1
7
  ## [0.7.1] — 2026-07-31
2
8
 
3
9
  ### Added
data/README.md CHANGED
@@ -1,35 +1,42 @@
1
1
  # ask-graph
2
2
 
3
- **Durable workflow graphs for the ask-rb ecosystem.** Define workflows as graphs of named steps with conditional routing, parallel execution, human-in-the-loop approval, per-item checkpointing loops, and sub-graph composition. Each step is a plain Ruby class.
3
+ Durable workflow graphs for the ask-rb ecosystem. Define workflows as graphs
4
+ of named steps with conditional routing, parallel execution, human-in-the-loop
5
+ approval, per-item checkpointing loops, and sub-graph composition. Each step
6
+ is a plain Ruby class with a `call(context)` method.
7
+
8
+ ask-graph is for deterministic pipelines: you declare the steps and the
9
+ routing. For LLM-driven loops that decide their own next step, use
10
+ ask-agent instead.
11
+
12
+ ## Installation
4
13
 
5
14
  ```ruby
6
15
  gem "ask-graph"
7
16
  ```
8
17
 
9
- ## Quick Start
10
-
11
- ```ruby
12
- require "ask-graph"
13
-
14
- # Define a workflow
15
- module ProcessOrder
16
- class Workflow < Ask::Graph
17
- step ValidateOrder
18
- step ChargeCustomer, if: :valid?
19
- step SendConfirmation, if: :valid?
20
- step NotifyAdmin, unless: :valid?
21
-
22
- private
23
-
24
- def valid?
25
- context.order.valid?
26
- end
27
- end
28
- end
29
-
30
- # Run it
31
- result = ProcessOrder::Workflow.call(order: order)
32
- ```
18
+ ## Quick Start
19
+
20
+ ```ruby
21
+ require "ask-graph"
22
+
23
+ module ProcessOrder
24
+ class Workflow < Ask::Graph
25
+ step ValidateOrder
26
+ step ChargeCustomer, if: :valid?
27
+ step SendConfirmation, if: :valid?
28
+ step NotifyAdmin, unless: :valid?
29
+
30
+ private
31
+
32
+ def valid?
33
+ context.order.valid?
34
+ end
35
+ end
36
+ end
37
+
38
+ result = ProcessOrder::Workflow.call(order: order)
39
+ ```
33
40
 
34
41
  ## Steps
35
42
 
@@ -43,188 +50,74 @@ class ValidateOrder
43
50
  end
44
51
  ```
45
52
 
46
- ### Sequential steps
47
-
48
- ```ruby
49
- module HandleCall
50
- class Workflow < Ask::Graph
51
- step Transcribe
52
- step Classify
53
- step Respond
54
- end
55
- end
56
- ```
57
-
58
- ### Conditional steps
59
-
60
- ```ruby
61
- module HandleCall
62
- class Workflow < Ask::Graph
63
- step BookAppointment, if: :booking?
64
- step EmergencyAlert, if: :emergency?
65
- step HandleInquiry, unless: :known_intent?
66
-
67
- private
68
-
69
- def booking? = context.intent == "booking"
70
- def emergency? = context.intent == "emergency"
71
- def known_intent? = %w[booking emergency inquiry].include?(context.intent)
72
- end
73
- end
74
- ```
75
-
76
- ### Parallel steps
77
-
78
- Use `steps` (plural) to run multiple steps simultaneously:
79
-
80
- ```ruby
81
- module SyncData
82
- class Workflow < Ask::Graph
83
- step FetchRecords
84
-
85
- # All three run in parallel
86
- steps CrmUpdate, CalendarSync, SendNotification
87
-
88
- step ConfirmResponse
89
- end
90
- end
91
- ```
92
-
93
- ### Human-in-the-loop (approve)
94
-
95
- `approve` runs a step, then pauses the workflow and waits for external input:
96
-
97
- ```ruby
98
- module ProcessBooking
99
- class Workflow < Ask::Graph
100
- step BookAppointment
101
- approve ReviewBooking, if: :expensive?
102
- step ConfirmBooking
103
- end
104
- end
105
- ```
106
-
107
- After `approve` pauses, resume with:
108
-
109
- ```ruby
110
- graph = ProcessBooking::Workflow.new
111
- result = graph.run # runs, pauses after ReviewBooking
112
- result = graph.resume(input: "approved") # resumes, runs ConfirmBooking
113
- ```
114
-
115
- ### Per-item loops
116
-
117
- Use `context.each` inside a step for iteration with automatic checkpointing:
118
-
119
- ```ruby
120
- class SendVoiceReminders
121
- def call(context)
122
- context.each(context.appointments) do |appt|
123
- PhoneService.call(appt.number, appt.message)
124
- end
125
- end
126
- end
127
-
128
- module DailyReminders
129
- class Workflow < Ask::Graph
130
- step FetchAppointments
131
- step SendVoiceReminders # loops with per-item checkpointing
132
- step MarkComplete
133
- end
134
- end
135
- ```
53
+ | Declaration | Behavior |
54
+ |---|---|
55
+ | `step Klass` | Run a step in order |
56
+ | `step Klass, if: :method?` / `unless: :method?` | Conditional routing |
57
+ | `step Klass, timeout: 30, retry: 3` | Per-step timeout and retry |
58
+ | `step Klass, description: "..."` | Human-readable label |
59
+ | `steps A, B, C` | Run multiple steps in parallel |
60
+ | `approve Klass` | Run the step, then pause and wait for external input |
136
61
 
137
- If the process crashes mid-loop, it resumes from the last completed item.
62
+ A paused workflow resumes with `graph.resume(input: "approved")` after
63
+ `graph.run`.
138
64
 
139
65
  ### Context
140
66
 
141
- Shared state flows between steps via `context`:
67
+ Shared state flows between steps. Set and read values by method or bracket,
68
+ iterate with per-item checkpointing, and run sub-workflows:
142
69
 
143
70
  ```ruby
144
- class SetValue
71
+ class SendVoiceReminders
145
72
  def call(context)
146
- context.greeting = "hello" # set via method
147
- context[:color] = "blue" # set via bracket
148
- end
149
- end
150
-
151
- class ReadValue
152
- def call(context)
153
- puts context.greeting # => "hello"
154
- puts context[:color] # => "blue"
155
- end
156
- end
157
- ```
158
-
159
- ### Sub-workflows
73
+ context.greeting = "hello" # method access
74
+ context[:color] = "blue" # bracket access
160
75
 
161
- A step can delegate to another workflow by calling `Workflow.call(context)`.
162
- The context is automatically exported, passed to the sub-workflow, and
163
- results are merged back on completion:
164
-
165
- ```ruby
166
- module OrderFulfillment
167
- class NotifyCustomer
168
- def call(context)
169
- NotifyCustomer::Workflow.call(context)
76
+ context.each(context.appointments) do |appt| # per-item checkpointing
77
+ PhoneService.call(appt.number, appt.message)
170
78
  end
171
- end
172
79
 
173
- class Workflow < Ask::Graph
174
- step ValidatePayment
175
- step NotifyCustomer
176
- step ShipOrder
80
+ context.run(NotifyCustomer::Workflow) # sub-workflow
177
81
  end
178
82
  end
179
83
  ```
180
84
 
181
- Or use `context.run` for the same thing:
182
-
183
- ```ruby
184
- class NotifyCustomer
185
- def call(context)
186
- context.run(NotifyCustomer::Workflow)
187
- end
188
- end
189
- ```
85
+ If the process crashes mid-loop, it resumes from the last completed item.
190
86
 
191
- ### Checkpointing
87
+ ## Storage and Checkpointing
192
88
 
193
- Pass a `storage` backend to make workflows durable across crashes:
89
+ Pass a storage backend to make workflows durable across crashes. The default
90
+ is `Ask::State::Memory`; any backend from ask-state-providers works:
194
91
 
195
92
  ```ruby
196
93
  store = Ask::State::Memory.new # or Redis, SQLite, etc.
197
94
 
198
- # Runs through all steps, saving after each
199
- result = OrderFulfillment::Workflow.call(input, storage: store)
200
-
201
- # If a crash occurs, resume from the last completed step
95
+ # Runs through all steps, saving after each; a later call resumes
202
96
  result = OrderFulfillment::Workflow.call(input, storage: store)
203
97
  ```
204
98
 
205
- ### Error handling
99
+ `Ask::Graph.storage(store)` sets a class-level default for the workflow.
206
100
 
207
- Failed steps raise `Ask::Graph::StepFailed` with the step name:
101
+ ## Errors and Hooks
208
102
 
209
- ```ruby
210
- module MyWorkflow
211
- class Workflow < Ask::Graph
212
- step RiskyOperation
213
- end
214
- end
103
+ Failed steps raise `Ask::Graph::StepFailed`; timeouts raise `StepTimeout`,
104
+ `WorkflowTimeout`, and approval pauses raise `Paused`.
215
105
 
216
- begin
217
- MyWorkflow::Workflow.call
218
- rescue Ask::Graph::StepFailed => e
219
- puts e.message # => "RiskyOperation failed: connection timeout"
220
- end
221
- ```
106
+ Declare lifecycle hooks with `before_step :method`, `after_step :method`, and
107
+ `on_failure :method`. Set timeouts at the class level with `step_timeout` (or
108
+ its alias `default_step_timeout`) and `workflow_timeout`.
109
+
110
+ ## Full documentation
111
+
112
+ The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
113
+ https://ask-rb.github.io/ask-docs/core/graph covers ask-graph in depth,
114
+ including crash recovery, inheritance, and the full configuration reference.
115
+ API reference: https://ask-rb.github.io/ask-docs/reference/api.
222
116
 
223
117
  ## Development
224
118
 
225
- ```bash
119
+ bundle install
226
120
  bundle exec rake test
227
- ```
228
121
 
229
122
  ## License
230
123
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "timeout"
5
+ require "time"
5
6
 
6
7
  module Ask
7
8
  class Graph
@@ -158,8 +159,15 @@ module Ask
158
159
  end
159
160
 
160
161
  def run_parallel(classes, _name, context)
162
+ # Inherit the caller's thread-local state (Rails CurrentAttributes
163
+ # and similar frameworks store per-request context in Thread.current)
164
+ # so parallel steps see the same context as sequential ones.
165
+ inherited_locals = {}
166
+ Thread.current.keys.each { |key| inherited_locals[key] = Thread.current[key] }
167
+
161
168
  threads = classes.map do |klass|
162
169
  Thread.new do
170
+ inherited_locals.each { |key, value| Thread.current[key] = value }
163
171
  instance = klass.new
164
172
  instance.call(context)
165
173
  rescue => e
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  class Graph
5
- VERSION = "0.7.1"
5
+ VERSION = "0.7.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto