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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +50 -0
- data/.github/workflows/release.yml +49 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +126 -0
- data/CHANGELOG.md +26 -0
- data/CLAUDE.md +128 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +94 -0
- data/LICENSE +21 -0
- data/Makefile +114 -0
- data/README.md +464 -0
- data/Rakefile +15 -0
- data/docs/README.md +55 -0
- data/docs/api_reference.md +832 -0
- data/docs/concepts.md +216 -0
- data/docs/edges.md +265 -0
- data/docs/error_handling.md +241 -0
- data/docs/human_in_the_loop.md +231 -0
- data/docs/persistence.md +276 -0
- data/docs/quickstart.md +154 -0
- data/docs/send_and_command.md +218 -0
- data/docs/state.md +181 -0
- data/docs/streaming.md +172 -0
- data/graph-agent.gemspec +48 -0
- data/lib/graph_agent/channels/base_channel.rb +52 -0
- data/lib/graph_agent/channels/binary_operator_aggregate.rb +56 -0
- data/lib/graph_agent/channels/ephemeral_value.rb +59 -0
- data/lib/graph_agent/channels/last_value.rb +49 -0
- data/lib/graph_agent/channels/topic.rb +58 -0
- data/lib/graph_agent/checkpoint/base_saver.rb +38 -0
- data/lib/graph_agent/checkpoint/in_memory_saver.rb +145 -0
- data/lib/graph_agent/constants.rb +9 -0
- data/lib/graph_agent/errors.rb +41 -0
- data/lib/graph_agent/graph/compiled_state_graph.rb +362 -0
- data/lib/graph_agent/graph/conditional_edge.rb +57 -0
- data/lib/graph_agent/graph/edge.rb +23 -0
- data/lib/graph_agent/graph/mermaid_visualizer.rb +154 -0
- data/lib/graph_agent/graph/message_graph.rb +18 -0
- data/lib/graph_agent/graph/node.rb +61 -0
- data/lib/graph_agent/graph/state_graph.rb +197 -0
- data/lib/graph_agent/reducers.rb +34 -0
- data/lib/graph_agent/state/schema.rb +54 -0
- data/lib/graph_agent/types/cache_policy.rb +12 -0
- data/lib/graph_agent/types/command.rb +26 -0
- data/lib/graph_agent/types/interrupt.rb +28 -0
- data/lib/graph_agent/types/retry_policy.rb +42 -0
- data/lib/graph_agent/types/send.rb +26 -0
- data/lib/graph_agent/types/state_snapshot.rb +28 -0
- data/lib/graph_agent/version.rb +5 -0
- data/lib/graph_agent.rb +29 -0
- metadata +158 -0
data/README.md
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# GraphAgent
|
|
2
|
+
|
|
3
|
+
A Ruby framework for building stateful, multi-actor agent workflows. Ruby port of [LangGraph](https://github.com/langchain-ai/langgraph).
|
|
4
|
+
|
|
5
|
+
GraphAgent provides low-level infrastructure for building agents that can persist through failures, support human-in-the-loop workflows, and maintain comprehensive memory across sessions.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'graph-agent'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "graph_agent"
|
|
25
|
+
|
|
26
|
+
# Define your state schema with reducers
|
|
27
|
+
schema = GraphAgent::State::Schema.new do
|
|
28
|
+
field :messages, type: Array, reducer: ->(a, b) { a + b }, default: []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create a graph
|
|
32
|
+
graph = GraphAgent::Graph::StateGraph.new(schema)
|
|
33
|
+
|
|
34
|
+
# Add nodes (functions that process state)
|
|
35
|
+
graph.add_node("greet") do |state|
|
|
36
|
+
{ messages: [{ role: "ai", content: "Hello! How can I help you?" }] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Define edges
|
|
40
|
+
graph.add_edge(GraphAgent::START, "greet")
|
|
41
|
+
graph.add_edge("greet", GraphAgent::END_NODE)
|
|
42
|
+
|
|
43
|
+
# Compile and run
|
|
44
|
+
app = graph.compile
|
|
45
|
+
result = app.invoke({ messages: [{ role: "user", content: "Hi!" }] })
|
|
46
|
+
puts result[:messages]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Core Concepts
|
|
50
|
+
|
|
51
|
+
### State
|
|
52
|
+
|
|
53
|
+
State is the shared data structure that represents the current snapshot of your application. Define it using a Schema with fields and optional reducers:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
schema = GraphAgent::State::Schema.new do
|
|
57
|
+
field :messages, type: Array, reducer: GraphAgent::Reducers::ADD, default: []
|
|
58
|
+
field :count, type: Integer, reducer: GraphAgent::Reducers::ADD, default: 0
|
|
59
|
+
field :last_input, type: String # No reducer = last-value semantics
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can also use a Hash-based schema:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
graph = GraphAgent::Graph::StateGraph.new({
|
|
67
|
+
messages: { type: Array, reducer: ->(a, b) { a + b }, default: [] },
|
|
68
|
+
count: { type: Integer, reducer: ->(a, b) { a + b }, default: 0 },
|
|
69
|
+
status: {} # last-value semantics
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Built-in Reducers
|
|
74
|
+
|
|
75
|
+
| Reducer | Description |
|
|
76
|
+
|---------|-------------|
|
|
77
|
+
| `Reducers::ADD` | Adds/concatenates values (`a + b`) |
|
|
78
|
+
| `Reducers::APPEND` | Appends to array (`Array(a) + Array(b)`) |
|
|
79
|
+
| `Reducers::MERGE` | Merges hashes (`a.merge(b)`) |
|
|
80
|
+
| `Reducers::REPLACE` | Always replaces (`b`) |
|
|
81
|
+
| `Reducers.add_messages` | Smart message merging by ID |
|
|
82
|
+
|
|
83
|
+
### Nodes
|
|
84
|
+
|
|
85
|
+
Nodes are functions that process the current state and return updates. They receive the state as input and return a Hash of updates:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
graph.add_node("process") do |state|
|
|
89
|
+
{ count: 1, status: "processed" }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Or with config access:
|
|
93
|
+
graph.add_node("process") do |state, config|
|
|
94
|
+
thread_id = config.dig(:configurable, :thread_id)
|
|
95
|
+
{ count: 1 }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Using a method:
|
|
99
|
+
def my_processor(state)
|
|
100
|
+
{ count: state[:count] + 1 }
|
|
101
|
+
end
|
|
102
|
+
graph.add_node("process", method(:my_processor))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Edges
|
|
106
|
+
|
|
107
|
+
Edges define how the graph transitions between nodes:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Normal edge: always go from A to B
|
|
111
|
+
graph.add_edge("node_a", "node_b")
|
|
112
|
+
|
|
113
|
+
# Entry point: where to start
|
|
114
|
+
graph.add_edge(GraphAgent::START, "first_node")
|
|
115
|
+
# or
|
|
116
|
+
graph.set_entry_point("first_node")
|
|
117
|
+
|
|
118
|
+
# Exit point: where to finish
|
|
119
|
+
graph.add_edge("last_node", GraphAgent::END_NODE)
|
|
120
|
+
# or
|
|
121
|
+
graph.set_finish_point("last_node")
|
|
122
|
+
|
|
123
|
+
# Conditional edge: route based on state
|
|
124
|
+
graph.add_conditional_edges(
|
|
125
|
+
"classifier",
|
|
126
|
+
->(state) { state[:urgency] == "high" ? "escalate" : "auto_reply" }
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# With path map:
|
|
130
|
+
graph.add_conditional_edges(
|
|
131
|
+
"classifier",
|
|
132
|
+
->(state) { state[:category] },
|
|
133
|
+
{ "billing" => "billing_handler", "tech" => "tech_handler", "other" => "general_handler" }
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Conditional Edges
|
|
138
|
+
|
|
139
|
+
Conditional edges allow dynamic routing based on state:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
should_continue = ->(state) do
|
|
143
|
+
last_message = state[:messages].last
|
|
144
|
+
if last_message[:tool_calls]&.any?
|
|
145
|
+
"tool_node"
|
|
146
|
+
else
|
|
147
|
+
GraphAgent::END_NODE.to_s
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
graph.add_conditional_edges("llm_call", should_continue)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Send (Map-Reduce Pattern)
|
|
155
|
+
|
|
156
|
+
Use `Send` to dynamically create parallel tasks:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
continue_to_jokes = ->(state) do
|
|
160
|
+
state[:subjects].map do |subject|
|
|
161
|
+
GraphAgent::Send.new("generate_joke", { subject: subject })
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
graph.add_conditional_edges("get_subjects", continue_to_jokes)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Command (Dynamic Routing + State Updates)
|
|
169
|
+
|
|
170
|
+
Use `Command` to combine state updates with routing decisions:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
graph.add_node("router") do |state|
|
|
174
|
+
GraphAgent::Command.new(
|
|
175
|
+
update: { routed: true },
|
|
176
|
+
goto: "target_node"
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Compilation & Execution
|
|
182
|
+
|
|
183
|
+
### Compile
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# Basic compilation
|
|
187
|
+
app = graph.compile
|
|
188
|
+
|
|
189
|
+
# With checkpointer for persistence
|
|
190
|
+
checkpointer = GraphAgent::Checkpoint::InMemorySaver.new
|
|
191
|
+
app = graph.compile(checkpointer: checkpointer)
|
|
192
|
+
|
|
193
|
+
# With interrupts for human-in-the-loop
|
|
194
|
+
app = graph.compile(
|
|
195
|
+
checkpointer: checkpointer,
|
|
196
|
+
interrupt_before: ["human_review"],
|
|
197
|
+
interrupt_after: ["draft"]
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Invoke
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
result = app.invoke(
|
|
205
|
+
{ messages: [{ role: "user", content: "Hello" }] },
|
|
206
|
+
config: { configurable: { thread_id: "thread-1" } },
|
|
207
|
+
recursion_limit: 50
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Stream
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# Stream state values after each step
|
|
215
|
+
app.stream(input, stream_mode: :values) do |state|
|
|
216
|
+
puts "State: #{state}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Stream per-node updates
|
|
220
|
+
app.stream(input, stream_mode: :updates) do |updates|
|
|
221
|
+
puts "Updates: #{updates}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Using Enumerator
|
|
225
|
+
events = app.stream(input, stream_mode: :values)
|
|
226
|
+
events.each { |state| puts state }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Persistence & Checkpointing
|
|
230
|
+
|
|
231
|
+
### InMemorySaver
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
checkpointer = GraphAgent::Checkpoint::InMemorySaver.new
|
|
235
|
+
app = graph.compile(checkpointer: checkpointer)
|
|
236
|
+
|
|
237
|
+
config = { configurable: { thread_id: "user-123" } }
|
|
238
|
+
|
|
239
|
+
# First invocation
|
|
240
|
+
result1 = app.invoke({ messages: [{ role: "user", content: "Hi" }] }, config: config)
|
|
241
|
+
|
|
242
|
+
# Second invocation continues from checkpoint
|
|
243
|
+
result2 = app.invoke({ messages: [{ role: "user", content: "What did I say?" }] }, config: config)
|
|
244
|
+
|
|
245
|
+
# Inspect state
|
|
246
|
+
snapshot = app.get_state(config)
|
|
247
|
+
puts snapshot.values
|
|
248
|
+
puts snapshot.metadata
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### State Management
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# Get current state
|
|
255
|
+
snapshot = app.get_state(config)
|
|
256
|
+
|
|
257
|
+
# Update state manually
|
|
258
|
+
app.update_state(config, { messages: [{ role: "system", content: "Updated" }] })
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Human-in-the-Loop
|
|
262
|
+
|
|
263
|
+
Use interrupts to pause execution and wait for human input:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
app = graph.compile(
|
|
267
|
+
checkpointer: checkpointer,
|
|
268
|
+
interrupt_before: ["human_review"]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
config = { configurable: { thread_id: "review-1" } }
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
result = app.invoke(input, config: config)
|
|
275
|
+
rescue GraphAgent::GraphInterrupt => e
|
|
276
|
+
puts "Interrupted: #{e.interrupts}"
|
|
277
|
+
# Human reviews and updates state...
|
|
278
|
+
app.update_state(config, { approved: true })
|
|
279
|
+
# Resume execution
|
|
280
|
+
result = app.invoke(nil, config: config)
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Error Handling
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
begin
|
|
288
|
+
result = app.invoke(input, config: config)
|
|
289
|
+
rescue GraphAgent::GraphRecursionError
|
|
290
|
+
puts "Graph exceeded maximum steps"
|
|
291
|
+
rescue GraphAgent::InvalidUpdateError => e
|
|
292
|
+
puts "Invalid state update: #{e.message}"
|
|
293
|
+
rescue GraphAgent::NodeExecutionError => e
|
|
294
|
+
puts "Error in node '#{e.node_name}': #{e.original_error.message}"
|
|
295
|
+
rescue GraphAgent::GraphInterrupt => e
|
|
296
|
+
puts "Graph interrupted with #{e.interrupts.length} interrupt(s)"
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Retry Policy
|
|
301
|
+
|
|
302
|
+
Configure automatic retries for nodes:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
retry_policy = GraphAgent::RetryPolicy.new(
|
|
306
|
+
max_attempts: 3,
|
|
307
|
+
initial_interval: 0.5,
|
|
308
|
+
backoff_factor: 2.0,
|
|
309
|
+
max_interval: 128.0,
|
|
310
|
+
jitter: true,
|
|
311
|
+
retry_on: [Net::ReadTimeout, Timeout::Error]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
graph.add_node("api_call", method(:call_api), retry_policy: retry_policy)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## MessageGraph
|
|
318
|
+
|
|
319
|
+
A convenience wrapper with pre-configured message state:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
graph = GraphAgent::Graph::MessageGraph.new
|
|
323
|
+
|
|
324
|
+
graph.add_node("chat") do |state|
|
|
325
|
+
{ messages: [{ role: "ai", content: "Hello!" }] }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
graph.add_edge(GraphAgent::START, "chat")
|
|
329
|
+
graph.add_edge("chat", GraphAgent::END_NODE)
|
|
330
|
+
|
|
331
|
+
app = graph.compile
|
|
332
|
+
result = app.invoke({ messages: [{ role: "user", content: "Hi" }] })
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Advanced Patterns
|
|
336
|
+
|
|
337
|
+
### Multi-Agent Handoff
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
schema = GraphAgent::State::Schema.new do
|
|
341
|
+
field :messages, reducer: GraphAgent::Reducers::ADD, default: []
|
|
342
|
+
field :current_agent, default: "router"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
graph = GraphAgent::Graph::StateGraph.new(schema)
|
|
346
|
+
|
|
347
|
+
graph.add_node("router") do |state|
|
|
348
|
+
topic = classify_topic(state[:messages].last)
|
|
349
|
+
GraphAgent::Command.new(
|
|
350
|
+
update: { current_agent: topic },
|
|
351
|
+
goto: topic
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
graph.add_node("billing") do |state|
|
|
356
|
+
{ messages: [handle_billing(state)] }
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
graph.add_node("technical") do |state|
|
|
360
|
+
{ messages: [handle_technical(state)] }
|
|
361
|
+
end
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Sequences
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
graph.add_sequence([
|
|
368
|
+
["step1", ->(s) { { data: "processed" } }],
|
|
369
|
+
["step2", ->(s) { { data: s[:data] + "_validated" } }],
|
|
370
|
+
["step3", ->(s) { { result: s[:data] } }]
|
|
371
|
+
])
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Visualization
|
|
375
|
+
|
|
376
|
+
### Mermaid Diagram
|
|
377
|
+
|
|
378
|
+
Generate a Mermaid flowchart to visualize your graph structure:
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
# Get Mermaid diagram as string
|
|
382
|
+
mermaid = graph.to_mermaid
|
|
383
|
+
puts mermaid
|
|
384
|
+
|
|
385
|
+
# Or print directly
|
|
386
|
+
graph.print_mermaid
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Output example:
|
|
390
|
+
|
|
391
|
+
```mermaid
|
|
392
|
+
graph TD
|
|
393
|
+
classDef start fill:#e1f5e1,stroke:#4caf50,stroke-width:2px
|
|
394
|
+
classDef end fill:#ffebee,stroke:#f44336,stroke-width:2px
|
|
395
|
+
classDef node fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,rx:5px
|
|
396
|
+
classDef condition fill:#fff9c4,stroke:#ffc107,stroke-width:2px
|
|
397
|
+
|
|
398
|
+
__start__["START"] --> step1
|
|
399
|
+
step1 --> step2
|
|
400
|
+
step2 --> __end__["END"]
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
For conditional edges:
|
|
404
|
+
|
|
405
|
+
```mermaid
|
|
406
|
+
graph TD
|
|
407
|
+
__start__["START"] --> router
|
|
408
|
+
router --> router_cond_0{"a / b"}
|
|
409
|
+
router_cond_0 -.->|a| handler_a
|
|
410
|
+
router_cond_0 -.->|b| handler_b
|
|
411
|
+
handler_a --> __end__
|
|
412
|
+
handler_b --> __end__
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
You can copy the output and paste it into [Mermaid Live Editor](https://mermaid.live) or any Markdown viewer that supports Mermaid diagrams.
|
|
416
|
+
|
|
417
|
+
## Architecture
|
|
418
|
+
|
|
419
|
+
GraphAgent implements the Pregel execution model (Bulk Synchronous Parallel):
|
|
420
|
+
|
|
421
|
+
1. **PLAN**: Determine which nodes to execute based on edges and state
|
|
422
|
+
2. **EXECUTE**: Run all active nodes with a frozen state snapshot
|
|
423
|
+
3. **UPDATE**: Apply all writes atomically via reducers
|
|
424
|
+
4. **CHECKPOINT**: Save state if checkpointer is configured
|
|
425
|
+
5. **REPEAT**: Continue until END is reached or recursion limit hit
|
|
426
|
+
|
|
427
|
+
## API Reference
|
|
428
|
+
|
|
429
|
+
### `GraphAgent::Graph::StateGraph`
|
|
430
|
+
|
|
431
|
+
| Method | Description |
|
|
432
|
+
|--------|-------------|
|
|
433
|
+
| `new(schema)` | Create a new graph with state schema |
|
|
434
|
+
| `add_node(name, action)` | Add a node to the graph |
|
|
435
|
+
| `add_edge(from, to)` | Add a directed edge |
|
|
436
|
+
| `add_conditional_edges(source, path, path_map)` | Add conditional routing |
|
|
437
|
+
| `add_sequence(nodes)` | Add a sequence of nodes |
|
|
438
|
+
| `set_entry_point(node)` | Set the entry node |
|
|
439
|
+
| `set_finish_point(node)` | Set the exit node |
|
|
440
|
+
| `compile(options)` | Compile into executable graph |
|
|
441
|
+
|
|
442
|
+
### `GraphAgent::Graph::CompiledStateGraph`
|
|
443
|
+
|
|
444
|
+
| Method | Description |
|
|
445
|
+
|--------|-------------|
|
|
446
|
+
| `invoke(input, config:, recursion_limit:)` | Run graph to completion |
|
|
447
|
+
| `stream(input, config:, stream_mode:)` | Stream execution events |
|
|
448
|
+
| `get_state(config)` | Get current state snapshot |
|
|
449
|
+
| `update_state(config, values)` | Manually update state |
|
|
450
|
+
|
|
451
|
+
### Constants
|
|
452
|
+
|
|
453
|
+
| Constant | Description |
|
|
454
|
+
|----------|-------------|
|
|
455
|
+
| `GraphAgent::START` | Entry point sentinel |
|
|
456
|
+
| `GraphAgent::END_NODE` | Terminal node sentinel |
|
|
457
|
+
|
|
458
|
+
## License
|
|
459
|
+
|
|
460
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
461
|
+
|
|
462
|
+
## Acknowledgements
|
|
463
|
+
|
|
464
|
+
Inspired by [LangGraph](https://github.com/langchain-ai/langgraph) by LangChain Inc, which is in turn inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require_relative "lib/graph_agent/version"
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
+
|
|
9
|
+
task default: :spec
|
|
10
|
+
|
|
11
|
+
# Custom release task
|
|
12
|
+
desc "Release gem to RubyGems"
|
|
13
|
+
task release_gem: [:build] do
|
|
14
|
+
sh "gem push graph-agent-#{GraphAgent::VERSION}.gem"
|
|
15
|
+
end
|
data/docs/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# GraphAgent Documentation
|
|
2
|
+
|
|
3
|
+
A Ruby framework for building stateful, multi-actor agent workflows.
|
|
4
|
+
Ruby port of [LangGraph](https://github.com/langchain-ai/langgraph).
|
|
5
|
+
|
|
6
|
+
## Guides
|
|
7
|
+
|
|
8
|
+
| Document | Description |
|
|
9
|
+
|----------|-------------|
|
|
10
|
+
| [Quickstart](quickstart.md) | Installation, minimal examples, and first steps |
|
|
11
|
+
| [Core Concepts](concepts.md) | Graphs, state, nodes, edges, supersteps, channels, and compilation |
|
|
12
|
+
| [State Management](state.md) | Schema DSL, reducers, defaults, and atomic updates |
|
|
13
|
+
| [Edges](edges.md) | Normal, conditional, entry/exit, waiting, and sequence edges |
|
|
14
|
+
| [Send & Command](send_and_command.md) | Map-reduce fan-out/fan-in with `Send` and combined routing with `Command` |
|
|
15
|
+
| [Persistence](persistence.md) | Checkpointing, thread-based conversations, and state snapshots |
|
|
16
|
+
| [Streaming](streaming.md) | Stream modes (`:values`, `:updates`, `:debug`), block and enumerator usage |
|
|
17
|
+
| [Human-in-the-Loop](human_in_the_loop.md) | Interrupts, inspecting/modifying state, and resuming execution |
|
|
18
|
+
| [Error Handling](error_handling.md) | Error classes, recursion limits, retry policies |
|
|
19
|
+
| [API Reference](api_reference.md) | Full reference for every class, method, and constant |
|
|
20
|
+
|
|
21
|
+
## Source Layout
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
lib/
|
|
25
|
+
graph_agent.rb # Entry point & requires
|
|
26
|
+
graph_agent/
|
|
27
|
+
constants.rb # START, END_NODE sentinels
|
|
28
|
+
errors.rb # Error hierarchy
|
|
29
|
+
reducers.rb # Built-in reducer functions
|
|
30
|
+
state/
|
|
31
|
+
schema.rb # Schema DSL for state definition
|
|
32
|
+
channels/
|
|
33
|
+
base_channel.rb # Abstract channel interface
|
|
34
|
+
last_value.rb # Single-value channel
|
|
35
|
+
binary_operator_aggregate.rb # Reducer-based aggregation channel
|
|
36
|
+
ephemeral_value.rb # Per-step ephemeral channel
|
|
37
|
+
topic.rb # Multi-value topic channel
|
|
38
|
+
types/
|
|
39
|
+
send.rb # Send (fan-out routing)
|
|
40
|
+
command.rb # Command (update + routing)
|
|
41
|
+
interrupt.rb # Interrupt value type
|
|
42
|
+
retry_policy.rb # Retry configuration
|
|
43
|
+
cache_policy.rb # Cache configuration
|
|
44
|
+
state_snapshot.rb # Snapshot of graph state
|
|
45
|
+
checkpoint/
|
|
46
|
+
base_saver.rb # Abstract checkpoint saver
|
|
47
|
+
in_memory_saver.rb # In-memory checkpoint implementation
|
|
48
|
+
graph/
|
|
49
|
+
node.rb # Node wrapper with retry support
|
|
50
|
+
edge.rb # Static edge
|
|
51
|
+
conditional_edge.rb # Dynamic conditional edge
|
|
52
|
+
state_graph.rb # Graph builder
|
|
53
|
+
compiled_state_graph.rb # Compiled executable graph (Pregel)
|
|
54
|
+
message_graph.rb # Pre-built message-oriented graph
|
|
55
|
+
```
|