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,262 @@
1
+ # Phase 3 Extensions - Implementation & Test Coverage Todo
2
+
3
+ ## Status Legend
4
+
5
+ - [ ] Not started
6
+ - [~] In progress
7
+ - [x] Completed
8
+
9
+ ---
10
+
11
+ ## 1. EXTENSION SYSTEM (01-EXTENSION-SYSTEM.md)
12
+
13
+ ### 1.1 Implementation
14
+
15
+ - [ ] Create `lib/durable_workflow/extensions/` directory
16
+ - [ ] Create `lib/durable_workflow/extensions/base.rb` with Extensions::Base class
17
+ - [ ] Update `lib/durable_workflow/core/types/configs.rb` to add `Core.register_config(type, klass)` method
18
+ - [ ] Update `lib/durable_workflow/core/types/configs.rb` to add `Core.config_registered?(type)` method
19
+ - [ ] Update `lib/durable_workflow/core/parser.rb` with `before_parse`, `after_parse`, `transform_config` hooks
20
+ - [ ] Update `lib/durable_workflow.rb` to require extensions/base
21
+
22
+ ### 1.2 Tests - Extensions::Base
23
+
24
+ - [ ] Test: `Extensions::Base.extension_name` returns class-derived name
25
+ - [ ] Test: `Extensions::Base.extension_name=` sets custom name
26
+ - [ ] Test: `Extensions::Base.register!` calls register_configs, register_executors, register_parser_hooks
27
+ - [ ] Test: `Extensions::Base.data_from(workflow)` returns extension data
28
+ - [ ] Test: `Extensions::Base.store_in(workflow, data)` stores in extensions hash
29
+
30
+ ### 1.3 Tests - Extensions Registry
31
+
32
+ - [ ] Test: `Extensions.register(name, klass)` registers extension
33
+ - [ ] Test: `Extensions.register(name, klass)` calls `klass.register!`
34
+ - [ ] Test: `Extensions[name]` returns registered extension
35
+ - [ ] Test: `Extensions.loaded?(name)` returns true for registered
36
+ - [ ] Test: `Extensions.loaded?(name)` returns false for unregistered
37
+
38
+ ### 1.4 Tests - Config Registration
39
+
40
+ - [ ] Test: `Core.register_config(type, klass)` adds to CONFIG_REGISTRY
41
+ - [ ] Test: `Core.config_registered?(type)` returns true for registered
42
+ - [ ] Test: `Core.config_registered?(type)` returns false for unregistered
43
+
44
+ ### 1.5 Tests - Parser Hooks
45
+
46
+ - [ ] Test: `Parser.before_parse` hooks run before parsing
47
+ - [ ] Test: `Parser.before_parse` hooks can modify raw YAML
48
+ - [ ] Test: `Parser.after_parse` hooks run after parsing
49
+ - [ ] Test: `Parser.after_parse` hooks receive WorkflowDef
50
+ - [ ] Test: `Parser.after_parse` hooks can return modified WorkflowDef
51
+ - [ ] Test: `Parser.transform_config(type)` transforms config for specific type
52
+ - [ ] Test: Multiple hooks run in order registered
53
+
54
+ ### 1.6 Tests - Extension Loading
55
+
56
+ - [ ] Test: Requiring extension auto-registers it
57
+ - [ ] Test: Extension step types available after loading
58
+ - [ ] Test: Unknown step types fail validation when extension not loaded
59
+
60
+ ---
61
+
62
+ ## 2. AI EXTENSION (02-AI-PLUGIN.md)
63
+
64
+ ### 2.1 Implementation - Core Files
65
+
66
+ - [ ] Create `lib/durable_workflow/extensions/ai/` directory
67
+ - [ ] Create `lib/durable_workflow/extensions/ai/ai.rb` (main loader & Extension class)
68
+ - [ ] Create `lib/durable_workflow/extensions/ai/types.rb` (AI-specific types)
69
+ - [ ] Create `lib/durable_workflow/extensions/ai/provider.rb` (abstract provider interface)
70
+ - [ ] Create `lib/durable_workflow/extensions/ai/providers/ruby_llm.rb` (RubyLLM provider)
71
+
72
+ ### 2.2 Implementation - Executors
73
+
74
+ - [ ] Create `lib/durable_workflow/extensions/ai/executors/agent.rb`
75
+ - [ ] Create `lib/durable_workflow/extensions/ai/executors/guardrail.rb`
76
+ - [ ] Create `lib/durable_workflow/extensions/ai/executors/handoff.rb`
77
+ - [ ] Create `lib/durable_workflow/extensions/ai/executors/file_search.rb`
78
+ - [ ] Create `lib/durable_workflow/extensions/ai/executors/mcp.rb`
79
+
80
+ ### 2.3 Tests - AI Types
81
+
82
+ - [ ] Test: `Types::MessageRole` accepts "system", "user", "assistant", "tool"
83
+ - [ ] Test: `HandoffDef` can be created with agent_id and description
84
+ - [ ] Test: `AgentDef` can be created with id, model, instructions, tools, handoffs
85
+ - [ ] Test: `AgentDef.tools` defaults to empty array
86
+ - [ ] Test: `AgentDef.handoffs` defaults to empty array
87
+ - [ ] Test: `ToolParam` can be created with name, type, required, description
88
+ - [ ] Test: `ToolParam.type` defaults to "string"
89
+ - [ ] Test: `ToolParam.required` defaults to true
90
+ - [ ] Test: `ToolDef` can be created with id, description, parameters, service, method_name
91
+ - [ ] Test: `ToolDef.to_function_schema` returns valid function schema
92
+ - [ ] Test: `ToolCall` can be created with id, name, arguments
93
+ - [ ] Test: `Message.system(content)` creates system message
94
+ - [ ] Test: `Message.user(content)` creates user message
95
+ - [ ] Test: `Message.assistant(content)` creates assistant message
96
+ - [ ] Test: `Message.tool(content, tool_call_id:)` creates tool message
97
+ - [ ] Test: `Message.tool_calls?` returns true when tool_calls present
98
+ - [ ] Test: `Response` stores content, tool_calls, finish_reason
99
+ - [ ] Test: `Response.tool_calls?` returns true when tool_calls present
100
+ - [ ] Test: `ModerationResult` stores flagged, categories, scores
101
+ - [ ] Test: `GuardrailCheck` stores type, pattern, block_on_match, max, min
102
+ - [ ] Test: `GuardrailResult` stores passed, check_type, reason
103
+
104
+ ### 2.4 Tests - AI Configs
105
+
106
+ - [ ] Test: `AgentConfig` requires agent_id, prompt, output
107
+ - [ ] Test: `GuardrailConfig` accepts content, input, checks, on_fail
108
+ - [ ] Test: `HandoffConfig` accepts to, from, reason
109
+ - [ ] Test: `FileSearchConfig` requires query, accepts files, max_results, output
110
+ - [ ] Test: `MCPConfig` requires server, tool, accepts arguments, output
111
+
112
+ ### 2.5 Tests - Provider Interface
113
+
114
+ - [ ] Test: `Provider.current` is nil by default
115
+ - [ ] Test: `Provider.current=` sets current provider
116
+ - [ ] Test: `Provider#complete` raises NotImplementedError
117
+ - [ ] Test: `Provider#moderate` raises NotImplementedError
118
+ - [ ] Test: `Provider#stream` falls back to complete
119
+
120
+ ### 2.6 Tests - RubyLLM Provider
121
+
122
+ - [ ] Test: `RubyLLM#complete` raises when RubyLLM gem not loaded
123
+ - [ ] Test: `RubyLLM#complete` converts Message to RubyLLM format
124
+ - [ ] Test: `RubyLLM#complete` returns Response
125
+ - [ ] Test: `RubyLLM#complete` parses tool_calls from response
126
+ - [ ] Test: `RubyLLM#moderate` returns ModerationResult (default unflagged)
127
+
128
+ ### 2.7 Tests - Agent Executor
129
+
130
+ - [ ] Test: Agent executor is registered as "agent"
131
+ - [ ] Test: Agent raises ExecutionError when agent_id not found
132
+ - [ ] Test: Agent builds messages with system instruction
133
+ - [ ] Test: Agent resolves prompt from state
134
+ - [ ] Test: Agent calls provider.complete
135
+ - [ ] Test: Agent stores response content in output
136
+ - [ ] Test: Agent handles tool calls
137
+ - [ ] Test: Agent respects MAX_TOOL_ITERATIONS
138
+ - [ ] Test: Agent handles handoff tool calls
139
+ - [ ] Test: Agent raises ExecutionError when provider not configured
140
+
141
+ ### 2.8 Tests - Guardrail Executor
142
+
143
+ - [ ] Test: Guardrail executor is registered as "guardrail"
144
+ - [ ] Test: Guardrail resolves content from state
145
+ - [ ] Test: Guardrail check "prompt_injection" detects injection patterns
146
+ - [ ] Test: Guardrail check "pii" detects SSN pattern
147
+ - [ ] Test: Guardrail check "pii" detects credit card pattern
148
+ - [ ] Test: Guardrail check "pii" detects email pattern
149
+ - [ ] Test: Guardrail check "pii" detects phone pattern
150
+ - [ ] Test: Guardrail check "moderation" calls provider.moderate
151
+ - [ ] Test: Guardrail check "regex" matches pattern
152
+ - [ ] Test: Guardrail check "regex" respects block_on_match=false
153
+ - [ ] Test: Guardrail check "length" validates max length
154
+ - [ ] Test: Guardrail check "length" validates min length
155
+ - [ ] Test: Guardrail on_fail routes to specified step
156
+ - [ ] Test: Guardrail raises ExecutionError when no on_fail and check fails
157
+ - [ ] Test: Guardrail stores failure info in ctx on fail
158
+
159
+ ### 2.9 Tests - Handoff Executor
160
+
161
+ - [ ] Test: Handoff executor is registered as "handoff"
162
+ - [ ] Test: Handoff uses config.to as target agent
163
+ - [ ] Test: Handoff falls back to ctx[:_handoff_to]
164
+ - [ ] Test: Handoff raises ExecutionError when no target
165
+ - [ ] Test: Handoff raises ExecutionError when target agent not found
166
+ - [ ] Test: Handoff sets _current_agent in ctx
167
+ - [ ] Test: Handoff sets _handoff_context in ctx
168
+ - [ ] Test: Handoff removes _handoff_to from ctx
169
+
170
+ ### 2.10 Tests - FileSearch Executor
171
+
172
+ - [ ] Test: FileSearch executor is registered as "file_search"
173
+ - [ ] Test: FileSearch resolves query from state
174
+ - [ ] Test: FileSearch stores results in output
175
+ - [ ] Test: FileSearch respects max_results
176
+
177
+ ### 2.11 Tests - MCP Executor
178
+
179
+ - [ ] Test: MCP executor is registered as "mcp"
180
+ - [ ] Test: MCP resolves arguments from state
181
+ - [ ] Test: MCP stores result in output
182
+ - [ ] Test: MCP includes server and tool in result
183
+
184
+ ### 2.12 Tests - AI Extension Class
185
+
186
+ - [ ] Test: `AI::Extension.extension_name` is "ai"
187
+ - [ ] Test: `AI::Extension.register_configs` registers all AI configs
188
+ - [ ] Test: `AI::Extension.parse_agents` parses agents from YAML
189
+ - [ ] Test: `AI::Extension.parse_tools` parses tools from YAML
190
+ - [ ] Test: `AI::Extension.parse_handoffs` parses handoffs
191
+ - [ ] Test: `AI::Extension.agents(workflow)` returns agents hash
192
+ - [ ] Test: `AI::Extension.tools(workflow)` returns tools hash
193
+ - [ ] Test: `AI.setup` sets default provider
194
+ - [ ] Test: `AI.setup(provider:)` sets custom provider
195
+
196
+ ### 2.13 Tests - AI Parser Integration
197
+
198
+ - [ ] Test: Parser parses agents section into extensions[:ai][:agents]
199
+ - [ ] Test: Parser parses tools section into extensions[:ai][:tools]
200
+ - [ ] Test: Agent steps resolve agent_id from extensions[:ai][:agents]
201
+ - [ ] Test: Tool calls resolve tool from extensions[:ai][:tools]
202
+
203
+ ---
204
+
205
+ ## 3. INTEGRATION TESTS
206
+
207
+ ### 3.1 Extension System Integration
208
+
209
+ - [ ] Test: Custom extension with step type works end-to-end
210
+ - [ ] Test: Multiple extensions can be loaded
211
+ - [ ] Test: Extension data persists through workflow execution
212
+
213
+ ### 3.2 AI Extension Integration
214
+
215
+ - [ ] Test: Workflow with agent step (mocked provider)
216
+ - [ ] Test: Workflow with guardrail -> agent pipeline
217
+ - [ ] Test: Workflow with multiple agents and handoffs
218
+ - [ ] Test: Agent tool calling works with defined tools
219
+ - [ ] Test: Guardrail rejection routes to error step
220
+
221
+ ---
222
+
223
+ ## 4. TEST FILE ORGANIZATION
224
+
225
+ ### 4.1 Test Files to Create
226
+
227
+ - [ ] Create `test/unit/extensions/base_test.rb`
228
+ - [ ] Create `test/unit/extensions/registry_test.rb`
229
+ - [ ] Create `test/unit/core/parser_hooks_test.rb`
230
+ - [ ] Create `test/unit/extensions/ai/types_test.rb`
231
+ - [ ] Create `test/unit/extensions/ai/provider_test.rb`
232
+ - [ ] Create `test/unit/extensions/ai/providers/ruby_llm_test.rb`
233
+ - [ ] Create `test/unit/extensions/ai/executors/agent_test.rb`
234
+ - [ ] Create `test/unit/extensions/ai/executors/guardrail_test.rb`
235
+ - [ ] Create `test/unit/extensions/ai/executors/handoff_test.rb`
236
+ - [ ] Create `test/unit/extensions/ai/executors/file_search_test.rb`
237
+ - [ ] Create `test/unit/extensions/ai/executors/mcp_test.rb`
238
+ - [ ] Create `test/unit/extensions/ai/extension_test.rb`
239
+ - [ ] Create `test/integration/extension_test.rb`
240
+ - [ ] Create `test/integration/ai_workflow_test.rb`
241
+ - [ ] Create `test/support/mock_provider.rb`
242
+
243
+ ---
244
+
245
+ ## Summary Stats
246
+
247
+ | Section | Implementation Tasks | Test Tasks | Total |
248
+ | --------------------------- | -------------------- | ---------- | ------- |
249
+ | 1. Extension System | 6 | 18 | 24 |
250
+ | 2. AI Extension | 10 | 76 | 86 |
251
+ | 3. Integration Tests | 0 | 8 | 8 |
252
+ | 4. Test Files | 15 | 0 | 15 |
253
+ | **TOTAL** | **31** | **102** | **133** |
254
+
255
+ ---
256
+
257
+ ## Notes
258
+
259
+ - Extension system must be completed before AI extension
260
+ - AI extension tests require mock provider for isolation
261
+ - RubyLLM provider tests should be skipped if gem not available
262
+ - FileSearch and MCP are placeholder implementations (integrate with real backends later)
@@ -0,0 +1,107 @@
1
+ # 01-DEPENDENCIES: RubyLLM + MCP as Runtime Dependencies
2
+
3
+ ## Goal
4
+
5
+ Remove provider abstraction. Make `ruby_llm` and `mcp` gems required runtime dependencies.
6
+
7
+ ## Gem Details
8
+
9
+ ### ruby_llm
10
+
11
+ - **Gem:** `ruby_llm`
12
+ - **Repo:** https://github.com/crmne/ruby_llm
13
+ - **Features:** Multi-provider (OpenAI, Anthropic, Gemini, etc.), chat, streaming, tools, embeddings, moderation
14
+
15
+ ```ruby
16
+ # Chat
17
+ chat = RubyLLM.chat
18
+ response = chat.ask("Hello")
19
+
20
+ # Streaming
21
+ chat.ask("Tell me a story") { |chunk| print chunk.content }
22
+
23
+ # Tools
24
+ class MyTool < RubyLLM::Tool
25
+ description "Does something"
26
+ param :input, desc: "The input"
27
+ def execute(input:)
28
+ # return result
29
+ end
30
+ end
31
+ chat.with_tool(MyTool).ask("Use the tool")
32
+
33
+ # Moderation
34
+ RubyLLM.moderate("content to check")
35
+ ```
36
+
37
+ ### mcp (Anthropic Ruby SDK)
38
+
39
+ - **Gem:** `mcp`
40
+ - **Repo:** https://github.com/modelcontextprotocol/ruby-sdk
41
+ - **Maintainers:** Anthropic + Shopify
42
+
43
+ ```ruby
44
+ # Server (expose tools)
45
+ server = MCP::Server.new(name: "my_server", tools: [...])
46
+ transport = MCP::Server::Transports::StdioTransport.new(server)
47
+ transport.open
48
+
49
+ # Client (consume external)
50
+ transport = MCP::Client::HTTP.new(url: "https://server.example.com/mcp")
51
+ client = MCP::Client.new(transport: transport)
52
+ tools = client.tools
53
+ result = client.call_tool(tool: tools.first, arguments: { foo: "bar" })
54
+ ```
55
+
56
+ ## Files to Modify
57
+
58
+ ### `durable_workflow.gemspec`
59
+
60
+ ```ruby
61
+ spec.add_dependency "ruby_llm", "~> 1.0"
62
+ spec.add_dependency "mcp", "~> 0.1"
63
+ spec.add_dependency "faraday", ">= 2.0"
64
+ ```
65
+
66
+ ### `Gemfile`
67
+
68
+ ```ruby
69
+ # Runtime
70
+ gem "ruby_llm", "~> 1.0"
71
+ gem "mcp", "~> 0.1"
72
+ gem "faraday", ">= 2.0"
73
+ ```
74
+
75
+ ## Files to Delete
76
+
77
+ | File | Reason |
78
+ | ---------------------------------------------------------- | ---------------------------------- |
79
+ | `lib/durable_workflow/extensions/ai/provider.rb` | Abstract provider no longer needed |
80
+ | `lib/durable_workflow/extensions/ai/providers/ruby_llm.rb` | Direct RubyLLM usage instead |
81
+ | `lib/durable_workflow/extensions/ai/providers/` | Entire directory |
82
+ | `test/unit/extensions/ai/provider_test.rb` | No provider to test |
83
+
84
+ ## Files to Update
85
+
86
+ ### `lib/durable_workflow/extensions/ai/ai.rb`
87
+
88
+ Remove:
89
+
90
+ ```ruby
91
+ require_relative "provider"
92
+ require_relative "providers/ruby_llm"
93
+ ```
94
+
95
+ Add:
96
+
97
+ ```ruby
98
+ require "ruby_llm"
99
+ require "mcp"
100
+ ```
101
+
102
+ ## Acceptance Criteria
103
+
104
+ 1. `bundle install` succeeds with new dependencies
105
+ 2. `require "durable_workflow/extensions/ai"` loads without error
106
+ 3. No references to `Provider` class remain
107
+ 4. Tests pass without provider abstraction
@@ -0,0 +1,123 @@
1
+ # 02-CONFIGURATION: Direct RubyLLM Configuration
2
+
3
+ ## Goal
4
+
5
+ Replace `Provider.current=` dance with direct RubyLLM configuration.
6
+
7
+ ## Files to Create
8
+
9
+ ### `lib/durable_workflow/extensions/ai/configuration.rb`
10
+
11
+ ```ruby
12
+ # frozen_string_literal: true
13
+
14
+ module DurableWorkflow
15
+ module Extensions
16
+ module AI
17
+ class Configuration
18
+ attr_accessor :default_model, :api_keys
19
+
20
+ def initialize
21
+ @default_model = "gpt-4o-mini"
22
+ @api_keys = {}
23
+ end
24
+ end
25
+
26
+ class << self
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def configure
32
+ yield configuration if block_given?
33
+ apply_ruby_llm_config
34
+ configuration
35
+ end
36
+
37
+ def chat(model: nil)
38
+ RubyLLM.chat(model: model || configuration.default_model)
39
+ end
40
+
41
+ private
42
+
43
+ def apply_ruby_llm_config
44
+ RubyLLM.configure do |c|
45
+ c.openai_api_key = configuration.api_keys[:openai] if configuration.api_keys[:openai]
46
+ c.anthropic_api_key = configuration.api_keys[:anthropic] if configuration.api_keys[:anthropic]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```ruby
58
+ # Configure AI extension
59
+ DurableWorkflow::Extensions::AI.configure do |c|
60
+ c.api_keys[:openai] = ENV["OPENAI_API_KEY"]
61
+ c.api_keys[:anthropic] = ENV["ANTHROPIC_API_KEY"]
62
+ c.default_model = "gpt-4o"
63
+ end
64
+
65
+ # Use directly
66
+ chat = DurableWorkflow::Extensions::AI.chat
67
+ response = chat.ask("Hello")
68
+
69
+ # Or with specific model
70
+ chat = DurableWorkflow::Extensions::AI.chat(model: "claude-3-5-sonnet")
71
+ ```
72
+
73
+ ## Tests
74
+
75
+ ### `test/unit/extensions/ai/configuration_test.rb`
76
+
77
+ ```ruby
78
+ class ConfigurationTest < Minitest::Test
79
+ def setup
80
+ @config = DurableWorkflow::Extensions::AI::Configuration.new
81
+ end
82
+
83
+ def test_default_model
84
+ assert_equal "gpt-4o-mini", @config.default_model
85
+ end
86
+
87
+ def test_api_keys_empty_by_default
88
+ assert_equal({}, @config.api_keys)
89
+ end
90
+
91
+ def test_configure_yields_configuration
92
+ DurableWorkflow::Extensions::AI.configure do |c|
93
+ c.default_model = "test-model"
94
+ end
95
+
96
+ assert_equal "test-model", DurableWorkflow::Extensions::AI.configuration.default_model
97
+ end
98
+
99
+ def test_chat_returns_ruby_llm_chat
100
+ # Mock RubyLLM.chat
101
+ mock_chat = Minitest::Mock.new
102
+ RubyLLM.stub :chat, mock_chat do
103
+ result = DurableWorkflow::Extensions::AI.chat
104
+ assert_equal mock_chat, result
105
+ end
106
+ end
107
+
108
+ def test_chat_with_model_override
109
+ RubyLLM.stub :chat, ->(model:) { "chat_with_#{model}" } do
110
+ result = DurableWorkflow::Extensions::AI.chat(model: "custom")
111
+ assert_equal "chat_with_custom", result
112
+ end
113
+ end
114
+ end
115
+ ```
116
+
117
+ ## Acceptance Criteria
118
+
119
+ 1. `AI.configure` block sets API keys
120
+ 2. `AI.configure` applies keys to RubyLLM
121
+ 3. `AI.chat` returns RubyLLM chat instance
122
+ 4. `AI.chat(model:)` overrides default model
123
+ 5. Default model is "gpt-4o-mini"