graph-agent 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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +50 -0
  3. data/.github/workflows/release.yml +49 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +126 -0
  7. data/CHANGELOG.md +26 -0
  8. data/CLAUDE.md +128 -0
  9. data/Gemfile +11 -0
  10. data/Gemfile.lock +94 -0
  11. data/LICENSE +21 -0
  12. data/Makefile +114 -0
  13. data/README.md +464 -0
  14. data/Rakefile +15 -0
  15. data/docs/README.md +55 -0
  16. data/docs/api_reference.md +832 -0
  17. data/docs/concepts.md +216 -0
  18. data/docs/edges.md +265 -0
  19. data/docs/error_handling.md +241 -0
  20. data/docs/human_in_the_loop.md +231 -0
  21. data/docs/persistence.md +276 -0
  22. data/docs/quickstart.md +154 -0
  23. data/docs/send_and_command.md +218 -0
  24. data/docs/state.md +181 -0
  25. data/docs/streaming.md +172 -0
  26. data/graph-agent.gemspec +48 -0
  27. data/lib/graph_agent/channels/base_channel.rb +52 -0
  28. data/lib/graph_agent/channels/binary_operator_aggregate.rb +56 -0
  29. data/lib/graph_agent/channels/ephemeral_value.rb +59 -0
  30. data/lib/graph_agent/channels/last_value.rb +49 -0
  31. data/lib/graph_agent/channels/topic.rb +58 -0
  32. data/lib/graph_agent/checkpoint/base_saver.rb +38 -0
  33. data/lib/graph_agent/checkpoint/in_memory_saver.rb +145 -0
  34. data/lib/graph_agent/constants.rb +9 -0
  35. data/lib/graph_agent/errors.rb +41 -0
  36. data/lib/graph_agent/graph/compiled_state_graph.rb +362 -0
  37. data/lib/graph_agent/graph/conditional_edge.rb +57 -0
  38. data/lib/graph_agent/graph/edge.rb +23 -0
  39. data/lib/graph_agent/graph/mermaid_visualizer.rb +154 -0
  40. data/lib/graph_agent/graph/message_graph.rb +18 -0
  41. data/lib/graph_agent/graph/node.rb +61 -0
  42. data/lib/graph_agent/graph/state_graph.rb +197 -0
  43. data/lib/graph_agent/reducers.rb +34 -0
  44. data/lib/graph_agent/state/schema.rb +54 -0
  45. data/lib/graph_agent/types/cache_policy.rb +12 -0
  46. data/lib/graph_agent/types/command.rb +26 -0
  47. data/lib/graph_agent/types/interrupt.rb +28 -0
  48. data/lib/graph_agent/types/retry_policy.rb +42 -0
  49. data/lib/graph_agent/types/send.rb +26 -0
  50. data/lib/graph_agent/types/state_snapshot.rb +28 -0
  51. data/lib/graph_agent/version.rb +5 -0
  52. data/lib/graph_agent.rb +29 -0
  53. metadata +158 -0
