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,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mermaid_visualizer"
4
+
5
+ module GraphAgent
6
+ module Graph
7
+ class StateGraph
8
+ attr_reader :schema, :nodes, :edges, :branches, :waiting_edges
9
+
10
+ def initialize(schema = nil, input_schema: nil, output_schema: nil)
11
+ @schema = _normalize_schema(schema)
12
+ @input_schema = input_schema ? _normalize_schema(input_schema) : nil
13
+ @output_schema = output_schema ? _normalize_schema(output_schema) : nil
14
+ @nodes = {}
15
+ @edges = Set.new
16
+ @branches = Hash.new { |h, k| h[k] = {} }
17
+ @waiting_edges = Set.new
18
+ end
19
+
20
+ def add_node(name, action = nil, metadata: nil, retry_policy: nil, cache_policy: nil, &block)
21
+ action = block if block && action.nil?
22
+ name = _get_node_name(name, action)
23
+
24
+ raise InvalidGraphError.new("Node action must be provided") if action.nil?
25
+ raise InvalidGraphError.new("Node '#{name}' already exists") if @nodes.key?(name)
26
+ raise InvalidGraphError.new("Node name '#{name}' is reserved") if [END_NODE.to_s, START.to_s].include?(name)
27
+
28
+ @nodes[name] = Node.new(
29
+ name, action,
30
+ metadata: metadata,
31
+ retry_policy: retry_policy,
32
+ cache_policy: cache_policy
33
+ )
34
+ self
35
+ end
36
+
37
+ def add_edge(start_key, end_key)
38
+ if start_key.is_a?(Array)
39
+ targets = start_key.map(&:to_s)
40
+ @waiting_edges.add([targets, end_key.to_s])
41
+ return self
42
+ end
43
+
44
+ start_key = start_key.to_s
45
+ end_key = end_key.to_s
46
+
47
+ raise InvalidGraphError.new("END cannot be a start node") if start_key == END_NODE.to_s
48
+ raise InvalidGraphError.new("START cannot be an end node") if end_key == START.to_s
49
+
50
+ @edges.add(Edge.new(start_key, end_key))
51
+ self
52
+ end
53
+
54
+ def add_conditional_edges(source, path, path_map = nil)
55
+ source = source.to_s
56
+ name = _branch_name(path)
57
+
58
+ if @branches[source].key?(name)
59
+ raise InvalidGraphError.new("Branch '#{name}' already exists for node '#{source}'")
60
+ end
61
+
62
+ @branches[source][name] = ConditionalEdge.new(source, path, path_map: path_map)
63
+ self
64
+ end
65
+
66
+ def add_sequence(nodes)
67
+ node_names = nodes.map do |node|
68
+ if node.is_a?(Array)
69
+ name, action = node
70
+ add_node(name, action)
71
+ name.to_s
72
+ elsif node.respond_to?(:call)
73
+ name = _get_node_name(nil, node)
74
+ add_node(name, node)
75
+ name
76
+ else
77
+ node.to_s
78
+ end
79
+ end
80
+
81
+ node_names.each_cons(2) { |a, b| add_edge(a, b) }
82
+ self
83
+ end
84
+
85
+ def set_entry_point(node_name)
86
+ add_edge(START, node_name)
87
+ end
88
+
89
+ def set_finish_point(node_name)
90
+ add_edge(node_name, END_NODE)
91
+ end
92
+
93
+ def set_conditional_entry_point(path, path_map = nil)
94
+ add_conditional_edges(START, path, path_map)
95
+ end
96
+
97
+ def compile(checkpointer: nil, interrupt_before: nil, interrupt_after: nil, debug: false)
98
+ validate!
99
+
100
+ CompiledStateGraph.new(
101
+ builder: self,
102
+ checkpointer: checkpointer,
103
+ interrupt_before: interrupt_before || [],
104
+ interrupt_after: interrupt_after || [],
105
+ debug: debug
106
+ )
107
+ end
108
+
109
+ # Generate a Mermaid diagram representation of the graph
110
+ def to_mermaid(options = {})
111
+ MermaidVisualizer.render(self, options)
112
+ end
113
+
114
+ # Print Mermaid diagram to stdout
115
+ def print_mermaid(options = {})
116
+ puts to_mermaid(options)
117
+ end
118
+
119
+ private
120
+
121
+ def _normalize_schema(schema)
122
+ case schema
123
+ when State::Schema then schema
124
+ when Hash then _schema_from_hash(schema)
125
+ when nil then nil
126
+ end
127
+ end
128
+
129
+ def _schema_from_hash(hash)
130
+ s = State::Schema.new
131
+ hash.each do |name, opts|
132
+ opts.is_a?(Hash) ? s.field(name, **opts) : s.field(name)
133
+ end
134
+ s
135
+ end
136
+
137
+ def _get_node_name(name, action)
138
+ return name.to_s if name
139
+
140
+ if action.respond_to?(:name) && !action.name.nil? && !action.name.empty?
141
+ action.name.split("::").last.gsub(/[^a-zA-Z0-9_]/, "_")
142
+ else
143
+ raise InvalidGraphError.new("Node name must be provided")
144
+ end
145
+ end
146
+
147
+ def _branch_name(path)
148
+ if path.respond_to?(:name) && path.name && !path.name.empty?
149
+ path.name
150
+ else
151
+ "condition_#{@branches.values.sum(&:size)}"
152
+ end
153
+ end
154
+
155
+ def validate!
156
+ _validate_entry_point!
157
+ _validate_edges!
158
+ _validate_outgoing_edges!
159
+ end
160
+
161
+ def _validate_entry_point!
162
+ entry_edges = @edges.any? { |e| e.source == START.to_s }
163
+ entry_branches = @branches[START.to_s]&.any?
164
+
165
+ return if entry_edges || entry_branches
166
+
167
+ raise InvalidGraphError.new("Graph must have an entry point. Use set_entry_point or add_edge(START, ...)")
168
+ end
169
+
170
+ def _validate_edges!
171
+ sentinel = [START.to_s, END_NODE.to_s]
172
+ @edges.each do |edge|
173
+ next if sentinel.include?(edge.source)
174
+
175
+ unless @nodes.key?(edge.source)
176
+ raise InvalidGraphError.new("Edge references unknown source node '#{edge.source}'")
177
+ end
178
+
179
+ next if edge.target == END_NODE.to_s
180
+
181
+ unless @nodes.key?(edge.target)
182
+ raise InvalidGraphError.new("Edge references unknown target node '#{edge.target}'")
183
+ end
184
+ end
185
+ end
186
+
187
+ def _validate_outgoing_edges!
188
+ @nodes.each_key do |name|
189
+ has_outgoing = @edges.any? { |e| e.source == name } ||
190
+ (@branches.key?(name) && !@branches[name].empty?)
191
+
192
+ raise InvalidGraphError.new("Node '#{name}' has no outgoing edges") unless has_outgoing
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ module Reducers
5
+ ADD = ->(a, b) { a + b }
6
+ MERGE = ->(a, b) { a.merge(b) }
7
+ REPLACE = ->(_, b) { b }
8
+ APPEND = ->(a, b) { Array(a) + Array(b) }
9
+
10
+ module_function
11
+
12
+ def add_messages(existing, new_messages)
13
+ existing = Array(existing)
14
+ new_messages = Array(new_messages)
15
+
16
+ existing_by_id = {}
17
+ existing.each_with_index do |msg, idx|
18
+ if msg.is_a?(Hash) && msg[:id]
19
+ existing_by_id[msg[:id]] = idx
20
+ end
21
+ end
22
+
23
+ result = existing.dup
24
+ new_messages.each do |msg|
25
+ if msg.is_a?(Hash) && msg[:id] && existing_by_id.key?(msg[:id])
26
+ result[existing_by_id[msg[:id]]] = msg
27
+ else
28
+ result << msg
29
+ end
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ module State
5
+ class Schema
6
+ attr_reader :fields
7
+
8
+ Field = Data.define(:name, :type, :reducer, :default)
9
+
10
+ def initialize(&block)
11
+ @fields = {}
12
+ instance_eval(&block) if block
13
+ end
14
+
15
+ def field(name, type: nil, reducer: nil, default: nil)
16
+ @fields[name.to_sym] = Field.new(
17
+ name: name.to_sym,
18
+ type: type,
19
+ reducer: reducer,
20
+ default: default
21
+ )
22
+ end
23
+
24
+ def initial_state
25
+ @fields.transform_values do |f|
26
+ if f.default.nil?
27
+ nil
28
+ elsif f.default.respond_to?(:dup)
29
+ begin
30
+ f.default.dup
31
+ rescue TypeError
32
+ f.default
33
+ end
34
+ else
35
+ f.default
36
+ end
37
+ end
38
+ end
39
+
40
+ def apply(state, updates)
41
+ updates.each do |key, value|
42
+ key = key.to_sym
43
+ f = @fields[key]
44
+ if f&.reducer
45
+ state[key] = f.reducer.call(state[key], value)
46
+ else
47
+ state[key] = value
48
+ end
49
+ end
50
+ state
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ class CachePolicy
5
+ attr_reader :key_func, :ttl
6
+
7
+ def initialize(key_func: nil, ttl: nil)
8
+ @key_func = key_func
9
+ @ttl = ttl
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ class Command
5
+ PARENT = :__parent__
6
+
7
+ attr_reader :graph, :update, :resume, :goto
8
+
9
+ def initialize(graph: nil, update: nil, resume: nil, goto: [])
10
+ @graph = graph
11
+ @update = update
12
+ @resume = resume
13
+ @goto = Array(goto)
14
+ end
15
+
16
+ def to_s
17
+ parts = []
18
+ parts << "graph=#{graph.inspect}" if graph
19
+ parts << "update=#{update.inspect}" if update
20
+ parts << "resume=#{resume.inspect}" if resume
21
+ parts << "goto=#{goto.inspect}" unless goto.empty?
22
+ "Command(#{parts.join(", ")})"
23
+ end
24
+ alias inspect to_s
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module GraphAgent
6
+ class Interrupt
7
+ attr_reader :value, :id
8
+
9
+ def initialize(value, id: nil)
10
+ @value = value
11
+ @id = id || SecureRandom.hex(16)
12
+ end
13
+
14
+ def ==(other)
15
+ other.is_a?(Interrupt) && id == other.id && value == other.value
16
+ end
17
+ alias eql? ==
18
+
19
+ def hash
20
+ [id, value].hash
21
+ end
22
+
23
+ def to_s
24
+ "Interrupt(value=#{value.inspect}, id=#{id.inspect})"
25
+ end
26
+ alias inspect to_s
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ class RetryPolicy
5
+ attr_reader :initial_interval, :backoff_factor, :max_interval,
6
+ :max_attempts, :jitter, :retry_on
7
+
8
+ def initialize(
9
+ initial_interval: 0.5,
10
+ backoff_factor: 2.0,
11
+ max_interval: 128.0,
12
+ max_attempts: 3,
13
+ jitter: true,
14
+ retry_on: StandardError
15
+ )
16
+ @initial_interval = initial_interval
17
+ @backoff_factor = backoff_factor
18
+ @max_interval = max_interval
19
+ @max_attempts = max_attempts
20
+ @jitter = jitter
21
+ @retry_on = retry_on
22
+ end
23
+
24
+ def should_retry?(error)
25
+ case @retry_on
26
+ when Proc
27
+ @retry_on.call(error)
28
+ when Array
29
+ @retry_on.any? { |klass| error.is_a?(klass) }
30
+ else
31
+ error.is_a?(@retry_on)
32
+ end
33
+ end
34
+
35
+ def interval_for(attempt)
36
+ interval = @initial_interval * (@backoff_factor**attempt)
37
+ interval = [interval, @max_interval].min
38
+ interval += rand * interval * 0.1 if @jitter
39
+ interval
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ class Send
5
+ attr_reader :node, :arg
6
+
7
+ def initialize(node, arg)
8
+ @node = node.to_s
9
+ @arg = arg
10
+ end
11
+
12
+ def ==(other)
13
+ other.is_a?(Send) && node == other.node && arg == other.arg
14
+ end
15
+ alias eql? ==
16
+
17
+ def hash
18
+ [node, arg].hash
19
+ end
20
+
21
+ def to_s
22
+ "Send(node=#{node.inspect}, arg=#{arg.inspect})"
23
+ end
24
+ alias inspect to_s
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ class StateSnapshot
5
+ attr_reader :values, :next_nodes, :config, :metadata,
6
+ :created_at, :parent_config, :tasks, :interrupts
7
+
8
+ def initialize(
9
+ values:,
10
+ next_nodes: [],
11
+ config: {},
12
+ metadata: nil,
13
+ created_at: nil,
14
+ parent_config: nil,
15
+ tasks: [],
16
+ interrupts: []
17
+ )
18
+ @values = values
19
+ @next_nodes = Array(next_nodes)
20
+ @config = config
21
+ @metadata = metadata
22
+ @created_at = created_at
23
+ @parent_config = parent_config
24
+ @tasks = Array(tasks)
25
+ @interrupts = Array(interrupts)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAgent
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "graph_agent/version"
4
+ require_relative "graph_agent/constants"
5
+ require_relative "graph_agent/errors"
6
+ require_relative "graph_agent/reducers"
7
+ require_relative "graph_agent/state/schema"
8
+ require_relative "graph_agent/channels/base_channel"
9
+ require_relative "graph_agent/channels/last_value"
10
+ require_relative "graph_agent/channels/binary_operator_aggregate"
11
+ require_relative "graph_agent/channels/ephemeral_value"
12
+ require_relative "graph_agent/channels/topic"
13
+ require_relative "graph_agent/types/send"
14
+ require_relative "graph_agent/types/command"
15
+ require_relative "graph_agent/types/retry_policy"
16
+ require_relative "graph_agent/types/cache_policy"
17
+ require_relative "graph_agent/types/interrupt"
18
+ require_relative "graph_agent/types/state_snapshot"
19
+ require_relative "graph_agent/checkpoint/base_saver"
20
+ require_relative "graph_agent/checkpoint/in_memory_saver"
21
+ require_relative "graph_agent/graph/node"
22
+ require_relative "graph_agent/graph/edge"
23
+ require_relative "graph_agent/graph/conditional_edge"
24
+ require_relative "graph_agent/graph/state_graph"
25
+ require_relative "graph_agent/graph/compiled_state_graph"
26
+ require_relative "graph_agent/graph/message_graph"
27
+
28
+ module GraphAgent
29
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graph-agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - GraphAgent Contributors
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.12'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.12'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.50'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.50'
68
+ description: Ruby port of LangGraph - build stateful, multi-actor applications with
69
+ LLMs using a graph-based workflow engine with Pregel execution model
70
+ email:
71
+ - richard.sun@ai-firstly.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".github/workflows/ci.yml"
77
+ - ".github/workflows/release.yml"
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - ".rubocop.yml"
81
+ - CHANGELOG.md
82
+ - CLAUDE.md
83
+ - Gemfile
84
+ - Gemfile.lock
85
+ - LICENSE
86
+ - Makefile
87
+ - README.md
88
+ - Rakefile
89
+ - docs/README.md
90
+ - docs/api_reference.md
91
+ - docs/concepts.md
92
+ - docs/edges.md
93
+ - docs/error_handling.md
94
+ - docs/human_in_the_loop.md
95
+ - docs/persistence.md
96
+ - docs/quickstart.md
97
+ - docs/send_and_command.md
98
+ - docs/state.md
99
+ - docs/streaming.md
100
+ - graph-agent.gemspec
101
+ - lib/graph_agent.rb
102
+ - lib/graph_agent/channels/base_channel.rb
103
+ - lib/graph_agent/channels/binary_operator_aggregate.rb
104
+ - lib/graph_agent/channels/ephemeral_value.rb
105
+ - lib/graph_agent/channels/last_value.rb
106
+ - lib/graph_agent/channels/topic.rb
107
+ - lib/graph_agent/checkpoint/base_saver.rb
108
+ - lib/graph_agent/checkpoint/in_memory_saver.rb
109
+ - lib/graph_agent/constants.rb
110
+ - lib/graph_agent/errors.rb
111
+ - lib/graph_agent/graph/compiled_state_graph.rb
112
+ - lib/graph_agent/graph/conditional_edge.rb
113
+ - lib/graph_agent/graph/edge.rb
114
+ - lib/graph_agent/graph/mermaid_visualizer.rb
115
+ - lib/graph_agent/graph/message_graph.rb
116
+ - lib/graph_agent/graph/node.rb
117
+ - lib/graph_agent/graph/state_graph.rb
118
+ - lib/graph_agent/reducers.rb
119
+ - lib/graph_agent/state/schema.rb
120
+ - lib/graph_agent/types/cache_policy.rb
121
+ - lib/graph_agent/types/command.rb
122
+ - lib/graph_agent/types/interrupt.rb
123
+ - lib/graph_agent/types/retry_policy.rb
124
+ - lib/graph_agent/types/send.rb
125
+ - lib/graph_agent/types/state_snapshot.rb
126
+ - lib/graph_agent/version.rb
127
+ homepage: https://github.com/ai-firstly/graph-agent
128
+ licenses:
129
+ - MIT
130
+ metadata:
131
+ allowed_push_host: https://rubygems.org
132
+ homepage_uri: https://github.com/ai-firstly/graph-agent
133
+ source_code_uri: https://github.com/ai-firstly/graph-agent
134
+ changelog_uri: https://github.com/ai-firstly/graph-agent/blob/main/CHANGELOG.md
135
+ documentation_uri: https://rubydoc.info/gems/graph-agent
136
+ bug_tracker_uri: https://github.com/ai-firstly/graph-agent/issues
137
+ rubygems_mfa_required: 'true'
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: 3.1.0
146
+ - - "<"
147
+ - !ruby/object:Gem::Version
148
+ version: '5.0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubygems_version: 3.6.9
156
+ specification_version: 4
157
+ summary: A Ruby framework for building stateful, multi-actor agent workflows
158
+ test_files: []