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,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ # Stateless resolver - all methods take state explicitly
6
+ class Resolver
7
+ PATTERN = /\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/
8
+
9
+ class << self
10
+ def resolve(state, value)
11
+ case value
12
+ when String then resolve_string(state, value)
13
+ when Hash then value.transform_values { resolve(state, _1) }
14
+ when Array then value.map { resolve(state, _1) }
15
+ when nil then nil
16
+ else value
17
+ end
18
+ end
19
+
20
+ def resolve_ref(state, ref)
21
+ parts = ref.split('.')
22
+ root = parts.shift.to_sym
23
+
24
+ base = case root
25
+ when :input then state.input
26
+ when :now then return Time.now
27
+ when :history then return state.history
28
+ else state.ctx[root]
29
+ end
30
+
31
+ dig(base, parts)
32
+ end
33
+
34
+ private
35
+
36
+ def resolve_string(state, str)
37
+ # Whole string is single reference -> return actual value (not stringified)
38
+ return resolve_ref(state, str[1..]) if str.match?(/\A\$[a-zA-Z_][a-zA-Z0-9_.]*\z/)
39
+
40
+ # Embedded references -> interpolate as strings
41
+ str.gsub(PATTERN) { resolve_ref(state, _1[1..]).to_s }
42
+ end
43
+
44
+ def dig(value, keys)
45
+ return value if keys.empty?
46
+
47
+ key = keys.shift
48
+
49
+ next_val = case value
50
+ when Hash then Utils.fetch(value, key)
51
+ when Array then key.match?(/\A\d+\z/) ? value[key.to_i] : nil
52
+ when Struct then value.respond_to?(key) ? value.send(key) : nil
53
+ else value.respond_to?(key) ? value.send(key) : nil
54
+ end
55
+
56
+ dig(next_val, keys)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ # Runtime JSON Schema validation (optional - requires json_schemer gem)
6
+ class SchemaValidator
7
+ def self.validate!(value, schema, context:)
8
+ return true if schema.nil?
9
+
10
+ begin
11
+ require 'json_schemer'
12
+ rescue LoadError
13
+ # If json_schemer not available, skip runtime validation
14
+ DurableWorkflow.log(:debug, 'json_schemer not available, skipping runtime schema validation')
15
+ return true
16
+ end
17
+
18
+ schemer = JSONSchemer.schema(normalize(schema))
19
+ errors = schemer.validate(jsonify(value)).to_a
20
+
21
+ return true if errors.empty?
22
+
23
+ messages = errors.map { _1['error'] }.join('; ')
24
+ raise ValidationError, "#{context}: #{messages}"
25
+ end
26
+
27
+ def self.normalize(schema)
28
+ deep_stringify(schema)
29
+ end
30
+
31
+ def self.jsonify(value)
32
+ JSON.parse(value.to_json)
33
+ end
34
+
35
+ def self.deep_stringify(obj)
36
+ case obj
37
+ when Hash
38
+ obj.transform_keys(&:to_s).transform_values { deep_stringify(_1) }
39
+ when Array
40
+ obj.map { deep_stringify(_1) }
41
+ else
42
+ obj
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-types'
4
+ require 'dry-struct'
5
+
6
+ module DurableWorkflow
7
+ module Types
8
+ include Dry.Types()
9
+
10
+ # StepType is just a string - validated at parse time against executor registry
11
+ # This decouples core from extensions (no hardcoded AI types)
12
+ StepType = Types::Strict::String
13
+
14
+ # Condition operator enum
15
+ Operator = Types::Strict::String.enum(
16
+ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte',
17
+ 'contains', 'starts_with', 'ends_with', 'matches',
18
+ 'in', 'not_in', 'exists', 'empty', 'truthy', 'falsy'
19
+ )
20
+
21
+ # Entry action enum
22
+ EntryAction = Types::Strict::Symbol.enum(:completed, :halted, :failed)
23
+
24
+ # Wait mode for parallel - default "all"
25
+ WaitMode = Types::Strict::String.default('all').enum('all', 'any') | Types::Strict::Integer
26
+ end
27
+
28
+ class BaseStruct < Dry::Struct
29
+ transform_keys(&:to_sym)
30
+
31
+ def to_h
32
+ super.transform_values do |v|
33
+ case v
34
+ when BaseStruct then v.to_h
35
+ when Array then v.map { |e| e.is_a?(BaseStruct) ? e.to_h : e }
36
+ else v
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ # Operator type with default
6
+ OperatorType = Types::Strict::String.default('eq').enum(
7
+ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte',
8
+ 'contains', 'starts_with', 'ends_with', 'matches',
9
+ 'in', 'not_in', 'exists', 'empty', 'truthy', 'falsy'
10
+ )
11
+
12
+ class Condition < BaseStruct
13
+ attribute :field, Types::Strict::String
14
+ attribute :op, OperatorType
15
+ attribute :value, Types::Any
16
+ end
17
+
18
+ class Route < BaseStruct
19
+ attribute :field, Types::Strict::String
20
+ attribute :op, OperatorType
21
+ attribute :value, Types::Any
22
+ attribute :target, Types::Strict::String
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ # Base for all step configs
6
+ class StepConfig < BaseStruct; end
7
+
8
+ class StartConfig < StepConfig
9
+ attribute? :validate_input, Types::Strict::Bool.default(false)
10
+ end
11
+
12
+ class EndConfig < StepConfig
13
+ attribute? :result, Types::Any
14
+ end
15
+
16
+ # Output with optional schema validation
17
+ class OutputConfig < BaseStruct
18
+ attribute :key, Types::Coercible::Symbol
19
+ attribute? :schema, Types::Hash.optional
20
+ end
21
+
22
+ class CallConfig < StepConfig
23
+ attribute :service, Types::Strict::String
24
+ attribute :method_name, Types::Strict::String
25
+ attribute? :input, Types::Any
26
+ attribute? :output, Types::Coercible::Symbol.optional | OutputConfig
27
+ attribute? :timeout, Types::Strict::Integer.optional
28
+ attribute? :retries, Types::Strict::Integer.optional.default(0)
29
+ attribute? :retry_delay, Types::Strict::Float.optional.default(1.0)
30
+ attribute? :retry_backoff, Types::Strict::Float.optional.default(2.0)
31
+ end
32
+
33
+ class AssignConfig < StepConfig
34
+ attribute :set, Types::Hash.default({}.freeze)
35
+ end
36
+
37
+ class RouterConfig < StepConfig
38
+ attribute :routes, Types::Strict::Array.default([].freeze)
39
+ attribute? :default, Types::Strict::String.optional
40
+ end
41
+
42
+ class LoopConfig < StepConfig
43
+ # foreach mode
44
+ attribute? :over, Types::Any
45
+ attribute? :as, Types::Coercible::Symbol.optional.default(:item)
46
+ attribute? :index_as, Types::Coercible::Symbol.optional.default(:index)
47
+ # while mode
48
+ attribute? :while, Types::Any
49
+ # shared
50
+ attribute? :do, Types::Strict::Array.default([].freeze)
51
+ attribute? :output, Types::Coercible::Symbol.optional
52
+ attribute? :max, Types::Strict::Integer.optional.default(100)
53
+ attribute? :on_exhausted, Types::Strict::String.optional
54
+ end
55
+
56
+ class HaltConfig < StepConfig
57
+ attribute? :reason, Types::Strict::String.optional
58
+ attribute? :data, Types::Hash.default({}.freeze)
59
+ attribute? :resume_step, Types::Strict::String.optional
60
+ end
61
+
62
+ class ApprovalConfig < StepConfig
63
+ attribute :prompt, Types::Strict::String
64
+ attribute? :context, Types::Any
65
+ attribute? :approvers, Types::Strict::Array.of(Types::Strict::String).optional
66
+ attribute? :on_reject, Types::Strict::String.optional
67
+ attribute? :timeout, Types::Strict::Integer.optional
68
+ attribute? :on_timeout, Types::Strict::String.optional
69
+ end
70
+
71
+ class TransformConfig < StepConfig
72
+ attribute? :input, Types::Strict::String.optional
73
+ attribute :expression, Types::Hash
74
+ attribute :output, Types::Coercible::Symbol
75
+ end
76
+
77
+ class ParallelConfig < StepConfig
78
+ attribute :branches, Types::Strict::Array.default([].freeze)
79
+ attribute? :wait, Types::WaitMode
80
+ attribute? :output, Types::Coercible::Symbol.optional
81
+ end
82
+
83
+ class WorkflowConfig < StepConfig
84
+ attribute :workflow_id, Types::Strict::String
85
+ attribute? :input, Types::Any
86
+ attribute? :output, Types::Coercible::Symbol.optional
87
+ attribute? :timeout, Types::Strict::Integer.optional
88
+ end
89
+
90
+ # Register core config types
91
+ Core.register_config('start', StartConfig)
92
+ Core.register_config('end', EndConfig)
93
+ Core.register_config('call', CallConfig)
94
+ Core.register_config('assign', AssignConfig)
95
+ Core.register_config('router', RouterConfig)
96
+ Core.register_config('loop', LoopConfig)
97
+ Core.register_config('halt', HaltConfig)
98
+ Core.register_config('approval', ApprovalConfig)
99
+ Core.register_config('transform', TransformConfig)
100
+ Core.register_config('parallel', ParallelConfig)
101
+ Core.register_config('workflow', WorkflowConfig)
102
+ end
103
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ class Entry < BaseStruct
6
+ attribute :id, Types::Strict::String
7
+ attribute :execution_id, Types::Strict::String
8
+ attribute :step_id, Types::Strict::String
9
+ attribute :step_type, Types::StepType # Just a string
10
+ attribute :action, Types::EntryAction
11
+ attribute? :duration_ms, Types::Strict::Integer.optional
12
+ attribute? :input, Types::Any
13
+ attribute? :output, Types::Any
14
+ attribute? :error, Types::Strict::String.optional
15
+ attribute :timestamp, Types::Any
16
+
17
+ def self.from_h(h)
18
+ new(
19
+ **h,
20
+ action: h[:action]&.to_sym,
21
+ timestamp: h[:timestamp].is_a?(String) ? Time.parse(h[:timestamp]) : h[:timestamp]
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ class ContinueResult < BaseStruct
6
+ attribute? :next_step, Types::Strict::String.optional
7
+ attribute? :output, Types::Any
8
+ end
9
+
10
+ class HaltResult < BaseStruct
11
+ # data is required (no default) - distinguishes from ContinueResult in union
12
+ attribute :data, Types::Hash
13
+ attribute? :resume_step, Types::Strict::String.optional
14
+ attribute? :prompt, Types::Strict::String.optional
15
+
16
+ # Common interface with ContinueResult
17
+ def output = data
18
+ end
19
+
20
+ # ExecutionResult - returned by Engine.run/resume
21
+ # Note: ErrorResult removed - errors are captured in Execution.error field
22
+ class ExecutionResult < BaseStruct
23
+ attribute :status, ExecutionStatus
24
+ attribute :execution_id, Types::Strict::String
25
+ attribute? :output, Types::Any
26
+ attribute? :halt, HaltResult.optional
27
+ attribute? :error, Types::Strict::String.optional
28
+
29
+ def completed? = status == :completed
30
+ def halted? = status == :halted
31
+ def failed? = status == :failed
32
+ end
33
+
34
+ # Outcome of executing a step: new state + result
35
+ # HaltResult first - has required `data` field, so union can distinguish
36
+ class StepOutcome < BaseStruct
37
+ attribute :state, State
38
+ attribute :result, HaltResult | ContinueResult
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ # Runtime state - immutable during execution
6
+ # Used by executors. Contains only workflow variables in ctx (no internal _prefixed fields).
7
+ class State < BaseStruct
8
+ attribute :execution_id, Types::Strict::String
9
+ attribute :workflow_id, Types::Strict::String
10
+ attribute :input, Types::Hash.default({}.freeze)
11
+ attribute :ctx, Types::Hash.default({}.freeze) # User workflow variables only
12
+ attribute? :current_step, Types::Strict::String.optional
13
+ attribute :history, Types::Strict::Array.default([].freeze)
14
+
15
+ # Immutable update helpers
16
+ def with(**updates)
17
+ self.class.new(to_h.merge(updates))
18
+ end
19
+
20
+ def with_ctx(**updates)
21
+ with(ctx: ctx.merge(DurableWorkflow::Utils.deep_symbolize(updates)))
22
+ end
23
+
24
+ def with_current_step(step_id)
25
+ with(current_step: step_id)
26
+ end
27
+ end
28
+
29
+ # Execution status enum
30
+ ExecutionStatus = Types::Strict::Symbol.enum(:pending, :running, :completed, :halted, :failed)
31
+
32
+ # Typed execution record for storage
33
+ # Storage layer saves/loads Execution, Engine works with State internally.
34
+ # This separation keeps ctx clean and provides typed status/halt_data/error fields.
35
+ class Execution < BaseStruct
36
+ attribute :id, Types::Strict::String
37
+ attribute :workflow_id, Types::Strict::String
38
+ attribute :status, ExecutionStatus
39
+ attribute :input, Types::Hash.default({}.freeze)
40
+ attribute :ctx, Types::Hash.default({}.freeze) # User workflow variables only
41
+ attribute? :current_step, Types::Strict::String.optional
42
+ attribute? :result, Types::Any # Final output when completed
43
+ attribute? :recover_to, Types::Strict::String.optional # Step to resume from
44
+ attribute? :halt_data, Types::Hash.optional # Data from HaltResult
45
+ attribute? :error, Types::Strict::String.optional # Error message when failed
46
+ attribute? :created_at, Types::Any
47
+ attribute? :updated_at, Types::Any
48
+
49
+ # Convert to State for executor use
50
+ def to_state
51
+ State.new(
52
+ execution_id: id,
53
+ workflow_id: workflow_id,
54
+ input: input,
55
+ ctx: ctx,
56
+ current_step: current_step
57
+ )
58
+ end
59
+
60
+ # Build from State + ExecutionResult
61
+ def self.from_state(state, result)
62
+ new(
63
+ id: state.execution_id,
64
+ workflow_id: state.workflow_id,
65
+ status: result.status,
66
+ input: state.input,
67
+ ctx: state.ctx,
68
+ current_step: state.current_step,
69
+ result: result.output,
70
+ recover_to: result.halt&.resume_step,
71
+ halt_data: result.halt&.data,
72
+ error: result.error,
73
+ updated_at: Time.now
74
+ )
75
+ end
76
+
77
+ def self.from_h(hash)
78
+ new(
79
+ id: hash[:id],
80
+ workflow_id: hash[:workflow_id],
81
+ status: hash[:status]&.to_sym || :pending,
82
+ input: DurableWorkflow::Utils.deep_symbolize(hash[:input] || {}),
83
+ ctx: DurableWorkflow::Utils.deep_symbolize(hash[:ctx] || {}),
84
+ current_step: hash[:current_step],
85
+ result: hash[:result],
86
+ recover_to: hash[:recover_to],
87
+ halt_data: hash[:halt_data],
88
+ error: hash[:error],
89
+ created_at: hash[:created_at],
90
+ updated_at: hash[:updated_at]
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ class StepDef < BaseStruct
6
+ attribute :id, Types::Strict::String
7
+ attribute :type, Types::StepType # Just a string, not enum
8
+ attribute :config, Types::Any
9
+ attribute? :next_step, Types::Strict::String.optional
10
+ attribute? :on_error, Types::Strict::String.optional
11
+
12
+ def terminal? = type == 'end'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ class InputDef < BaseStruct
6
+ attribute :name, Types::Strict::String
7
+ attribute? :type, Types::Strict::String.optional.default('string')
8
+ attribute? :required, Types::Strict::Bool.default(true)
9
+ attribute? :default, Types::Any
10
+ attribute? :description, Types::Strict::String.optional
11
+ end
12
+
13
+ class WorkflowDef < BaseStruct
14
+ attribute :id, Types::Strict::String
15
+ attribute :name, Types::Strict::String
16
+ attribute? :version, Types::Strict::String.optional.default('1.0')
17
+ attribute? :description, Types::Strict::String.optional
18
+ attribute? :timeout, Types::Strict::Integer.optional
19
+ attribute :inputs, Types::Strict::Array.of(InputDef).default([].freeze)
20
+ attribute :steps, Types::Strict::Array.of(StepDef).default([].freeze)
21
+ # Generic extension data - AI extension stores agents/tools here
22
+ attribute? :extensions, Types::Hash.default({}.freeze)
23
+
24
+ def find_step(id) = steps.find { _1.id == id }
25
+ def first_step = steps.first
26
+ def step_ids = steps.map(&:id)
27
+
28
+ # Immutable update - preserve struct instances
29
+ def with(**updates)
30
+ self.class.new(
31
+ id: updates.fetch(:id, id),
32
+ name: updates.fetch(:name, name),
33
+ version: updates.fetch(:version, version),
34
+ description: updates.fetch(:description, description),
35
+ timeout: updates.fetch(:timeout, timeout),
36
+ inputs: updates.fetch(:inputs, inputs),
37
+ steps: updates.fetch(:steps, steps),
38
+ extensions: updates.fetch(:extensions, extensions)
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DurableWorkflow
4
+ module Core
5
+ class << self
6
+ def config_registry
7
+ @config_registry ||= {}
8
+ end
9
+
10
+ def register_config(type, klass)
11
+ config_registry[type.to_s] = klass
12
+ end
13
+
14
+ def config_registered?(type)
15
+ config_registry.key?(type.to_s)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ # Load base types first
22
+ require_relative 'types/base'
23
+ require_relative 'types/condition'
24
+ require_relative 'types/configs'
25
+ require_relative 'types/step_def'
26
+ require_relative 'types/workflow_def'
27
+ require_relative 'types/state'
28
+ require_relative 'types/entry'
29
+ require_relative 'types/results'