@@ -0,0 +1,218 @@
1
+ # Send & Command
2
+
3
+ GraphAgent provides two types for advanced routing beyond simple edges:
4
+ `Send` for fan-out/fan-in (map-reduce) patterns, and `Command` for combining
5
+ state updates with routing decisions.
6
+
7
+ ---
8
+
9
+ ## Send
10
+
11
+ `GraphAgent::Send` dispatches work to a specific node with custom input. It is
12
+ primarily used inside conditional edge functions to create **map-reduce**
13
+ patterns.
14
+
15
+ ### Constructor
16
+
17
+ ```ruby
18
+ GraphAgent::Send.new(node, arg)
19
+ ```
20
+
21
+ | Parameter | Type | Description |
22
+ |-----------|------|-------------|
23
+ | `node` | String/Symbol | Target node name |
24
+ | `arg` | Hash | State updates to apply before executing the target node |
25
+
26
+ ### Fan-Out (Map)
27
+
28
+ Return an array of `Send` objects from a conditional edge to fan out work
29
+ across multiple invocations of the same (or different) nodes:
30
+
31
+ ```ruby
32
+ schema = GraphAgent::State::Schema.new do
33
+ field :subjects, type: Array, default: []
34
+ field :jokes, type: Array, reducer: GraphAgent::Reducers::ADD, default: []
35
+ end
36
+
37
+ graph = GraphAgent::Graph::StateGraph.new(schema)
38
+
39
+ graph.add_node("get_subjects") do |state|
40
+ { subjects: ["cats", "dogs", "programming"] }
41
+ end
42
+
43
+ graph.add_node("generate_joke") do |state|
44
+ subject = state[:subjects].first
45
+ { jokes: ["Why did the #{subject} cross the road? ..."] }
46
+ end
47
+
48
+ fan_out = ->(state) do
49
+ state[:subjects].map do |subject|
50
+ GraphAgent::Send.new("generate_joke", { subjects: [subject] })
51
+ end
52
+ end
53
+
54
+ graph.add_edge(GraphAgent::START, "get_subjects")
55
+ graph.add_conditional_edges("get_subjects", fan_out)
56
+ graph.add_edge("generate_joke", GraphAgent::END_NODE)
57
+
58
+ app = graph.compile
59
+ result = app.invoke({})
60
+ puts result[:jokes]
61
+ ```
62
+
63
+ Each `Send` triggers a separate execution of `"generate_joke"` with a different
64
+ subject. All results are aggregated back via the `:jokes` reducer.
65
+
66
+ ### Fan-In (Reduce)
67
+
68
+ Fan-in happens automatically: all `Send` targets write to the same shared
69
+ state, and reducers aggregate the results. You can also use
70
+ [waiting edges](edges.md#waiting-edges-multi-source-synchronization) to
71
+ synchronize before a downstream node.
72
+
73
+ ---
74
+
75
+ ## Command
76
+
77
+ `GraphAgent::Command` combines a **state update** with a **routing decision**
78
+ in a single return value from a node.
79
+
80
+ ### Constructor
81
+
82
+ ```ruby
83
+ GraphAgent::Command.new(
84
+ graph: nil, # reserved for subgraph routing
85
+ update: nil, # Hash of state updates to apply
86
+ resume: nil, # resume value (for interrupt workflows)
87
+ goto: [] # String, Symbol, Send, or Array thereof — next node(s)
88
+ )
89
+ ```
90
+
91
+ ### Basic Usage
92
+
93
+ Return a `Command` from a node to update state and route simultaneously:
94
+
95
+ ```ruby
96
+ graph.add_node("router") do |state|
97
+ target = state[:intent] == "buy" ? "purchase" : "browse"
98
+ GraphAgent::Command.new(
99
+ update: { routed: true, route: target },
100
+ goto: target
101
+ )
102
+ end
103
+ ```
104
+
105
+ ### Goto with Multiple Targets
106
+
107
+ `goto` accepts an array to route to multiple nodes in the next step:
108
+
109
+ ```ruby
110
+ graph.add_node("fork") do |state|
111
+ GraphAgent::Command.new(
112
+ goto: ["analyze", "log"]
113
+ )
114
+ end
115
+ ```
116
+
117
+ ### Goto with Send
118
+
119
+ You can mix `Send` objects in `goto` for dynamic fan-out:
120
+
121
+ ```ruby
122
+ graph.add_node("dispatch") do |state|
123
+ sends = state[:tasks].map do |task|
124
+ GraphAgent::Send.new("worker", { current_task: task })
125
+ end
126
+ GraphAgent::Command.new(goto: sends)
127
+ end
128
+ ```
129
+
130
+ ### Update Without Routing
131
+
132
+ If you only need state updates (with routing handled by normal edges), use a
133
+ plain Hash return instead of `Command`. Use `Command` specifically when you
134
+ need the node to **control routing**.
135
+
136
+ ---
137
+
138
+ ## When to Use Command vs Conditional Edges
139
+
140
+ | Use Case | Recommended |
141
+ |----------|-------------|
142
+ | Route based on state, node doesn't modify state | Conditional edge |
143
+ | Route based on state **and** modify state atomically | `Command` |
144
+ | Fan-out to multiple instances of the same node | `Send` via conditional edge |
145
+ | Fan-out with per-instance state modifications | `Command` with `Send` in `goto` |
146
+ | Simple sequential flow | Normal edge |
147
+
148
+ ### Command vs Conditional Edge Example
149
+
150
+ **Conditional edge** — routing logic is separate from the node:
151
+
152
+ ```ruby
153
+ graph.add_node("classify") do |state|
154
+ { category: detect_category(state[:input]) }
155
+ end
156
+
157
+ graph.add_conditional_edges("classify", ->(state) {
158
+ state[:category]
159
+ }, { "a" => "handler_a", "b" => "handler_b" })
160
+ ```
161
+
162
+ **Command** — routing logic is inside the node:
163
+
164
+ ```ruby
165
+ graph.add_node("classify") do |state|
166
+ category = detect_category(state[:input])
167
+ GraphAgent::Command.new(
168
+ update: { category: category },
169
+ goto: "handler_#{category}"
170
+ )
171
+ end
172
+
173
+ # Still need outgoing edges for validation — use conditional edges
174
+ # that won't actually be reached, or structure so that Command targets
175
+ # are valid node names.
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Complete Map-Reduce Example
181
+
182
+ ```ruby
183
+ require "graph_agent"
184
+
185
+ schema = GraphAgent::State::Schema.new do
186
+ field :urls, type: Array, default: []
187
+ field :pages, type: Array, reducer: GraphAgent::Reducers::ADD, default: []
188
+ field :summary, type: String
189
+ end
190
+
191
+ graph = GraphAgent::Graph::StateGraph.new(schema)
192
+
193
+ graph.add_node("plan") do |state|
194
+ { urls: ["https://example.com/a", "https://example.com/b"] }
195
+ end
196
+
197
+ graph.add_node("fetch") do |state|
198
+ url = state[:urls].first
199
+ { pages: ["Content from #{url}"] }
200
+ end
201
+
202
+ graph.add_node("summarize") do |state|
203
+ { summary: "Summarized #{state[:pages].length} pages" }
204
+ end
205
+
206
+ fan_out_fetches = ->(state) do
207
+ state[:urls].map { |url| GraphAgent::Send.new("fetch", { urls: [url] }) }
208
+ end
209
+
210
+ graph.add_edge(GraphAgent::START, "plan")
211
+ graph.add_conditional_edges("plan", fan_out_fetches)
212
+ graph.add_edge("fetch", "summarize")
213
+ graph.add_edge("summarize", GraphAgent::END_NODE)
214
+
215
+ app = graph.compile
216
+ result = app.invoke({})
217
+ puts result[:summary]
218
+ ```
data/docs/state.md ADDED
@@ -0,0 +1,181 @@
1
+ # State Management
2
+
3
+ ## Overview
4
+
5
+ State is the shared data structure that every node reads from and writes to.
6
+ GraphAgent applies updates atomically after each superstep using **reducers**.
7
+
8
+ ---
9
+
10
+ ## Defining State with Schema DSL
11
+
12
+ Use `GraphAgent::State::Schema` with a block:
13
+
14
+ ```ruby
15
+ schema = GraphAgent::State::Schema.new do
16
+ field :messages, type: Array, reducer: GraphAgent::Reducers::ADD, default: []
17
+ field :count, type: Integer, reducer: GraphAgent::Reducers::ADD, default: 0
18
+ field :status, type: String
19
+ field :metadata, type: Hash, reducer: GraphAgent::Reducers::MERGE, default: {}
20
+ end
21
+ ```
22
+
23
+ Each `field` call accepts:
24
+
25
+ | Parameter | Type | Description |
26
+ |-----------|------|-------------|
27
+ | `name` | Symbol/String | Field name (converted to Symbol) |
28
+ | `type:` | Class/nil | Optional type annotation (not enforced at runtime) |
29
+ | `reducer:` | Proc/nil | Callable `(current, new) → merged`; nil means last-value semantics |
30
+ | `default:` | Object/nil | Initial value; duplicated per invocation to avoid shared state |
31
+
32
+ ---
33
+
34
+ ## Defining State with Hash Shorthand
35
+
36
+ Pass a Hash directly to `StateGraph.new` instead of a `Schema` object:
37
+
38
+ ```ruby
39
+ graph = GraphAgent::Graph::StateGraph.new({
40
+ messages: { type: Array, reducer: ->(a, b) { a + b }, default: [] },
41
+ count: { type: Integer, reducer: ->(a, b) { a + b }, default: 0 },
42
+ status: {} # last-value semantics, no default
43
+ })
44
+ ```
45
+
46
+ Each key becomes a field name. Values can be:
47
+
48
+ - A **Hash** with `:type`, `:reducer`, `:default` keys (all optional).
49
+ - Any non-Hash value is treated as a field with no options.
50
+
51
+ ---
52
+
53
+ ## Built-in Reducers
54
+
55
+ Defined in `GraphAgent::Reducers`:
56
+
57
+ | Reducer | Lambda | Behavior |
58
+ |---------|--------|----------|
59
+ | `ADD` | `(a, b) → a + b` | Concatenates arrays, adds numbers, joins strings |
60
+ | `APPEND` | `(a, b) → Array(a) + Array(b)` | Wraps both sides in arrays then concatenates |
61
+ | `MERGE` | `(a, b) → a.merge(b)` | Shallow-merges hashes |
62
+ | `REPLACE` | `(_, b) → b` | Always replaces with the new value |
63
+
64
+ ### `add_messages`
65
+
66
+ A special reducer for chat message lists. It matches messages by `:id` and
67
+ replaces existing messages in-place; new messages (without matching IDs) are
68
+ appended:
69
+
70
+ ```ruby
71
+ field :messages, reducer: GraphAgent::Reducers.method(:add_messages), default: []
72
+ ```
73
+
74
+ Example:
75
+
76
+ ```ruby
77
+ existing = [
78
+ { id: "1", role: "user", content: "Hi" },
79
+ { id: "2", role: "ai", content: "Hello" }
80
+ ]
81
+
82
+ new_msgs = [
83
+ { id: "2", role: "ai", content: "Hello! How can I help?" }, # updates id "2"
84
+ { id: "3", role: "user", content: "Tell me a joke" } # appended
85
+ ]
86
+
87
+ result = GraphAgent::Reducers.add_messages(existing, new_msgs)
88
+ # => [
89
+ # { id: "1", role: "user", content: "Hi" },
90
+ # { id: "2", role: "ai", content: "Hello! How can I help?" },
91
+ # { id: "3", role: "user", content: "Tell me a joke" }
92
+ # ]
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Custom Reducers
98
+
99
+ Any callable that takes two arguments works as a reducer:
100
+
101
+ ```ruby
102
+ # Keep only the last N messages
103
+ keep_last_10 = ->(existing, new_msgs) do
104
+ (Array(existing) + Array(new_msgs)).last(10)
105
+ end
106
+
107
+ schema = GraphAgent::State::Schema.new do
108
+ field :messages, reducer: keep_last_10, default: []
109
+ end
110
+ ```
111
+
112
+ ```ruby
113
+ # Union of sets
114
+ set_union = ->(a, b) { (Array(a) | Array(b)) }
115
+
116
+ schema = GraphAgent::State::Schema.new do
117
+ field :tags, reducer: set_union, default: []
118
+ end
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Initial State and Defaults
124
+
125
+ When a graph is invoked, `Schema#initial_state` produces the starting state by
126
+ duplicating each field's default value:
127
+
128
+ ```ruby
129
+ schema = GraphAgent::State::Schema.new do
130
+ field :items, default: []
131
+ field :count, default: 0
132
+ end
133
+
134
+ schema.initial_state
135
+ # => { items: [], count: 0 }
136
+ ```
137
+
138
+ - Defaults are `.dup`'d to prevent shared mutation between invocations.
139
+ - Fields without a default start as `nil`.
140
+ - Input values passed to `invoke` are merged on top of the initial state.
141
+
142
+ ---
143
+
144
+ ## How Updates Are Applied
145
+
146
+ After each superstep, the compiled graph calls `Schema#apply` (or the internal
147
+ `_apply_updates` method) for every update returned by the nodes:
148
+
149
+ ```ruby
150
+ # Pseudocode for one superstep:
151
+ # 1. All nodes run on a frozen snapshot
152
+ # 2. Collect updates: { "node_a" => { count: 1 }, "node_b" => { count: 2 } }
153
+ # 3. For each update hash, for each key:
154
+ # if field has a reducer → state[key] = reducer.call(state[key], value)
155
+ # else → state[key] = value (last-value)
156
+ ```
157
+
158
+ This means:
159
+
160
+ - **With a reducer** (`ADD`), two nodes returning `{ count: 1 }` and `{ count: 2 }`
161
+ produce `count = 0 + 1 + 2 = 3` (assuming default 0).
162
+ - **Without a reducer**, the last update wins (order depends on iteration of
163
+ the updates hash).
164
+
165
+ ---
166
+
167
+ ## Private / Input / Output Schemas
168
+
169
+ `StateGraph` accepts `input_schema:` and `output_schema:` parameters:
170
+
171
+ ```ruby
172
+ graph = GraphAgent::Graph::StateGraph.new(
173
+ full_schema,
174
+ input_schema: input_only_schema,
175
+ output_schema: output_only_schema
176
+ )
177
+ ```
178
+
179
+ These are stored on the builder for future use (e.g., input validation and
180
+ output filtering) but are not yet enforced at runtime. The main `schema`
181
+ parameter defines all fields the graph operates on.
data/docs/streaming.md ADDED
@@ -0,0 +1,172 @@
1
+ # Streaming
2
+
3
+ GraphAgent supports streaming intermediate results as the graph executes,
4
+ instead of waiting for the final output.
5
+
6
+ ---
7
+
8
+ ## Stream Modes
9
+
10
+ The `stream` method accepts a `stream_mode` parameter:
11
+
12
+ | Mode | Yields | Description |
13
+ |------|--------|-------------|
14
+ | `:values` | Full state Hash | Emits the complete state after each superstep |
15
+ | `:updates` | Per-node updates Hash | Emits only the changes each node produced |
16
+ | `:debug` | Raw event Hash | Emits all internal events with type, step, state, and updates |
17
+
18
+ ---
19
+
20
+ ## stream_mode: :values
21
+
22
+ Yields the full state after each superstep. This is the default mode.
23
+
24
+ ```ruby
25
+ app.stream({ query: "hello" }, stream_mode: :values) do |state|
26
+ puts "Messages: #{state[:messages].length}"
27
+ puts "Status: #{state[:status]}"
28
+ end
29
+ ```
30
+
31
+ The final yield contains the complete output state (same as what `invoke`
32
+ would return).
33
+
34
+ ---
35
+
36
+ ## stream_mode: :updates
37
+
38
+ Yields only the updates produced by each node in the step:
39
+
40
+ ```ruby
41
+ app.stream({ query: "hello" }, stream_mode: :updates) do |updates|
42
+ updates.each do |node_name, node_updates|
43
+ puts "#{node_name} produced: #{node_updates}"
44
+ end
45
+ end
46
+ ```
47
+
48
+ The updates hash maps node names (strings) to the Hash each node returned.
49
+
50
+ ---
51
+
52
+ ## stream_mode: :debug
53
+
54
+ Yields raw event hashes with full internal detail:
55
+
56
+ ```ruby
57
+ app.stream({ query: "hello" }, stream_mode: :debug) do |event|
58
+ case event[:type]
59
+ when :values
60
+ puts "Step #{event[:step]}: state = #{event[:state]}"
61
+ when :updates
62
+ puts "Step #{event[:step]}: updates = #{event[:updates]}"
63
+ end
64
+ end
65
+ ```
66
+
67
+ Event structure:
68
+
69
+ ```ruby
70
+ {
71
+ type: :values | :updates,
72
+ step: Integer,
73
+ state: Hash, # present for :values events
74
+ updates: Hash # present for :updates events
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Using Block Form
81
+
82
+ Pass a block to `stream` to process events as they arrive:
83
+
84
+ ```ruby
85
+ app.stream(input, config: config, stream_mode: :values) do |state|
86
+ render_state(state)
87
+ end
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Using Enumerator (No Block)
93
+
94
+ When called without a block, `stream` returns an `Enumerator`:
95
+
96
+ ```ruby
97
+ events = app.stream(input, stream_mode: :values)
98
+
99
+ events.each do |state|
100
+ puts state[:messages].last
101
+ end
102
+ ```
103
+
104
+ The `Enumerator` is lazy — events are produced as the graph executes. You can
105
+ use standard Enumerable methods:
106
+
107
+ ```ruby
108
+ # Get the first 3 states
109
+ first_three = app.stream(input, stream_mode: :values).take(3)
110
+
111
+ # Find the first state where processing is complete
112
+ done = app.stream(input, stream_mode: :values).find { |s| s[:done] }
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Streaming with Config
118
+
119
+ All `stream` options from `invoke` are available:
120
+
121
+ ```ruby
122
+ app.stream(
123
+ input,
124
+ config: { configurable: { thread_id: "t1" } },
125
+ recursion_limit: 50,
126
+ stream_mode: :updates
127
+ ) do |updates|
128
+ puts updates
129
+ end
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Complete Example
135
+
136
+ ```ruby
137
+ require "graph_agent"
138
+
139
+ schema = GraphAgent::State::Schema.new do
140
+ field :numbers, type: Array, default: []
141
+ field :sum, type: Integer, reducer: GraphAgent::Reducers::ADD, default: 0
142
+ field :step_name, type: String
143
+ end
144
+
145
+ graph = GraphAgent::Graph::StateGraph.new(schema)
146
+
147
+ graph.add_node("add_evens") do |state|
148
+ evens = state[:numbers].select(&:even?)
149
+ { sum: evens.sum, step_name: "add_evens" }
150
+ end
151
+
152
+ graph.add_node("add_odds") do |state|
153
+ odds = state[:numbers].select(&:odd?)
154
+ { sum: odds.sum, step_name: "add_odds" }
155
+ end
156
+
157
+ graph.add_edge(GraphAgent::START, "add_evens")
158
+ graph.add_edge("add_evens", "add_odds")
159
+ graph.add_edge("add_odds", GraphAgent::END_NODE)
160
+
161
+ app = graph.compile
162
+
163
+ puts "=== :values mode ==="
164
+ app.stream({ numbers: [1, 2, 3, 4, 5] }, stream_mode: :values) do |state|
165
+ puts "sum=#{state[:sum]} step=#{state[:step_name]}"
166
+ end
167
+
168
+ puts "\n=== :updates mode ==="
169
+ app.stream({ numbers: [1, 2, 3, 4, 5] }, stream_mode: :updates) do |updates|
170
+ updates.each { |node, u| puts "#{node}: #{u}" }
171
+ end
172
+ ```
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/graph_agent/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "graph-agent"
7
+ spec.version = GraphAgent::VERSION
8
+ spec.authors = ["GraphAgent Contributors"]
9
+ spec.email = ["richard.sun@ai-firstly.com"]
10
+ spec.summary = "A Ruby framework for building stateful, multi-actor agent workflows"
11
+ spec.description = "Ruby port of LangGraph - build stateful, multi-actor applications " \
12
+ "with LLMs using a graph-based workflow engine with Pregel execution model"
13
+ spec.homepage = "https://github.com/ai-firstly/graph-agent"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = [">= 3.1.0", "< 5.0"]
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/ai-firstly/graph-agent"
20
+ spec.metadata["changelog_uri"] = "https://github.com/ai-firstly/graph-agent/blob/main/CHANGELOG.md"
21
+ spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/graph-agent"
22
+ spec.metadata["bug_tracker_uri"] = "https://github.com/ai-firstly/graph-agent/issues"
23
+ spec.metadata["rubygems_mfa_required"] = "true"
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ if File.exist?(".git")
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{\A(?:test|spec|features)/})
29
+ end
30
+ else
31
+ Dir.glob("**/*").reject do |f|
32
+ File.directory?(f) ||
33
+ f.match(%r{\A(?:test|spec|features)/}) ||
34
+ f.match(/\A\./) ||
35
+ f.match(/\.gem$/)
36
+ end
37
+ end
38
+ end
39
+ spec.bindir = "exe"
40
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
41
+ spec.require_paths = ["lib"]
42
+
43
+ spec.add_dependency "concurrent-ruby", "~> 1.2"
44
+
45
+ spec.add_development_dependency "rake", "~> 13.0"
46
+ spec.add_development_dependency "rspec", "~> 3.12"
47
+ spec.add_development_dependency "rubocop", "~> 1.50"
48
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ module Channels
5
+ class BaseChannel
6
+ MISSING = Object.new.freeze
7
+
8
+ attr_accessor :key
9
+
10
+ def initialize(key: "")
11
+ @key = key
12
+ end
13
+
14
+ def get
15
+ raise NotImplementedError.new("#{self.class}#get must be implemented")
16
+ end
17
+
18
+ def update(values)
19
+ raise NotImplementedError.new("#{self.class}#update must be implemented")
20
+ end
21
+
22
+ def checkpoint
23
+ get
24
+ rescue EmptyChannelError
25
+ MISSING
26
+ end
27
+
28
+ def from_checkpoint(checkpoint)
29
+ raise NotImplementedError.new("#{self.class}#from_checkpoint must be implemented")
30
+ end
31
+
32
+ def available?
33
+ get
34
+ true
35
+ rescue EmptyChannelError
36
+ false
37
+ end
38
+
39
+ def consume
40
+ false
41
+ end
42
+
43
+ def finish
44
+ false
45
+ end
46
+
47
+ def copy
48
+ from_checkpoint(checkpoint)
49
+ end
50
+ end
51
+ end
52
+ end