ask-graph 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2bcc4cd34c3c6b401b4a5e6e47ea28bfeb8fcfe6cc117c46230ddf58df6249f8
4
+ data.tar.gz: 20bec4635a80dd53b1b497c38985327efcef79a7acace61d8725fac85842e2ce
5
+ SHA512:
6
+ metadata.gz: a122a01aa42024e4be602f8fe859e0b3148867c248e36d434e86b63ad62561b53ea081cc1903c033d6aa603b89e232d981b94fda01b1f6fcf09356b31ef801eb
7
+ data.tar.gz: 96d289a9595599a663da22cef7357af63dc74dde4a6c399589eea339441c5a32f7b49ff77f82bfbf1232e1f548b6c3ae85aa0dd4bd2b6e430ff26a3d689d28fd
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ ## [0.1.0] — 2026-07-28
2
+
3
+ ### Added
4
+
5
+ - **Initial release** — Durable workflow graphs for the ask-rb ecosystem.
6
+
7
+ - **Graph definition** — Define workflows with declarative steps:
8
+
9
+ ```ruby
10
+ class HandleCall < Ask::Graph
11
+ step Transcribe
12
+ step Classify, if: :needs_classification?
13
+ step BookAppointment
14
+ end
15
+ ```
16
+
17
+ - **Conditional steps** — `step ClassName, if: :method?` and `step ClassName, unless: :method?` — methods on the graph class control whether a step runs.
18
+
19
+ - **Parallel execution** — `steps ClassA, ClassB, ClassC` runs multiple steps simultaneously and waits for all to complete before continuing.
20
+
21
+ - **Human-in-the-loop** — `approve ReviewBooking` runs a step then pauses execution, persisting state and waiting for external input via `resume`.
22
+
23
+ - **Per-item checkpointing** — `context.each(items)` iterates with automatic checkpointing after each item. Crashes resume from the last completed item, not the beginning.
24
+
25
+ - **Step checkpointing** — every completed step is persisted via an optional store. Crashes resume from the last completed step.
26
+
27
+ - **Step failure handling** — failed steps raise `Ask::Graph::StepFailed` with the step name in the message.
28
+
29
+ - **Inheritance** — child graphs inherit parent steps without affecting the parent.
30
+
31
+ - **Class and instance execution** — `Graph.run` (class method) or `Graph.new.run` (instance method).
32
+
33
+ ### Tested
34
+
35
+ - 31 tests, 42 assertions, 0 failures
36
+ - Covers: step order, conditions, parallel, approve, checkpointing, context, inheritance, edge cases
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaka Ruto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # ask-graph
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.
4
+
5
+ ```ruby
6
+ gem "ask-graph"
7
+ ```
8
+
9
+ ## Quick Start
10
+
11
+ ```ruby
12
+ require "ask-graph"
13
+
14
+ # Define a workflow
15
+ class ProcessOrder < Ask::Graph
16
+ step ValidateOrder
17
+ step ChargeCustomer, if: :valid?
18
+ step SendConfirmation, if: :valid?
19
+ step NotifyAdmin, unless: :valid?
20
+
21
+ private
22
+
23
+ def valid?
24
+ context.order.valid?
25
+ end
26
+ end
27
+
28
+ # Run it
29
+ result = ProcessOrder.run(order: order)
30
+ ```
31
+
32
+ ## Steps
33
+
34
+ Steps are plain Ruby classes with a `call(context)` method:
35
+
36
+ ```ruby
37
+ class ValidateOrder
38
+ def call(context)
39
+ context.order = OrderValidator.validate(context.order)
40
+ end
41
+ end
42
+ ```
43
+
44
+ ### Sequential steps
45
+
46
+ ```ruby
47
+ class HandleCall < Ask::Graph
48
+ step Transcribe
49
+ step Classify
50
+ step Respond
51
+ end
52
+ ```
53
+
54
+ ### Conditional steps
55
+
56
+ ```ruby
57
+ class HandleCall < Ask::Graph
58
+ step BookAppointment, if: :booking?
59
+ step EmergencyAlert, if: :emergency?
60
+ step HandleInquiry, unless: :known_intent?
61
+
62
+ private
63
+
64
+ def booking? = context.intent == "booking"
65
+ def emergency? = context.intent == "emergency"
66
+ def known_intent? = %w[booking emergency inquiry].include?(context.intent)
67
+ end
68
+ ```
69
+
70
+ ### Parallel steps
71
+
72
+ Use `steps` (plural) to run multiple steps simultaneously:
73
+
74
+ ```ruby
75
+ class SyncData < Ask::Graph
76
+ step FetchRecords
77
+
78
+ # All three run in parallel
79
+ steps CrmUpdate, CalendarSync, SendNotification
80
+
81
+ step ConfirmResponse
82
+ end
83
+ ```
84
+
85
+ ### Human-in-the-loop (approve)
86
+
87
+ `approve` runs a step, then pauses the workflow and waits for external input:
88
+
89
+ ```ruby
90
+ class ProcessBooking < Ask::Graph
91
+ step BookAppointment
92
+ approve ReviewBooking, if: :expensive?
93
+ step ConfirmBooking
94
+ end
95
+ ```
96
+
97
+ After `approve` pauses, resume with:
98
+
99
+ ```ruby
100
+ graph = ProcessBooking.new
101
+ result = graph.run # runs, pauses after ReviewBooking
102
+ result = graph.resume(input: "approved") # resumes, runs ConfirmBooking
103
+ ```
104
+
105
+ ### Per-item loops
106
+
107
+ Use `context.each` inside a step for iteration with automatic checkpointing:
108
+
109
+ ```ruby
110
+ class SendVoiceReminders
111
+ def call(context)
112
+ context.each(context.appointments) do |appt|
113
+ PhoneService.call(appt.number, appt.message)
114
+ end
115
+ end
116
+ end
117
+
118
+ class DailyReminders < Ask::Graph
119
+ step FetchAppointments
120
+ step SendVoiceReminders # loops with per-item checkpointing
121
+ step MarkComplete
122
+ end
123
+ ```
124
+
125
+ If the process crashes mid-loop, it resumes from the last completed item.
126
+
127
+ ### Context
128
+
129
+ Shared state flows between steps via `context`:
130
+
131
+ ```ruby
132
+ class SetValue
133
+ def call(context)
134
+ context.greeting = "hello" # set via method
135
+ context[:color] = "blue" # set via bracket
136
+ end
137
+ end
138
+
139
+ class ReadValue
140
+ def call(context)
141
+ puts context.greeting # => "hello"
142
+ puts context[:color] # => "blue"
143
+ end
144
+ end
145
+ ```
146
+
147
+ ### Sub-workflows
148
+
149
+ A step can delegate to another workflow:
150
+
151
+ ```ruby
152
+ class IntentHandler
153
+ def call(context)
154
+ case context.intent
155
+ when "booking"
156
+ BookingWorkflow.new.call(context) # sub-workflow as a step
157
+ when "emergency"
158
+ EmergencyWorkflow.new.call(context)
159
+ end
160
+ end
161
+ end
162
+ ```
163
+
164
+ ### Checkpointing
165
+
166
+ Pass a checkpoint store to make workflows durable across crashes:
167
+
168
+ ```ruby
169
+ store = Ask::State::Memory.new # or Redis, SQLite, etc.
170
+
171
+ # Runs through all steps, saving after each
172
+ result = MyWorkflow.run(input, checkpoint_store: store)
173
+
174
+ # If a crash occurs, resume from the last completed step
175
+ result = MyWorkflow.run(input, checkpoint_store: store)
176
+ ```
177
+
178
+ ### Error handling
179
+
180
+ Failed steps raise `Ask::Graph::StepFailed` with the step name:
181
+
182
+ ```ruby
183
+ class MyGraph < Ask::Graph
184
+ step RiskyOperation
185
+ end
186
+
187
+ begin
188
+ MyGraph.run
189
+ rescue Ask::Graph::StepFailed => e
190
+ puts e.message # => "RiskyOperation failed: connection timeout"
191
+ end
192
+ ```
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ bundle exec rake test
198
+ ```
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ class Graph
5
+ # Shared state that flows through every step of a graph execution.
6
+ #
7
+ # Stores input, output, and intermediate data produced by steps.
8
+ # Accessed by step classes via +context.data_key+ or +context[:data_key]+.
9
+ # Thread-safe by design — each context is scoped to one graph run.
10
+ #
11
+ # @example
12
+ # class MyStep
13
+ # def call(context)
14
+ # context.transcript = "Hello"
15
+ # context[:classification] = "booking"
16
+ # end
17
+ # end
18
+ #
19
+ class Context
20
+ # @return [Ask::Graph] the graph this context belongs to
21
+ attr_reader :graph
22
+
23
+ # @return [Object, nil] the current item when iterating via {#each}
24
+ attr_reader :item
25
+
26
+ def initialize(graph, input = nil)
27
+ @graph = graph
28
+ @store = {}
29
+ @each_index = nil
30
+ @each_items = nil
31
+ @item = nil
32
+ @mutex = Mutex.new
33
+ @store[:input] = input if input
34
+ end
35
+
36
+ # Access a stored value by method name.
37
+ def method_missing(name, *args, &block)
38
+ if name.to_s.end_with?("=")
39
+ key = name.to_s.chomp("=").to_sym
40
+ @mutex.synchronize { @store[key] = args.first }
41
+ elsif args.empty? && !block
42
+ @mutex.synchronize { @store[name] }
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ def respond_to_missing?(name, include_private = false)
49
+ clean = name.to_s.sub(/=$/, "").to_sym
50
+ @store.key?(clean) || super
51
+ end
52
+
53
+ # Access a stored value by key.
54
+ # @param key [Symbol] the key
55
+ # @return [Object, nil]
56
+ def [](key)
57
+ @mutex.synchronize { @store[key.to_sym] }
58
+ end
59
+
60
+ # Store a value by key.
61
+ # @param key [Symbol] the key
62
+ # @param value [Object] the value
63
+ def []=(key, value)
64
+ @mutex.synchronize { @store[key.to_sym] = value }
65
+ end
66
+
67
+ # Iterate over items with per-item checkpointing.
68
+ #
69
+ # Each iteration is checkpointed so that if the process crashes,
70
+ # it resumes from the last completed item rather than starting over.
71
+ #
72
+ # @param items [Array] the items to iterate over
73
+ # @yield [item] called once per item
74
+ # @yieldparam item [Object] the current item
75
+ def each(items, &block)
76
+ @each_index = graph.runner.resume_index_for(items) || 0
77
+ @each_items = items
78
+
79
+ items.each_with_index do |item, idx|
80
+ break if idx < @each_index
81
+
82
+ @item = item
83
+ block.call(item)
84
+ graph.runner.checkpoint_each!(idx)
85
+ end
86
+ ensure
87
+ @each_index = nil
88
+ @each_items = nil
89
+ @item = nil
90
+ end
91
+
92
+ # @return [Hash] serializable snapshot of the context state
93
+ def to_h
94
+ @mutex.synchronize do
95
+ serializable = {}
96
+ @store.each do |k, v|
97
+ serializable[k] = v.respond_to?(:to_h) ? v.to_h : v
98
+ end
99
+ serializable
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ class Graph
7
+ # Error raised when a step fails.
8
+ class StepFailed < StandardError; end
9
+
10
+ # Error raised when a step pauses for human approval.
11
+ class Paused < StandardError; end
12
+
13
+ # Executes the declared steps of a graph, managing checkpointing,
14
+ # condition evaluation, parallel execution, and human-in-the-loop pauses.
15
+ class Runner
16
+ # @return [Array<Hash>] step declarations with name, type, condition
17
+ attr_reader :declarations
18
+
19
+ # @param declarations [Array<Hash>] step declarations
20
+ # @param checkpoint_store [#set, #get, #delete] optional persistent store
21
+ def initialize(declarations, checkpoint_store: nil)
22
+ @declarations = declarations
23
+ @store = checkpoint_store
24
+ @completed_steps = []
25
+ end
26
+
27
+ # Run the graph from the beginning or resume from the last checkpoint.
28
+ # @param context [Ask::Graph::Context] the shared context
29
+ # @return [Ask::Graph::Context] the completed context
30
+ def run(context)
31
+ resume_index = load_checkpoint
32
+
33
+ @declarations.each_with_index do |decl, idx|
34
+ next if resume_index && idx <= resume_index
35
+
36
+ unless condition_met?(decl, context)
37
+ record_completion(idx, :skipped)
38
+ next
39
+ end
40
+
41
+ execute_step(decl, context)
42
+ record_completion(idx, :completed)
43
+ save_checkpoint(idx)
44
+ end
45
+
46
+ context
47
+ end
48
+
49
+ # Resume a paused graph with human input.
50
+ # @param context [Ask::Graph::Context] the context at pause point
51
+ # @param input [Object] the human's input
52
+ # @return [Ask::Graph::Context] the completed context
53
+ def resume(context, input:)
54
+ context.resume_input = input
55
+ run(context)
56
+ end
57
+
58
+ # Get the index to resume from for an {Context#each} iteration.
59
+ # @param items [Array] the full list of items
60
+ # @return [Integer, nil] the index to resume from, or nil
61
+ def resume_index_for(items)
62
+ key = each_checkpoint_key
63
+ raw = @store&.get(key)
64
+ raw ? raw.to_i : nil
65
+ end
66
+
67
+ # Checkpoint after a single iteration of {Context#each}.
68
+ # @param index [Integer] the completed iteration index
69
+ def checkpoint_each!(index)
70
+ key = each_checkpoint_key
71
+ @store&.set(key, index.to_s)
72
+ end
73
+
74
+ private
75
+
76
+ def execute_step(decl, context)
77
+ case decl[:type]
78
+ when :step
79
+ run_single(decl[:class], decl[:name], context)
80
+ when :steps
81
+ run_parallel(decl[:classes], decl[:name], context)
82
+ when :approve
83
+ run_approve(decl[:class], decl[:name], context)
84
+ end
85
+ end
86
+
87
+ def run_single(klass, _name, context)
88
+ instance = klass.new
89
+ instance.call(context)
90
+ rescue Paused
91
+ raise
92
+ rescue StandardError => e
93
+ raise StepFailed, "#{klass.name} failed: #{e.message}"
94
+ end
95
+
96
+ def run_parallel(classes, _name, context)
97
+ threads = classes.map do |klass|
98
+ Thread.new do
99
+ begin
100
+ instance = klass.new
101
+ instance.call(context)
102
+ rescue StandardError => e
103
+ raise StepFailed, "#{klass.name} failed: #{e.message}"
104
+ end
105
+ end
106
+ end
107
+ threads.each(&:join)
108
+ end
109
+
110
+ def run_approve(klass, _name, context)
111
+ instance = klass.new
112
+ instance.call(context)
113
+ # After the step runs, pause and wait for human input
114
+ save_checkpoint(current_index)
115
+ raise Paused, "Graph paused for approval after #{klass.name}"
116
+ end
117
+
118
+ def condition_met?(decl, context)
119
+ if decl[:if]
120
+ context.graph.send(decl[:if])
121
+ elsif decl[:unless]
122
+ !context.graph.send(decl[:unless])
123
+ else
124
+ true
125
+ end
126
+ end
127
+
128
+ def record_completion(idx, status)
129
+ @completed_steps << { index: idx, status: status }
130
+ end
131
+
132
+ # --- Checkpointing ---
133
+
134
+ RUN_KEY = "ask:graph:run:%s"
135
+
136
+ def checkpoint_key
137
+ RUN_KEY % @declarations.object_id.to_s
138
+ end
139
+
140
+ def each_checkpoint_key
141
+ "#{checkpoint_key}:each"
142
+ end
143
+
144
+ def save_checkpoint(idx)
145
+ data = { completed_index: idx, timestamp: Time.now.iso8601 }
146
+ @store&.set(checkpoint_key, JSON.generate(data))
147
+ end
148
+
149
+ def load_checkpoint
150
+ raw = @store&.get(checkpoint_key)
151
+ return nil unless raw
152
+
153
+ data = JSON.parse(raw)
154
+ data["completed_index"]
155
+ end
156
+
157
+ def current_index
158
+ @completed_steps.size
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ class Graph
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/ask/graph.rb ADDED
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "graph/version"
4
+ require_relative "graph/context"
5
+ require_relative "graph/runner"
6
+
7
+ module Ask
8
+ # Define durable workflow graphs with named steps, conditional routing,
9
+ # parallel execution, human-in-the-loop approval, and per-item
10
+ # checkpointing loops.
11
+ #
12
+ # @example Basic workflow
13
+ # class HandleCall < Ask::Graph
14
+ # step Transcribe
15
+ # step Classify, if: :needs_classification?
16
+ # step BookAppointment
17
+ # end
18
+ #
19
+ # @example With parallel steps
20
+ # class SyncData < Ask::Graph
21
+ # step FetchRecords
22
+ #
23
+ # steps CrmUpdate, CalendarSync, SendNotification
24
+ #
25
+ # step ConfirmResponse
26
+ # end
27
+ #
28
+ # @example With human-in-the-loop
29
+ # class ProcessBooking < Ask::Graph
30
+ # step BookAppointment
31
+ # approve ReviewBooking, if: :expensive?
32
+ # step ConfirmBooking
33
+ # end
34
+ #
35
+ # @example With per-item looping
36
+ # class SendReminders < Ask::Graph
37
+ # step FetchAppointments
38
+ # step RemindEach # calls context.each(:appointments) internally
39
+ # step MarkComplete
40
+ # end
41
+ #
42
+ class Graph
43
+ class << self
44
+ # All step declarations collected across the class hierarchy.
45
+ def declarations
46
+ @declarations ||= []
47
+ end
48
+
49
+ # Inherited hook — copies declarations to subclasses.
50
+ def inherited(subclass)
51
+ super
52
+ subclass.instance_variable_set(:@declarations, declarations.dup)
53
+ end
54
+
55
+ # Declare a sequential step.
56
+ #
57
+ # @param klass [Class] a class that responds to +#call(context)+
58
+ # @param conditions [Hash] optional +if:+ or +unless:+ condition method name
59
+ def step(klass, **conditions)
60
+ declarations << {
61
+ class: klass,
62
+ type: :step,
63
+ name: klass.name || klass.to_s,
64
+ if: conditions[:if],
65
+ unless: conditions[:unless]
66
+ }
67
+ end
68
+
69
+ # Declare parallel steps — all run simultaneously.
70
+ #
71
+ # @param klasses [Array<Class>] step classes to run in parallel
72
+ # @param conditions [Hash] optional +if:+ or +unless:+ condition method name
73
+ def steps(*klasses, **conditions)
74
+ declarations << {
75
+ classes: klasses,
76
+ type: :steps,
77
+ name: klasses.map { |k| k.name || k.to_s }.join(", "),
78
+ if: conditions[:if],
79
+ unless: conditions[:unless]
80
+ }
81
+ end
82
+
83
+ # Declare a human-in-the-loop step. Runs the step, then pauses
84
+ # and waits for external input via {Ask::Graph#resume}.
85
+ #
86
+ # @param klass [Class] a class that responds to +#call(context)+
87
+ # @param conditions [Hash] optional +if:+ or +unless:+ condition method name
88
+ def approve(klass, **conditions)
89
+ declarations << {
90
+ class: klass,
91
+ type: :approve,
92
+ name: klass.name || klass.to_s,
93
+ if: conditions[:if],
94
+ unless: conditions[:unless]
95
+ }
96
+ end
97
+
98
+ # Create a new instance and run the graph.
99
+ # @param input [Object] optional initial input
100
+ # @param checkpoint_store [#set, #get, #delete] optional persistent store
101
+ # @return [Ask::Graph::Context] the completed context
102
+ def run(input = nil, checkpoint_store: nil)
103
+ new(checkpoint_store: checkpoint_store).run(input)
104
+ end
105
+
106
+ # Resume a paused graph with human input.
107
+ # @param input [Object] the human's input
108
+ # @param checkpoint_store [#set, #get, #delete] the same store used for the original run
109
+ # @return [Ask::Graph::Context] the completed context
110
+ def resume(input:, checkpoint_store:)
111
+ new(checkpoint_store: checkpoint_store).resume(input: input)
112
+ end
113
+ end
114
+
115
+ # @param checkpoint_store [#set, #get, #delete, nil] optional persistent store
116
+ def initialize(checkpoint_store: nil)
117
+ @runner = Runner.new(self.class.declarations, checkpoint_store: checkpoint_store)
118
+ @context = nil
119
+ end
120
+
121
+ # @return [Ask::Graph::Runner] the runner (exposed for checkpoint coordination)
122
+ attr_reader :runner
123
+
124
+ # @return [Ask::Graph::Context, nil] the shared context
125
+ attr_reader :context
126
+
127
+ # Run the graph with the given input.
128
+ # @param input [Object] optional initial input
129
+ # @return [Ask::Graph::Context] the completed context
130
+ def run(input = nil)
131
+ @context = Context.new(self, input)
132
+ @runner.run(@context)
133
+ @context
134
+ rescue => e
135
+ raise unless e.class.name&.end_with?("Paused")
136
+ @context
137
+ end
138
+
139
+ # Resume a paused graph with human input.
140
+ # @param input [Object] the human's input
141
+ # @return [Ask::Graph::Context] the completed context
142
+ def resume(input:)
143
+ @context.resume_input = input.to_s
144
+ @runner.run(@context)
145
+ @context
146
+ end
147
+ end
148
+ end
data/lib/ask-graph.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask"
4
+ require_relative "ask/graph"
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-graph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ask-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.25'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.25'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mocha
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ description: Define workflows as graphs of named steps with conditional routing, parallel
69
+ execution, human-in-the-loop approval, per-item checkpointing loops, and sub-graph
70
+ composition. Each step is a plain Ruby class.
71
+ email:
72
+ - kaka@myrrlabs.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - CHANGELOG.md
78
+ - LICENSE
79
+ - README.md
80
+ - lib/ask-graph.rb
81
+ - lib/ask/graph.rb
82
+ - lib/ask/graph/context.rb
83
+ - lib/ask/graph/runner.rb
84
+ - lib/ask/graph/version.rb
85
+ homepage: https://github.com/ask-rb/ask-graph
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/ask-rb/ask-graph
90
+ source_code_uri: https://github.com/ask-rb/ask-graph
91
+ changelog_uri: https://github.com/ask-rb/ask-graph/blob/master/CHANGELOG.md
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '3.2'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 4.0.3
107
+ specification_version: 4
108
+ summary: Durable workflow graphs for the ask-rb ecosystem
109
+ test_files: []