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,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"
|