durable_workflow 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/.claude/todo/01.amend.md +133 -0
- data/.claude/todo/02.amend.md +444 -0
- data/.claude/todo/phase-1-core/01-GEMSPEC.md +193 -0
- data/.claude/todo/phase-1-core/02-TYPES.md +462 -0
- data/.claude/todo/phase-1-core/03-EXECUTION.md +551 -0
- data/.claude/todo/phase-1-core/04-STEPS.md +603 -0
- data/.claude/todo/phase-1-core/05-PARSER.md +719 -0
- data/.claude/todo/phase-1-core/todo.md +574 -0
- data/.claude/todo/phase-2-runtime/01-STORAGE.md +641 -0
- data/.claude/todo/phase-2-runtime/02-RUNNERS.md +511 -0
- data/.claude/todo/phase-3-extensions/01-EXTENSION-SYSTEM.md +298 -0
- data/.claude/todo/phase-3-extensions/02-AI-PLUGIN.md +936 -0
- data/.claude/todo/phase-3-extensions/todo.md +262 -0
- data/.claude/todo/phase-4-ai-rework/01-DEPENDENCIES.md +107 -0
- data/.claude/todo/phase-4-ai-rework/02-CONFIGURATION.md +123 -0
- data/.claude/todo/phase-4-ai-rework/03-TOOL-REGISTRY.md +237 -0
- data/.claude/todo/phase-4-ai-rework/04-MCP-SERVER.md +432 -0
- data/.claude/todo/phase-4-ai-rework/05-MCP-CLIENT.md +333 -0
- data/.claude/todo/phase-4-ai-rework/06-EXECUTORS.md +397 -0
- data/.claude/todo/phase-4-ai-rework/todo.md +265 -0
- data/.claude/todo/phase-5-validation/.DS_Store +0 -0
- data/.claude/todo/phase-5-validation/01-TEST-GAPS.md +615 -0
- data/.claude/todo/phase-5-validation/01-TESTS.md +2378 -0
- data/.claude/todo/phase-5-validation/02-EXAMPLES-SIMPLE.md +744 -0
- data/.claude/todo/phase-5-validation/02-EXAMPLES.md +1857 -0
- data/.claude/todo/phase-5-validation/03-EXAMPLE-SUPPORT-AGENT.md +95 -0
- data/.claude/todo/phase-5-validation/04-EXAMPLE-ORDER-FULFILLMENT.md +94 -0
- data/.claude/todo/phase-5-validation/05-EXAMPLE-DATA-PIPELINE.md +145 -0
- data/.env.example +3 -0
- data/.rubocop.yml +64 -0
- data/0.3.amend.md +89 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +192 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +16 -0
- data/durable_workflow.gemspec +43 -0
- data/examples/approval_request.rb +106 -0
- data/examples/calculator.rb +154 -0
- data/examples/file_search_demo.rb +77 -0
- data/examples/hello_workflow.rb +57 -0
- data/examples/item_processor.rb +96 -0
- data/examples/order_fulfillment/Gemfile +6 -0
- data/examples/order_fulfillment/README.md +84 -0
- data/examples/order_fulfillment/run.rb +85 -0
- data/examples/order_fulfillment/services.rb +146 -0
- data/examples/order_fulfillment/workflow.yml +188 -0
- data/examples/parallel_fetch.rb +102 -0
- data/examples/service_integration.rb +137 -0
- data/examples/support_agent/Gemfile +6 -0
- data/examples/support_agent/README.md +91 -0
- data/examples/support_agent/config/claude_desktop.json +12 -0
- data/examples/support_agent/mcp_server.rb +49 -0
- data/examples/support_agent/run.rb +67 -0
- data/examples/support_agent/services.rb +113 -0
- data/examples/support_agent/workflow.yml +286 -0
- data/lib/durable_workflow/core/condition.rb +45 -0
- data/lib/durable_workflow/core/engine.rb +145 -0
- data/lib/durable_workflow/core/executors/approval.rb +51 -0
- data/lib/durable_workflow/core/executors/assign.rb +18 -0
- data/lib/durable_workflow/core/executors/base.rb +90 -0
- data/lib/durable_workflow/core/executors/call.rb +76 -0
- data/lib/durable_workflow/core/executors/end.rb +19 -0
- data/lib/durable_workflow/core/executors/halt.rb +24 -0
- data/lib/durable_workflow/core/executors/loop.rb +118 -0
- data/lib/durable_workflow/core/executors/parallel.rb +77 -0
- data/lib/durable_workflow/core/executors/registry.rb +34 -0
- data/lib/durable_workflow/core/executors/router.rb +26 -0
- data/lib/durable_workflow/core/executors/start.rb +61 -0
- data/lib/durable_workflow/core/executors/transform.rb +71 -0
- data/lib/durable_workflow/core/executors/workflow.rb +32 -0
- data/lib/durable_workflow/core/parser.rb +189 -0
- data/lib/durable_workflow/core/resolver.rb +61 -0
- data/lib/durable_workflow/core/schema_validator.rb +47 -0
- data/lib/durable_workflow/core/types/base.rb +41 -0
- data/lib/durable_workflow/core/types/condition.rb +25 -0
- data/lib/durable_workflow/core/types/configs.rb +103 -0
- data/lib/durable_workflow/core/types/entry.rb +26 -0
- data/lib/durable_workflow/core/types/results.rb +41 -0
- data/lib/durable_workflow/core/types/state.rb +95 -0
- data/lib/durable_workflow/core/types/step_def.rb +15 -0
- data/lib/durable_workflow/core/types/workflow_def.rb +43 -0
- data/lib/durable_workflow/core/types.rb +29 -0
- data/lib/durable_workflow/core/validator.rb +318 -0
- data/lib/durable_workflow/extensions/ai/ai.rb +149 -0
- data/lib/durable_workflow/extensions/ai/configuration.rb +41 -0
- data/lib/durable_workflow/extensions/ai/executors/agent.rb +150 -0
- data/lib/durable_workflow/extensions/ai/executors/file_search.rb +52 -0
- data/lib/durable_workflow/extensions/ai/executors/guardrail.rb +152 -0
- data/lib/durable_workflow/extensions/ai/executors/handoff.rb +33 -0
- data/lib/durable_workflow/extensions/ai/executors/mcp.rb +47 -0
- data/lib/durable_workflow/extensions/ai/mcp/adapter.rb +73 -0
- data/lib/durable_workflow/extensions/ai/mcp/client.rb +77 -0
- data/lib/durable_workflow/extensions/ai/mcp/rack_app.rb +66 -0
- data/lib/durable_workflow/extensions/ai/mcp/server.rb +122 -0
- data/lib/durable_workflow/extensions/ai/tool_registry.rb +63 -0
- data/lib/durable_workflow/extensions/ai/types.rb +213 -0
- data/lib/durable_workflow/extensions/ai.rb +6 -0
- data/lib/durable_workflow/extensions/base.rb +77 -0
- data/lib/durable_workflow/runners/adapters/inline.rb +42 -0
- data/lib/durable_workflow/runners/adapters/sidekiq.rb +69 -0
- data/lib/durable_workflow/runners/async.rb +100 -0
- data/lib/durable_workflow/runners/stream.rb +126 -0
- data/lib/durable_workflow/runners/sync.rb +40 -0
- data/lib/durable_workflow/storage/active_record.rb +148 -0
- data/lib/durable_workflow/storage/redis.rb +133 -0
- data/lib/durable_workflow/storage/sequel.rb +144 -0
- data/lib/durable_workflow/storage/store.rb +43 -0
- data/lib/durable_workflow/utils.rb +25 -0
- data/lib/durable_workflow/version.rb +5 -0
- data/lib/durable_workflow.rb +70 -0
- data/sig/durable_workflow.rbs +4 -0
- metadata +275 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# 01-EXTENSION-SYSTEM: Plugin Architecture
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Define the extension system that allows plugins (like AI) to register:
|
|
6
|
+
|
|
7
|
+
1. New step types (executors)
|
|
8
|
+
2. New config classes
|
|
9
|
+
3. Parser hooks for workflow-level data
|
|
10
|
+
4. Custom workflow definition attributes via `extensions` hash
|
|
11
|
+
|
|
12
|
+
## Dependencies
|
|
13
|
+
|
|
14
|
+
- Phase 1 complete
|
|
15
|
+
- Phase 2 complete
|
|
16
|
+
|
|
17
|
+
## Design Principles
|
|
18
|
+
|
|
19
|
+
1. **No Monkey Patching** - Extensions use hooks, not `alias_method`
|
|
20
|
+
2. **Registry Pattern** - Step types and configs register themselves
|
|
21
|
+
3. **Fail Fast** - Unknown step types fail at parse/validation time
|
|
22
|
+
4. **Isolation** - Extensions can't break core functionality
|
|
23
|
+
|
|
24
|
+
## How Extensions Work
|
|
25
|
+
|
|
26
|
+
### 1. Executor Registration
|
|
27
|
+
|
|
28
|
+
Extensions register their executors in the global registry:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# In extension code
|
|
32
|
+
DurableWorkflow::Core::Executors::Registry.register("agent", AgentExecutor)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Config Registration
|
|
36
|
+
|
|
37
|
+
Extensions register their config classes:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# In extension code
|
|
41
|
+
DurableWorkflow::Core.register_config("agent", AgentConfig)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Parser Hooks
|
|
45
|
+
|
|
46
|
+
Extensions inject parsing logic:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Before parse - modify raw YAML
|
|
50
|
+
DurableWorkflow::Core::Parser.before_parse do |yaml|
|
|
51
|
+
# Transform raw YAML before parsing
|
|
52
|
+
yaml
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# After parse - modify WorkflowDef
|
|
56
|
+
DurableWorkflow::Core::Parser.after_parse do |workflow|
|
|
57
|
+
# Parse extension-specific data and store in extensions hash
|
|
58
|
+
workflow.with(extensions: workflow.extensions.merge(my_data: parsed))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Config transformer - modify config for specific type
|
|
62
|
+
DurableWorkflow::Core::Parser.transform_config("agent") do |raw_config|
|
|
63
|
+
# Transform raw config before building typed config
|
|
64
|
+
raw_config
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Extension Data in WorkflowDef
|
|
69
|
+
|
|
70
|
+
Extensions store their data in `workflow.extensions`:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
workflow.extensions[:ai] # => { agents: {...}, tools: {...} }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Files to Create
|
|
77
|
+
|
|
78
|
+
### 1. `lib/durable_workflow/extensions/base.rb`
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# frozen_string_literal: true
|
|
82
|
+
|
|
83
|
+
module DurableWorkflow
|
|
84
|
+
module Extensions
|
|
85
|
+
# Base class for extensions
|
|
86
|
+
# Extensions inherit from this and call register! to set up
|
|
87
|
+
class Base
|
|
88
|
+
class << self
|
|
89
|
+
# Extension name (used as key in workflow.extensions)
|
|
90
|
+
def extension_name
|
|
91
|
+
@extension_name ||= name.split("::").last.downcase
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def extension_name=(name)
|
|
95
|
+
@extension_name = name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Register all components of the extension
|
|
99
|
+
def register!
|
|
100
|
+
register_configs
|
|
101
|
+
register_executors
|
|
102
|
+
register_parser_hooks
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Override in subclass to register config classes
|
|
106
|
+
def register_configs
|
|
107
|
+
# Example:
|
|
108
|
+
# DurableWorkflow::Core.register_config("agent", AgentConfig)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Override in subclass to register executors
|
|
112
|
+
def register_executors
|
|
113
|
+
# Example:
|
|
114
|
+
# DurableWorkflow::Core::Executors::Registry.register("agent", AgentExecutor)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Override in subclass to register parser hooks
|
|
118
|
+
def register_parser_hooks
|
|
119
|
+
# Example:
|
|
120
|
+
# DurableWorkflow::Core::Parser.after_parse { |wf| ... }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Helper to get extension data from workflow
|
|
124
|
+
def data_from(workflow)
|
|
125
|
+
workflow.extensions[extension_name.to_sym] || {}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Helper to store extension data in workflow
|
|
129
|
+
def store_in(workflow, data)
|
|
130
|
+
workflow.with(extensions: workflow.extensions.merge(extension_name.to_sym => data))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Registry of loaded extensions
|
|
136
|
+
@extensions = {}
|
|
137
|
+
|
|
138
|
+
class << self
|
|
139
|
+
attr_reader :extensions
|
|
140
|
+
|
|
141
|
+
def register(name, extension_class)
|
|
142
|
+
@extensions[name.to_sym] = extension_class
|
|
143
|
+
extension_class.register!
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def [](name)
|
|
147
|
+
@extensions[name.to_sym]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def loaded?(name)
|
|
151
|
+
@extensions.key?(name.to_sym)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 2. Update `lib/durable_workflow/core/types/configs.rb`
|
|
159
|
+
|
|
160
|
+
Ensure the registry supports extension registration:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# frozen_string_literal: true
|
|
164
|
+
|
|
165
|
+
module DurableWorkflow
|
|
166
|
+
module Core
|
|
167
|
+
# ... existing config classes ...
|
|
168
|
+
|
|
169
|
+
# Mutable registry - extensions add their configs here
|
|
170
|
+
CONFIG_REGISTRY = {
|
|
171
|
+
"start" => StartConfig,
|
|
172
|
+
"end" => EndConfig,
|
|
173
|
+
"call" => CallConfig,
|
|
174
|
+
"assign" => AssignConfig,
|
|
175
|
+
"router" => RouterConfig,
|
|
176
|
+
"loop" => LoopConfig,
|
|
177
|
+
"halt" => HaltConfig,
|
|
178
|
+
"approval" => ApprovalConfig,
|
|
179
|
+
"transform" => TransformConfig,
|
|
180
|
+
"parallel" => ParallelConfig,
|
|
181
|
+
"workflow" => WorkflowConfig
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Allow extensions to register config classes
|
|
185
|
+
def self.register_config(type, klass)
|
|
186
|
+
CONFIG_REGISTRY[type.to_s] = klass
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check if a config type is registered
|
|
190
|
+
def self.config_registered?(type)
|
|
191
|
+
CONFIG_REGISTRY.key?(type.to_s)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 3. Example Extension Structure
|
|
198
|
+
|
|
199
|
+
Here's how an extension should be structured:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# lib/durable_workflow/extensions/my_extension/my_extension.rb
|
|
203
|
+
|
|
204
|
+
module DurableWorkflow
|
|
205
|
+
module Extensions
|
|
206
|
+
module MyExtension
|
|
207
|
+
class Extension < Base
|
|
208
|
+
self.extension_name = "my_extension"
|
|
209
|
+
|
|
210
|
+
def self.register_configs
|
|
211
|
+
Core.register_config("my_step", MyStepConfig)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.register_executors
|
|
215
|
+
Core::Executors::Registry.register("my_step", MyStepExecutor)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.register_parser_hooks
|
|
219
|
+
Core::Parser.after_parse do |workflow|
|
|
220
|
+
# Parse extension-specific YAML keys
|
|
221
|
+
raw = workflow.to_h
|
|
222
|
+
my_data = parse_my_data(raw)
|
|
223
|
+
store_in(workflow, my_data)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.parse_my_data(raw)
|
|
228
|
+
# Parse extension-specific data from raw workflow hash
|
|
229
|
+
{}
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Config class
|
|
234
|
+
class MyStepConfig < Core::StepConfig
|
|
235
|
+
attribute :some_field, Types::Strict::String
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Executor class
|
|
239
|
+
class MyStepExecutor < Core::Executors::Base
|
|
240
|
+
def call(state)
|
|
241
|
+
# Do work
|
|
242
|
+
continue(state)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Auto-register when required
|
|
250
|
+
DurableWorkflow::Extensions.register(:my_extension, DurableWorkflow::Extensions::MyExtension::Extension)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Extension Loading Pattern
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# User code - load core
|
|
257
|
+
require "durable_workflow"
|
|
258
|
+
|
|
259
|
+
# Load extension (auto-registers)
|
|
260
|
+
require "durable_workflow/extensions/ai"
|
|
261
|
+
|
|
262
|
+
# Now AI step types are available
|
|
263
|
+
wf = DurableWorkflow.load("workflow_with_agents.yml")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Validation with Extensions
|
|
267
|
+
|
|
268
|
+
The validator automatically checks step types against the registry:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# In validator.rb
|
|
272
|
+
def check_step_types!
|
|
273
|
+
@workflow.steps.each do |step|
|
|
274
|
+
unless Executors::Registry.registered?(step.type)
|
|
275
|
+
@errors << "Unknown step type '#{step.type}' in step '#{step.id}'"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
If an extension isn't loaded, its step types will fail validation.
|
|
282
|
+
|
|
283
|
+
## Best Practices for Extensions
|
|
284
|
+
|
|
285
|
+
1. **Namespace your data** - Store in `extensions[:my_extension]`, not top-level
|
|
286
|
+
2. **Register early** - Call `register!` when the extension is required
|
|
287
|
+
3. **Validate your configs** - Use dry-types constraints
|
|
288
|
+
4. **Don't modify core types** - Extend, don't mutate
|
|
289
|
+
5. **Document YAML schema** - Users need to know what to write
|
|
290
|
+
|
|
291
|
+
## Acceptance Criteria
|
|
292
|
+
|
|
293
|
+
1. `Extensions.register(:name, ExtensionClass)` registers an extension
|
|
294
|
+
2. Extensions can add step types via `Registry.register`
|
|
295
|
+
3. Extensions can add configs via `Core.register_config`
|
|
296
|
+
4. Parser hooks run in order (before -> parse -> after)
|
|
297
|
+
5. Unknown step types fail validation if extension not loaded
|
|
298
|
+
6. `workflow.extensions[:name]` returns extension data
|