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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/todo/01.amend.md +133 -0
  3. data/.claude/todo/02.amend.md +444 -0
  4. data/.claude/todo/phase-1-core/01-GEMSPEC.md +193 -0
  5. data/.claude/todo/phase-1-core/02-TYPES.md +462 -0
  6. data/.claude/todo/phase-1-core/03-EXECUTION.md +551 -0
  7. data/.claude/todo/phase-1-core/04-STEPS.md +603 -0
  8. data/.claude/todo/phase-1-core/05-PARSER.md +719 -0
  9. data/.claude/todo/phase-1-core/todo.md +574 -0
  10. data/.claude/todo/phase-2-runtime/01-STORAGE.md +641 -0
  11. data/.claude/todo/phase-2-runtime/02-RUNNERS.md +511 -0
  12. data/.claude/todo/phase-3-extensions/01-EXTENSION-SYSTEM.md +298 -0
  13. data/.claude/todo/phase-3-extensions/02-AI-PLUGIN.md +936 -0
  14. data/.claude/todo/phase-3-extensions/todo.md +262 -0
  15. data/.claude/todo/phase-4-ai-rework/01-DEPENDENCIES.md +107 -0
  16. data/.claude/todo/phase-4-ai-rework/02-CONFIGURATION.md +123 -0
  17. data/.claude/todo/phase-4-ai-rework/03-TOOL-REGISTRY.md +237 -0
  18. data/.claude/todo/phase-4-ai-rework/04-MCP-SERVER.md +432 -0
  19. data/.claude/todo/phase-4-ai-rework/05-MCP-CLIENT.md +333 -0
  20. data/.claude/todo/phase-4-ai-rework/06-EXECUTORS.md +397 -0
  21. data/.claude/todo/phase-4-ai-rework/todo.md +265 -0
  22. data/.claude/todo/phase-5-validation/.DS_Store +0 -0
  23. data/.claude/todo/phase-5-validation/01-TEST-GAPS.md +615 -0
  24. data/.claude/todo/phase-5-validation/01-TESTS.md +2378 -0
  25. data/.claude/todo/phase-5-validation/02-EXAMPLES-SIMPLE.md +744 -0
  26. data/.claude/todo/phase-5-validation/02-EXAMPLES.md +1857 -0
  27. data/.claude/todo/phase-5-validation/03-EXAMPLE-SUPPORT-AGENT.md +95 -0
  28. data/.claude/todo/phase-5-validation/04-EXAMPLE-ORDER-FULFILLMENT.md +94 -0
  29. data/.claude/todo/phase-5-validation/05-EXAMPLE-DATA-PIPELINE.md +145 -0
  30. data/.env.example +3 -0
  31. data/.rubocop.yml +64 -0
  32. data/0.3.amend.md +89 -0
  33. data/CHANGELOG.md +5 -0
  34. data/CODE_OF_CONDUCT.md +84 -0
  35. data/Gemfile +22 -0
  36. data/Gemfile.lock +192 -0
  37. data/LICENSE.txt +21 -0
  38. data/README.md +39 -0
  39. data/Rakefile +16 -0
  40. data/durable_workflow.gemspec +43 -0
  41. data/examples/approval_request.rb +106 -0
  42. data/examples/calculator.rb +154 -0
  43. data/examples/file_search_demo.rb +77 -0
  44. data/examples/hello_workflow.rb +57 -0
  45. data/examples/item_processor.rb +96 -0
  46. data/examples/order_fulfillment/Gemfile +6 -0
  47. data/examples/order_fulfillment/README.md +84 -0
  48. data/examples/order_fulfillment/run.rb +85 -0
  49. data/examples/order_fulfillment/services.rb +146 -0
  50. data/examples/order_fulfillment/workflow.yml +188 -0
  51. data/examples/parallel_fetch.rb +102 -0
  52. data/examples/service_integration.rb +137 -0
  53. data/examples/support_agent/Gemfile +6 -0
  54. data/examples/support_agent/README.md +91 -0
  55. data/examples/support_agent/config/claude_desktop.json +12 -0
  56. data/examples/support_agent/mcp_server.rb +49 -0
  57. data/examples/support_agent/run.rb +67 -0
  58. data/examples/support_agent/services.rb +113 -0
  59. data/examples/support_agent/workflow.yml +286 -0
  60. data/lib/durable_workflow/core/condition.rb +45 -0
  61. data/lib/durable_workflow/core/engine.rb +145 -0
  62. data/lib/durable_workflow/core/executors/approval.rb +51 -0
  63. data/lib/durable_workflow/core/executors/assign.rb +18 -0
  64. data/lib/durable_workflow/core/executors/base.rb +90 -0
  65. data/lib/durable_workflow/core/executors/call.rb +76 -0
  66. data/lib/durable_workflow/core/executors/end.rb +19 -0
  67. data/lib/durable_workflow/core/executors/halt.rb +24 -0
  68. data/lib/durable_workflow/core/executors/loop.rb +118 -0
  69. data/lib/durable_workflow/core/executors/parallel.rb +77 -0
  70. data/lib/durable_workflow/core/executors/registry.rb +34 -0
  71. data/lib/durable_workflow/core/executors/router.rb +26 -0
  72. data/lib/durable_workflow/core/executors/start.rb +61 -0
  73. data/lib/durable_workflow/core/executors/transform.rb +71 -0
  74. data/lib/durable_workflow/core/executors/workflow.rb +32 -0
  75. data/lib/durable_workflow/core/parser.rb +189 -0
  76. data/lib/durable_workflow/core/resolver.rb +61 -0
  77. data/lib/durable_workflow/core/schema_validator.rb +47 -0
  78. data/lib/durable_workflow/core/types/base.rb +41 -0
  79. data/lib/durable_workflow/core/types/condition.rb +25 -0
  80. data/lib/durable_workflow/core/types/configs.rb +103 -0
  81. data/lib/durable_workflow/core/types/entry.rb +26 -0
  82. data/lib/durable_workflow/core/types/results.rb +41 -0
  83. data/lib/durable_workflow/core/types/state.rb +95 -0
  84. data/lib/durable_workflow/core/types/step_def.rb +15 -0
  85. data/lib/durable_workflow/core/types/workflow_def.rb +43 -0
  86. data/lib/durable_workflow/core/types.rb +29 -0
  87. data/lib/durable_workflow/core/validator.rb +318 -0
  88. data/lib/durable_workflow/extensions/ai/ai.rb +149 -0
  89. data/lib/durable_workflow/extensions/ai/configuration.rb +41 -0
  90. data/lib/durable_workflow/extensions/ai/executors/agent.rb +150 -0
  91. data/lib/durable_workflow/extensions/ai/executors/file_search.rb +52 -0
  92. data/lib/durable_workflow/extensions/ai/executors/guardrail.rb +152 -0
  93. data/lib/durable_workflow/extensions/ai/executors/handoff.rb +33 -0
  94. data/lib/durable_workflow/extensions/ai/executors/mcp.rb +47 -0
  95. data/lib/durable_workflow/extensions/ai/mcp/adapter.rb +73 -0
  96. data/lib/durable_workflow/extensions/ai/mcp/client.rb +77 -0
  97. data/lib/durable_workflow/extensions/ai/mcp/rack_app.rb +66 -0
  98. data/lib/durable_workflow/extensions/ai/mcp/server.rb +122 -0
  99. data/lib/durable_workflow/extensions/ai/tool_registry.rb +63 -0
  100. data/lib/durable_workflow/extensions/ai/types.rb +213 -0
  101. data/lib/durable_workflow/extensions/ai.rb +6 -0
  102. data/lib/durable_workflow/extensions/base.rb +77 -0
  103. data/lib/durable_workflow/runners/adapters/inline.rb +42 -0
  104. data/lib/durable_workflow/runners/adapters/sidekiq.rb +69 -0
  105. data/lib/durable_workflow/runners/async.rb +100 -0
  106. data/lib/durable_workflow/runners/stream.rb +126 -0
  107. data/lib/durable_workflow/runners/sync.rb +40 -0
  108. data/lib/durable_workflow/storage/active_record.rb +148 -0
  109. data/lib/durable_workflow/storage/redis.rb +133 -0
  110. data/lib/durable_workflow/storage/sequel.rb +144 -0
  111. data/lib/durable_workflow/storage/store.rb +43 -0
  112. data/lib/durable_workflow/utils.rb +25 -0
  113. data/lib/durable_workflow/version.rb +5 -0
  114. data/lib/durable_workflow.rb +70 -0
  115. data/sig/durable_workflow.rbs +4 -0
  116. 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